Merge branch 'copy-button-in-modals' into 'master'

Add a New Copy Button That Works in Modals

Closes #60988

See merge request gitlab-org/gitlab-ce!28676
This commit is contained in:
Filipa Lacerda 2019-06-06 09:36:18 +00:00
commit e53f475e4a
4 changed files with 180 additions and 0 deletions

View File

@ -0,0 +1,121 @@
<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>

View File

@ -0,0 +1,5 @@
---
title: Add a New Copy Button That Works in Modals
merge_request: 28676
author:
type: added

View File

@ -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.

View File

@ -0,0 +1,40 @@
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');
});
});
});