@gitlab/ui 79.1.1 → 79.2.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, 24 Apr 2024 14:36:20 GMT
3
+ * Generated on Wed, 24 Apr 2024 16:36:49 GMT
4
4
  */
5
5
 
6
6
  :root {
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Do not edit directly
3
- * Generated on Wed, 24 Apr 2024 14:36:21 GMT
3
+ * Generated on Wed, 24 Apr 2024 16:36:49 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, 24 Apr 2024 14:36:21 GMT
3
+ * Generated on Wed, 24 Apr 2024 16:36:49 GMT
4
4
  */
5
5
 
6
6
  export const DATA_VIZ_GREEN_50 = "#133a03";
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Do not edit directly
3
- * Generated on Wed, 24 Apr 2024 14:36:20 GMT
3
+ * Generated on Wed, 24 Apr 2024 16:36:49 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, 24 Apr 2024 14:36:21 GMT
3
+ // Generated on Wed, 24 Apr 2024 16:36:49 GMT
4
4
 
5
5
  $gl-text-tertiary: #737278 !default;
6
6
  $gl-text-secondary: #89888d !default;
@@ -1,6 +1,6 @@
1
1
 
2
2
  // Do not edit directly
3
- // Generated on Wed, 24 Apr 2024 14:36:20 GMT
3
+ // Generated on Wed, 24 Apr 2024 16:36:49 GMT
4
4
 
5
5
  $gl-text-tertiary: #89888d !default;
6
6
  $gl-text-secondary: #737278 !default;
@@ -194,4 +194,22 @@ function filterVisible(els) {
194
194
  return (els || []).filter(el => isVisible(el));
195
195
  }
196
196
 
197
- export { colorFromBackground, debounceByAnimationFrame, filterVisible, focusFirstFocusableElement, getColorContrast, hexToRgba, isDev, isElementFocusable, isElementTabbable, logWarning, relativeLuminance, rgbFromHex, rgbFromString, stopEvent, throttle, toSrgb, uid };
197
+ /**
198
+ * Given an element, returns a Rect object
199
+ * with top and bottom boundaries removed.
200
+ */
201
+ function getHorizontalBoundingClientRect(el) {
202
+ const rect = el === null || el === void 0 ? void 0 : el.getBoundingClientRect();
203
+ if (rect) {
204
+ return {
205
+ x: rect.x,
206
+ width: rect.width,
207
+ y: 0,
208
+ // top of the document
209
+ height: document.documentElement.clientHeight // bottom of the document
210
+ };
211
+ }
212
+ return null;
213
+ }
214
+
215
+ export { colorFromBackground, debounceByAnimationFrame, filterVisible, focusFirstFocusableElement, getColorContrast, getHorizontalBoundingClientRect, hexToRgba, isDev, isElementFocusable, isElementTabbable, logWarning, relativeLuminance, rgbFromHex, rgbFromString, stopEvent, throttle, toSrgb, uid };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gitlab/ui",
3
- "version": "79.1.1",
3
+ "version": "79.2.0",
4
4
  "description": "GitLab UI Components",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -116,15 +116,16 @@ describe('base dropdown', () => {
116
116
  strategy: 'absolute',
117
117
  middleware: [
118
118
  offset({ mainAxis: DEFAULT_OFFSET }),
119
- autoPlacement({
120
- alignment: 'start',
121
- boundary: document.querySelector('main'),
122
- allowedPlacements: ['bottom-start', 'top-start', 'bottom-end', 'top-end'],
123
- }),
119
+ autoPlacement(expect.any(Function)),
124
120
  shift(),
125
121
  ],
126
122
  }
127
123
  );
124
+ expect(autoPlacement.mock.calls[0][0]()).toEqual({
125
+ alignment: 'start',
126
+ boundary: { x: 0, y: 0, width: 0, height: 0 },
127
+ allowedPlacements: ['bottom-start', 'top-start', 'bottom-end', 'top-end'],
128
+ });
128
129
 
129
130
  document.body.innerHTML = '';
130
131
  });
@@ -141,15 +142,16 @@ describe('base dropdown', () => {
141
142
  strategy: 'absolute',
142
143
  middleware: [
143
144
  offset({ mainAxis: DEFAULT_OFFSET }),
144
- autoPlacement({
145
- alignment: 'start',
146
- boundary: 'clippingAncestors',
147
- allowedPlacements: ['bottom-start', 'top-start', 'bottom-end', 'top-end'],
148
- }),
145
+ autoPlacement(expect.any(Function)),
149
146
  shift(),
150
147
  ],
151
148
  }
152
149
  );
150
+ expect(autoPlacement.mock.calls[0][0]()).toEqual({
151
+ alignment: 'start',
152
+ boundary: 'clippingAncestors',
153
+ allowedPlacements: ['bottom-start', 'top-start', 'bottom-end', 'top-end'],
154
+ });
153
155
  });
154
156
 
155
157
  it('initializes Floating UI with reference and floating elements and config for center-aligned menu', async () => {
@@ -164,15 +166,16 @@ describe('base dropdown', () => {
164
166
  strategy: 'absolute',
165
167
  middleware: [
166
168
  offset({ mainAxis: DEFAULT_OFFSET }),
167
- autoPlacement({
168
- alignment: undefined,
169
- boundary: 'clippingAncestors',
170
- allowedPlacements: ['bottom', 'top'],
171
- }),
169
+ autoPlacement(expect.any(Function)),
172
170
  shift(),
173
171
  ],
174
172
  }
175
173
  );
174
+ expect(autoPlacement.mock.calls[0][0]()).toEqual({
175
+ alignment: undefined,
176
+ boundary: 'clippingAncestors',
177
+ allowedPlacements: ['bottom', 'top'],
178
+ });
176
179
  });
177
180
 
178
181
  it('initializes Floating UI with reference and floating elements and config for right-aligned menu', async () => {
@@ -187,15 +190,16 @@ describe('base dropdown', () => {
187
190
  strategy: 'absolute',
188
191
  middleware: [
189
192
  offset({ mainAxis: DEFAULT_OFFSET }),
190
- autoPlacement({
191
- alignment: 'end',
192
- boundary: 'clippingAncestors',
193
- allowedPlacements: ['bottom-start', 'top-start', 'bottom-end', 'top-end'],
194
- }),
193
+ autoPlacement(expect.any(Function)),
195
194
  shift(),
196
195
  ],
197
196
  }
198
197
  );
198
+ expect(autoPlacement.mock.calls[0][0]()).toEqual({
199
+ alignment: 'end',
200
+ boundary: 'clippingAncestors',
201
+ allowedPlacements: ['bottom-start', 'top-start', 'bottom-end', 'top-end'],
202
+ });
199
203
  });
200
204
 
201
205
  it('initializes Floating UI with reference and floating elements and config for `right-start` aligned menu', async () => {
@@ -210,15 +214,16 @@ describe('base dropdown', () => {
210
214
  strategy: 'absolute',
211
215
  middleware: [
212
216
  offset({ mainAxis: DEFAULT_OFFSET }),
213
- autoPlacement({
214
- alignment: 'start',
215
- boundary: 'clippingAncestors',
216
- allowedPlacements: ['right-start', 'right-end', 'left-start', 'left-end'],
217
- }),
217
+ autoPlacement(expect.any(Function)),
218
218
  shift(),
219
219
  ],
220
220
  }
221
221
  );
222
+ expect(autoPlacement.mock.calls[0][0]()).toEqual({
223
+ alignment: 'start',
224
+ boundary: 'clippingAncestors',
225
+ allowedPlacements: ['right-start', 'right-end', 'left-start', 'left-end'],
226
+ });
222
227
  });
223
228
 
224
229
  it("passes custom offset to Floating UI's middleware", async () => {
@@ -235,7 +240,7 @@ describe('base dropdown', () => {
235
240
  {
236
241
  placement: 'bottom-end',
237
242
  strategy: 'absolute',
238
- middleware: [offset(customOffset), autoPlacement(expect.any(Object)), shift()],
243
+ middleware: [offset(customOffset), autoPlacement(expect.any(Function)), shift()],
239
244
  }
240
245
  );
241
246
  });
@@ -9,7 +9,7 @@ import {
9
9
  dropdownVariantOptions,
10
10
  } from '../../../../utils/constants';
11
11
  import {
12
- GL_DROPDOWN_BOUNDARY_SELECTOR,
12
+ GL_DROPDOWN_HORIZONTAL_BOUNDARY_SELECTOR,
13
13
  GL_DROPDOWN_SHOWN,
14
14
  GL_DROPDOWN_HIDDEN,
15
15
  GL_DROPDOWN_BEFORE_CLOSE,
@@ -21,7 +21,12 @@ import {
21
21
  POSITION_ABSOLUTE,
22
22
  POSITION_FIXED,
23
23
  } from '../constants';
24
- import { logWarning, isElementTabbable, isElementFocusable } from '../../../../utils/utils';
24
+ import {
25
+ logWarning,
26
+ isElementTabbable,
27
+ isElementFocusable,
28
+ getHorizontalBoundingClientRect,
29
+ } from '../../../../utils/utils';
25
30
 
26
31
  import GlButton from '../../button/button.vue';
27
32
  import GlIcon from '../../icon/icon.vue';
@@ -271,10 +276,15 @@ export default {
271
276
  strategy: this.positioningStrategy,
272
277
  middleware: [
273
278
  offset(this.offset),
274
- autoPlacement({
275
- alignment,
276
- boundary: this.$el.closest(GL_DROPDOWN_BOUNDARY_SELECTOR) || 'clippingAncestors',
277
- allowedPlacements: dropdownAllowedAutoPlacements[this.placement],
279
+ autoPlacement(() => {
280
+ const autoHorizontalBoundary = getHorizontalBoundingClientRect(
281
+ this.$el.closest(GL_DROPDOWN_HORIZONTAL_BOUNDARY_SELECTOR)
282
+ );
283
+ return {
284
+ alignment,
285
+ boundary: autoHorizontalBoundary || 'clippingAncestors',
286
+ allowedPlacements: dropdownAllowedAutoPlacements[this.placement],
287
+ };
278
288
  }),
279
289
  shift(),
280
290
  size({
@@ -1,5 +1,5 @@
1
1
  // base dropdown events
2
- export const GL_DROPDOWN_BOUNDARY_SELECTOR = 'main';
2
+ export const GL_DROPDOWN_HORIZONTAL_BOUNDARY_SELECTOR = 'main';
3
3
  export const GL_DROPDOWN_SHOWN = 'shown';
4
4
  export const GL_DROPDOWN_HIDDEN = 'hidden';
5
5
  export const GL_DROPDOWN_BEFORE_CLOSE = 'beforeClose';
@@ -1,10 +1,15 @@
1
1
  .duo-chat-message {
2
2
  max-width: 90%;
3
+ position: relative;
3
4
 
4
5
  code {
5
6
  @include gl-bg-gray-100;
6
7
  }
7
8
 
9
+ pre.code {
10
+ @include gl-bg-white;
11
+ }
12
+
8
13
  pre code {
9
14
  @include gl-font-sm;
10
15
  @include gl-line-height-1;
@@ -30,4 +35,13 @@
30
35
  @include gl-opacity-10;
31
36
  }
32
37
  }
33
- }
38
+
39
+ .has-error {
40
+ margin-left: $gl-spacing-scale-6;
41
+ }
42
+
43
+ .error-icon {
44
+ top: 14px;
45
+ position: absolute;
46
+ }
47
+ }
@@ -1,6 +1,7 @@
1
1
  import { nextTick } from 'vue';
2
2
  import { shallowMount } from '@vue/test-utils';
3
3
  import GlDuoUserFeedback from '../../../user_feedback/user_feedback.vue';
4
+ import GlIcon from '../../../../../base/icon/icon.vue';
4
5
  import {
5
6
  MOCK_USER_PROMPT_MESSAGE,
6
7
  MOCK_RESPONSE_MESSAGE,
@@ -12,10 +13,13 @@ import GlDuoChatMessage from './duo_chat_message.vue';
12
13
  describe('DuoChatMessage', () => {
13
14
  let wrapper;
14
15
 
16
+ const findContentWrapper = () => wrapper.findComponent({ ref: 'content-wrapper' });
15
17
  const findContent = () => wrapper.findComponent({ ref: 'content' });
18
+ const findErrorMessage = () => wrapper.findComponent({ ref: 'error-message' });
16
19
  const findDocumentSources = () => wrapper.findComponent(DocumentationSources);
17
20
  const findUserFeedback = () => wrapper.findComponent(GlDuoUserFeedback);
18
21
  const findCopyCodeButton = () => wrapper.find('copy-code');
22
+ const findErrorIcon = () => wrapper.findComponent(GlIcon);
19
23
  const mockMarkdownContent = 'foo **bar**';
20
24
 
21
25
  let renderMarkdown;
@@ -82,6 +86,10 @@ describe('DuoChatMessage', () => {
82
86
  it('does not render the user feedback component', () => {
83
87
  expect(findUserFeedback().exists()).toBe(false);
84
88
  });
89
+
90
+ it('does not render the error icon', () => {
91
+ expect(findErrorIcon().exists()).toBe(false);
92
+ });
85
93
  });
86
94
 
87
95
  describe('rendering with assistant message', () => {
@@ -100,6 +108,10 @@ describe('DuoChatMessage', () => {
100
108
  expect(findDocumentSources().props('sources')).toEqual(MOCK_RESPONSE_MESSAGE.extras.sources);
101
109
  });
102
110
 
111
+ it('does not render the error icon', () => {
112
+ expect(findErrorIcon().exists()).toBe(false);
113
+ });
114
+
103
115
  it.each([null, undefined, ''])(
104
116
  'does not render sources component when `sources` is %s',
105
117
  (sources) => {
@@ -139,7 +151,19 @@ describe('DuoChatMessage', () => {
139
151
  });
140
152
 
141
153
  describe('message output', () => {
142
- it('outputs errors if they are present', async () => {
154
+ it('renders the warning icon when message has errors', () => {
155
+ createComponent({
156
+ message: {
157
+ ...MOCK_USER_PROMPT_MESSAGE,
158
+ errors: ['foo'],
159
+ },
160
+ });
161
+ expect(findErrorIcon().exists()).toBe(true);
162
+ expect(findErrorMessage().text()).toBe('foo');
163
+ expect(findContentWrapper().classes()).toContain('has-error');
164
+ });
165
+
166
+ it('outputs errors as icon if they are present', async () => {
143
167
  const errors = ['error1', 'error2', 'error3'];
144
168
 
145
169
  createComponent({
@@ -147,15 +171,30 @@ describe('DuoChatMessage', () => {
147
171
  ...MOCK_USER_PROMPT_MESSAGE,
148
172
  errors,
149
173
  contentHtml: 'fooHtml barHtml',
174
+ content: 'foo bar',
175
+ chunks: ['a', 'b', 'c'],
150
176
  },
151
177
  });
152
178
 
153
179
  await nextTick();
154
180
 
155
- const contentText = findContent().text();
156
- expect(contentText).toContain(errors[0]);
157
- expect(contentText).toContain(errors[1]);
158
- expect(contentText).toContain(errors[2]);
181
+ const errorMessage = findErrorMessage().text();
182
+ expect(errorMessage).toContain(errors[0]);
183
+ expect(errorMessage).toContain(errors[1]);
184
+ expect(errorMessage).toContain(errors[2]);
185
+ });
186
+
187
+ it('outputs errors if message has no content', async () => {
188
+ createComponent({
189
+ message: {
190
+ ...MOCK_USER_PROMPT_MESSAGE,
191
+ contentHtml: '',
192
+ content: '',
193
+ errors: ['error'],
194
+ },
195
+ });
196
+ await nextTick();
197
+ expect(findErrorMessage().text()).toBe('error');
159
198
  });
160
199
 
161
200
  it('outputs contentHtml if it is present', async () => {
@@ -216,23 +255,6 @@ describe('DuoChatMessage', () => {
216
255
  expect(renderGFM).toHaveBeenCalled();
217
256
  });
218
257
 
219
- it('sanitizes html produced by errors', async () => {
220
- createComponent({
221
- options: {
222
- provide: null,
223
- },
224
- message: {
225
- ...MOCK_USER_PROMPT_MESSAGE,
226
- errors: ['[click here](javascript:prompt(1))'],
227
- contentHtml: undefined,
228
- content: '',
229
- },
230
- });
231
-
232
- await nextTick();
233
- expect(findContent().html()).toContain('<p><a>click here</a></p>');
234
- });
235
-
236
258
  it('sanitizes html produced by content', async () => {
237
259
  createComponent({
238
260
  options: {
@@ -339,27 +361,11 @@ describe('DuoChatMessage', () => {
339
361
  errors: ['error'],
340
362
  },
341
363
  });
342
- expect(findContent().text()).not.toContain(newContent);
343
- expect(findContent().text()).not.toContain(MOCK_USER_PROMPT_MESSAGE.content);
344
- expect(findContent().text()).toContain('error');
345
- });
364
+ await nextTick();
365
+ expect(findContent().exists()).toBe(false);
346
366
 
347
- it('merges all the errors for output', async () => {
348
- const errors = ['foo', 'bar', 'baz'];
349
- // setProps is justified here because we are testing the component's
350
- // reactive behavior which consistutes an exception
351
- // See https://docs.gitlab.com/ee/development/fe_guide/style/vue.html#setting-component-state
352
- await wrapper.setProps({
353
- message: {
354
- ...MOCK_USER_PROMPT_MESSAGE,
355
- contentHtml: '',
356
- content: '',
357
- errors,
358
- },
359
- });
360
- expect(findContent().text()).toContain(errors[0]);
361
- expect(findContent().text()).toContain(errors[1]);
362
- expect(findContent().text()).toContain(errors[2]);
367
+ expect(findErrorMessage().text()).toBe('error');
368
+ expect(findErrorIcon().exists()).toBe(true);
363
369
  });
364
370
 
365
371
  it('hydrates the output message with GLFM if its not a chunk', async () => {
@@ -479,7 +485,7 @@ describe('DuoChatMessage', () => {
479
485
 
480
486
  // setProps is justified here because we are testing the component's
481
487
  // reactive behavior which consistutes an exception
482
- // See https://docs.gitlab.com/ee/development/fe_guide/style/vue.html#setting-component-state
488
+ // See https://docs.gitlab.com/ee/devoutputs errors if message has no contentelopment/fe_guide/style/vue.html#setting-component-state
483
489
  await wrapper.setProps({
484
490
  message: CHUNK2,
485
491
  });
@@ -519,8 +525,8 @@ describe('DuoChatMessage', () => {
519
525
  errors,
520
526
  },
521
527
  });
522
- expect(renderGFM).toHaveBeenCalled();
523
- expect(findContent().text()).toBe(expectedContent);
528
+
529
+ expect(findContentWrapper().text()).toBe(expectedContent);
524
530
  }
525
531
  );
526
532
  });
@@ -33,6 +33,14 @@ Response.args = generateProps({
33
33
  message: MOCK_RESPONSE_MESSAGE,
34
34
  });
35
35
 
36
+ export const ErrorResponse = Template.bind({});
37
+ ErrorResponse.args = generateProps({
38
+ message: {
39
+ ...MOCK_RESPONSE_MESSAGE,
40
+ errors: ['Error: Whatever you see is wrong'],
41
+ },
42
+ });
43
+
36
44
  export default {
37
45
  title: 'experimental/duo/chat/duo-chat-message',
38
46
  component: GlDuoChatMessage,
@@ -1,4 +1,6 @@
1
1
  <script>
2
+ import GlIcon from '../../../../../base/icon/icon.vue';
3
+ import { GlTooltipDirective } from '../../../../../../directives/tooltip';
2
4
  import GlDuoUserFeedback from '../../../user_feedback/user_feedback.vue';
3
5
  import GlFormGroup from '../../../../../base/form/form_group/form_group.vue';
4
6
  import GlFormTextarea from '../../../../../base/form/form_textarea/form_textarea.vue';
@@ -19,11 +21,13 @@ export const i18n = {
19
21
  INTERACTION: 'The situation in which you interacted with GitLab Duo Chat.',
20
22
  IMPROVE_WHAT: 'How could the response be improved?',
21
23
  BETTER_RESPONSE: 'How the response might better meet your needs.',
24
+ MESSAGE_ERROR: 'Error sending the message',
22
25
  },
23
26
  };
24
27
 
25
28
  export default {
26
29
  name: 'GlDuoChatMessage',
30
+ i18n,
27
31
  safeHtmlConfigExtension: {
28
32
  ADD_TAGS: ['copy-code'],
29
33
  },
@@ -32,9 +36,11 @@ export default {
32
36
  GlDuoUserFeedback,
33
37
  GlFormGroup,
34
38
  GlFormTextarea,
39
+ GlIcon,
35
40
  },
36
41
  directives: {
37
42
  SafeHtml,
43
+ GlTooltip: GlTooltipDirective,
38
44
  },
39
45
  provide() {
40
46
  return {
@@ -92,9 +98,6 @@ export default {
92
98
  return this.message.extras?.hasFeedback;
93
99
  },
94
100
  defaultContent() {
95
- if (this.message.errors.length > 0)
96
- return this.renderMarkdown(this.message.errors.join('; '));
97
-
98
101
  if (this.message.contentHtml) {
99
102
  return this.message.contentHtml;
100
103
  }
@@ -105,8 +108,13 @@ export default {
105
108
  if (this.isAssistantMessage && this.isChunk) {
106
109
  return this.renderMarkdown(concatUntilEmpty(this.messageChunks));
107
110
  }
111
+
108
112
  return this.defaultContent || this.renderMarkdown(concatUntilEmpty(this.message.chunks));
109
113
  },
114
+
115
+ error() {
116
+ return Boolean(this.message?.errors?.length) && this.message.errors.join('; ');
117
+ },
110
118
  },
111
119
  beforeCreate() {
112
120
  if (!customElements.get('copy-code')) {
@@ -142,7 +150,7 @@ export default {
142
150
  }
143
151
  },
144
152
  hydrateContentWithGFM() {
145
- if (!this.isChunk) {
153
+ if (!this.isChunk && this.$refs.content) {
146
154
  this.$nextTick(this.renderGFM(this.$refs.content));
147
155
  }
148
156
  },
@@ -161,7 +169,6 @@ export default {
161
169
  }
162
170
  },
163
171
  },
164
- i18n,
165
172
  };
166
173
  </script>
167
174
  <template>
@@ -171,33 +178,50 @@ export default {
171
178
  'gl-ml-auto gl-bg-blue-100 gl-text-blue-900 gl-rounded-bottom-right-none': isUserMessage,
172
179
  'gl-rounded-bottom-left-none gl-text-gray-900 gl-bg-white gl-border-1 gl-border-solid gl-border-gray-50':
173
180
  isAssistantMessage,
181
+ 'gl-bg-red-50 gl-border-none!': error,
174
182
  }"
175
183
  >
176
- <div ref="content" v-safe-html:[$options.safeHtmlConfigExtension]="messageContent"></div>
184
+ <gl-icon
185
+ v-if="error"
186
+ :aria-label="$options.i18n.MESSAGE_ERROR"
187
+ name="status_warning_borderless"
188
+ :size="16"
189
+ class="gl-text-red-600 gl-border gl-border-red-500 gl-rounded-full gl-mr-3 gl-flex-shrink-0 error-icon"
190
+ data-testid="error"
191
+ />
192
+ <div ref="content-wrapper" :class="{ 'has-error': error }">
193
+ <div v-if="error" ref="error-message" class="error-message">{{ error }}</div>
194
+ <div v-else>
195
+ <div ref="content" v-safe-html:[$options.safeHtmlConfigExtension]="messageContent"></div>
177
196
 
178
- <template v-if="isAssistantMessage">
179
- <documentation-sources v-if="sources" :sources="sources" />
197
+ <template v-if="isAssistantMessage">
198
+ <documentation-sources v-if="sources" :sources="sources" />
180
199
 
181
- <div class="gl-display-flex gl-align-items-flex-end gl-mt-4 duo-chat-message-feedback">
182
- <gl-duo-user-feedback
183
- :feedback-received="hasFeedback"
184
- :modal-title="$options.i18n.MODAL.TITLE"
185
- :modal-alert="$options.i18n.MODAL.ALERT_TEXT"
186
- @feedback="logEvent"
187
- >
188
- <template #feedback-extra-fields>
189
- <gl-form-group :label="$options.i18n.MODAL.DID_WHAT" optional>
190
- <gl-form-textarea v-model="didWhat" :placeholder="$options.i18n.MODAL.INTERACTION" />
191
- </gl-form-group>
192
- <gl-form-group :label="$options.i18n.MODAL.IMPROVE_WHAT" optional>
193
- <gl-form-textarea
194
- v-model="improveWhat"
195
- :placeholder="$options.i18n.MODAL.BETTER_RESPONSE"
196
- />
197
- </gl-form-group>
198
- </template>
199
- </gl-duo-user-feedback>
200
+ <div class="gl-display-flex gl-align-items-flex-end gl-mt-4 duo-chat-message-feedback">
201
+ <gl-duo-user-feedback
202
+ :feedback-received="hasFeedback"
203
+ :modal-title="$options.i18n.MODAL.TITLE"
204
+ :modal-alert="$options.i18n.MODAL.ALERT_TEXT"
205
+ @feedback="logEvent"
206
+ >
207
+ <template #feedback-extra-fields>
208
+ <gl-form-group :label="$options.i18n.MODAL.DID_WHAT" optional>
209
+ <gl-form-textarea
210
+ v-model="didWhat"
211
+ :placeholder="$options.i18n.MODAL.INTERACTION"
212
+ />
213
+ </gl-form-group>
214
+ <gl-form-group :label="$options.i18n.MODAL.IMPROVE_WHAT" optional>
215
+ <gl-form-textarea
216
+ v-model="improveWhat"
217
+ :placeholder="$options.i18n.MODAL.BETTER_RESPONSE"
218
+ />
219
+ </gl-form-group>
220
+ </template>
221
+ </gl-duo-user-feedback>
222
+ </div>
223
+ </template>
200
224
  </div>
201
- </template>
225
+ </div>
202
226
  </div>
203
227
  </template>
@@ -204,3 +204,21 @@ export function stopEvent(
204
204
  export function filterVisible(els) {
205
205
  return (els || []).filter((el) => isVisible(el));
206
206
  }
207
+
208
+ /**
209
+ * Given an element, returns a Rect object
210
+ * with top and bottom boundaries removed.
211
+ */
212
+ export function getHorizontalBoundingClientRect(el) {
213
+ const rect = el?.getBoundingClientRect();
214
+
215
+ if (rect) {
216
+ return {
217
+ x: rect.x,
218
+ width: rect.width,
219
+ y: 0, // top of the document
220
+ height: document.documentElement.clientHeight, // bottom of the document
221
+ };
222
+ }
223
+ return null;
224
+ }