@gitlab/ui 68.6.0 → 68.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Do not edit directly
3
- * Generated on Wed, 15 Nov 2023 08:05:48 GMT
3
+ * Generated on Wed, 15 Nov 2023 12:15:36 GMT
4
4
  */
5
5
 
6
6
  :root {
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Do not edit directly
3
- * Generated on Wed, 15 Nov 2023 08:05:48 GMT
3
+ * Generated on Wed, 15 Nov 2023 12:15:36 GMT
4
4
  */
5
5
 
6
6
  :root.gl-dark {
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Do not edit directly
3
- * Generated on Wed, 15 Nov 2023 08:05:48 GMT
3
+ * Generated on Wed, 15 Nov 2023 12:15:36 GMT
4
4
  */
5
5
 
6
6
  export const BLACK = "#fff";
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Do not edit directly
3
- * Generated on Wed, 15 Nov 2023 08:05:48 GMT
3
+ * Generated on Wed, 15 Nov 2023 12:15:36 GMT
4
4
  */
5
5
 
6
6
  export const DATA_VIZ_GREEN_50 = "#ddfab7";
@@ -1,6 +1,6 @@
1
1
 
2
2
  // Do not edit directly
3
- // Generated on Wed, 15 Nov 2023 08:05:48 GMT
3
+ // Generated on Wed, 15 Nov 2023 12:15:36 GMT
4
4
 
5
5
  $red-950: #fff4f3;
6
6
  $red-900: #fcf1ef;
@@ -1,6 +1,6 @@
1
1
 
2
2
  // Do not edit directly
3
- // Generated on Wed, 15 Nov 2023 08:05:48 GMT
3
+ // Generated on Wed, 15 Nov 2023 12:15:36 GMT
4
4
 
5
5
  $gl-line-height-52: 3.25rem;
6
6
  $gl-line-height-44: 2.75rem;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gitlab/ui",
3
- "version": "68.6.0",
3
+ "version": "68.7.0",
4
4
  "description": "GitLab UI Components",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -0,0 +1,44 @@
1
+ import iconsPath from '@gitlab/svgs/dist/icons.svg';
2
+
3
+ const createButton = () => {
4
+ const button = document.createElement('button');
5
+ button.type = 'button';
6
+ button.classList.add(
7
+ 'btn',
8
+ 'btn-default',
9
+ 'btn-md',
10
+ 'gl-button',
11
+ 'btn-default-secondary',
12
+ 'btn-icon'
13
+ );
14
+ button.dataset.title = 'Copy to clipboard';
15
+
16
+ // Create an SVG element with the correct namespace
17
+ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
18
+ svg.setAttribute('role', 'img');
19
+ svg.setAttribute('aria-hidden', 'true');
20
+ svg.classList.add('gl-button-icon', 'gl-icon', 's16');
21
+
22
+ // Create a 'use' element with the correct namespace
23
+ const use = document.createElementNS('http://www.w3.org/2000/svg', 'use');
24
+ use.setAttribute('href', `${iconsPath}#copy-to-clipboard`);
25
+
26
+ svg.appendChild(use);
27
+ button.appendChild(svg);
28
+
29
+ return button;
30
+ };
31
+
32
+ export class CopyCodeElement extends HTMLElement {
33
+ constructor() {
34
+ super();
35
+ const btn = createButton();
36
+ const wrapper = this.parentNode;
37
+
38
+ this.appendChild(btn);
39
+ btn.addEventListener('click', async () => {
40
+ const textToCopy = wrapper.innerText;
41
+ await navigator.clipboard.writeText(textToCopy);
42
+ });
43
+ }
44
+ }
@@ -0,0 +1,64 @@
1
+ import { CopyCodeElement } from './copy_code_element';
2
+
3
+ describe('copy-code element', () => {
4
+ customElements.define('copy-code', CopyCodeElement);
5
+ const code = 'function sum(a, b) {\n return a + b;\n}';
6
+ const findCustomElement = () => document.querySelector('copy-code');
7
+ const findButton = () => document.querySelector('copy-code button');
8
+ const findButtonIcon = () => document.querySelector('copy-code button svg use');
9
+
10
+ beforeEach(() => {
11
+ document.body.innerHTML = `<div><pre><code>${code}</code></pre><copy-code></copy-code></div>`;
12
+ });
13
+
14
+ it('should create a button', () => {
15
+ expect(customElements.get('copy-code')).toBeDefined();
16
+ });
17
+
18
+ it('does not setup shadowDom on the custom element', () => {
19
+ expect(findCustomElement().shadowRoot).toBeNull();
20
+ });
21
+
22
+ it('adds a button to the DOM as a direct child', () => {
23
+ expect(findButton()).toBeDefined();
24
+ });
25
+
26
+ it('adds the correct icon to the button', () => {
27
+ expect(findButtonIcon().getAttribute('href')).toContain('#copy-to-clipboard');
28
+ });
29
+
30
+ describe('interaction', () => {
31
+ let copiedText = '';
32
+
33
+ beforeEach(() => {
34
+ Object.defineProperty(HTMLElement.prototype, 'innerText', {
35
+ get() {
36
+ return this.textContent.trim();
37
+ },
38
+ configurable: true,
39
+ });
40
+
41
+ global.navigator.clipboard = {
42
+ writeText: jest.fn().mockImplementation((text) => {
43
+ copiedText = text;
44
+ return Promise.resolve();
45
+ }),
46
+ readText: jest.fn().mockImplementation(() => Promise.resolve(copiedText)),
47
+ };
48
+ });
49
+
50
+ afterEach(() => {
51
+ // In JSDOM, `innerText` doesn't exist on the prototype.
52
+ // However, we can not set it to `undefined` as the property description should be an object
53
+ Object.defineProperty(HTMLElement.prototype, 'innerText', {});
54
+
55
+ jest.resetAllMocks();
56
+ });
57
+
58
+ it('copies the content of the parentNode to the clipboard when the button is clicked', async () => {
59
+ findButton().click();
60
+ const text = await navigator.clipboard.readText();
61
+ expect(text).toBe(code);
62
+ });
63
+ });
64
+ });
@@ -15,4 +15,19 @@
15
15
  p:last-of-type {
16
16
  @include gl-mb-0;
17
17
  }
18
+
19
+ copy-code {
20
+ @include gl-absolute;
21
+ @include gl-transition-medium;
22
+ @include gl-opacity-0;
23
+ @include gl-right-4;
24
+ @include gl-top-3;
25
+ }
26
+
27
+ .js-markdown-code.markdown-code-block:hover {
28
+ copy-code,
29
+ copy-code:focus-within {
30
+ @include gl-opacity-10;
31
+ }
32
+ }
18
33
  }
@@ -47,6 +47,12 @@ describe('DuoChatMessage', () => {
47
47
  jest.clearAllMocks();
48
48
  });
49
49
 
50
+ it('registers the custom `copy-code` element', () => {
51
+ expect(customElements.get('copy-code')).toBeUndefined();
52
+ createComponent();
53
+ expect(customElements.get('copy-code')).toBeDefined();
54
+ });
55
+
50
56
  describe('rendering', () => {
51
57
  beforeEach(() => {
52
58
  renderMarkdown.mockImplementation(() => mockMarkdownContent);
@@ -79,6 +85,10 @@ describe('DuoChatMessage', () => {
79
85
  });
80
86
  });
81
87
 
88
+ it('renders the `copy-code` button for the code snippet', () => {
89
+ expect(findCopyCodeButton().exists()).toBe(true);
90
+ });
91
+
82
92
  it('renders the documentation sources component by default', () => {
83
93
  expect(findDocumentSources().exists()).toBe(true);
84
94
  expect(findDocumentSources().props('sources')).toEqual(MOCK_RESPONSE_MESSAGE.extras.sources);
@@ -107,10 +117,6 @@ describe('DuoChatMessage', () => {
107
117
  findUserFeedback().vm.$emit('feedback', 'foo');
108
118
  expect(wrapper.emitted('track-feedback')).toEqual([['foo']]);
109
119
  });
110
-
111
- it('does not strip out the <copy-code/> element from HTML output', () => {
112
- expect(findCopyCodeButton().exists()).toBe(true);
113
- });
114
120
  });
115
121
 
116
122
  describe('message output', () => {
@@ -3,6 +3,7 @@ import GlDuoUserFeedback from '../../../user_feedback/user_feedback.vue';
3
3
  import { SafeHtmlDirective as SafeHtml } from '../../../../../../directives/safe_html/safe_html';
4
4
  import { MESSAGE_MODEL_ROLES } from '../../constants';
5
5
  import DocumentationSources from '../duo_chat_message_sources/duo_chat_message_sources.vue';
6
+ import { CopyCodeElement } from './copy_code_element';
6
7
 
7
8
  const concatIndicesUntilEmpty = (arr) => {
8
9
  const start = arr.findIndex((el) => el);
@@ -66,6 +67,9 @@ export default {
66
67
  * Is intentionally non-reactive
67
68
  */
68
69
  this.messageChunks = [];
70
+ if (!customElements.get('copy-code')) {
71
+ customElements.define('copy-code', CopyCodeElement);
72
+ }
69
73
  },
70
74
  mounted() {
71
75
  this.messageContent = this.content;