Commit 8bea9eed authored by Andrew Fontaine's avatar Andrew Fontaine Committed by Filipa Lacerda

Add a New Copy Button That Works in Modals

This copy button manages a local instance of the Clipboard plugin
specific to it, which means it is created/destroyed on the
creation/destruction of the component. This allows it to work well in
gitlab-ui modals, as the event listeners are bound on creation of the
button.

It also allows for bindings to the `container` option of the Clipboard
plugin, which allows it to work within the focus trap set by bootstrap's
modals.
parent 7468ed5f
<script>
import $ from 'jquery';
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
import { __ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import Clipboard from 'clipboard';
export default {
components: {
GlButton,
Icon,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
text: {
type: String,
required: false,
default: '',
},
container: {
type: String,
required: false,
default: '',
},
modalId: {
type: String,
required: false,
default: '',
},
target: {
type: String,
required: false,
default: '',
},
title: {
type: String,
required: true,
},
tooltipPlacement: {
type: String,
required: false,
default: 'top',
},
tooltipContainer: {
type: String,
required: false,
default: null,
},
},
copySuccessText: __('Copied'),
computed: {
modalDomId() {
return this.modalId ? `#${this.modalId}` : '';
},
},
mounted() {
this.$nextTick(() => {
this.clipboard = new Clipboard(this.$el, {
container:
document.querySelector(`${this.modalDomId} div.modal-content`) ||
document.getElementById(this.container) ||
document.body,
});
this.clipboard
.on('success', e => {
this.updateTooltip(e.trigger);
this.$emit('success', e);
// Clear the selection and blur the trigger so it loses its border
e.clearSelection();
$(e.trigger).blur();
})
.on('error', e => this.$emit('error', e));
});
},
destroyed() {
if (this.clipboard) {
this.clipboard.destroy();
}
},
methods: {
updateTooltip(target) {
const $target = $(target);
const originalTitle = $target.data('originalTitle');
if ($target.tooltip) {
/**
* The original tooltip will continue staying there unless we remove it by hand.
* $target.tooltip('hide') isn't working.
*/
$('.tooltip').remove();
$target.attr('title', this.$options.copySuccessText);
$target.tooltip('_fixTitle');
$target.tooltip('show');
$target.attr('title', originalTitle);
$target.tooltip('_fixTitle');
}
},
},
};
</script>
<template>
<gl-button
v-gl-tooltip="{ placement: tooltipPlacement, container: tooltipContainer }"
:data-clipboard-target="target"
:data-clipboard-text="text"
:title="title"
>
<slot>
<icon name="duplicate" />
</slot>
</gl-button>
</template>
---
title: Add a New Copy Button That Works in Modals
merge_request: 28676
author:
type: added
......@@ -25,3 +25,17 @@ document.body.dataset.page
```
Find here the [source code setting the attribute](https://gitlab.com/gitlab-org/gitlab-ce/blob/cc5095edfce2b4d4083a4fb1cdc7c0a1898b9921/app/views/layouts/application.html.haml#L4).
### `modal_copy_button` vs `clipboard_button`
The `clipboard_button` uses the `copy_to_clipboard.js` behaviour, which is
initialized on page load, so if there are vue-based clipboard buttons that
don't exist at page load (such as ones in a `GlModal`), they do not have the
click handlers associated with the clipboard package.
`modal_copy_button` was added that manages an instance of the
[`clipboard` plugin](https://www.npmjs.com/package/clipboard) specific to
the instance of that component, which means that clipboard events are
bound on mounting and destroyed when the button is, mitigating the above
issue. It also has bindings to a particular container or modal ID
available, to work with the focus trap created by our GlModal.
import Vue from 'vue';
import { shallowMount } from '@vue/test-utils';
import modalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
describe('modal copy button', () => {
const Component = Vue.extend(modalCopyButton);
let wrapper;
afterEach(() => {
wrapper.destroy();
});
beforeEach(() => {
wrapper = shallowMount(Component, {
propsData: {
text: 'copy me',
title: 'Copy this value into Clipboard!',
},
});
});
describe('clipboard', () => {
it('should fire a `success` event on click', () => {
document.execCommand = jest.fn(() => true);
window.getSelection = jest.fn(() => ({
toString: jest.fn(() => 'test'),
removeAllRanges: jest.fn(),
}));
wrapper.trigger('click');
expect(wrapper.emitted().success).not.toBeEmpty();
expect(document.execCommand).toHaveBeenCalledWith('copy');
});
it("should propagate the clipboard error event if execCommand doesn't work", () => {
document.execCommand = jest.fn(() => false);
wrapper.trigger('click');
expect(wrapper.emitted().error).not.toBeEmpty();
expect(document.execCommand).toHaveBeenCalledWith('copy');
});
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment