@gitlab/ui 52.2.0 → 52.3.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.2.0",
3
+ "version": "52.3.0",
4
4
  "description": "GitLab UI Components",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -0,0 +1,48 @@
1
+ $search-icon-size: 12px;
2
+ $clear-button-size: 24px;
3
+
4
+ .gl-listbox-search {
5
+ @include gl-relative;
6
+
7
+ .gl-listbox-search-input {
8
+ @include gl-w-full;
9
+ @include gl-line-height-normal;
10
+ @include gl-h-auto;
11
+ @include gl-border-none;
12
+ @include gl-pl-7;
13
+ padding-right: calc(#{$gl-spacing-scale-6} + #{$gl-spacing-scale-2});
14
+ @include gl-py-4;
15
+ @include gl-font-base;
16
+
17
+ &:focus {
18
+ @include gl-focus($inset: true);
19
+ }
20
+
21
+ &::placeholder {
22
+ @include gl-text-gray-400;
23
+ }
24
+
25
+ &::-webkit-search-cancel-button {
26
+ @include gl-display-none;
27
+ }
28
+ }
29
+
30
+ .gl-listbox-search-icon {
31
+ @include gl-absolute;
32
+ top: calc(50% - #{$search-icon-size} / 2);
33
+ left: $gl-spacing-scale-4;
34
+ @include gl-text-gray-500;
35
+ }
36
+
37
+ .gl-listbox-search-clear-button {
38
+ @include gl-absolute;
39
+ top: calc(50% - #{$clear-button-size} / 2);
40
+ right: $gl-spacing-scale-2;
41
+ }
42
+ }
43
+
44
+ .gl-listbox-item {
45
+ &:focus {
46
+ @include gl-focus($inset: true);
47
+ }
48
+ }
@@ -248,25 +248,25 @@ describe('GlListbox', () => {
248
248
  });
249
249
 
250
250
  describe('when `searchable` is enabled', () => {
251
- let searchbox;
251
+ let searchboxInput;
252
252
 
253
253
  beforeEach(() => {
254
254
  buildWrapper({ items: mockOptions, searchable: true });
255
255
  findBaseDropdown().vm.$emit(GL_DROPDOWN_SHOWN);
256
256
  firstItem = findListItem(0);
257
- searchbox = findSearchBox();
257
+ searchboxInput = findSearchBox().find('input');
258
258
  });
259
259
 
260
260
  it('should move focus to the first item on search input `ARROW_DOWN`', async () => {
261
- expect(searchbox.element).toHaveFocus();
262
- searchbox.trigger('keydown', { code: ARROW_DOWN });
261
+ expect(searchboxInput.element).toHaveFocus();
262
+ searchboxInput.trigger('keydown', { code: ARROW_DOWN });
263
263
  expect(firstItem.element).toHaveFocus();
264
264
  });
265
265
 
266
266
  it('should move focus to the search input on first item `ARROW_UP', async () => {
267
- searchbox.trigger('keydown', { code: ARROW_DOWN });
267
+ searchboxInput.trigger('keydown', { code: ARROW_DOWN });
268
268
  firstItem.trigger('keydown', { code: ARROW_UP });
269
- expect(searchbox.element).toHaveFocus();
269
+ expect(searchboxInput.element).toHaveFocus();
270
270
  });
271
271
  });
272
272
  });
@@ -29,6 +29,7 @@ const generateProps = ({
29
29
  searchable = defaultValue('searchable'),
30
30
  searching = defaultValue('searching'),
31
31
  noResultsText = defaultValue('noResultsText'),
32
+ searchPlaceholder = defaultValue('searchPlaceholder'),
32
33
  noCaret = defaultValue('noCaret'),
33
34
  right = defaultValue('right'),
34
35
  toggleText,
@@ -51,6 +52,7 @@ const generateProps = ({
51
52
  searchable,
52
53
  searching,
53
54
  noResultsText,
55
+ searchPlaceholder,
54
56
  noCaret,
55
57
  right,
56
58
  toggleText,
@@ -76,6 +78,7 @@ const makeBindings = (overrides = {}) =>
76
78
  ':searchable': 'searchable',
77
79
  ':searching': 'searching',
78
80
  ':no-results-text': 'noResultsText',
81
+ ':search-placeholder': 'searchPlaceholder',
79
82
  ':no-caret': 'noCaret',
80
83
  ':right': 'right',
81
84
  ':toggle-text': 'toggleText',
@@ -437,7 +440,11 @@ export const Searchable = (args, { argTypes }) => ({
437
440
  }
438
441
  ),
439
442
  });
440
- Searchable.args = generateProps({ headerText: 'Assign to department', searchable: true });
443
+ Searchable.args = generateProps({
444
+ headerText: 'Assign to department',
445
+ searchable: true,
446
+ searchPlaceholder: 'Find department',
447
+ });
441
448
  Searchable.decorators = [makeContainer({ height: '370px' })];
442
449
 
443
450
  export const SearchableGroups = (args, { argTypes }) => ({
@@ -20,13 +20,14 @@ import GlLoadingIcon from '../../loading_icon/loading_icon.vue';
20
20
  import GlSearchBoxByType from '../../search_box_by_type/search_box_by_type.vue';
21
21
  import GlBaseDropdown from '../base_dropdown/base_dropdown.vue';
22
22
  import GlListboxItem from './listbox_item.vue';
23
+ import GlListboxSearchInput from './listbox_search_input.vue';
23
24
  import GlListboxGroup from './listbox_group.vue';
24
25
  import { isOption, itemsValidator, flattenedOptions } from './utils';
25
26
 
26
27
  export const ITEM_SELECTOR = '[role="option"]';
27
28
  const HEADER_ITEMS_BORDER_CLASSES = ['gl-border-b-1', 'gl-border-b-solid', 'gl-border-b-gray-200'];
28
29
  const GROUP_TOP_BORDER_CLASSES = ['gl-border-t', 'gl-pt-3', 'gl-mt-3'];
29
- export const SEARCH_INPUT_SELECTOR = '.gl-search-box-by-type-input';
30
+ export const SEARCH_INPUT_SELECTOR = '.gl-listbox-search-input';
30
31
 
31
32
  export default {
32
33
  HEADER_ITEMS_BORDER_CLASSES,
@@ -40,6 +41,7 @@ export default {
40
41
  GlListboxGroup,
41
42
  GlButton,
42
43
  GlSearchBoxByType,
44
+ GlListboxSearchInput,
43
45
  GlLoadingIcon,
44
46
  },
45
47
  model: {
@@ -57,7 +59,7 @@ export default {
57
59
  validator: itemsValidator,
58
60
  },
59
61
  /**
60
- * array of selected items values for multi-select and selected item value for single-select
62
+ * Array of selected items values for multi-select and selected item value for single-select
61
63
  */
62
64
  selected: {
63
65
  type: [Array, String, Number],
@@ -221,6 +223,14 @@ export default {
221
223
  required: false,
222
224
  default: 'No results found',
223
225
  },
226
+ /**
227
+ * Search input placeholder text and aria-label
228
+ */
229
+ searchPlaceholder: {
230
+ type: String,
231
+ required: false,
232
+ default: 'Search',
233
+ },
224
234
  /**
225
235
  * The reset button's label, to be rendered in the header. If this is omitted, the button is not
226
236
  * rendered.
@@ -484,7 +494,7 @@ export default {
484
494
  >
485
495
  <div
486
496
  v-if="headerText"
487
- class="gl-display-flex gl-align-items-center gl-px-3 gl-py-2! gl-min-h-8"
497
+ class="gl-display-flex gl-align-items-center gl-p-4! gl-min-h-8"
488
498
  :class="$options.HEADER_ITEMS_BORDER_CLASSES"
489
499
  >
490
500
  <div
@@ -506,11 +516,12 @@ export default {
506
516
  </div>
507
517
 
508
518
  <div v-if="searchable" :class="$options.HEADER_ITEMS_BORDER_CLASSES">
509
- <gl-search-box-by-type
519
+ <gl-listbox-search-input
510
520
  ref="searchBox"
511
521
  v-model="searchStr"
512
522
  :aria-owns="listboxId"
513
523
  data-testid="listbox-search-input"
524
+ :placeholder="searchPlaceholder"
514
525
  @input="search"
515
526
  @keydown="onKeydown"
516
527
  />
@@ -51,7 +51,7 @@ export default {
51
51
 
52
52
  <template>
53
53
  <li
54
- class="gl-dropdown-item"
54
+ class="gl-dropdown-item gl-listbox-item"
55
55
  role="option"
56
56
  :tabindex="isFocused ? 0 : -1"
57
57
  :aria-selected="isSelected"
@@ -0,0 +1,64 @@
1
+ import { mount, shallowMount } from '@vue/test-utils';
2
+ import { nextTick } from 'vue';
3
+ import ClearIcon from '~/components/shared_components/clear_icon_button/clear_icon_button.vue';
4
+ import ListboxSearchInput from './listbox_search_input.vue';
5
+
6
+ const modelEvent = ListboxSearchInput.model.event;
7
+ const newSearchValue = 'new value';
8
+
9
+ describe('listbox search input component', () => {
10
+ let wrapper;
11
+ const searchValue = 'some value';
12
+
13
+ const createComponent = ({ listeners, ...propsData }, mountFn = shallowMount) => {
14
+ wrapper = mountFn(ListboxSearchInput, { propsData, listeners });
15
+ };
16
+
17
+ const findClearIcon = () => wrapper.findComponent(ClearIcon);
18
+ const findInput = () => wrapper.findComponent({ ref: 'input' });
19
+
20
+ describe('clear icon component', () => {
21
+ beforeEach(() => {
22
+ createComponent({ value: searchValue });
23
+ });
24
+
25
+ it('is not rendered when value is empty', () => {
26
+ createComponent({ value: '' });
27
+
28
+ expect(findClearIcon().exists()).toBe(false);
29
+ });
30
+
31
+ it('is rendered when value is provided', () => {
32
+ expect(findClearIcon().exists()).toBe(true);
33
+ });
34
+
35
+ it('emits empty value when clicked', () => {
36
+ findClearIcon().vm.$emit('click', { stopPropagation: jest.fn() });
37
+
38
+ expect(wrapper.emitted('input')).toEqual([['']]);
39
+ });
40
+ });
41
+
42
+ describe('v-model', () => {
43
+ beforeEach(() => {
44
+ jest.useFakeTimers();
45
+ createComponent({ value: searchValue }, mount);
46
+ });
47
+
48
+ it('syncs value prop to input value', async () => {
49
+ expect(findInput().element.value).toEqual(searchValue);
50
+ wrapper.setProps({ value: newSearchValue });
51
+ await nextTick();
52
+
53
+ expect(findInput().element.value).toEqual(newSearchValue);
54
+ });
55
+
56
+ it(`emits ${modelEvent} event when input value changes`, () => {
57
+ findInput().setValue(newSearchValue);
58
+ jest.advanceTimersByTime(199);
59
+ expect(wrapper.emitted('input')).toEqual(undefined);
60
+ jest.advanceTimersByTime(1);
61
+ expect(wrapper.emitted('input')).toEqual([[newSearchValue]]);
62
+ });
63
+ });
64
+ });
@@ -0,0 +1,76 @@
1
+ <script>
2
+ import debounce from 'lodash/debounce';
3
+ import GlClearIconButton from '../../../shared_components/clear_icon_button/clear_icon_button.vue';
4
+ import GlIcon from '../../icon/icon.vue';
5
+
6
+ export default {
7
+ components: {
8
+ GlClearIconButton,
9
+ GlIcon,
10
+ },
11
+ model: {
12
+ prop: 'value',
13
+ event: 'input',
14
+ },
15
+ props: {
16
+ /**
17
+ * If provided, used as value of search input
18
+ */
19
+ value: {
20
+ type: String,
21
+ required: false,
22
+ default: '',
23
+ },
24
+ /**
25
+ * Search input placeholder text and aria-label
26
+ */
27
+ placeholder: {
28
+ type: String,
29
+ required: false,
30
+ default: 'Search',
31
+ },
32
+ },
33
+ computed: {
34
+ hasValue() {
35
+ return Boolean(this.value.length);
36
+ },
37
+ inputListeners() {
38
+ return {
39
+ ...this.$listeners,
40
+ input: debounce((event) => {
41
+ this.$emit('input', event.target.value);
42
+ }, 200),
43
+ };
44
+ },
45
+ },
46
+ methods: {
47
+ clearInput() {
48
+ this.$emit('input', '');
49
+ this.focusInput();
50
+ },
51
+ focusInput() {
52
+ this.$refs.input.focus();
53
+ },
54
+ },
55
+ };
56
+ </script>
57
+
58
+ <template>
59
+ <div class="gl-listbox-search">
60
+ <gl-icon name="search-sm" :size="12" class="gl-listbox-search-icon" />
61
+ <input
62
+ ref="input"
63
+ type="search"
64
+ :value="value"
65
+ class="gl-listbox-search-input"
66
+ :aria-label="placeholder"
67
+ :placeholder="placeholder"
68
+ v-on="inputListeners"
69
+ />
70
+ <gl-clear-icon-button
71
+ v-if="hasValue"
72
+ class="gl-listbox-search-clear-button"
73
+ @click.stop="clearInput"
74
+ />
75
+ </div>
76
+ </template>
@@ -109,7 +109,7 @@ export default {
109
109
  >
110
110
  <span
111
111
  v-if="unit"
112
- class="gl-font-sm gl-mr-2 gl-transition-medium gl-opacity-10"
112
+ class="gl-font-sm gl-mx-2 gl-transition-medium gl-opacity-10"
113
113
  :class="{ 'gl-opacity-0!': hideUnits }"
114
114
  data-testid="unit"
115
115
  >{{ unit }}</span
@@ -74,3 +74,4 @@
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/listbox/listbox';