@gitlab/ui 52.7.3 → 52.9.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gitlab/ui",
3
- "version": "52.7.3",
3
+ "version": "52.9.0",
4
4
  "description": "GitLab UI Components",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -0,0 +1,5 @@
1
+ .gl-dropdown-custom-toggle {
2
+ &:focus {
3
+ @include gl-focus;
4
+ }
5
+ }
@@ -42,14 +42,15 @@ describe('base dropdown', () => {
42
42
  jest.clearAllMocks();
43
43
  });
44
44
 
45
- const findDropdownToggle = () => wrapper.find('.btn.gl-dropdown-toggle');
45
+ const findDefaultDropdownToggle = () => wrapper.find('.btn.gl-dropdown-toggle');
46
+ const findCustomDropdownToggle = () => wrapper.find('.gl-dropdown-custom-toggle');
46
47
  const findDropdownMenu = () => wrapper.find('.dropdown-menu');
47
48
 
48
49
  describe('popper.js instance', () => {
49
50
  it('should initialize popper.js instance with toggle and menu elements and config for left-aligned menu', async () => {
50
51
  await buildWrapper();
51
52
  expect(mockCreatePopper).toHaveBeenCalledWith(
52
- findDropdownToggle().element,
53
+ findDefaultDropdownToggle().element,
53
54
  findDropdownMenu().element,
54
55
  { ...POPPER_CONFIG, placement: 'bottom-start' }
55
56
  );
@@ -58,7 +59,7 @@ describe('base dropdown', () => {
58
59
  it('should initialize popper.js instance with toggle and menu elements and config for right-aligned menu', async () => {
59
60
  await buildWrapper({ right: true });
60
61
  expect(mockCreatePopper).toHaveBeenCalledWith(
61
- findDropdownToggle().element,
62
+ findDefaultDropdownToggle().element,
62
63
  findDropdownMenu().element,
63
64
  { ...POPPER_CONFIG, placement: 'bottom-end' }
64
65
  );
@@ -66,7 +67,7 @@ describe('base dropdown', () => {
66
67
 
67
68
  it('should update popper instance when component is updated', async () => {
68
69
  await buildWrapper();
69
- await findDropdownToggle().trigger('click');
70
+ await findDefaultDropdownToggle().trigger('click');
70
71
  await wrapper.setProps({ category: 'tertiary' });
71
72
  expect(updatePopper).toHaveBeenCalled();
72
73
  });
@@ -105,7 +106,7 @@ describe('base dropdown', () => {
105
106
  });
106
107
 
107
108
  it(`sets toggle button classes to '${toggleClasses}'`, () => {
108
- const classes = findDropdownToggle().classes().sort();
109
+ const classes = findDefaultDropdownToggle().classes().sort();
109
110
 
110
111
  expect(classes).toEqual([...DEFAULT_BTN_TOGGLE_CLASSES, ...toggleClasses].sort());
111
112
  });
@@ -125,7 +126,7 @@ describe('base dropdown', () => {
125
126
  });
126
127
 
127
128
  it(`class is inherited from toggle class of type ${type}`, () => {
128
- expect(findDropdownToggle().classes().sort()).toEqual(
129
+ expect(findDefaultDropdownToggle().classes().sort()).toEqual(
129
130
  expect.arrayContaining(expectedClasses.sort())
130
131
  );
131
132
  });
@@ -137,7 +138,7 @@ describe('base dropdown', () => {
137
138
  });
138
139
 
139
140
  it('should toggle menu visibility on toggle button click', async () => {
140
- const toggle = findDropdownToggle();
141
+ const toggle = findDefaultDropdownToggle();
141
142
  const menu = findDropdownMenu();
142
143
 
143
144
  // open menu clicking toggle btn
@@ -155,7 +156,7 @@ describe('base dropdown', () => {
155
156
  });
156
157
 
157
158
  it('should close the menu when Escape is pressed inside menu and focus toggle', async () => {
158
- const toggle = findDropdownToggle();
159
+ const toggle = findDefaultDropdownToggle();
159
160
  const menu = findDropdownMenu();
160
161
 
161
162
  // open menu clicking toggle btn
@@ -170,4 +171,59 @@ describe('base dropdown', () => {
170
171
  expect(toggle.element).toHaveFocus();
171
172
  });
172
173
  });
174
+
175
+ describe('Custom toggle', () => {
176
+ const toggleContent = '<div>Custom toggle</div>';
177
+
178
+ beforeEach(() => {
179
+ const slots = { toggle: toggleContent };
180
+ buildWrapper({}, slots);
181
+ });
182
+
183
+ it('does not render default toggle button', () => {
184
+ expect(findDefaultDropdownToggle().exists()).toBe(false);
185
+ });
186
+
187
+ it('renders the custom toggle instead', () => {
188
+ expect(findCustomDropdownToggle().exists()).toBe(true);
189
+ });
190
+
191
+ it('renders provided via slot content as custom toggle', () => {
192
+ expect(findCustomDropdownToggle().html()).toContain(toggleContent);
193
+ });
194
+
195
+ describe('toggle visibility', () => {
196
+ it('should toggle menu visibility on toggle button ENTER', async () => {
197
+ const toggle = findCustomDropdownToggle();
198
+ const menu = findDropdownMenu();
199
+ // open menu clicking toggle btn
200
+ await toggle.trigger('keydown.enter');
201
+ expect(menu.classes('show')).toBe(true);
202
+ expect(toggle.attributes('aria-expanded')).toBe('true');
203
+ await nextTick();
204
+ expect(wrapper.emitted(GL_DROPDOWN_SHOWN).length).toBe(1);
205
+
206
+ // close menu clicking toggle btn again
207
+ await toggle.trigger('keydown.enter');
208
+ expect(menu.classes('show')).toBe(false);
209
+ expect(toggle.attributes('aria-expanded')).toBeUndefined();
210
+ expect(wrapper.emitted(GL_DROPDOWN_HIDDEN).length).toBe(1);
211
+ });
212
+
213
+ it('should close the menu when Escape is pressed inside menu and focus toggle', async () => {
214
+ const toggle = findCustomDropdownToggle();
215
+ const menu = findDropdownMenu();
216
+ // open menu clicking toggle btn
217
+ await toggle.trigger('click');
218
+ expect(menu.classes('show')).toBe(true);
219
+
220
+ // close menu pressing ESC on it
221
+ await menu.trigger('keydown.esc');
222
+ expect(menu.classes('show')).toBe(false);
223
+ expect(toggle.attributes('aria-expanded')).toBeUndefined();
224
+ expect(wrapper.emitted(GL_DROPDOWN_HIDDEN).length).toBe(1);
225
+ expect(toggle.element).toHaveFocus();
226
+ });
227
+ });
228
+ });
173
229
  });
@@ -131,6 +131,32 @@ export default {
131
131
  toggleLabelledBy() {
132
132
  return this.ariaLabelledby ? `${this.ariaLabelledby} ${this.toggleId}` : this.toggleId;
133
133
  },
134
+ isDefaultToggle() {
135
+ return !this.$scopedSlots.toggle;
136
+ },
137
+ toggleOptions() {
138
+ if (this.isDefaultToggle) {
139
+ return {
140
+ is: GlButton,
141
+ icon: this.icon,
142
+ category: this.category,
143
+ variant: this.variant,
144
+ size: this.size,
145
+ disabled: this.disabled,
146
+ loading: this.loading,
147
+ class: this.toggleButtonClasses,
148
+ };
149
+ }
150
+
151
+ return {
152
+ is: 'div',
153
+ class: 'gl-dropdown-custom-toggle gl-hover-cursor-pointer',
154
+ tabindex: '0',
155
+ };
156
+ },
157
+ toggleElement() {
158
+ return this.$refs.toggle.$el || this.$refs.toggle;
159
+ },
134
160
  popperConfig() {
135
161
  return {
136
162
  placement: this.right ? 'bottom-end' : 'bottom-start',
@@ -140,7 +166,7 @@ export default {
140
166
  },
141
167
  mounted() {
142
168
  this.$nextTick(() => {
143
- this.popper = createPopper(this.$refs.toggle.$el, this.$refs.content, this.popperConfig);
169
+ this.popper = createPopper(this.toggleElement, this.$refs.content, this.popperConfig);
144
170
  });
145
171
  },
146
172
  beforeDestroy() {
@@ -159,7 +185,7 @@ export default {
159
185
  "Unfortunately there's not any way to compute the position of an element not rendered in the document".
160
186
  Then we `await` while the new dropdown position is calculated and DOM updated accordingly.
161
187
  After we can emit the `GL_DROPDOWN_SHOWN` event to the parent which might interact with updated dropdown,
162
- e.g. set focus..
188
+ e.g. set focus.
163
189
  */
164
190
  await this.$nextTick();
165
191
  await this.popper?.update();
@@ -188,35 +214,37 @@ export default {
188
214
  this.focusToggle();
189
215
  },
190
216
  focusToggle() {
191
- this.$refs.toggle.$el.focus();
217
+ this.toggleElement.focus();
192
218
  },
193
219
  },
194
220
  };
195
221
  </script>
196
222
 
197
223
  <template>
198
- <div v-outside="close" class="gl-dropdown dropdown btn-group">
199
- <gl-button
224
+ <div
225
+ v-outside="close"
226
+ class="gl-dropdown dropdown gl-display-inline-flex gl-vertical-align-middle"
227
+ >
228
+ <component
229
+ :is="toggleOptions.is"
230
+ v-bind="toggleOptions"
200
231
  :id="toggleId"
201
232
  ref="toggle"
202
233
  data-testid="base-dropdown-toggle"
203
- :icon="icon"
204
- :category="category"
205
- :variant="variant"
206
- :size="size"
207
- :disabled="disabled"
208
- :loading="loading"
209
- :class="toggleButtonClasses"
210
234
  :aria-haspopup="ariaHaspopup"
211
235
  :aria-expanded="visible"
212
236
  :aria-labelledby="toggleLabelledBy"
237
+ @keydown.enter="toggle"
213
238
  @click="toggle"
214
239
  >
215
- <span class="gl-dropdown-button-text" :class="{ 'gl-sr-only': textSrOnly }">
216
- {{ toggleText }}
217
- </span>
218
- <gl-icon v-if="!noCaret" class="gl-button-icon dropdown-chevron" name="chevron-down" />
219
- </gl-button>
240
+ <!-- @slot Custom toggle button content -->
241
+ <slot name="toggle">
242
+ <span class="gl-dropdown-button-text" :class="{ 'gl-sr-only': textSrOnly }">
243
+ {{ toggleText }}
244
+ </span>
245
+ <gl-icon v-if="!noCaret" class="gl-button-icon dropdown-chevron" name="chevron-down" />
246
+ </slot>
247
+ </component>
220
248
 
221
249
  <div
222
250
  ref="content"
@@ -16,7 +16,7 @@ import { makeContainer } from '../../../../utils/story_decorators/container';
16
16
  import { disableControls } from '../../../../utils/stories_utils';
17
17
  import { setStoryTimeout } from '../../../../utils/test_utils';
18
18
  import readme from './listbox.md';
19
- import { mockOptions, mockGroups } from './mock_data';
19
+ import { mockOptions, mockGroups, mockUsers } from './mock_data';
20
20
  import { flattenedOptions } from './utils';
21
21
 
22
22
  const defaultValue = (prop) => GlListbox.props[prop].default;
@@ -202,7 +202,7 @@ export const CustomListItem = (args, { argTypes }) => ({
202
202
  props: Object.keys(argTypes),
203
203
  data() {
204
204
  return {
205
- selected: ['mikegreiling'],
205
+ selected: [mockUsers[0].value],
206
206
  };
207
207
  },
208
208
  components: {
@@ -229,15 +229,15 @@ export const CustomListItem = (args, { argTypes }) => ({
229
229
  },
230
230
  template: template(
231
231
  `<template #list-item="{ item }">
232
- <span class="gl-display-flex gl-align-items-center">
233
- <gl-avatar :size="32" class-="gl-mr-3"/>
234
- <span class="gl-display-flex gl-flex-direction-column">
235
- <span class="gl-font-weight-bold gl-white-space-nowrap">{{ item.text }}</span>
236
- <span class="gl-text-gray-400"> {{ item.secondaryText }}</span>
232
+ <span class="gl-display-flex gl-align-items-center">
233
+ <gl-avatar :size="32" :entity-name="item.value" class-="gl-mr-3"/>
234
+ <span class="gl-display-flex gl-flex-direction-column">
235
+ <span class="gl-font-weight-bold gl-white-space-nowrap">{{ item.text }}</span>
236
+ <span class="gl-text-gray-400"> {{ item.secondaryText }}</span>
237
+ </span>
237
238
  </span>
238
- </span>
239
- </template>
240
- `,
239
+ </template>
240
+ `,
241
241
  {
242
242
  bindingOverrides: {
243
243
  ':toggle-text': 'customToggleText',
@@ -248,16 +248,7 @@ export const CustomListItem = (args, { argTypes }) => ({
248
248
  });
249
249
 
250
250
  CustomListItem.args = generateProps({
251
- items: [
252
- {
253
- value: 'mikegreiling',
254
- text: 'Mike Greiling',
255
- secondaryText: '@mikegreiling',
256
- icon: 'foo',
257
- },
258
- { value: 'ohoral', text: 'Olena Horal-Koretska', secondaryText: '@ohoral', icon: 'bar' },
259
- { value: 'markian', text: 'Mark Florian', secondaryText: '@markian', icon: 'bin' },
260
- ],
251
+ items: mockUsers,
261
252
  multiple: true,
262
253
  isCheckCentered: true,
263
254
  headerText: 'Select assignees',
@@ -265,6 +256,45 @@ CustomListItem.args = generateProps({
265
256
  });
266
257
  CustomListItem.decorators = [makeContainer({ height: '200px' })];
267
258
 
259
+ export const CustomToggle = (args, { argTypes }) => ({
260
+ props: Object.keys(argTypes),
261
+ components: {
262
+ GlListbox,
263
+ GlAvatar,
264
+ },
265
+ data() {
266
+ return {
267
+ selected: mockUsers[1].value,
268
+ };
269
+ },
270
+ mounted() {
271
+ if (this.startOpened) {
272
+ openListbox(this);
273
+ }
274
+ },
275
+ template: template(
276
+ `
277
+ <template #toggle>
278
+ <gl-avatar :size="32" :entity-name="selected"></gl-avatar>
279
+ </template>
280
+ <template #list-item="{ item }">
281
+ <span class="gl-display-flex gl-align-items-center">
282
+ <gl-avatar :size="32" :entity-name="item.value" class-="gl-mr-3"/>
283
+ <span class="gl-display-flex gl-flex-direction-column">
284
+ <span class="gl-font-weight-bold gl-white-space-nowrap">{{ item.text }}</span>
285
+ <span class="gl-text-gray-400"> {{ item.secondaryText }}</span>
286
+ </span>
287
+ </span>
288
+ </template>
289
+ `
290
+ ),
291
+ });
292
+ CustomToggle.args = generateProps({
293
+ items: mockUsers,
294
+ isCheckCentered: true,
295
+ });
296
+ CustomToggle.decorators = [makeContainer({ height: '200px' })];
297
+
268
298
  const makeGroupedExample = (changes) => {
269
299
  const story = (args, { argTypes }) => ({
270
300
  props: Object.keys(argTypes),
@@ -334,6 +334,9 @@ export default {
334
334
  showIntersectionObserver() {
335
335
  return this.infiniteScroll && !this.infiniteScrollLoading && !this.loading && !this.searching;
336
336
  },
337
+ hasCustomToggle() {
338
+ return Boolean(this.$scopedSlots.toggle);
339
+ },
337
340
  },
338
341
  watch: {
339
342
  selected: {
@@ -558,6 +561,11 @@ export default {
558
561
  @[$options.events.GL_DROPDOWN_SHOWN]="onShow"
559
562
  @[$options.events.GL_DROPDOWN_HIDDEN]="onHide"
560
563
  >
564
+ <template v-if="hasCustomToggle" #toggle>
565
+ <!-- @slot Custom toggle content -->
566
+ <slot name="toggle"></slot>
567
+ </template>
568
+
561
569
  <div
562
570
  v-if="headerText"
563
571
  class="gl-display-flex gl-align-items-center gl-p-4! gl-min-h-8"
@@ -66,3 +66,24 @@ export const mockGroups = [
66
66
  ],
67
67
  },
68
68
  ];
69
+
70
+ export const mockUsers = [
71
+ {
72
+ value: 'mikegreiling',
73
+ text: 'Mike Greiling',
74
+ secondaryText: '@mikegreiling',
75
+ icon: 'foo',
76
+ },
77
+ {
78
+ value: 'ohoral',
79
+ text: 'Olena Horal-Koretska',
80
+ secondaryText: '@ohoral',
81
+ icon: 'bar',
82
+ },
83
+ {
84
+ value: 'markian',
85
+ text: 'Mark Florian',
86
+ secondaryText: '@markian',
87
+ icon: 'bin',
88
+ },
89
+ ];
@@ -1,10 +1,12 @@
1
+ $gl-search-box-by-type-search-icon-size: 16px;
1
2
  $gl-search-box-by-type-input-padding: 3.5 * $grid-size;
2
3
 
3
4
  .gl-search-box-by-type-search-icon {
4
- @include gl-m-3;
5
5
  @include gl-text-gray-500;
6
6
  @include gl-w-5;
7
7
  @include gl-absolute;
8
+ @include gl-left-3;
9
+ top: calc(50% - #{$gl-search-box-by-type-search-icon-size} / 2);
8
10
  }
9
11
 
10
12
  .gl-search-box-by-type {
@@ -33,11 +35,37 @@ $gl-search-box-by-type-input-padding: 3.5 * $grid-size;
33
35
  }
34
36
  }
35
37
 
38
+ .gl-search-box-by-type-input-borderless,
39
+ .gl-search-box-by-type-input-borderless.gl-form-input {
40
+ @include gl-border-none;
41
+ @include gl-font-base;
42
+ @include gl-h-auto;
43
+ @include gl-line-height-normal;
44
+ @include gl-pl-7;
45
+ @include gl-py-4;
46
+ @include gl-shadow-none;
47
+ @include gl-w-full;
48
+ border-radius: 0;
49
+ padding-right: calc(#{$gl-spacing-scale-6} + #{$gl-spacing-scale-2});
50
+
51
+ &:not(.form-control-plaintext):focus {
52
+ @include gl-focus($inset: true);
53
+ }
54
+
55
+ &::placeholder {
56
+ @include gl-text-gray-400;
57
+ }
58
+
59
+ &::-webkit-search-cancel-button {
60
+ @include gl-display-none;
61
+ }
62
+ }
63
+
36
64
  .gl-search-box-by-type-right-icons {
37
65
  @include gl-display-flex;
38
66
  @include gl-align-items-center;
39
67
  @include gl-line-height-0;
40
- @include gl-right-0;
68
+ @include gl-right-2;
41
69
  @include gl-absolute;
42
70
  @include gl-h-full;
43
71
  }
@@ -16,6 +16,20 @@ describe('search box by type component', () => {
16
16
  const findClearIcon = () => wrapper.findComponent(ClearIcon);
17
17
  const findInput = () => wrapper.findComponent({ ref: 'input' });
18
18
 
19
+ describe('borderless', () => {
20
+ it('renders default class on input when `borderless` prop is false', () => {
21
+ createComponent({ borderless: false });
22
+
23
+ expect(findInput().classes()).toContain('gl-search-box-by-type-input');
24
+ });
25
+
26
+ it('renders borderless class on input when `borderless` prop is true', () => {
27
+ createComponent({ borderless: true });
28
+
29
+ expect(findInput().classes()).toContain('gl-search-box-by-type-input-borderless');
30
+ });
31
+ });
32
+
19
33
  describe('clear icon component', () => {
20
34
  beforeEach(() => {
21
35
  createComponent({ value: 'somevalue' });
@@ -5,6 +5,7 @@ import readme from './search_box_by_type.md';
5
5
  const template = `
6
6
  <gl-search-box-by-type
7
7
  v-model="searchQuery"
8
+ :borderless="borderless"
8
9
  :clear-button-title="clearButtonTitle"
9
10
  :disabled="disabled"
10
11
  :is-loading="isLoading"
@@ -15,11 +16,13 @@ const template = `
15
16
  const defaultValue = (prop) => GlSearchBoxByType.props[prop].default;
16
17
 
17
18
  const generateProps = ({
19
+ borderless = defaultValue('borderless'),
18
20
  clearButtonTitle = defaultValue('clearButtonTitle'),
19
21
  disabled = defaultValue('disabled'),
20
22
  placeholder = 'Search',
21
23
  isLoading = defaultValue('isLoading'),
22
24
  } = {}) => ({
25
+ borderless,
23
26
  clearButtonTitle,
24
27
  disabled,
25
28
  placeholder,
@@ -34,9 +37,15 @@ const Template = (args, { argTypes }) => ({
34
37
  data: () => ({ searchQuery: '' }),
35
38
  template,
36
39
  });
40
+
37
41
  export const Default = Template.bind({});
38
42
  Default.args = generateProps();
39
43
 
44
+ export const Borderless = Template.bind({});
45
+ Borderless.args = generateProps({
46
+ borderless: true,
47
+ });
48
+
40
49
  export default {
41
50
  title: 'base/search-box-by-type',
42
51
  component: GlSearchBoxByType,
@@ -25,6 +25,11 @@ export default {
25
25
  required: false,
26
26
  default: '',
27
27
  },
28
+ borderless: {
29
+ type: Boolean,
30
+ required: false,
31
+ default: false,
32
+ },
28
33
  clearButtonTitle: {
29
34
  type: String,
30
35
  required: false,
@@ -113,11 +118,14 @@ export default {
113
118
  ref="input"
114
119
  :value="value"
115
120
  :disabled="disabled"
116
- class="gl-search-box-by-type-input"
121
+ :class="{
122
+ 'gl-search-box-by-type-input': !borderless,
123
+ 'gl-search-box-by-type-input-borderless': borderless,
124
+ }"
117
125
  v-bind="inputAttributes"
118
126
  v-on="inputListeners"
119
127
  />
120
- <div class="gl-search-box-by-type-right-icons">
128
+ <div v-if="isLoading || showClearButton" class="gl-search-box-by-type-right-icons">
121
129
  <gl-loading-icon v-if="isLoading" class="gl-search-box-by-type-loading-icon" />
122
130
  <gl-clear-icon-button
123
131
  v-if="showClearButton"
@@ -74,4 +74,5 @@
74
74
  @import '../components/charts/tooltip/tooltip';
75
75
  @import '../components/shared_components/charts/tooltip_default_format';
76
76
  @import '../components/utilities/truncate/truncate';
77
+ @import '../components/base/new_dropdowns/base_dropdown/base_dropdown';
77
78
  @import '../components/base/new_dropdowns/listbox/listbox';
File without changes