@gitlab/ui 39.3.1 → 39.5.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.
Files changed (46) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/components/base/alert/alert.js +1 -1
  3. package/dist/components/base/new_dropdowns/base_dropdown/base_dropdown.js +240 -0
  4. package/dist/components/base/new_dropdowns/constants.js +20 -0
  5. package/dist/components/base/new_dropdowns/listbox/listbox.js +381 -0
  6. package/dist/components/base/new_dropdowns/listbox/listbox_item.js +77 -0
  7. package/dist/index.css +1 -1
  8. package/dist/index.css.map +1 -1
  9. package/dist/index.js +2 -0
  10. package/dist/utility_classes.css +1 -1
  11. package/dist/utility_classes.css.map +1 -1
  12. package/dist/utils/utils.js +24 -1
  13. package/package.json +6 -5
  14. package/scss_to_js/scss_variables.js +1 -0
  15. package/scss_to_js/scss_variables.json +5 -0
  16. package/src/components/base/alert/alert.spec.js +3 -1
  17. package/src/components/base/alert/alert.vue +1 -1
  18. package/src/components/base/avatar_labeled/avatar_labeled.stories.js +2 -1
  19. package/src/components/base/avatar_link/avatar_link.stories.js +2 -3
  20. package/src/components/base/breadcrumb/breadcrumb.md +1 -1
  21. package/src/components/base/breadcrumb/breadcrumb.stories.js +2 -1
  22. package/src/components/base/broadcast_message/broadcast_message.scss +1 -1
  23. package/src/components/base/button/button.scss +1 -1
  24. package/src/components/base/dropdown/dropdown.scss +10 -3
  25. package/src/components/base/dropdown/dropdown_item.scss +1 -0
  26. package/src/components/base/link/link.stories.js +9 -7
  27. package/src/components/base/new_dropdowns/base_dropdown/base_dropdown.spec.js +171 -0
  28. package/src/components/base/new_dropdowns/base_dropdown/base_dropdown.vue +221 -0
  29. package/src/components/base/new_dropdowns/constants.js +22 -0
  30. package/src/components/base/new_dropdowns/listbox/listbox.md +71 -0
  31. package/src/components/base/new_dropdowns/listbox/listbox.spec.js +236 -0
  32. package/src/components/base/new_dropdowns/listbox/listbox.stories.js +276 -0
  33. package/src/components/base/new_dropdowns/listbox/listbox.vue +348 -0
  34. package/src/components/base/new_dropdowns/listbox/listbox_item.spec.js +104 -0
  35. package/src/components/base/new_dropdowns/listbox/listbox_item.vue +59 -0
  36. package/src/components/utilities/friendly_wrap/friendly_wrap.stories.js +10 -11
  37. package/src/components/utilities/sprintf/sprintf.stories.js +11 -9
  38. package/src/index.js +4 -0
  39. package/src/scss/utilities.scss +18 -0
  40. package/src/scss/utility-mixins/color.scss +4 -0
  41. package/src/scss/utility-mixins/composite.scss +20 -0
  42. package/src/scss/utility-mixins/index.scss +1 -0
  43. package/src/scss/variables.scss +1 -0
  44. package/src/utils/data_utils.js +2 -21
  45. package/src/utils/utils.js +18 -0
  46. package/src/utils/utils.spec.js +41 -1
@@ -119,5 +119,28 @@ function logWarning() {
119
119
  console.warn(message); // eslint-disable-line no-console
120
120
  }
121
121
  }
122
+ /**
123
+ * Stop default event handling and propagation
124
+ */
125
+
126
+ function stopEvent(event) {
127
+ let {
128
+ preventDefault = true,
129
+ stopPropagation = true,
130
+ stopImmediatePropagation = false
131
+ } = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
132
+
133
+ if (preventDefault) {
134
+ event.preventDefault();
135
+ }
136
+
137
+ if (stopPropagation) {
138
+ event.stopPropagation();
139
+ }
140
+
141
+ if (stopImmediatePropagation) {
142
+ event.stopImmediatePropagation();
143
+ }
144
+ }
122
145
 
123
- export { colorFromBackground, debounceByAnimationFrame, focusFirstFocusableElement, hexToRgba, isDev, isElementFocusable, logWarning, rgbFromHex, rgbFromString, throttle, uid };
146
+ export { colorFromBackground, debounceByAnimationFrame, focusFirstFocusableElement, hexToRgba, isDev, isElementFocusable, logWarning, rgbFromHex, rgbFromString, stopEvent, throttle, uid };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gitlab/ui",
3
- "version": "39.3.1",
3
+ "version": "39.5.0",
4
4
  "description": "GitLab UI Components",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -55,6 +55,7 @@
55
55
  "generate:component": "plop"
56
56
  },
57
57
  "dependencies": {
58
+ "@popperjs/core": "^2.11.2",
58
59
  "bootstrap-vue": "2.20.1",
59
60
  "dompurify": "^2.3.6",
60
61
  "echarts": "^5.2.1",
@@ -82,7 +83,7 @@
82
83
  "@babel/preset-env": "^7.10.2",
83
84
  "@gitlab/eslint-plugin": "12.0.1",
84
85
  "@gitlab/stylelint-config": "4.0.0",
85
- "@gitlab/svgs": "2.10.0",
86
+ "@gitlab/svgs": "2.11.0",
86
87
  "@rollup/plugin-commonjs": "^11.1.0",
87
88
  "@rollup/plugin-node-resolve": "^7.1.3",
88
89
  "@rollup/plugin-replace": "^2.3.2",
@@ -122,10 +123,10 @@
122
123
  "npm-run-all": "^4.1.5",
123
124
  "pikaday": "^1.8.0",
124
125
  "plop": "^2.5.4",
125
- "postcss": "8.4.5",
126
+ "postcss": "8.4.12",
126
127
  "postcss-loader": "^3.0.0",
127
- "postcss-scss": "^2.1.1",
128
- "prettier": "2.2.1",
128
+ "postcss-scss": "4.0.4",
129
+ "prettier": "2.6.2",
129
130
  "puppeteer": "11.0.0",
130
131
  "raw-loader": "^0.5.1",
131
132
  "rollup": "^2.53.1",
@@ -27,6 +27,7 @@ export const limitedLayoutWidth = '990px'
27
27
  export const black = '#000'
28
28
  export const blackNormal = '#333'
29
29
  export const white = '#fff'
30
+ export const whiteContrast = '#fff'
30
31
  export const whiteNormal = '#f0f0f0'
31
32
  export const whiteDark = '#eaeaea'
32
33
  export const whiteTransparent = 'rgba(255, 255, 255, 0.8)'
@@ -172,6 +172,11 @@
172
172
  "value": "#fff",
173
173
  "compiledValue": "#fff"
174
174
  },
175
+ {
176
+ "name": "$white-contrast",
177
+ "value": "#fff",
178
+ "compiledValue": "#fff"
179
+ },
175
180
  {
176
181
  "name": "$white-normal",
177
182
  "value": "#f0f0f0",
@@ -71,13 +71,15 @@ describe('Alert component', () => {
71
71
  expect(findDismissButton().exists()).toBe(false);
72
72
  });
73
73
 
74
- it('renders the provided title', () => {
74
+ it('renders the provided title with heading level 2', () => {
75
75
  const title = 'foo';
76
76
  createComponent({ propsData: { title } });
77
77
 
78
78
  const titleWrapper = findTitle();
79
79
  expect(titleWrapper.exists()).toBe(true);
80
80
  expect(titleWrapper.text()).toContain(title);
81
+ // the title needs to be in a level 2 heading for accessibility reasons
82
+ expect(titleWrapper.element.tagName).toEqual('H2');
81
83
  });
82
84
 
83
85
  describe('given primaryButtonText', () => {
@@ -184,7 +184,7 @@ export default {
184
184
  />
185
185
 
186
186
  <div class="gl-alert-content" role="alert">
187
- <h4 v-if="title" class="gl-alert-title">{{ title }}</h4>
187
+ <h2 v-if="title" class="gl-alert-title">{{ title }}</h2>
188
188
 
189
189
  <div class="gl-alert-body">
190
190
  <!-- @slot The alert message to display. -->
@@ -2,6 +2,7 @@ import Vue from 'vue';
2
2
  import { GlAvatarLabeled, GlBadge } from '../../../index';
3
3
  import { GlTooltipDirective } from '../../../directives/tooltip';
4
4
  import { avatarSizeOptions, avatarShapeOptions, tooltipPlacements } from '../../../utils/constants';
5
+ import avatarPath from '../../../../static/img/avatar.png';
5
6
  import readme from './avatar_labeled.md';
6
7
 
7
8
  Vue.directive('gl-tooltip', GlTooltipDirective);
@@ -13,7 +14,7 @@ const generateProps = ({
13
14
  subLabel = '@gitlab',
14
15
  size = 32,
15
16
  shape = 'circle',
16
- src = 'https://assets.gitlab-static.net/uploads/-/system/group/avatar/9970/logo-extra-whitespace.png?width=64',
17
+ src = avatarPath,
17
18
  } = {}) => ({
18
19
  label,
19
20
  subLabel,
@@ -1,5 +1,6 @@
1
1
  import { GlAvatarLink, GlAvatar, GlAvatarLabeled } from '../../../index';
2
2
  import { avatarSizeOptions, avatarShapeOptions } from '../../../utils/constants';
3
+ import avatarPath from '../../../../static/img/avatar.png';
3
4
  import readme from './avatar_link.md';
4
5
 
5
6
  const components = { GlAvatarLink, GlAvatar, GlAvatarLabeled };
@@ -19,9 +20,7 @@ const generateLabelsProps = ({ label = 'GitLab User', subLabel = '@gitlab' } = {
19
20
  subLabel,
20
21
  });
21
22
 
22
- const generateImageProps = ({
23
- src = 'https://assets.gitlab-static.net/uploads/-/system/project/avatar/278964/logo-extra-whitespace.png?width=64',
24
- } = {}) => ({
23
+ const generateImageProps = ({ src = avatarPath } = {}) => ({
25
24
  src,
26
25
  });
27
26
 
@@ -18,7 +18,7 @@ to draw a `/`
18
18
  <img
19
19
  alt=""
20
20
  class="gl-breadcrumb-avatar-tile"
21
- src="https://assets.gitlab-static.net/uploads/-/system/group/avatar/9970/logo-extra-whitespace.png?width=16"
21
+ src="/path/to/image.png"
22
22
  width="16"
23
23
  height="16"
24
24
  />
@@ -1,4 +1,5 @@
1
1
  import { GlBreadcrumb } from '../../../index';
2
+ import avatarPath from '../../../../static/img/avatar.png';
2
3
  import readme from './breadcrumb.md';
3
4
 
4
5
  const template = `
@@ -7,7 +8,7 @@ const template = `
7
8
  >
8
9
  <template #avatar>
9
10
  <img alt=""
10
- class="gl-breadcrumb-avatar-tile" src="https://assets.gitlab-static.net/uploads/-/system/group/avatar/9970/logo-extra-whitespace.png?width=16"
11
+ class="gl-breadcrumb-avatar-tile" src="${avatarPath}"
11
12
  width="16"
12
13
  height="16" />
13
14
  </template>
@@ -1,5 +1,5 @@
1
1
  .gl-broadcast-message {
2
- @include gl-text-white;
2
+ @include gl-text-contrast-light;
3
3
  @include gl-display-flex;
4
4
  @include gl-justify-content-space-between;
5
5
  @include gl-align-items-flex-start;
@@ -136,7 +136,7 @@
136
136
  &.btn-success,
137
137
  &.btn-danger,
138
138
  &.btn-warning {
139
- @include gl-text-white;
139
+ @include gl-text-contrast-light;
140
140
  }
141
141
 
142
142
  &.btn-confirm,
@@ -128,8 +128,14 @@
128
128
  }
129
129
  }
130
130
 
131
- .gl-dropdown-toggle.btn-block {
132
- @include gl-justify-content-space-between;
131
+ .gl-dropdown-toggle {
132
+ &.btn-block {
133
+ @include gl-justify-content-space-between;
134
+ }
135
+
136
+ .gl-button-text {
137
+ @include gl-display-inline-flex;
138
+ }
133
139
  }
134
140
 
135
141
  .gl-new-dropdown-button-text {
@@ -173,7 +179,8 @@
173
179
  }
174
180
 
175
181
  .dropdown-icon-only {
176
- .dropdown-icon {
182
+ .dropdown-icon,
183
+ .gl-button-icon.gl-button-icon {
177
184
  @include gl-mr-0;
178
185
  }
179
186
 
@@ -20,6 +20,7 @@
20
20
  .gl-new-dropdown-item {
21
21
  .dropdown-item {
22
22
  @include gl-tmp-dropdown-item-style;
23
+ @include gl-cursor-pointer;
23
24
 
24
25
  .gl-avatar {
25
26
  @include gl-flex-shrink-0;
@@ -7,13 +7,15 @@ const generateProps = ({ href = '#', target = null } = {}) => ({
7
7
  target,
8
8
  });
9
9
 
10
- const makeStory = (options) => (args, { argTypes }) => ({
11
- components: {
12
- GlLink,
13
- },
14
- props: Object.keys(argTypes),
15
- ...options,
16
- });
10
+ const makeStory =
11
+ (options) =>
12
+ (args, { argTypes }) => ({
13
+ components: {
14
+ GlLink,
15
+ },
16
+ props: Object.keys(argTypes),
17
+ ...options,
18
+ });
17
19
 
18
20
  export const DefaultLink = makeStory({
19
21
  components: { GlLink },
@@ -0,0 +1,171 @@
1
+ import { mount } from '@vue/test-utils';
2
+ import { nextTick } from 'vue';
3
+ import { GL_DROPDOWN_HIDDEN, GL_DROPDOWN_SHOWN, POPPER_CONFIG } from '../constants';
4
+ import GlBaseDropdown from './base_dropdown.vue';
5
+
6
+ const destroyPopper = jest.fn();
7
+ const updatePopper = jest.fn();
8
+ const mockCreatePopper = jest.fn().mockImplementation(() => ({
9
+ destroy: destroyPopper,
10
+ update: updatePopper,
11
+ }));
12
+
13
+ jest.mock('@popperjs/core', () => ({
14
+ createPopper: (...args) => mockCreatePopper(...args),
15
+ }));
16
+
17
+ const DEFAULT_BTN_TOGGLE_CLASSES = [
18
+ 'btn',
19
+ 'btn-default',
20
+ 'btn-md',
21
+ 'gl-button',
22
+ 'dropdown-toggle',
23
+ 'gl-dropdown-toggle',
24
+ ];
25
+
26
+ describe('base dropdown', () => {
27
+ let wrapper;
28
+
29
+ const buildWrapper = (propsData, slots = {}) => {
30
+ wrapper = mount(GlBaseDropdown, {
31
+ propsData: {
32
+ toggleId: 'dropdown-toggle-btn-1',
33
+ ...propsData,
34
+ },
35
+ slots,
36
+ attachTo: document.body,
37
+ });
38
+ };
39
+
40
+ beforeEach(() => {
41
+ jest.clearAllMocks();
42
+ });
43
+
44
+ const findDropdownToggle = () => wrapper.find('.btn.gl-dropdown-toggle');
45
+ const findDropdownMenu = () => wrapper.find('.dropdown-menu');
46
+
47
+ describe('popper.js instance', () => {
48
+ it('should initialize popper.js instance with toggle and menu elements and config for left-aligned menu', async () => {
49
+ await buildWrapper();
50
+ expect(mockCreatePopper).toHaveBeenCalledWith(
51
+ findDropdownToggle().element,
52
+ findDropdownMenu().element,
53
+ { ...POPPER_CONFIG, placement: 'bottom-start' }
54
+ );
55
+ });
56
+
57
+ it('should initialize popper.js instance with toggle and menu elements and config for right-aligned menu', async () => {
58
+ await buildWrapper({ right: true });
59
+ expect(mockCreatePopper).toHaveBeenCalledWith(
60
+ findDropdownToggle().element,
61
+ findDropdownMenu().element,
62
+ { ...POPPER_CONFIG, placement: 'bottom-end' }
63
+ );
64
+ });
65
+
66
+ it('should update popper instance when component is updated', async () => {
67
+ await buildWrapper();
68
+ await findDropdownToggle().trigger('click');
69
+ await wrapper.setProps({ category: 'tertiary' });
70
+ expect(updatePopper).toHaveBeenCalled();
71
+ });
72
+
73
+ it('should destroy popper instance when component is destroyed', async () => {
74
+ await buildWrapper();
75
+ wrapper.destroy();
76
+ expect(destroyPopper).toHaveBeenCalled();
77
+ });
78
+ });
79
+
80
+ describe('renders content to the default slot', () => {
81
+ const defaultContent = 'Some content here';
82
+ const slots = { default: defaultContent };
83
+
84
+ it('renders the content', () => {
85
+ buildWrapper({}, slots);
86
+ expect(wrapper.find('.gl-new-dropdown-inner').html()).toContain(defaultContent);
87
+ });
88
+ });
89
+
90
+ describe.each`
91
+ props | toggleClasses
92
+ ${{}} | ${[]}
93
+ ${{ toggleText: 'toggleText' }} | ${[]}
94
+ ${{ toggleText: 'toggleText', icon: 'close' }} | ${['dropdown-icon-text']}
95
+ ${{ icon: 'close' }} | ${['dropdown-icon-only']}
96
+ ${{ icon: 'close', toggleText: 'toggleText', textSrOnly: true }} | ${['dropdown-icon-only']}
97
+ ${{ icon: 'close', textSrOnly: true }} | ${['dropdown-icon-only']}
98
+ ${{ toggleText: 'toggleText', noCaret: true }} | ${['dropdown-toggle-no-caret']}
99
+ `('dropdown with props $props', ({ props, toggleClasses }) => {
100
+ beforeEach(async () => {
101
+ buildWrapper(props);
102
+
103
+ await nextTick();
104
+ });
105
+
106
+ it(`sets toggle button classes to '${toggleClasses}'`, () => {
107
+ const classes = findDropdownToggle().classes().sort();
108
+
109
+ expect(classes).toEqual([...DEFAULT_BTN_TOGGLE_CLASSES, ...toggleClasses].sort());
110
+ });
111
+ });
112
+
113
+ describe.each`
114
+ toggleClass | expectedClasses | type
115
+ ${'my-class'} | ${[...DEFAULT_BTN_TOGGLE_CLASSES, 'my-class']} | ${'string'}
116
+ ${{ 'my-class': true }} | ${[...DEFAULT_BTN_TOGGLE_CLASSES, 'my-class']} | ${'object'}
117
+ ${['cls-1', 'cls-2']} | ${[...DEFAULT_BTN_TOGGLE_CLASSES, 'cls-1', 'cls-2']} | ${'array'}
118
+ ${null} | ${[...DEFAULT_BTN_TOGGLE_CLASSES]} | ${'null'}
119
+ `('with toggle classes', ({ toggleClass, expectedClasses, type }) => {
120
+ beforeEach(async () => {
121
+ buildWrapper({ toggleClass });
122
+
123
+ await nextTick();
124
+ });
125
+
126
+ it(`class is inherited from toggle class of type ${type}`, () => {
127
+ expect(findDropdownToggle().classes().sort()).toEqual(
128
+ expect.arrayContaining(expectedClasses.sort())
129
+ );
130
+ });
131
+ });
132
+
133
+ describe('toggle visibility', () => {
134
+ beforeEach(() => {
135
+ buildWrapper();
136
+ });
137
+
138
+ it('should toggle menu visibility on toggle button click ', async () => {
139
+ const toggle = findDropdownToggle();
140
+ const menu = findDropdownMenu();
141
+
142
+ // open menu clicking toggle btn
143
+ await toggle.trigger('click');
144
+ expect(menu.classes('show')).toBe(true);
145
+ expect(toggle.attributes('aria-expanded')).toBe('true');
146
+ expect(wrapper.emitted(GL_DROPDOWN_SHOWN).length).toBe(1);
147
+
148
+ // close menu clicking toggle btn again
149
+ await toggle.trigger('click');
150
+ expect(menu.classes('show')).toBe(false);
151
+ expect(toggle.attributes('aria-expanded')).toBeUndefined();
152
+ expect(wrapper.emitted(GL_DROPDOWN_HIDDEN).length).toBe(1);
153
+ });
154
+
155
+ it('should close the menu when Escape is pressed inside menu and focus toggle', async () => {
156
+ const toggle = findDropdownToggle();
157
+ const menu = findDropdownMenu();
158
+
159
+ // open menu clicking toggle btn
160
+ await toggle.trigger('click');
161
+ expect(menu.classes('show')).toBe(true);
162
+
163
+ // close menu pressing ESC on it
164
+ await menu.trigger('keydown.esc');
165
+ expect(menu.classes('show')).toBe(false);
166
+ expect(toggle.attributes('aria-expanded')).toBeUndefined();
167
+ expect(wrapper.emitted(GL_DROPDOWN_HIDDEN).length).toBe(1);
168
+ expect(toggle.element).toHaveFocus();
169
+ });
170
+ });
171
+ });
@@ -0,0 +1,221 @@
1
+ <script>
2
+ import { createPopper } from '@popperjs/core';
3
+ import {
4
+ buttonCategoryOptions,
5
+ buttonSizeOptions,
6
+ dropdownVariantOptions,
7
+ } from '../../../../utils/constants';
8
+ import { POPPER_CONFIG, GL_DROPDOWN_SHOWN, GL_DROPDOWN_HIDDEN } from '../constants';
9
+
10
+ import GlButton from '../../button/button.vue';
11
+ import GlIcon from '../../icon/icon.vue';
12
+ import { OutsideDirective } from '../../../../directives/outside/outside';
13
+
14
+ export default {
15
+ components: {
16
+ GlButton,
17
+ GlIcon,
18
+ },
19
+ directives: { Outside: OutsideDirective },
20
+ props: {
21
+ toggleText: {
22
+ type: String,
23
+ required: false,
24
+ default: '',
25
+ },
26
+ textSrOnly: {
27
+ type: Boolean,
28
+ required: false,
29
+ default: false,
30
+ },
31
+ category: {
32
+ type: String,
33
+ required: false,
34
+ default: buttonCategoryOptions.primary,
35
+ validator: (value) => Object.keys(buttonCategoryOptions).includes(value),
36
+ },
37
+ variant: {
38
+ type: String,
39
+ required: false,
40
+ default: dropdownVariantOptions.default,
41
+ validator: (value) => Object.keys(dropdownVariantOptions).includes(value),
42
+ },
43
+ size: {
44
+ type: String,
45
+ required: false,
46
+ default: buttonSizeOptions.medium,
47
+ validator: (value) => Object.keys(buttonSizeOptions).includes(value),
48
+ },
49
+ icon: {
50
+ type: String,
51
+ required: false,
52
+ default: '',
53
+ },
54
+ disabled: {
55
+ type: Boolean,
56
+ required: false,
57
+ default: false,
58
+ },
59
+ loading: {
60
+ type: Boolean,
61
+ required: false,
62
+ default: false,
63
+ },
64
+ toggleClass: {
65
+ type: [String, Array, Object],
66
+ required: false,
67
+ default: null,
68
+ },
69
+ noCaret: {
70
+ type: Boolean,
71
+ required: false,
72
+ default: false,
73
+ },
74
+ /**
75
+ * Right align dropdown menu with respect to the toggle button
76
+ */
77
+ right: {
78
+ type: Boolean,
79
+ required: false,
80
+ default: false,
81
+ },
82
+ // ARIA props
83
+ ariaHaspopup: {
84
+ type: [String, Boolean],
85
+ required: false,
86
+ default: false,
87
+ validator: (value) => {
88
+ return ['menu', 'listbox', 'tree', 'grid', 'dialog', true, false].includes(value);
89
+ },
90
+ },
91
+ /**
92
+ * Id that will be referenced by `aria-labelledby` attribute of the dropdown content`
93
+ */
94
+ toggleId: {
95
+ type: String,
96
+ required: true,
97
+ },
98
+ /**
99
+ * The `aria-labelledby` attribute value for the toggle `button`
100
+ */
101
+ ariaLabelledby: {
102
+ type: String,
103
+ required: false,
104
+ default: null,
105
+ },
106
+ },
107
+ data() {
108
+ return {
109
+ visible: false,
110
+ };
111
+ },
112
+ computed: {
113
+ isIconOnly() {
114
+ return Boolean(this.icon && (!this.toggleText?.length || this.textSrOnly));
115
+ },
116
+ isIconWithText() {
117
+ return Boolean(this.icon && this.toggleText?.length && !this.textSrOnly);
118
+ },
119
+ toggleButtonClasses() {
120
+ return [
121
+ this.toggleClass,
122
+ {
123
+ 'gl-dropdown-toggle': true,
124
+ 'dropdown-toggle': true,
125
+ 'dropdown-icon-only': this.isIconOnly,
126
+ 'dropdown-icon-text': this.isIconWithText,
127
+ 'dropdown-toggle-no-caret': this.noCaret,
128
+ },
129
+ ];
130
+ },
131
+ toggleLabelledBy() {
132
+ return this.ariaLabelledby ? `${this.ariaLabelledby} ${this.toggleId}` : this.toggleId;
133
+ },
134
+ popperConfig() {
135
+ return {
136
+ placement: this.right ? 'bottom-end' : 'bottom-start',
137
+ ...POPPER_CONFIG,
138
+ };
139
+ },
140
+ },
141
+ updated() {
142
+ if (this.visible) {
143
+ this.popper?.update();
144
+ }
145
+ },
146
+ mounted() {
147
+ this.$nextTick(() => {
148
+ this.popper = createPopper(this.$refs.toggle.$el, this.$refs.content, this.popperConfig);
149
+ });
150
+ },
151
+ beforeDestroy() {
152
+ this.popper.destroy();
153
+ },
154
+ methods: {
155
+ toggle() {
156
+ this.visible = !this.visible;
157
+
158
+ if (this.visible) {
159
+ this.popper.update();
160
+ this.$emit(GL_DROPDOWN_SHOWN);
161
+ } else {
162
+ this.$emit(GL_DROPDOWN_HIDDEN);
163
+ }
164
+ },
165
+ close() {
166
+ if (!this.visible) {
167
+ return;
168
+ }
169
+ this.toggle();
170
+ },
171
+ closeAndFocus() {
172
+ if (!this.visible) {
173
+ return;
174
+ }
175
+ this.toggle();
176
+ this.focusToggle();
177
+ },
178
+ focusToggle() {
179
+ this.$refs.toggle.$el.focus();
180
+ },
181
+ },
182
+ };
183
+ </script>
184
+
185
+ <template>
186
+ <div v-outside="close" class="gl-new-dropdown dropdown btn-group">
187
+ <gl-button
188
+ :id="toggleId"
189
+ ref="toggle"
190
+ data-testid="base-dropdown-toggle"
191
+ :icon="icon"
192
+ :category="category"
193
+ :variant="variant"
194
+ :size="size"
195
+ :disabled="disabled"
196
+ :loading="loading"
197
+ :class="toggleButtonClasses"
198
+ :aria-haspopup="ariaHaspopup"
199
+ :aria-expanded="visible"
200
+ :aria-labelledby="toggleLabelledBy"
201
+ @click="toggle"
202
+ >
203
+ <span class="gl-new-dropdown-button-text" :class="{ 'gl-sr-only': textSrOnly }">
204
+ {{ toggleText }}
205
+ </span>
206
+ <gl-icon v-if="!noCaret" class="gl-button-icon dropdown-chevron" name="chevron-down" />
207
+ </gl-button>
208
+
209
+ <div
210
+ ref="content"
211
+ data-testid="base-dropdown-menu"
212
+ class="dropdown-menu"
213
+ :class="{ show: visible }"
214
+ @keydown.esc.stop.prevent="closeAndFocus"
215
+ >
216
+ <div class="gl-new-dropdown-inner">
217
+ <slot></slot>
218
+ </div>
219
+ </div>
220
+ </div>
221
+ </template>
@@ -0,0 +1,22 @@
1
+ export const POPPER_CONFIG = {
2
+ modifiers: [
3
+ {
4
+ name: 'offset',
5
+ options: {
6
+ offset: [0, 4],
7
+ },
8
+ },
9
+ ],
10
+ };
11
+
12
+ // base dropdown events
13
+ export const GL_DROPDOWN_SHOWN = 'shown';
14
+ export const GL_DROPDOWN_HIDDEN = 'hidden';
15
+
16
+ // KEY Codes
17
+ export const HOME = 'Home';
18
+ export const END = 'End';
19
+ export const ARROW_UP = 'ArrowUp';
20
+ export const ARROW_DOWN = 'ArrowDown';
21
+ export const ENTER = 'Enter';
22
+ export const SPACE = 'Space';