@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.
- package/CHANGELOG.md +29 -0
- package/dist/components/base/new_dropdowns/base_dropdown/base_dropdown.js +76 -25
- package/dist/components/base/new_dropdowns/base_dropdown/constants.js +3 -2
- package/dist/components/base/new_dropdowns/constants.js +2 -10
- package/dist/components/base/new_dropdowns/disclosure/disclosure_dropdown.js +9 -8
- package/dist/components/base/new_dropdowns/listbox/listbox.js +9 -8
- package/dist/components/charts/bar/bar.js +8 -5
- package/dist/index.css +1 -1
- package/dist/index.css.map +1 -1
- package/dist/utility_classes.css +1 -1
- package/dist/utility_classes.css.map +1 -1
- package/package.json +20 -20
- package/src/components/base/filtered_search/filtered_search.scss +5 -2
- package/src/components/base/new_dropdowns/base_dropdown/base_dropdown.spec.js +97 -58
- package/src/components/base/new_dropdowns/base_dropdown/base_dropdown.vue +78 -23
- package/src/components/base/new_dropdowns/base_dropdown/constants.js +2 -1
- package/src/components/base/new_dropdowns/constants.js +2 -11
- package/src/components/base/new_dropdowns/disclosure/disclosure_dropdown.spec.js +10 -4
- package/src/components/base/new_dropdowns/disclosure/disclosure_dropdown.vue +9 -7
- package/src/components/base/new_dropdowns/dropdown.scss +3 -0
- package/src/components/base/new_dropdowns/listbox/listbox.spec.js +10 -4
- package/src/components/base/new_dropdowns/listbox/listbox.stories.js +2 -2
- package/src/components/base/new_dropdowns/listbox/listbox.vue +8 -6
- package/src/components/charts/bar/bar.vue +5 -5
- package/src/scss/utilities.scss +10 -0
- 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": "
|
|
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
|
-
"@
|
|
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.
|
|
88
|
-
"@babel/preset-env": "^7.
|
|
89
|
-
"@babel/preset-react": "^7.
|
|
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.
|
|
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.
|
|
98
|
-
"@storybook/addon-docs": "7.0.
|
|
99
|
-
"@storybook/addon-essentials": "7.0.
|
|
100
|
-
"@storybook/addon-storyshots": "7.0.
|
|
101
|
-
"@storybook/addon-storyshots-puppeteer": "7.0.
|
|
102
|
-
"@storybook/addon-viewport": "7.0.
|
|
103
|
-
"@storybook/builder-webpack5": "7.0.
|
|
104
|
-
"@storybook/theming": "7.0.
|
|
105
|
-
"@storybook/vue": "7.0.
|
|
106
|
-
"@storybook/vue-webpack5": "7.0.
|
|
107
|
-
"@storybook/vue3": "7.0.
|
|
108
|
-
"@storybook/vue3-webpack5": "7.0.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
9
|
+
GL_DROPDOWN_CONTENTS_CLASS,
|
|
9
10
|
} from '../constants';
|
|
10
|
-
import {
|
|
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
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
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('
|
|
57
|
-
it(
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
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(
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
85
|
-
|
|
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(
|
|
94
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
wrapper.destroy();
|
|
103
|
-
expect(destroyPopper).toHaveBeenCalled();
|
|
80
|
+
expect(autoUpdate).toHaveBeenCalledTimes(2);
|
|
81
|
+
expect(mockStopAutoUpdate).toHaveBeenCalledTimes(1);
|
|
104
82
|
});
|
|
105
83
|
|
|
106
|
-
it(
|
|
84
|
+
it("stops Floating UI's auto updates on destroy", async () => {
|
|
107
85
|
buildWrapper();
|
|
86
|
+
await findDefaultDropdownToggle().trigger('click');
|
|
108
87
|
wrapper.destroy();
|
|
109
|
-
|
|
110
|
-
expect(
|
|
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
|
|
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 {
|
|
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
|
-
|
|
123
|
-
|
|
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
|
-
|
|
214
|
+
floatingUIConfig() {
|
|
210
215
|
return {
|
|
211
216
|
placement: dropdownPlacements[this.placement],
|
|
212
|
-
|
|
213
|
-
|
|
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.
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
|
|
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
|
|
41
|
-
const
|
|
42
|
-
buildWrapper({
|
|
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('
|
|
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
|
-
*
|
|
174
|
-
*
|
|
174
|
+
* Custom offset to be applied to Floating UI's offset middleware.
|
|
175
|
+
* https://floating-ui.com/docs/offset
|
|
175
176
|
*/
|
|
176
|
-
|
|
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
|
-
:
|
|
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="
|
|
345
|
+
:class="$options.GL_DROPDOWN_CONTENTS_CLASS"
|
|
344
346
|
tabindex="-1"
|
|
345
347
|
@keydown="onKeydown"
|
|
346
348
|
@click="handleAutoClose"
|
|
@@ -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
|
|
48
|
-
const
|
|
49
|
-
buildWrapper({
|
|
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('
|
|
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
|
|
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>
|