Commit 3a67e397 authored by Peter Hegman's avatar Peter Hegman Committed by Mike Greiling

Update clipboard tooltip to say `Copied` after clicking

parent ff10dbff
import Clipboard from 'clipboard'; import ClipboardJS from 'clipboard';
import $ from 'jquery'; import $ from 'jquery';
import { sprintf, __ } from '~/locale';
import { fixTitle, add, show, once } from '~/tooltips'; import { parseBoolean } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
import { fixTitle, add, show, hide, once } from '~/tooltips';
const CLIPBOARD_SUCCESS_EVENT = 'clipboard-success';
const CLIPBOARD_ERROR_EVENT = 'clipboard-error';
const I18N_ERROR_MESSAGE = __('Copy failed. Please manually copy the value.');
function showTooltip(target, title) { function showTooltip(target, title) {
const { title: originalTitle } = target.dataset; const { title: originalTitle } = target.dataset;
...@@ -9,20 +15,31 @@ function showTooltip(target, title) { ...@@ -9,20 +15,31 @@ function showTooltip(target, title) {
once('hidden', (tooltip) => { once('hidden', (tooltip) => {
if (tooltip.target === target) { if (tooltip.target === target) {
target.setAttribute('title', originalTitle); target.setAttribute('title', originalTitle);
target.setAttribute('aria-label', originalTitle);
fixTitle(target); fixTitle(target);
} }
}); });
target.setAttribute('title', title); target.setAttribute('title', title);
target.setAttribute('aria-label', title);
fixTitle(target); fixTitle(target);
show(target); show(target);
setTimeout(() => target.blur(), 1000); setTimeout(() => {
hide(target);
}, 1000);
} }
function genericSuccess(e) { function genericSuccess(e) {
// Clear the selection and blur the trigger so it loses its border // Clear the selection
e.clearSelection(); e.clearSelection();
showTooltip(e.trigger, __('Copied')); e.trigger.focus();
e.trigger.dispatchEvent(new Event(CLIPBOARD_SUCCESS_EVENT));
const { clipboardHandleTooltip = true } = e.trigger.dataset;
if (parseBoolean(clipboardHandleTooltip)) {
// Update tooltip
showTooltip(e.trigger, __('Copied'));
}
} }
/** /**
...@@ -30,17 +47,16 @@ function genericSuccess(e) { ...@@ -30,17 +47,16 @@ function genericSuccess(e) {
* See http://clipboardjs.com/#browser-support * See http://clipboardjs.com/#browser-support
*/ */
function genericError(e) { function genericError(e) {
let key; e.trigger.dispatchEvent(new Event(CLIPBOARD_ERROR_EVENT));
if (/Mac/i.test(navigator.userAgent)) {
key = '⌘'; // Command const { clipboardHandleTooltip = true } = e.trigger.dataset;
} else { if (parseBoolean(clipboardHandleTooltip)) {
key = 'Ctrl'; showTooltip(e.trigger, I18N_ERROR_MESSAGE);
} }
showTooltip(e.trigger, sprintf(__(`Press %{key}-C to copy`), { key }));
} }
export default function initCopyToClipboard() { export default function initCopyToClipboard() {
const clipboard = new Clipboard('[data-clipboard-target], [data-clipboard-text]'); const clipboard = new ClipboardJS('[data-clipboard-target], [data-clipboard-text]');
clipboard.on('success', genericSuccess); clipboard.on('success', genericSuccess);
clipboard.on('error', genericError); clipboard.on('error', genericError);
...@@ -74,6 +90,8 @@ export default function initCopyToClipboard() { ...@@ -74,6 +90,8 @@ export default function initCopyToClipboard() {
clipboardData.setData('text/plain', json.text); clipboardData.setData('text/plain', json.text);
clipboardData.setData('text/x-gfm', json.gfm); clipboardData.setData('text/x-gfm', json.gfm);
}); });
return clipboard;
} }
/** /**
...@@ -89,3 +107,5 @@ export function clickCopyToClipboardButton(btnElement) { ...@@ -89,3 +107,5 @@ export function clickCopyToClipboardButton(btnElement) {
btnElement.click(); btnElement.click();
} }
export { CLIPBOARD_SUCCESS_EVENT, CLIPBOARD_ERROR_EVENT, I18N_ERROR_MESSAGE };
...@@ -13,9 +13,23 @@ ...@@ -13,9 +13,23 @@
* /> * />
*/ */
import { GlButton, GlTooltipDirective } from '@gitlab/ui'; import { GlButton, GlTooltipDirective } from '@gitlab/ui';
import { uniqueId } from 'lodash';
import { __ } from '~/locale';
import {
CLIPBOARD_SUCCESS_EVENT,
CLIPBOARD_ERROR_EVENT,
I18N_ERROR_MESSAGE,
} from '~/behaviors/copy_to_clipboard';
export default { export default {
name: 'ClipboardButton', name: 'ClipboardButton',
i18n: {
copied: __('Copied'),
error: I18N_ERROR_MESSAGE,
},
CLIPBOARD_SUCCESS_EVENT,
CLIPBOARD_ERROR_EVENT,
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
}, },
...@@ -72,6 +86,13 @@ export default { ...@@ -72,6 +86,13 @@ export default {
default: 'default', default: 'default',
}, },
}, },
data() {
return {
localTitle: this.title,
titleTimeout: null,
id: null,
};
},
computed: { computed: {
clipboardText() { clipboardText() {
if (this.gfm !== null) { if (this.gfm !== null) {
...@@ -79,25 +100,50 @@ export default { ...@@ -79,25 +100,50 @@ export default {
} }
return this.text; return this.text;
}, },
tooltipDirectiveOptions() {
return {
placement: this.tooltipPlacement,
container: this.tooltipContainer,
boundary: this.tooltipBoundary,
};
},
},
created() {
this.id = uniqueId('clipboard-button-');
},
methods: {
updateTooltip(title) {
this.localTitle = title;
this.$root.$emit('bv::show::tooltip', this.id);
clearTimeout(this.titleTimeout);
this.titleTimeout = setTimeout(() => {
this.localTitle = this.title;
this.$root.$emit('bv::hide::tooltip', this.id);
}, 1000);
},
}, },
}; };
</script> </script>
<template> <template>
<gl-button <gl-button
v-gl-tooltip.hover.blur.viewport="{ :id="id"
placement: tooltipPlacement, ref="copyButton"
container: tooltipContainer, v-gl-tooltip.hover.focus.click.viewport="tooltipDirectiveOptions"
boundary: tooltipBoundary,
}"
:class="cssClass" :class="cssClass"
:title="title" :title="localTitle"
:data-clipboard-text="clipboardText" :data-clipboard-text="clipboardText"
data-clipboard-handle-tooltip="false"
:category="category" :category="category"
:size="size" :size="size"
icon="copy-to-clipboard" icon="copy-to-clipboard"
:aria-label="__('Copy this value')"
:variant="variant" :variant="variant"
:aria-label="localTitle"
aria-live="polite"
@[$options.CLIPBOARD_SUCCESS_EVENT]="updateTooltip($options.i18n.copied)"
@[$options.CLIPBOARD_ERROR_EVENT]="updateTooltip($options.i18n.error)"
v-on="$listeners" v-on="$listeners"
> >
<slot></slot> <slot></slot>
......
<script> <script>
import { GlButton, GlTooltipDirective } from '@gitlab/ui'; import { GlButton, GlTooltipDirective } from '@gitlab/ui';
import Clipboard from 'clipboard'; import ClipboardJS from 'clipboard';
import { uniqueId } from 'lodash'; import { uniqueId } from 'lodash';
import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants'; import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
...@@ -69,7 +69,7 @@ export default { ...@@ -69,7 +69,7 @@ export default {
}, },
mounted() { mounted() {
this.$nextTick(() => { this.$nextTick(() => {
this.clipboard = new Clipboard(this.$el, { this.clipboard = new ClipboardJS(this.$el, {
container: container:
document.querySelector(`${this.modalDomId} div.modal-content`) || document.querySelector(`${this.modalDomId} div.modal-content`) ||
document.getElementById(this.container) || document.getElementById(this.container) ||
......
...@@ -50,7 +50,7 @@ module ButtonHelper ...@@ -50,7 +50,7 @@ module ButtonHelper
data: data, data: data,
type: :button, type: :button,
title: title, title: title,
aria: { label: title }, aria: { label: title, live: 'polite' },
itemprop: item_prop itemprop: item_prop
} }
......
<script> <script>
import { GlModal, GlSprintf, GlLink } from '@gitlab/ui'; import { GlModal, GlSprintf, GlLink } from '@gitlab/ui';
import Clipboard from 'clipboard'; import ClipboardJS from 'clipboard';
import { getBaseURL, setUrlParams, redirectTo } from '~/lib/utils/url_utility'; import { getBaseURL, setUrlParams, redirectTo } from '~/lib/utils/url_utility';
import { sprintf, s__, __ } from '~/locale'; import { sprintf, s__, __ } from '~/locale';
import { CODE_SNIPPET_SOURCE_URL_PARAM } from '~/pipeline_editor/components/code_snippet_alert/constants'; import { CODE_SNIPPET_SOURCE_URL_PARAM } from '~/pipeline_editor/components/code_snippet_alert/constants';
...@@ -74,7 +74,7 @@ export default { ...@@ -74,7 +74,7 @@ export default {
}, },
copySnippet(andRedirect = true) { copySnippet(andRedirect = true) {
const id = andRedirect ? 'copy-yaml-snippet-and-edit-button' : 'copy-yaml-snippet-button'; const id = andRedirect ? 'copy-yaml-snippet-and-edit-button' : 'copy-yaml-snippet-button';
const clipboard = new Clipboard(`#${id}`, { const clipboard = new ClipboardJS(`#${id}`, {
text: () => this.yaml, text: () => this.yaml,
}); });
clipboard.on('success', () => { clipboard.on('success', () => {
......
import { GlModal, GlSprintf } from '@gitlab/ui'; import { GlModal, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import Clipboard from 'clipboard'; import ClipboardJS from 'clipboard';
import { merge } from 'lodash'; import { merge } from 'lodash';
import ConfigurationSnippetModal from 'ee/security_configuration/components/configuration_snippet_modal.vue'; import ConfigurationSnippetModal from 'ee/security_configuration/components/configuration_snippet_modal.vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import { extendedWrapper } from 'helpers/vue_test_utils_helper';
...@@ -87,7 +87,7 @@ describe('EE - SecurityConfigurationSnippetModal', () => { ...@@ -87,7 +87,7 @@ describe('EE - SecurityConfigurationSnippetModal', () => {
it('on primary event, text is copied to the clipbard and user is redirected to CI editor', async () => { it('on primary event, text is copied to the clipbard and user is redirected to CI editor', async () => {
findModal().vm.$emit('primary'); findModal().vm.$emit('primary');
expect(Clipboard).toHaveBeenCalledWith('#copy-yaml-snippet-and-edit-button', { expect(ClipboardJS).toHaveBeenCalledWith('#copy-yaml-snippet-and-edit-button', {
text: expect.any(Function), text: expect.any(Function),
}); });
expect(redirectTo).toHaveBeenCalledWith( expect(redirectTo).toHaveBeenCalledWith(
...@@ -98,7 +98,7 @@ describe('EE - SecurityConfigurationSnippetModal', () => { ...@@ -98,7 +98,7 @@ describe('EE - SecurityConfigurationSnippetModal', () => {
it('on secondary event, text is copied to the clipbard', async () => { it('on secondary event, text is copied to the clipbard', async () => {
findModal().vm.$emit('secondary'); findModal().vm.$emit('secondary');
expect(Clipboard).toHaveBeenCalledWith('#copy-yaml-snippet-button', { expect(ClipboardJS).toHaveBeenCalledWith('#copy-yaml-snippet-button', {
text: expect.any(Function), text: expect.any(Function),
}); });
}); });
......
...@@ -9673,6 +9673,9 @@ msgstr "" ...@@ -9673,6 +9673,9 @@ msgstr ""
msgid "Copy evidence SHA" msgid "Copy evidence SHA"
msgstr "" msgstr ""
msgid "Copy failed. Please manually copy the value."
msgstr ""
msgid "Copy file contents" msgid "Copy file contents"
msgstr "" msgstr ""
...@@ -9712,9 +9715,6 @@ msgstr "" ...@@ -9712,9 +9715,6 @@ msgstr ""
msgid "Copy this registration token." msgid "Copy this registration token."
msgstr "" msgstr ""
msgid "Copy this value"
msgstr ""
msgid "Copy to clipboard" msgid "Copy to clipboard"
msgstr "" msgstr ""
...@@ -26665,9 +26665,6 @@ msgstr "" ...@@ -26665,9 +26665,6 @@ msgstr ""
msgid "Preferences|Use relative times" msgid "Preferences|Use relative times"
msgstr "" msgstr ""
msgid "Press %{key}-C to copy"
msgstr ""
msgid "Prev" msgid "Prev"
msgstr "" msgstr ""
......
import initCopyToClipboard, {
CLIPBOARD_SUCCESS_EVENT,
CLIPBOARD_ERROR_EVENT,
I18N_ERROR_MESSAGE,
} from '~/behaviors/copy_to_clipboard';
import { show, hide, fixTitle, once } from '~/tooltips';
let onceCallback = () => {};
jest.mock('~/tooltips', () => ({
show: jest.fn(),
hide: jest.fn(),
fixTitle: jest.fn(),
once: jest.fn((event, callback) => {
onceCallback = callback;
}),
}));
describe('initCopyToClipboard', () => {
let clearSelection;
let focusSpy;
let dispatchEventSpy;
let button;
let clipboardInstance;
afterEach(() => {
document.body.innerHTML = '';
clipboardInstance = null;
});
const title = 'Copy this value';
const defaultButtonAttributes = {
'data-clipboard-text': 'foo bar',
title,
'data-title': title,
};
const createButton = (attributes = {}) => {
const combinedAttributes = { ...defaultButtonAttributes, ...attributes };
button = document.createElement('button');
Object.keys(combinedAttributes).forEach((attributeName) => {
button.setAttribute(attributeName, combinedAttributes[attributeName]);
});
document.body.appendChild(button);
};
const init = () => {
clipboardInstance = initCopyToClipboard();
};
const setupSpies = () => {
clearSelection = jest.fn();
focusSpy = jest.spyOn(button, 'focus');
dispatchEventSpy = jest.spyOn(button, 'dispatchEvent');
};
const emitSuccessEvent = () => {
clipboardInstance.emit('success', {
action: 'copy',
text: 'foo bar',
trigger: button,
clearSelection,
});
};
const emitErrorEvent = () => {
clipboardInstance.emit('error', {
action: 'copy',
text: 'foo bar',
trigger: button,
clearSelection,
});
};
const itHandlesTooltip = (expectedTooltip) => {
it('handles tooltip', () => {
expect(button.getAttribute('title')).toBe(expectedTooltip);
expect(button.getAttribute('aria-label')).toBe(expectedTooltip);
expect(fixTitle).toHaveBeenCalledWith(button);
expect(show).toHaveBeenCalledWith(button);
expect(once).toHaveBeenCalledWith('hidden', expect.any(Function));
expect(hide).not.toHaveBeenCalled();
jest.runAllTimers();
expect(hide).toHaveBeenCalled();
onceCallback({ target: button });
expect(button.getAttribute('title')).toBe(title);
expect(button.getAttribute('aria-label')).toBe(title);
expect(fixTitle).toHaveBeenCalledWith(button);
});
};
describe('when value is successfully copied', () => {
it(`calls clearSelection, focuses the button, and dispatches ${CLIPBOARD_SUCCESS_EVENT} event`, () => {
createButton();
init();
setupSpies();
emitSuccessEvent();
expect(clearSelection).toHaveBeenCalled();
expect(focusSpy).toHaveBeenCalled();
expect(dispatchEventSpy).toHaveBeenCalledWith(new Event(CLIPBOARD_SUCCESS_EVENT));
});
describe('when `data-clipboard-handle-tooltip` is set to `false`', () => {
beforeEach(() => {
createButton({
'data-clipboard-handle-tooltip': 'false',
});
init();
emitSuccessEvent();
});
it('does not handle success tooltip', () => {
expect(show).not.toHaveBeenCalled();
});
});
describe('when `data-clipboard-handle-tooltip` is set to `true`', () => {
beforeEach(() => {
createButton({
'data-clipboard-handle-tooltip': 'true',
});
init();
emitSuccessEvent();
});
itHandlesTooltip('Copied');
});
describe('when `data-clipboard-handle-tooltip` is not set', () => {
beforeEach(() => {
createButton();
init();
emitSuccessEvent();
});
itHandlesTooltip('Copied');
});
});
describe('when there is an error copying the value', () => {
it(`dispatches ${CLIPBOARD_ERROR_EVENT} event`, () => {
createButton();
init();
setupSpies();
emitErrorEvent();
expect(dispatchEventSpy).toHaveBeenCalledWith(new Event(CLIPBOARD_ERROR_EVENT));
});
describe('when `data-clipboard-handle-tooltip` is set to `false`', () => {
beforeEach(() => {
createButton({
'data-clipboard-handle-tooltip': 'false',
});
init();
emitErrorEvent();
});
it('does not handle error tooltip', () => {
expect(show).not.toHaveBeenCalled();
});
});
describe('when `data-clipboard-handle-tooltip` is set to `true`', () => {
beforeEach(() => {
createButton({
'data-clipboard-handle-tooltip': 'true',
});
init();
emitErrorEvent();
});
itHandlesTooltip(I18N_ERROR_MESSAGE);
});
describe('when `data-clipboard-handle-tooltip` is not set', () => {
beforeEach(() => {
createButton();
init();
emitErrorEvent();
});
itHandlesTooltip(I18N_ERROR_MESSAGE);
});
});
});
...@@ -15,11 +15,14 @@ exports[`FileSha renders 1`] = ` ...@@ -15,11 +15,14 @@ exports[`FileSha renders 1`] = `
foo foo
<gl-button-stub <gl-button-stub
aria-label="Copy this value" aria-label="Copy SHA"
aria-live="polite"
buttontextclasses="" buttontextclasses=""
category="tertiary" category="tertiary"
data-clipboard-handle-tooltip="false"
data-clipboard-text="foo" data-clipboard-text="foo"
icon="copy-to-clipboard" icon="copy-to-clipboard"
id="clipboard-button-1"
size="small" size="small"
title="Copy SHA" title="Copy SHA"
variant="default" variant="default"
......
...@@ -4,6 +4,8 @@ import FileSha from '~/packages_and_registries/infrastructure_registry/details/c ...@@ -4,6 +4,8 @@ import FileSha from '~/packages_and_registries/infrastructure_registry/details/c
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import DetailsRow from '~/vue_shared/components/registry/details_row.vue'; import DetailsRow from '~/vue_shared/components/registry/details_row.vue';
jest.mock('lodash/uniqueId', () => (prefix) => (prefix ? `${prefix}1` : 1));
describe('FileSha', () => { describe('FileSha', () => {
let wrapper; let wrapper;
......
...@@ -15,11 +15,14 @@ exports[`FileSha renders 1`] = ` ...@@ -15,11 +15,14 @@ exports[`FileSha renders 1`] = `
foo foo
<gl-button-stub <gl-button-stub
aria-label="Copy this value" aria-label="Copy SHA"
aria-live="polite"
buttontextclasses="" buttontextclasses=""
category="tertiary" category="tertiary"
data-clipboard-handle-tooltip="false"
data-clipboard-text="foo" data-clipboard-text="foo"
icon="copy-to-clipboard" icon="copy-to-clipboard"
id="clipboard-button-1"
size="small" size="small"
title="Copy SHA" title="Copy SHA"
variant="default" variant="default"
......
...@@ -4,6 +4,8 @@ import FileSha from '~/packages_and_registries/package_registry/components/detai ...@@ -4,6 +4,8 @@ import FileSha from '~/packages_and_registries/package_registry/components/detai
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import DetailsRow from '~/vue_shared/components/registry/details_row.vue'; import DetailsRow from '~/vue_shared/components/registry/details_row.vue';
jest.mock('lodash/uniqueId', () => (prefix) => (prefix ? `${prefix}1` : 1));
describe('FileSha', () => { describe('FileSha', () => {
let wrapper; let wrapper;
......
import { GlButton } from '@gitlab/ui'; import { GlButton } from '@gitlab/ui';
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import initCopyToClipboard from '~/behaviors/copy_to_clipboard'; import { nextTick } from 'vue';
import initCopyToClipboard, {
CLIPBOARD_SUCCESS_EVENT,
CLIPBOARD_ERROR_EVENT,
I18N_ERROR_MESSAGE,
} from '~/behaviors/copy_to_clipboard';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
jest.mock('lodash/uniqueId', () => (prefix) => (prefix ? `${prefix}1` : 1));
describe('clipboard button', () => { describe('clipboard button', () => {
let wrapper; let wrapper;
...@@ -15,6 +23,42 @@ describe('clipboard button', () => { ...@@ -15,6 +23,42 @@ describe('clipboard button', () => {
const findButton = () => wrapper.find(GlButton); const findButton = () => wrapper.find(GlButton);
const expectConfirmationTooltip = async ({ event, message }) => {
const title = 'Copy this value';
createWrapper({
text: 'copy me',
title,
});
wrapper.vm.$root.$emit = jest.fn();
const button = findButton();
expect(button.attributes()).toMatchObject({
title,
'aria-label': title,
});
await button.trigger(event);
expect(wrapper.vm.$root.$emit).toHaveBeenCalledWith('bv::show::tooltip', 'clipboard-button-1');
expect(button.attributes()).toMatchObject({
title: message,
'aria-label': message,
});
jest.runAllTimers();
await nextTick();
expect(button.attributes()).toMatchObject({
title,
'aria-label': title,
});
expect(wrapper.vm.$root.$emit).toHaveBeenCalledWith('bv::hide::tooltip', 'clipboard-button-1');
};
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
wrapper = null; wrapper = null;
...@@ -99,6 +143,32 @@ describe('clipboard button', () => { ...@@ -99,6 +143,32 @@ describe('clipboard button', () => {
expect(findButton().props('variant')).toBe(variant); expect(findButton().props('variant')).toBe(variant);
}); });
describe('confirmation tooltip', () => {
it('adds `id` and `data-clipboard-handle-tooltip` attributes to button', () => {
createWrapper({
text: 'copy me',
title: 'Copy this value',
});
expect(findButton().attributes()).toMatchObject({
id: 'clipboard-button-1',
'data-clipboard-handle-tooltip': 'false',
'aria-live': 'polite',
});
});
it('shows success tooltip after successful copy', () => {
expectConfirmationTooltip({
event: CLIPBOARD_SUCCESS_EVENT,
message: ClipboardButton.i18n.copied,
});
});
it('shows error tooltip after failed copy', () => {
expectConfirmationTooltip({ event: CLIPBOARD_ERROR_EVENT, message: I18N_ERROR_MESSAGE });
});
});
describe('integration', () => { describe('integration', () => {
it('actually copies to clipboard', () => { it('actually copies to clipboard', () => {
initCopyToClipboard(); initCopyToClipboard();
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
exports[`Package code instruction multiline to match the snapshot 1`] = ` exports[`Package code instruction multiline to match the snapshot 1`] = `
<div> <div>
<label <label
for="instruction-input_3" for="instruction-input_1"
> >
foo_label foo_label
</label> </label>
...@@ -23,7 +23,7 @@ multiline text ...@@ -23,7 +23,7 @@ multiline text
exports[`Package code instruction single line to match the default snapshot 1`] = ` exports[`Package code instruction single line to match the default snapshot 1`] = `
<div> <div>
<label <label
for="instruction-input_2" for="instruction-input_1"
> >
foo_label foo_label
</label> </label>
...@@ -37,7 +37,7 @@ exports[`Package code instruction single line to match the default snapshot 1`] ...@@ -37,7 +37,7 @@ exports[`Package code instruction single line to match the default snapshot 1`]
<input <input
class="form-control gl-font-monospace" class="form-control gl-font-monospace"
data-testid="instruction-input" data-testid="instruction-input"
id="instruction-input_2" id="instruction-input_1"
readonly="readonly" readonly="readonly"
type="text" type="text"
/> />
...@@ -47,9 +47,12 @@ exports[`Package code instruction single line to match the default snapshot 1`] ...@@ -47,9 +47,12 @@ exports[`Package code instruction single line to match the default snapshot 1`]
data-testid="instruction-button" data-testid="instruction-button"
> >
<button <button
aria-label="Copy this value" aria-label="Copy npm install command"
aria-live="polite"
class="btn input-group-text btn-default btn-md gl-button btn-default-secondary btn-icon" class="btn input-group-text btn-default btn-md gl-button btn-default-secondary btn-icon"
data-clipboard-handle-tooltip="false"
data-clipboard-text="npm i @my-package" data-clipboard-text="npm i @my-package"
id="clipboard-button-1"
title="Copy npm install command" title="Copy npm install command"
type="button" type="button"
> >
......
...@@ -3,6 +3,8 @@ import Tracking from '~/tracking'; ...@@ -3,6 +3,8 @@ import Tracking from '~/tracking';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import CodeInstruction from '~/vue_shared/components/registry/code_instruction.vue'; import CodeInstruction from '~/vue_shared/components/registry/code_instruction.vue';
jest.mock('lodash/uniqueId', () => (prefix) => (prefix ? `${prefix}1` : 1));
describe('Package code instruction', () => { describe('Package code instruction', () => {
let wrapper; let wrapper;
......
...@@ -167,6 +167,7 @@ RSpec.describe ButtonHelper do ...@@ -167,6 +167,7 @@ RSpec.describe ButtonHelper do
expect(element.attr('class')).to eq('btn btn-clipboard btn-transparent') expect(element.attr('class')).to eq('btn btn-clipboard btn-transparent')
expect(element.attr('type')).to eq('button') expect(element.attr('type')).to eq('button')
expect(element.attr('aria-label')).to eq('Copy') expect(element.attr('aria-label')).to eq('Copy')
expect(element.attr('aria-live')).to eq('polite')
expect(element.attr('data-toggle')).to eq('tooltip') expect(element.attr('data-toggle')).to eq('tooltip')
expect(element.attr('data-placement')).to eq('bottom') expect(element.attr('data-placement')).to eq('bottom')
expect(element.attr('data-container')).to eq('body') expect(element.attr('data-container')).to eq('body')
......
...@@ -3435,19 +3435,10 @@ cli-boxes@^2.2.0: ...@@ -3435,19 +3435,10 @@ cli-boxes@^2.2.0:
resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-2.2.0.tgz#538ecae8f9c6ca508e3c3c95b453fe93cb4c168d" resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-2.2.0.tgz#538ecae8f9c6ca508e3c3c95b453fe93cb4c168d"
integrity sha512-gpaBrMAizVEANOpfZp/EEUixTXDyGt7DFzdK5hU+UbWt/J0lB0w20ncZj59Z9a93xHb9u12zF5BS6i9RKbtg4w== integrity sha512-gpaBrMAizVEANOpfZp/EEUixTXDyGt7DFzdK5hU+UbWt/J0lB0w20ncZj59Z9a93xHb9u12zF5BS6i9RKbtg4w==
clipboard@^1.7.1: clipboard@^2.0.0, clipboard@^2.0.8:
version "1.7.1" version "2.0.8"
resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-1.7.1.tgz#360d6d6946e99a7a1fef395e42ba92b5e9b5a16b" resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-2.0.8.tgz#ffc6c103dd2967a83005f3f61976aa4655a4cdba"
integrity sha1-Ng1taUbpmnof7zleQrqStem1oWs= integrity sha512-Y6WO0unAIQp5bLmk1zdThRhgJt/x3ks6f30s3oE3H1mgIEU33XyQjEf8gsf6DxC7NPX8Y1SsNWjUjL/ywLnnbQ==
dependencies:
good-listener "^1.2.2"
select "^1.1.2"
tiny-emitter "^2.0.0"
clipboard@^2.0.0:
version "2.0.6"
resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-2.0.6.tgz#52921296eec0fdf77ead1749421b21c968647376"
integrity sha512-g5zbiixBRk/wyKakSwCKd7vQXDjFnAMGHoEyBogG/bw9kTD9GvdAvaoRR1ALcEzt3pVKxZR0pViekPMIS0QyGg==
dependencies: dependencies:
good-listener "^1.2.2" good-listener "^1.2.2"
select "^1.1.2" select "^1.1.2"
......
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