@gitlab/ui 52.2.1 → 52.3.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.
@@ -15,6 +15,7 @@ const defaultFontSize = 12;
15
15
  const defaultHeight = 400;
16
16
  const defaultWidth = 300;
17
17
  const validRenderers = ['canvas', 'svg'];
18
+ const toolboxHeight = 14;
18
19
  const axes = {
19
20
  name: 'Value',
20
21
  type: 'value',
@@ -504,4 +505,4 @@ const getDefaultTooltipContent = function (params) {
504
505
  };
505
506
  };
506
507
 
507
- export { annotationsYAxisCoords, axes, dataZoomAdjustments, defaultAreaOpacity, defaultChartOptions, defaultFontSize, defaultHeight, defaultWidth, generateAnnotationSeries, generateBarSeries, generateLineSeries, getAnnotationsConfig, getDataZoomConfig, getDefaultTooltipContent, getThresholdConfig, grid, gridWithSecondaryYAxis, lineStyle, mergeAnnotationAxisToOptions, mergeSeriesToOptions, parseAnnotations, symbolSize, validRenderers, xAxis, yAxis };
508
+ export { annotationsYAxisCoords, axes, dataZoomAdjustments, defaultAreaOpacity, defaultChartOptions, defaultFontSize, defaultHeight, defaultWidth, generateAnnotationSeries, generateBarSeries, generateLineSeries, getAnnotationsConfig, getDataZoomConfig, getDefaultTooltipContent, getThresholdConfig, grid, gridWithSecondaryYAxis, lineStyle, mergeAnnotationAxisToOptions, mergeSeriesToOptions, parseAnnotations, symbolSize, toolboxHeight, validRenderers, xAxis, yAxis };
@@ -1,6 +1,7 @@
1
1
  import { scrollHandleSvgPath } from '../svgs/svg_paths';
2
2
  import { hexToRgba } from '../utils';
3
3
 
4
+ const white = '#fff';
4
5
  const whiteNormal = '#f0f0f0';
5
6
  const red500 = '#dd2b0e';
6
7
  const gray50 = '#ececef';
@@ -42,6 +43,7 @@ const dataVizOrange700 = '#92430a';
42
43
  const dataVizOrange800 = '#6f3500';
43
44
  const dataVizOrange900 = '#5e2f05';
44
45
  const dataVizOrange950 = '#4b2707';
46
+ const glBorderRadiusBase = '0.25rem';
45
47
 
46
48
  const themeName = 'gitlab';
47
49
  const heatmapHues = [gray100, dataVizBlue200, dataVizBlue400, dataVizBlue600, dataVizBlue800];
@@ -163,7 +165,10 @@ const createTheme = function () {
163
165
  emphasis: {
164
166
  iconStyle: {
165
167
  borderWidth: 0,
166
- color: gray900
168
+ color: gray900,
169
+ textBackgroundColor: white,
170
+ textBorderRadius: glBorderRadiusBase,
171
+ textPadding: [8, 12]
167
172
  }
168
173
  },
169
174
  iconStyle: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gitlab/ui",
3
- "version": "52.2.1",
3
+ "version": "52.3.1",
4
4
  "description": "GitLab UI Components",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -91,14 +91,14 @@
91
91
  "@rollup/plugin-commonjs": "^11.1.0",
92
92
  "@rollup/plugin-node-resolve": "^7.1.3",
93
93
  "@rollup/plugin-replace": "^2.3.2",
94
- "@storybook/addon-a11y": "6.5.13",
95
- "@storybook/addon-docs": "6.5.13",
96
- "@storybook/addon-essentials": "6.5.13",
97
- "@storybook/addon-storyshots": "6.5.13",
98
- "@storybook/addon-storyshots-puppeteer": "6.5.13",
99
- "@storybook/addon-viewport": "6.5.13",
100
- "@storybook/theming": "6.5.13",
101
- "@storybook/vue": "6.5.13",
94
+ "@storybook/addon-a11y": "6.5.14",
95
+ "@storybook/addon-docs": "6.5.14",
96
+ "@storybook/addon-essentials": "6.5.14",
97
+ "@storybook/addon-storyshots": "6.5.14",
98
+ "@storybook/addon-storyshots-puppeteer": "6.5.14",
99
+ "@storybook/addon-viewport": "6.5.14",
100
+ "@storybook/theming": "6.5.14",
101
+ "@storybook/vue": "6.5.14",
102
102
  "@vue/compat": "^3.2.40",
103
103
  "@vue/compiler-sfc": "^3.2.40",
104
104
  "@vue/test-utils": "1.3.0",
@@ -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>
@@ -1,5 +1,6 @@
1
1
  import { shallowMount } from '@vue/test-utils';
2
2
  import * as echarts from 'echarts';
3
+ import { toolboxHeight } from '~/utils/charts/config';
3
4
  import { createTheme } from '~/utils/charts/theme';
4
5
  import { useMockResizeObserver } from '~helpers/mock_dom_observer';
5
6
  import Chart from './chart.vue';
@@ -118,8 +119,11 @@ describe('chart component', () => {
118
119
 
119
120
  describe('draw method', () => {
120
121
  it('sets chart options', () => {
122
+ expect(wrapper.vm.chart.setOption).toHaveBeenCalledTimes(1);
123
+
121
124
  wrapper.vm.draw();
122
125
 
126
+ expect(wrapper.vm.chart.setOption).toHaveBeenCalledTimes(2);
123
127
  expect(wrapper.vm.chart.setOption).toHaveBeenCalledWith(options);
124
128
  });
125
129
 
@@ -142,4 +146,17 @@ describe('chart component', () => {
142
146
  });
143
147
  });
144
148
  });
149
+
150
+ describe('with toolbox in options', () => {
151
+ it('increases grid top by `toolboxHeight`', async () => {
152
+ const optionsWithToolbox = { toolbox: {} };
153
+ wrapper = shallowMount(Chart, { propsData: { options: optionsWithToolbox } });
154
+ await wrapper.vm.$nextTick();
155
+
156
+ expect(wrapper.vm.chart.setOption).toHaveBeenCalledWith({
157
+ ...optionsWithToolbox,
158
+ grid: { top: toolboxHeight },
159
+ });
160
+ });
161
+ });
145
162
  });
@@ -1,7 +1,12 @@
1
1
  <!-- eslint-disable vue/multi-word-component-names -->
2
2
  <script>
3
3
  import * as echarts from 'echarts';
4
- import { defaultHeight, defaultWidth, validRenderers } from '../../../utils/charts/config';
4
+ import {
5
+ defaultHeight,
6
+ defaultWidth,
7
+ validRenderers,
8
+ toolboxHeight,
9
+ } from '../../../utils/charts/config';
5
10
  import { createTheme, themeName } from '../../../utils/charts/theme';
6
11
  import { GlResizeObserverDirective } from '../../../directives/resize_observer/resize_observer';
7
12
 
@@ -11,6 +16,16 @@ import { GlResizeObserverDirective } from '../../../directives/resize_observer/r
11
16
  */
12
17
  const sizeValidator = (size) => Number.isFinite(size) || size === 'auto' || size == null;
13
18
 
19
+ const isChartWithToolbox = (options) => options?.toolbox !== undefined;
20
+
21
+ const increaseChartGridTop = (options, increaseBy) => ({
22
+ ...options,
23
+ grid: {
24
+ ...options.grid,
25
+ top: (options?.grid?.top || 0) + increaseBy,
26
+ },
27
+ });
28
+
14
29
  export default {
15
30
  directives: {
16
31
  resizeObserver: GlResizeObserverDirective,
@@ -78,6 +93,13 @@ export default {
78
93
  chart: null,
79
94
  };
80
95
  },
96
+ computed: {
97
+ normalizedOptions() {
98
+ return isChartWithToolbox(this.options)
99
+ ? increaseChartGridTop(this.options, toolboxHeight)
100
+ : this.options;
101
+ },
102
+ },
81
103
  watch: {
82
104
  options() {
83
105
  if (this.chart) {
@@ -123,7 +145,7 @@ export default {
123
145
  },
124
146
  methods: {
125
147
  draw() {
126
- this.chart.setOption(this.options);
148
+ this.chart.setOption(this.normalizedOptions);
127
149
  /**
128
150
  * Emitted after calling `echarts.setOption`
129
151
  */
@@ -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';
@@ -14,6 +14,7 @@ export const defaultFontSize = 12;
14
14
  export const defaultHeight = 400;
15
15
  export const defaultWidth = 300;
16
16
  export const validRenderers = ['canvas', 'svg'];
17
+ export const toolboxHeight = 14;
17
18
 
18
19
  export const axes = {
19
20
  name: 'Value',
@@ -40,6 +40,8 @@ import {
40
40
  dataVizOrange800,
41
41
  dataVizOrange950,
42
42
  dataVizOrange900,
43
+ glBorderRadiusBase,
44
+ white,
43
45
  } from '../../../scss_to_js/scss_variables';
44
46
  import { scrollHandleSvgPath } from '../svgs/svg_paths';
45
47
  import { hexToRgba } from '../utils';
@@ -209,6 +211,9 @@ export const createTheme = (options = {}) => ({
209
211
  iconStyle: {
210
212
  borderWidth: 0,
211
213
  color: gray900,
214
+ textBackgroundColor: white,
215
+ textBorderRadius: glBorderRadiusBase,
216
+ textPadding: [8, 12],
212
217
  },
213
218
  },
214
219
  iconStyle: {