@gitlab/ui 63.4.0 → 64.0.1

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.
Files changed (26) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/components/base/new_dropdowns/base_dropdown/base_dropdown.js +76 -25
  3. package/dist/components/base/new_dropdowns/base_dropdown/constants.js +3 -2
  4. package/dist/components/base/new_dropdowns/constants.js +2 -10
  5. package/dist/components/base/new_dropdowns/disclosure/disclosure_dropdown.js +9 -8
  6. package/dist/components/base/new_dropdowns/listbox/listbox.js +9 -8
  7. package/dist/components/charts/bar/bar.js +8 -5
  8. package/dist/index.css +1 -1
  9. package/dist/index.css.map +1 -1
  10. package/dist/utility_classes.css +1 -1
  11. package/dist/utility_classes.css.map +1 -1
  12. package/package.json +20 -20
  13. package/src/components/base/filtered_search/filtered_search.scss +5 -2
  14. package/src/components/base/new_dropdowns/base_dropdown/base_dropdown.spec.js +97 -58
  15. package/src/components/base/new_dropdowns/base_dropdown/base_dropdown.vue +78 -23
  16. package/src/components/base/new_dropdowns/base_dropdown/constants.js +2 -1
  17. package/src/components/base/new_dropdowns/constants.js +2 -11
  18. package/src/components/base/new_dropdowns/disclosure/disclosure_dropdown.spec.js +10 -4
  19. package/src/components/base/new_dropdowns/disclosure/disclosure_dropdown.vue +9 -7
  20. package/src/components/base/new_dropdowns/dropdown.scss +3 -0
  21. package/src/components/base/new_dropdowns/listbox/listbox.spec.js +10 -4
  22. package/src/components/base/new_dropdowns/listbox/listbox.stories.js +2 -2
  23. package/src/components/base/new_dropdowns/listbox/listbox.vue +8 -6
  24. package/src/components/charts/bar/bar.vue +5 -5
  25. package/src/scss/utilities.scss +10 -0
  26. package/src/scss/utility-mixins/border.scss +6 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gitlab/ui",
3
- "version": "63.4.0",
3
+ "version": "64.0.1",
4
4
  "description": "GitLab UI Components",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -62,7 +62,7 @@
62
62
  "generate:component": "plop"
63
63
  },
64
64
  "dependencies": {
65
- "@popperjs/core": "^2.11.2",
65
+ "@floating-ui/dom": "1.2.9",
66
66
  "bootstrap-vue": "2.23.1",
67
67
  "dompurify": "^2.4.5",
68
68
  "echarts": "^5.3.2",
@@ -84,28 +84,28 @@
84
84
  },
85
85
  "devDependencies": {
86
86
  "@arkweid/lefthook": "0.7.7",
87
- "@babel/core": "^7.21.8",
88
- "@babel/preset-env": "^7.21.5",
89
- "@babel/preset-react": "^7.18.6",
87
+ "@babel/core": "^7.22.1",
88
+ "@babel/preset-env": "^7.22.2",
89
+ "@babel/preset-react": "^7.22.0",
90
90
  "@gitlab/eslint-plugin": "19.0.0",
91
91
  "@gitlab/fonts": "^1.2.0",
92
92
  "@gitlab/stylelint-config": "4.1.0",
93
- "@gitlab/svgs": "3.48.0",
93
+ "@gitlab/svgs": "3.49.0",
94
94
  "@rollup/plugin-commonjs": "^11.1.0",
95
95
  "@rollup/plugin-node-resolve": "^7.1.3",
96
96
  "@rollup/plugin-replace": "^2.3.2",
97
- "@storybook/addon-a11y": "7.0.12",
98
- "@storybook/addon-docs": "7.0.12",
99
- "@storybook/addon-essentials": "7.0.12",
100
- "@storybook/addon-storyshots": "7.0.12",
101
- "@storybook/addon-storyshots-puppeteer": "7.0.12",
102
- "@storybook/addon-viewport": "7.0.12",
103
- "@storybook/builder-webpack5": "7.0.12",
104
- "@storybook/theming": "7.0.12",
105
- "@storybook/vue": "7.0.12",
106
- "@storybook/vue-webpack5": "7.0.12",
107
- "@storybook/vue3": "7.0.12",
108
- "@storybook/vue3-webpack5": "7.0.12",
97
+ "@storybook/addon-a11y": "7.0.18",
98
+ "@storybook/addon-docs": "7.0.18",
99
+ "@storybook/addon-essentials": "7.0.18",
100
+ "@storybook/addon-storyshots": "7.0.18",
101
+ "@storybook/addon-storyshots-puppeteer": "7.0.18",
102
+ "@storybook/addon-viewport": "7.0.18",
103
+ "@storybook/builder-webpack5": "7.0.18",
104
+ "@storybook/theming": "7.0.18",
105
+ "@storybook/vue": "7.0.18",
106
+ "@storybook/vue-webpack5": "7.0.18",
107
+ "@storybook/vue3": "7.0.18",
108
+ "@storybook/vue3-webpack5": "7.0.18",
109
109
  "@vue/compat": "^3.2.40",
110
110
  "@vue/compiler-sfc": "^3.2.40",
111
111
  "@vue/test-utils": "1.3.0",
@@ -117,7 +117,7 @@
117
117
  "babel-loader": "^8.0.5",
118
118
  "babel-plugin-require-context-hook": "^1.0.0",
119
119
  "bootstrap": "4.6.2",
120
- "cypress": "12.12.0",
120
+ "cypress": "12.13.0",
121
121
  "emoji-regex": "^10.0.0",
122
122
  "eslint": "8.41.0",
123
123
  "eslint-import-resolver-jest": "3.0.2",
@@ -153,7 +153,7 @@
153
153
  "sass-loader": "^10.2.0",
154
154
  "sass-true": "^6.1.0",
155
155
  "start-server-and-test": "^1.10.6",
156
- "storybook": "7.0.12",
156
+ "storybook": "7.0.18",
157
157
  "storybook-dark-mode": "3.0.0",
158
158
  "stylelint": "14.9.1",
159
159
  "stylelint-config-prettier": "9.0.4",
@@ -4,6 +4,10 @@
4
4
  @include gl-rounded-left-base;
5
5
  position: relative;
6
6
  flex-grow: 1;
7
+
8
+ .input-group-prepend + & {
9
+ @include gl-rounded-left-none;
10
+ }
7
11
  }
8
12
 
9
13
  .gl-filtered-search-scrollable {
@@ -14,9 +18,8 @@
14
18
  @include gl-py-2;
15
19
  @include gl-pl-4;
16
20
  @include gl-border-none;
17
- @include gl-rounded-left-base;
18
21
  position: absolute;
19
- max-width: calc(100% - $gl-spacing-scale-7);
22
+ max-width: calc(100% - #{$gl-spacing-scale-7});
20
23
  overflow: hidden;
21
24
  overflow-x: auto;
22
25
 
@@ -1,25 +1,20 @@
1
1
  import { mount } from '@vue/test-utils';
2
2
  import { nextTick } from 'vue';
3
+ import { computePosition, autoUpdate, offset } from '@floating-ui/dom';
3
4
  import {
4
5
  ARROW_DOWN,
5
6
  GL_DROPDOWN_FOCUS_CONTENT,
6
7
  GL_DROPDOWN_HIDDEN,
7
8
  GL_DROPDOWN_SHOWN,
8
- POPPER_CONFIG,
9
+ GL_DROPDOWN_CONTENTS_CLASS,
9
10
  } from '../constants';
10
- import { FIXED_WIDTH_CLASS } from './constants';
11
+ import { waitForAnimationFrame } from '../../../../utils/test_utils';
12
+ import { DEFAULT_OFFSET, FIXED_WIDTH_CLASS } from './constants';
11
13
  import GlBaseDropdown from './base_dropdown.vue';
12
14
 
13
- const destroyPopper = jest.fn();
14
- const updatePopper = jest.fn();
15
- const mockCreatePopper = jest.fn().mockImplementation(() => ({
16
- destroy: destroyPopper,
17
- update: updatePopper,
18
- }));
19
-
20
- jest.mock('@popperjs/core', () => ({
21
- createPopper: (...args) => mockCreatePopper(...args),
22
- }));
15
+ jest.mock('@floating-ui/dom');
16
+ const mockStopAutoUpdate = jest.fn();
17
+ offset.mockImplementation((options) => options);
23
18
 
24
19
  const DEFAULT_BTN_TOGGLE_CLASSES = [
25
20
  'btn',
@@ -38,14 +33,20 @@ describe('base dropdown', () => {
38
33
  toggleId: 'dropdown-toggle-btn-1',
39
34
  ...propsData,
40
35
  },
41
- slots,
36
+ slots: {
37
+ default: `<div class="${GL_DROPDOWN_CONTENTS_CLASS}" />`,
38
+ ...slots,
39
+ },
42
40
  attachTo: document.body,
43
41
  });
44
- return nextTick();
45
42
  };
46
43
 
47
44
  beforeEach(() => {
48
45
  jest.clearAllMocks();
46
+ autoUpdate.mockImplementation(() => {
47
+ return mockStopAutoUpdate;
48
+ });
49
+ computePosition.mockImplementation(() => new Promise(() => {}));
49
50
  });
50
51
 
51
52
  const findDefaultDropdownToggle = () => wrapper.find('.btn.gl-new-dropdown-toggle');
@@ -53,61 +54,99 @@ describe('base dropdown', () => {
53
54
  const findDropdownToggleText = () => findDefaultDropdownToggle().find('.gl-button-text');
54
55
  const findDropdownMenu = () => wrapper.find('.gl-new-dropdown-panel');
55
56
 
56
- describe('popper.js instance', () => {
57
- it('should initialize popper.js instance with toggle and menu elements and config for left-aligned menu', async () => {
58
- await buildWrapper();
59
- expect(mockCreatePopper).toHaveBeenCalledWith(
60
- findDefaultDropdownToggle().element,
61
- findDropdownMenu().element,
62
- { ...POPPER_CONFIG, placement: 'bottom-start' }
63
- );
64
- });
57
+ describe('Floating UI instance', () => {
58
+ it("starts Floating UI's when opening the dropdown", async () => {
59
+ buildWrapper();
60
+ await findDefaultDropdownToggle().trigger('click');
65
61
 
66
- it('should initialize popper.js instance with toggle and menu elements and config for center-aligned menu', async () => {
67
- await buildWrapper({ placement: 'center' });
68
- expect(mockCreatePopper).toHaveBeenCalledWith(
69
- findDefaultDropdownToggle().element,
70
- findDropdownMenu().element,
71
- { ...POPPER_CONFIG, placement: 'bottom' }
72
- );
62
+ expect(autoUpdate).toHaveBeenCalledTimes(1);
73
63
  });
74
64
 
75
- it('should initialize popper.js instance with toggle and menu elements and config for right-aligned menu', async () => {
76
- await buildWrapper({ placement: 'right' });
77
- expect(mockCreatePopper).toHaveBeenCalledWith(
78
- findDefaultDropdownToggle().element,
79
- findDropdownMenu().element,
80
- { ...POPPER_CONFIG, placement: 'bottom-end' }
81
- );
82
- });
65
+ it("stops Floating UI's when closing the dropdown", async () => {
66
+ buildWrapper();
67
+ await findDefaultDropdownToggle().trigger('click');
68
+ await findDefaultDropdownToggle().trigger('click');
83
69
 
84
- it('should pass custom options to popper.js, overriding built-in ones', async () => {
85
- await buildWrapper({ placement: 'right', popperOptions: { placement: 'auto-start' } });
86
- expect(mockCreatePopper).toHaveBeenCalledWith(
87
- findDefaultDropdownToggle().element,
88
- findDropdownMenu().element,
89
- { ...POPPER_CONFIG, placement: 'auto-start' }
90
- );
70
+ expect(autoUpdate).toHaveBeenCalledTimes(1);
71
+ expect(mockStopAutoUpdate).toHaveBeenCalledTimes(1);
91
72
  });
92
73
 
93
- it('should update popper instance when component is updated', async () => {
94
- await buildWrapper();
74
+ it("restarts Floating UI's when reopening the dropdown", async () => {
75
+ buildWrapper();
76
+ await findDefaultDropdownToggle().trigger('click');
77
+ await findDefaultDropdownToggle().trigger('click');
95
78
  await findDefaultDropdownToggle().trigger('click');
96
- await wrapper.setProps({ category: 'tertiary' });
97
- expect(updatePopper).toHaveBeenCalled();
98
- });
99
79
 
100
- it('should destroy popper instance when component is destroyed', async () => {
101
- await buildWrapper();
102
- wrapper.destroy();
103
- expect(destroyPopper).toHaveBeenCalled();
80
+ expect(autoUpdate).toHaveBeenCalledTimes(2);
81
+ expect(mockStopAutoUpdate).toHaveBeenCalledTimes(1);
104
82
  });
105
83
 
106
- it('should not destroy popper instance when component is not initiated', async () => {
84
+ it("stops Floating UI's auto updates on destroy", async () => {
107
85
  buildWrapper();
86
+ await findDefaultDropdownToggle().trigger('click');
108
87
  wrapper.destroy();
109
- await nextTick();
110
- expect(destroyPopper).not.toHaveBeenCalled();
88
+
89
+ expect(mockStopAutoUpdate).toHaveBeenCalled();
90
+ });
91
+
92
+ describe('computePosition', () => {
93
+ beforeEach(() => {
94
+ autoUpdate.mockImplementation(jest.requireActual('@floating-ui/dom').autoUpdate);
95
+ });
96
+
97
+ it('initializes Floating UI with reference and floating elements and config for left-aligned menu', async () => {
98
+ buildWrapper();
99
+ await findDefaultDropdownToggle().trigger('click');
100
+
101
+ expect(computePosition).toHaveBeenCalledWith(
102
+ findDefaultDropdownToggle().element,
103
+ findDropdownMenu().element,
104
+ {
105
+ placement: 'bottom-start',
106
+ middleware: [offset({ mainAxis: DEFAULT_OFFSET })],
107
+ }
108
+ );
109
+ });
110
+
111
+ it('initializes Floating UI with reference and floating elements and config for center-aligned menu', async () => {
112
+ buildWrapper({ placement: 'center' });
113
+ await findDefaultDropdownToggle().trigger('click');
114
+
115
+ expect(computePosition).toHaveBeenCalledWith(
116
+ findDefaultDropdownToggle().element,
117
+ findDropdownMenu().element,
118
+ { placement: 'bottom', middleware: [offset({ mainAxis: DEFAULT_OFFSET })] }
119
+ );
120
+ });
121
+
122
+ it('initializes Floating UI with reference and floating elements and config for right-aligned menu', async () => {
123
+ buildWrapper({ placement: 'right' });
124
+ await findDefaultDropdownToggle().trigger('click');
125
+
126
+ expect(computePosition).toHaveBeenCalledWith(
127
+ findDefaultDropdownToggle().element,
128
+ findDropdownMenu().element,
129
+ { placement: 'bottom-end', middleware: [offset({ mainAxis: DEFAULT_OFFSET })] }
130
+ );
131
+ });
132
+
133
+ it("passes custom offset to Floating UI's middleware", async () => {
134
+ const customOffset = { mainAxis: 10, crossAxis: 40 };
135
+ buildWrapper({
136
+ placement: 'right',
137
+ offset: customOffset,
138
+ });
139
+ await findDefaultDropdownToggle().trigger('click');
140
+
141
+ expect(computePosition).toHaveBeenCalledWith(
142
+ findDefaultDropdownToggle().element,
143
+ findDropdownMenu().element,
144
+ {
145
+ placement: 'bottom-end',
146
+ middleware: [offset(customOffset)],
147
+ }
148
+ );
149
+ });
111
150
  });
112
151
  });
113
152
 
@@ -260,7 +299,7 @@ describe('base dropdown', () => {
260
299
  await toggle.trigger('click');
261
300
  expect(menu.classes('gl-display-block!')).toBe(true);
262
301
  expect(firstToggleChild.attributes('aria-expanded')).toBe('true');
263
- await nextTick();
302
+ await waitForAnimationFrame();
264
303
  expect(wrapper.emitted(GL_DROPDOWN_SHOWN)).toHaveLength(1);
265
304
 
266
305
  // close menu clicking toggle btn again
@@ -1,6 +1,6 @@
1
1
  <script>
2
2
  import uniqueId from 'lodash/uniqueId';
3
- import { createPopper } from '@popperjs/core';
3
+ import { computePosition, autoUpdate, offset, size, flip } from '@floating-ui/dom';
4
4
  import {
5
5
  buttonCategoryOptions,
6
6
  buttonSizeOptions,
@@ -8,20 +8,20 @@ import {
8
8
  dropdownVariantOptions,
9
9
  } from '../../../../utils/constants';
10
10
  import {
11
- POPPER_CONFIG,
12
11
  GL_DROPDOWN_SHOWN,
13
12
  GL_DROPDOWN_HIDDEN,
14
13
  GL_DROPDOWN_FOCUS_CONTENT,
15
14
  ENTER,
16
15
  SPACE,
17
16
  ARROW_DOWN,
17
+ GL_DROPDOWN_CONTENTS_CLASS,
18
18
  } from '../constants';
19
19
  import { logWarning, isElementTabbable, isElementFocusable } from '../../../../utils/utils';
20
20
 
21
21
  import GlButton from '../../button/button.vue';
22
22
  import GlIcon from '../../icon/icon.vue';
23
23
  import { OutsideDirective } from '../../../../directives/outside/outside';
24
- import { FIXED_WIDTH_CLASS } from './constants';
24
+ import { DEFAULT_OFFSET, FIXED_WIDTH_CLASS } from './constants';
25
25
 
26
26
  export default {
27
27
  name: 'BaseDropdown',
@@ -119,10 +119,14 @@ export default {
119
119
  required: false,
120
120
  default: null,
121
121
  },
122
- popperOptions: {
123
- type: Object,
122
+ /**
123
+ * Custom value to be passed to the offset middleware.
124
+ * https://floating-ui.com/docs/offset
125
+ */
126
+ offset: {
127
+ type: [Number, Object],
124
128
  required: false,
125
- default: () => ({}),
129
+ default: () => ({ mainAxis: DEFAULT_OFFSET }),
126
130
  },
127
131
  fluidWidth: {
128
132
  type: Boolean,
@@ -133,6 +137,7 @@ export default {
133
137
  data() {
134
138
  return {
135
139
  visible: false,
140
+ openedYet: false,
136
141
  baseDropdownId: uniqueId('base-dropdown-'),
137
142
  };
138
143
  },
@@ -206,11 +211,27 @@ export default {
206
211
  [FIXED_WIDTH_CLASS]: !this.fluidWidth,
207
212
  };
208
213
  },
209
- popperConfig() {
214
+ floatingUIConfig() {
210
215
  return {
211
216
  placement: dropdownPlacements[this.placement],
212
- ...POPPER_CONFIG,
213
- ...this.popperOptions,
217
+ middleware: [
218
+ offset(this.offset),
219
+ flip(),
220
+ size({
221
+ apply: ({ availableHeight, elements }) => {
222
+ const contentsEl = elements.floating.querySelector(`.${GL_DROPDOWN_CONTENTS_CLASS}`);
223
+ if (!contentsEl) {
224
+ return;
225
+ }
226
+
227
+ const contentsAvailableHeight =
228
+ availableHeight - (this.nonScrollableContentHeight ?? 0) - DEFAULT_OFFSET;
229
+ Object.assign(contentsEl.style, {
230
+ maxHeight: `${Math.max(contentsAvailableHeight, 0)}px`,
231
+ });
232
+ },
233
+ }),
234
+ ],
214
235
  };
215
236
  },
216
237
  },
@@ -227,13 +248,10 @@ export default {
227
248
  },
228
249
  },
229
250
  mounted() {
230
- this.$nextTick(() => {
231
- this.popper = createPopper(this.toggleElement, this.$refs.content, this.popperConfig);
232
- });
233
251
  this.checkToggleFocusable();
234
252
  },
235
253
  beforeDestroy() {
236
- this.popper?.destroy();
254
+ this.stopFloating();
237
255
  },
238
256
  methods: {
239
257
  checkToggleFocusable() {
@@ -245,24 +263,52 @@ export default {
245
263
  );
246
264
  }
247
265
  },
266
+ startFloating() {
267
+ this.calculateNonScrollableAreaHeight();
268
+ this.observer = new MutationObserver(this.calculateNonScrollableAreaHeight);
269
+ this.observer.observe(this.$refs.content, {
270
+ attributes: false,
271
+ childList: true,
272
+ subtree: true,
273
+ });
274
+
275
+ this.stopAutoUpdate = autoUpdate(this.toggleElement, this.$refs.content, async () => {
276
+ const { x, y } = await computePosition(
277
+ this.toggleElement,
278
+ this.$refs.content,
279
+ this.floatingUIConfig
280
+ );
281
+
282
+ /**
283
+ * Due to the asynchronous nature of computePosition, it's technically possible for the
284
+ * component to have been destroyed by the time the promise resolves. In such case, we exit
285
+ * early to prevent a TypeError.
286
+ */
287
+ if (!this.$refs.content) return;
288
+
289
+ Object.assign(this.$refs.content.style, {
290
+ left: `${x}px`,
291
+ top: `${y}px`,
292
+ });
293
+ });
294
+ },
295
+ stopFloating() {
296
+ this.observer?.disconnect();
297
+ this.stopAutoUpdate?.();
298
+ },
248
299
  async toggle() {
249
300
  this.visible = !this.visible;
250
301
 
251
302
  if (this.visible) {
252
- /* Initially dropdown is hidden with `display="none"`.
253
- When `visible` prop is toggled ON, with the `nextTick` we wait for the DOM update -
254
- dropdown's `display="block"` is set (adding CSS class `show`).
255
- After that we can recalculate its position (calling `popper.update()`).
256
- https://github.com/floating-ui/floating-ui/issues/630:
257
- "Unfortunately there's not any way to compute the position of an element not rendered in the document".
258
- Then we `await` while the new dropdown position is calculated and DOM updated accordingly.
259
- After we can emit the `GL_DROPDOWN_SHOWN` event to the parent which might interact with updated dropdown,
260
- e.g. set focus.
303
+ /**
304
+ * We defer the following logic to the next tick as all that comes next relies on the
305
+ * dropdown actually being visible.
261
306
  */
262
307
  await this.$nextTick();
263
- await this.popper?.update();
308
+ this.startFloating();
264
309
  this.$emit(GL_DROPDOWN_SHOWN);
265
310
  } else {
311
+ this.stopFloating();
266
312
  this.$emit(GL_DROPDOWN_HIDDEN);
267
313
  }
268
314
  },
@@ -312,6 +358,15 @@ export default {
312
358
  this.$emit(GL_DROPDOWN_FOCUS_CONTENT, event);
313
359
  }
314
360
  },
361
+ calculateNonScrollableAreaHeight() {
362
+ const scrollableArea = this.$refs.content?.querySelector(`.${GL_DROPDOWN_CONTENTS_CLASS}`);
363
+ if (!scrollableArea) return;
364
+
365
+ const floatingElementBoundingBox = this.$refs.content.getBoundingClientRect();
366
+ const scrollableAreaBoundingBox = scrollableArea.getBoundingClientRect();
367
+ this.nonScrollableContentHeight =
368
+ floatingElementBoundingBox.height - scrollableAreaBoundingBox.height;
369
+ },
315
370
  },
316
371
  };
317
372
  </script>
@@ -1 +1,2 @@
1
- export const FIXED_WIDTH_CLASS = 'gl-w-31';
1
+ export const FIXED_WIDTH_CLASS = 'gl-w-31!';
2
+ export const DEFAULT_OFFSET = 4;
@@ -1,14 +1,3 @@
1
- export const POPPER_CONFIG = {
2
- modifiers: [
3
- {
4
- name: 'offset',
5
- options: {
6
- offset: [0, 4],
7
- },
8
- },
9
- ],
10
- };
11
-
12
1
  // base dropdown events
13
2
  export const GL_DROPDOWN_SHOWN = 'shown';
14
3
  export const GL_DROPDOWN_HIDDEN = 'hidden';
@@ -21,3 +10,5 @@ export const END = 'End';
21
10
  export const ENTER = 'Enter';
22
11
  export const HOME = 'Home';
23
12
  export const SPACE = 'Space';
13
+
14
+ export const GL_DROPDOWN_CONTENTS_CLASS = 'gl-new-dropdown-contents';
@@ -1,4 +1,5 @@
1
1
  import { mount } from '@vue/test-utils';
2
+ import { autoUpdate } from '@floating-ui/dom';
2
3
  import * as utils from '../../../../utils/utils';
3
4
  import GlBaseDropdown from '../base_dropdown/base_dropdown.vue';
4
5
  import {
@@ -15,6 +16,11 @@ import GlDisclosureDropdownItem from './disclosure_dropdown_item.vue';
15
16
  import GlDisclosureDropdownGroup from './disclosure_dropdown_group.vue';
16
17
  import { mockItems, mockGroups } from './mock_data';
17
18
 
19
+ jest.mock('@floating-ui/dom');
20
+ autoUpdate.mockImplementation(() => {
21
+ return () => {};
22
+ });
23
+
18
24
  const ITEM_SELECTOR = '[data-testid="disclosure-dropdown-item"]';
19
25
 
20
26
  describe('GlDisclosureDropdown', () => {
@@ -37,11 +43,11 @@ describe('GlDisclosureDropdown', () => {
37
43
 
38
44
  jest.spyOn(utils, 'filterVisible').mockImplementation((items) => items);
39
45
 
40
- it('passes custom popper.js options to the base dropdown', () => {
41
- const popperOptions = { foo: 'bar' };
42
- buildWrapper({ popperOptions });
46
+ it('passes custom offset to the base dropdown', () => {
47
+ const dropdownOffset = { mainAxis: 10, crossAxis: 40 };
48
+ buildWrapper({ dropdownOffset });
43
49
 
44
- expect(findBaseDropdown().props('popperOptions')).toEqual(popperOptions);
50
+ expect(findBaseDropdown().props('offset')).toEqual(dropdownOffset);
45
51
  });
46
52
 
47
53
  describe('toggle text', () => {
@@ -13,6 +13,7 @@ import {
13
13
  END,
14
14
  ARROW_DOWN,
15
15
  ARROW_UP,
16
+ GL_DROPDOWN_CONTENTS_CLASS,
16
17
  } from '../constants';
17
18
  import {
18
19
  buttonCategoryOptions,
@@ -170,13 +171,13 @@ export default {
170
171
  default: null,
171
172
  },
172
173
  /**
173
- * Options to be passed to the underlying Popper.js instance.
174
- * Overrides built-in options.
174
+ * Custom offset to be applied to Floating UI's offset middleware.
175
+ * https://floating-ui.com/docs/offset
175
176
  */
176
- popperOptions: {
177
- type: Object,
177
+ dropdownOffset: {
178
+ type: [Number, Object],
178
179
  required: false,
179
- default: () => ({}),
180
+ default: undefined,
180
181
  },
181
182
  /**
182
183
  * Lets the dropdown extend to match its content's width, up to a maximum width
@@ -300,6 +301,7 @@ export default {
300
301
  },
301
302
  isItem,
302
303
  },
304
+ GL_DROPDOWN_CONTENTS_CLASS,
303
305
  };
304
306
  </script>
305
307
 
@@ -319,7 +321,7 @@ export default {
319
321
  :loading="loading"
320
322
  :no-caret="noCaret"
321
323
  :placement="placement"
322
- :popper-options="popperOptions"
324
+ :offset="dropdownOffset"
323
325
  :fluid-width="fluidWidth"
324
326
  class="gl-disclosure-dropdown"
325
327
  @[$options.events.GL_DROPDOWN_SHOWN]="onShow"
@@ -340,7 +342,7 @@ export default {
340
342
  ref="content"
341
343
  :aria-labelledby="listAriaLabelledBy || toggleId"
342
344
  data-testid="disclosure-content"
343
- class="gl-new-dropdown-contents"
345
+ :class="$options.GL_DROPDOWN_CONTENTS_CLASS"
344
346
  tabindex="-1"
345
347
  @keydown="onKeydown"
346
348
  @click="handleAutoClose"
@@ -20,6 +20,9 @@
20
20
  @include gl-border-gray-200;
21
21
  @include gl-rounded-lg;
22
22
  @include gl-shadow-md;
23
+ position: absolute;
24
+ top: 0;
25
+ left: 0;
23
26
  min-width: $gl-new-dropdown-min-width;
24
27
  max-width: $gl-new-dropdown-max-width;
25
28
  z-index: 1000;
@@ -1,5 +1,6 @@
1
1
  import { mount } from '@vue/test-utils';
2
2
  import { nextTick } from 'vue';
3
+ import { autoUpdate } from '@floating-ui/dom';
3
4
  import { useMockIntersectionObserver } from '~/utils/use_mock_intersection_observer';
4
5
  import GlBaseDropdown from '../base_dropdown/base_dropdown.vue';
5
6
  import {
@@ -17,6 +18,11 @@ import GlListboxItem from './listbox_item.vue';
17
18
  import GlListboxGroup from './listbox_group.vue';
18
19
  import { mockOptions, mockGroups } from './mock_data';
19
20
 
21
+ jest.mock('@floating-ui/dom');
22
+ autoUpdate.mockImplementation(() => {
23
+ return () => {};
24
+ });
25
+
20
26
  describe('GlCollapsibleListbox', () => {
21
27
  let wrapper;
22
28
 
@@ -44,11 +50,11 @@ describe('GlCollapsibleListbox', () => {
44
50
  const findSelectAllButton = () => wrapper.find("[data-testid='listbox-select-all-button']");
45
51
  const findIntersectionObserver = () => wrapper.findComponent(GlIntersectionObserver);
46
52
 
47
- it('passes custom popper.js options to the base dropdown', () => {
48
- const popperOptions = { foo: 'bar' };
49
- buildWrapper({ popperOptions });
53
+ it('passes custom offset to the base dropdown', () => {
54
+ const dropdownOffset = { mainAxis: 10, crossAxis: 40 };
55
+ buildWrapper({ dropdownOffset });
50
56
 
51
- expect(findBaseDropdown().props('popperOptions')).toEqual(popperOptions);
57
+ expect(findBaseDropdown().props('offset')).toEqual(dropdownOffset);
52
58
  });
53
59
 
54
60
  describe('toggle text', () => {
@@ -197,10 +197,10 @@ export const HeaderAndFooter = (args, { argTypes }) => ({
197
197
  `
198
198
  <template #footer>
199
199
  <div class="gl-border-t-solid gl-border-t-1 gl-border-t-gray-100 gl-display-flex gl-flex-direction-column gl-p-2! gl-pt-0!">
200
- <gl-button @click="selectAllItems" category="tertiary" block class="gl-justify-content-start! gl-mt-2!"">
200
+ <gl-button @click="selectAllItems" category="tertiary" block class="gl-justify-content-start! gl-mt-2!">
201
201
  Select all
202
202
  </gl-button>
203
- <gl-button category="tertiary" block class="gl-justify-content-start! gl-mt-2!">
203
+ <gl-button category="tertiary" block class="gl-justify-content-start! gl-mt-2!" data-testid="footer-bottom-button">
204
204
  Manage departments
205
205
  </gl-button>
206
206
  </div>