@gitlab/ui 39.3.2 → 39.5.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.
Files changed (42) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/components/base/alert/alert.js +1 -1
  3. package/dist/components/base/filtered_search/filtered_search_term.js +2 -1
  4. package/dist/components/base/filtered_search/filtered_search_token.js +3 -2
  5. package/dist/components/base/filtered_search/filtered_search_token_segment.js +3 -2
  6. package/dist/components/base/new_dropdowns/base_dropdown/base_dropdown.js +240 -0
  7. package/dist/components/base/new_dropdowns/constants.js +20 -0
  8. package/dist/components/base/new_dropdowns/listbox/listbox.js +381 -0
  9. package/dist/components/base/new_dropdowns/listbox/listbox_item.js +77 -0
  10. package/dist/index.css +1 -1
  11. package/dist/index.css.map +1 -1
  12. package/dist/index.js +2 -0
  13. package/dist/utility_classes.css +1 -1
  14. package/dist/utility_classes.css.map +1 -1
  15. package/dist/utils/utils.js +24 -1
  16. package/package.json +5 -4
  17. package/src/components/base/alert/alert.spec.js +3 -1
  18. package/src/components/base/alert/alert.vue +1 -1
  19. package/src/components/base/dropdown/dropdown.scss +10 -3
  20. package/src/components/base/dropdown/dropdown_item.scss +1 -0
  21. package/src/components/base/filtered_search/filtered_search_term.vue +9 -1
  22. package/src/components/base/filtered_search/filtered_search_token.vue +16 -3
  23. package/src/components/base/filtered_search/filtered_search_token_segment.vue +5 -4
  24. package/src/components/base/link/link.stories.js +9 -7
  25. package/src/components/base/new_dropdowns/base_dropdown/base_dropdown.spec.js +171 -0
  26. package/src/components/base/new_dropdowns/base_dropdown/base_dropdown.vue +221 -0
  27. package/src/components/base/new_dropdowns/constants.js +22 -0
  28. package/src/components/base/new_dropdowns/listbox/listbox.md +71 -0
  29. package/src/components/base/new_dropdowns/listbox/listbox.spec.js +236 -0
  30. package/src/components/base/new_dropdowns/listbox/listbox.stories.js +276 -0
  31. package/src/components/base/new_dropdowns/listbox/listbox.vue +348 -0
  32. package/src/components/base/new_dropdowns/listbox/listbox_item.spec.js +104 -0
  33. package/src/components/base/new_dropdowns/listbox/listbox_item.vue +59 -0
  34. package/src/components/utilities/friendly_wrap/friendly_wrap.stories.js +10 -11
  35. package/src/components/utilities/sprintf/sprintf.stories.js +11 -9
  36. package/src/index.js +4 -0
  37. package/src/scss/utilities.scss +10 -0
  38. package/src/scss/utility-mixins/composite.scss +20 -0
  39. package/src/scss/utility-mixins/index.scss +1 -0
  40. package/src/utils/data_utils.js +2 -21
  41. package/src/utils/utils.js +18 -0
  42. package/src/utils/utils.spec.js +41 -1
@@ -0,0 +1,104 @@
1
+ import { mount } from '@vue/test-utils';
2
+ import GLIcon from '../../icon/icon.vue';
3
+
4
+ import { ENTER, SPACE } from '../constants';
5
+ import GlListboxItem from './listbox_item.vue';
6
+
7
+ describe('GlListboxItem', () => {
8
+ let wrapper;
9
+
10
+ const buildWrapper = (propsData, slots = {}) => {
11
+ wrapper = mount(GlListboxItem, {
12
+ propsData,
13
+ slots,
14
+ });
15
+ };
16
+ const findItem = () => wrapper.find('[role="option"]');
17
+ const findCheckIcon = () => findItem().findComponent(GLIcon);
18
+
19
+ describe('toggleSelection', () => {
20
+ describe('when selected ', () => {
21
+ beforeEach(() => {
22
+ buildWrapper({ isSelected: true });
23
+ });
24
+
25
+ it('should set `aria-selected` attribute on the list item to `true`', () => {
26
+ expect(findItem().attributes('aria-selected')).toBe('true');
27
+ });
28
+
29
+ it('should display check icon', () => {
30
+ expect(findCheckIcon().classes()).not.toContain('gl-visibility-hidden');
31
+ });
32
+
33
+ it('should emit the `select` event when clicked', async () => {
34
+ await findItem().trigger('click');
35
+ expect(wrapper.emitted('select')[0][0]).toBe(false);
36
+ });
37
+
38
+ it('should emit the `select` event on `ENTER` keydown event', async () => {
39
+ await findItem().trigger('click');
40
+ findItem().trigger('keydown', { code: SPACE });
41
+ expect(wrapper.emitted('select')[0][0]).toBe(false);
42
+ });
43
+
44
+ it('should emit the `select` event on `SPACE` keydown event', async () => {
45
+ await findItem().trigger('click');
46
+ findItem().trigger('keydown', { code: SPACE });
47
+ expect(wrapper.emitted('select')[0][0]).toBe(false);
48
+ });
49
+ });
50
+
51
+ describe('when not selected ', () => {
52
+ beforeEach(() => {
53
+ buildWrapper({ isSelected: false });
54
+ });
55
+
56
+ it('should set `aria-selected` attribute on the list item to `false`', () => {
57
+ expect(findItem().attributes('aria-selected')).toBeUndefined();
58
+ });
59
+
60
+ it('should not display check icon', () => {
61
+ expect(findCheckIcon().classes()).toContain('gl-visibility-hidden');
62
+ });
63
+
64
+ it('should emit the `select` event when clicked', async () => {
65
+ await findItem().trigger('click');
66
+ expect(wrapper.emitted('select')[0][0]).toBe(true);
67
+ });
68
+
69
+ it('should emit the `select` event on `ENTER` keydown event', async () => {
70
+ await findItem().trigger('click');
71
+ findItem().trigger('keydown', { code: ENTER });
72
+ expect(wrapper.emitted('select')[0][0]).toBe(true);
73
+ });
74
+
75
+ it('should emit the `select` event on `SPACE` keydown event', async () => {
76
+ await findItem().trigger('click');
77
+ findItem().trigger('keydown', { code: SPACE });
78
+ expect(wrapper.emitted('select')[0][0]).toBe(true);
79
+ });
80
+ });
81
+ });
82
+
83
+ describe('tabindex', () => {
84
+ it('should set tabindex to `-1` when item is not focused', () => {
85
+ buildWrapper({ isFocused: false });
86
+ expect(wrapper.attributes('tabindex')).toBe('-1');
87
+ });
88
+
89
+ it('should set tabindex to `0` when item is focused', () => {
90
+ buildWrapper({ isFocused: true });
91
+ expect(wrapper.attributes('tabindex')).toBe('0');
92
+ });
93
+ });
94
+
95
+ describe('when default slot content provided', () => {
96
+ const content = 'This is an item';
97
+ const slots = { default: content };
98
+
99
+ it('renders it', () => {
100
+ buildWrapper({}, slots);
101
+ expect(wrapper.text()).toContain(content);
102
+ });
103
+ });
104
+ });
@@ -0,0 +1,59 @@
1
+ <script>
2
+ import GlIcon from '../../icon/icon.vue';
3
+ import { ENTER, SPACE } from '../constants';
4
+ import { stopEvent } from '../../../../utils/utils';
5
+
6
+ export default {
7
+ components: {
8
+ GlIcon,
9
+ },
10
+ props: {
11
+ isSelected: {
12
+ type: Boolean,
13
+ default: false,
14
+ required: false,
15
+ },
16
+ isFocused: {
17
+ type: Boolean,
18
+ default: false,
19
+ required: false,
20
+ },
21
+ },
22
+ methods: {
23
+ toggleSelection() {
24
+ this.$emit('select', !this.isSelected);
25
+ },
26
+ onKeydown(event) {
27
+ const { code } = event;
28
+
29
+ if (code === ENTER || code === SPACE) {
30
+ stopEvent(event);
31
+ this.toggleSelection();
32
+ }
33
+ },
34
+ },
35
+ };
36
+ </script>
37
+
38
+ <template>
39
+ <li
40
+ class="gl-new-dropdown-item"
41
+ role="option"
42
+ :tabindex="isFocused ? 0 : -1"
43
+ :aria-selected="isSelected"
44
+ @click="toggleSelection"
45
+ @keydown="onKeydown"
46
+ >
47
+ <span class="dropdown-item">
48
+ <gl-icon
49
+ name="mobile-issue-close"
50
+ data-testid="dropdown-item-checkbox"
51
+ class="gl-mt-3 gl-align-self-start"
52
+ :class="['gl-new-dropdown-item-check-icon', { 'gl-visibility-hidden': !isSelected }]"
53
+ />
54
+ <span class="gl-new-dropdown-item-text-wrapper">
55
+ <slot></slot>
56
+ </span>
57
+ </span>
58
+ </li>
59
+ </template>
@@ -12,26 +12,26 @@ const generateProps = ({ text = '', symbols = defaultValue('symbols')() } = {})
12
12
  symbols,
13
13
  });
14
14
 
15
- const makeStory = (options = {}) => (args, { argTypes }) => ({
16
- components,
17
- props: Object.keys(argTypes),
18
- ...options,
19
- });
15
+ const makeStory =
16
+ (options = {}) =>
17
+ (args, { argTypes }) => ({
18
+ components,
19
+ props: Object.keys(argTypes),
20
+ ...options,
21
+ });
20
22
 
21
23
  export const Default = makeStory({
22
24
  template: `<gl-friendly-wrap :text="text" :symbols="symbols" />`,
23
25
  });
24
26
  Default.args = generateProps({
25
- text:
26
- '/lorem/ipsum/dolor/sit/amet/consectetur/adipiscing/elit/Aenean/tincidunt/urna/ac/tellus/cursus/laoreet/aenean/blandit/erat/vel/est/maximus/porta/Sed/id/nunc/non/sapien/cursus/ullamcorper',
27
+ text: '/lorem/ipsum/dolor/sit/amet/consectetur/adipiscing/elit/Aenean/tincidunt/urna/ac/tellus/cursus/laoreet/aenean/blandit/erat/vel/est/maximus/porta/Sed/id/nunc/non/sapien/cursus/ullamcorper',
27
28
  });
28
29
 
29
30
  export const BreakWord = makeStory({
30
31
  template: `<gl-friendly-wrap :text="text" :symbols="symbols" />`,
31
32
  });
32
33
  BreakWord.args = generateProps({
33
- text:
34
- 'LoremipsumdolorsitametLoremipsumdolorsitametLoremipsumdolorsitametLoremipsumdolorsitametLoremipsumdolorsitametLoremipsumdolorsitametLoremipsumdolorsitametLoremipsumdolorsitametLoremipsumdolorsitametLoremipsumdolorsitametLoremipsumdolorsitametLoremipsumdolorsitametLoremipsumdolorsitametLoremipsumdolorsitametLoremipsumdolorsitametLoremipsumdolorsitametLoremipsumdolorsitametLoremipsumdolorsitametLoremipsumdolorsitametLoremipsumdolorsitamet',
34
+ text: 'LoremipsumdolorsitametLoremipsumdolorsitametLoremipsumdolorsitametLoremipsumdolorsitametLoremipsumdolorsitametLoremipsumdolorsitametLoremipsumdolorsitametLoremipsumdolorsitametLoremipsumdolorsitametLoremipsumdolorsitametLoremipsumdolorsitametLoremipsumdolorsitametLoremipsumdolorsitametLoremipsumdolorsitametLoremipsumdolorsitametLoremipsumdolorsitametLoremipsumdolorsitametLoremipsumdolorsitametLoremipsumdolorsitametLoremipsumdolorsitamet',
35
35
  symbols: ['dolor'],
36
36
  });
37
37
 
@@ -39,8 +39,7 @@ export const MultipleSymbols = makeStory({
39
39
  template: `<gl-friendly-wrap :text="text" :symbols="symbols" />`,
40
40
  });
41
41
  MultipleSymbols.args = generateProps({
42
- text:
43
- 'LoremipsumdolorsitametLoremipsumdolorsitametLoremipsumdolorsitametLoremipsumdolorsitametLoremipsumdolorsitametLoremipsumdolorsitametLoremipsumdolorsitametLoremipsumdolorsitametLoremipsumdolorsitametLoremipsumdolorsitametLoremipsumdolorsitametLoremipsumdolorsitametLoremipsumdolorsitametLoremipsumdolorsitametLoremipsumdolorsitametLoremipsumdolorsitametLoremipsumdolorsitametLoremipsumdolorsitametLoremipsumdolorsitametLoremipsumdolorsitamet',
42
+ text: 'LoremipsumdolorsitametLoremipsumdolorsitametLoremipsumdolorsitametLoremipsumdolorsitametLoremipsumdolorsitametLoremipsumdolorsitametLoremipsumdolorsitametLoremipsumdolorsitametLoremipsumdolorsitametLoremipsumdolorsitametLoremipsumdolorsitametLoremipsumdolorsitametLoremipsumdolorsitametLoremipsumdolorsitametLoremipsumdolorsitametLoremipsumdolorsitametLoremipsumdolorsitametLoremipsumdolorsitametLoremipsumdolorsitametLoremipsumdolorsitamet',
44
43
  symbols: ['e', 'o'],
45
44
  });
46
45
 
@@ -6,15 +6,17 @@ const generateProps = ({ message = 'Written by %{author}', placeholders } = {})
6
6
  placeholders,
7
7
  });
8
8
 
9
- const makeStory = (options) => (args, { argTypes }) => ({
10
- components: {
11
- GlSprintf,
12
- GlButton,
13
- GlLink,
14
- },
15
- props: Object.keys(argTypes),
16
- ...options,
17
- });
9
+ const makeStory =
10
+ (options) =>
11
+ (args, { argTypes }) => ({
12
+ components: {
13
+ GlSprintf,
14
+ GlButton,
15
+ GlLink,
16
+ },
17
+ props: Object.keys(argTypes),
18
+ ...options,
19
+ });
18
20
 
19
21
  export const SentenceWithLink = makeStory({
20
22
  template: `
package/src/index.js CHANGED
@@ -54,6 +54,10 @@ export { default as GlDropdownSectionHeader } from './components/base/dropdown/d
54
54
  export { default as GlDropdownDivider } from './components/base/dropdown/dropdown_divider.vue';
55
55
  export { default as GlDropdownText } from './components/base/dropdown/dropdown_text.vue';
56
56
  export { default as GlDropdown } from './components/base/dropdown/dropdown.vue';
57
+ // new components aiming to replace GlDropdown - start
58
+ export { default as GlListbox } from './components/base/new_dropdowns/listbox/listbox.vue';
59
+ export { default as GlListboxItem } from './components/base/new_dropdowns/listbox/listbox_item.vue';
60
+ // new components aiming to replace GlDropdown - end
57
61
  export { default as GlPath } from './components/base/path/path.vue';
58
62
  export { default as GlTable } from './components/base/table/table.vue';
59
63
  export { default as GlBreadcrumb } from './components/base/breadcrumb/breadcrumb.vue';
@@ -2484,6 +2484,16 @@
2484
2484
  filter: invert(0.8) hue-rotate(180deg) !important;
2485
2485
  }
2486
2486
  }
2487
+ .gl--flex-center{
2488
+ @include gl-display-flex;
2489
+ @include gl-align-items-center;
2490
+ @include gl-justify-content-center
2491
+ }
2492
+ .gl--flex-center\!{
2493
+ @include gl-display-flex;
2494
+ @include gl-align-items-center;
2495
+ @include gl-justify-content-center
2496
+ }
2487
2497
  .gl-content-empty {
2488
2498
  content: ''
2489
2499
  }
@@ -0,0 +1,20 @@
1
+ //
2
+ // Composite utilities
3
+ //
4
+ // naming convention: gl--{name}
5
+ // purpose: This is for composite classes that provide significant readability and maintainability profit.
6
+ //
7
+ // PLEASE NOTE: When considering to abstract a set of utility classes, please prefer, in order:
8
+ // 1. Not abstracting
9
+ // 2. Create a component
10
+ // 3. Create a constant scoped within a specific module
11
+ // 4. Create a composite class if there are many preexisting occurrences with a clear design responsibility
12
+ //
13
+ // notes:
14
+ // - Composite classes should be very short and not have any nested rules.
15
+ //
16
+ @mixin gl--flex-center {
17
+ @include gl-display-flex;
18
+ @include gl-align-items-center;
19
+ @include gl-justify-content-center;
20
+ }
@@ -22,6 +22,7 @@
22
22
  @import './border';
23
23
  @import './box-shadow';
24
24
  @import './color';
25
+ @import './composite';
25
26
  @import './content';
26
27
  @import './cursor';
27
28
  @import './display';
@@ -2,27 +2,8 @@ import curry from 'lodash/fp/curry';
2
2
 
3
3
  const getRepeatingValue = (index) => {
4
4
  const values = [
5
- 100,
6
- 500,
7
- 400,
8
- 200,
9
- 100,
10
- 800,
11
- 400,
12
- 500,
13
- 600,
14
- 300,
15
- 800,
16
- 900,
17
- 110,
18
- 700,
19
- 400,
20
- 300,
21
- 500,
22
- 300,
23
- 400,
24
- 600,
25
- 700,
5
+ 100, 500, 400, 200, 100, 800, 400, 500, 600, 300, 800, 900, 110, 700, 400, 300, 500, 300, 400,
6
+ 600, 700,
26
7
  ];
27
8
  return index < values.length ? values[index] : values[index % values.length];
28
9
  };
@@ -122,3 +122,21 @@ export function logWarning(message = '') {
122
122
  console.warn(message); // eslint-disable-line no-console
123
123
  }
124
124
  }
125
+
126
+ /**
127
+ * Stop default event handling and propagation
128
+ */
129
+ export function stopEvent(
130
+ event,
131
+ { preventDefault = true, stopPropagation = true, stopImmediatePropagation = false } = {}
132
+ ) {
133
+ if (preventDefault) {
134
+ event.preventDefault();
135
+ }
136
+ if (stopPropagation) {
137
+ event.stopPropagation();
138
+ }
139
+ if (stopImmediatePropagation) {
140
+ event.stopImmediatePropagation();
141
+ }
142
+ }
@@ -1,4 +1,4 @@
1
- import { isElementFocusable, focusFirstFocusableElement } from './utils';
1
+ import { isElementFocusable, focusFirstFocusableElement, stopEvent } from './utils';
2
2
 
3
3
  describe('isElementFocusable', () => {
4
4
  const myBtn = () => document.querySelector('button');
@@ -77,3 +77,43 @@ describe('focusFirstFocusableElement', () => {
77
77
  expect(document.activeElement).toBe(myInput());
78
78
  });
79
79
  });
80
+
81
+ describe('stopEvent', () => {
82
+ beforeEach(() => {
83
+ jest.clearAllMocks();
84
+ });
85
+
86
+ const event = {
87
+ preventDefault: jest.fn(),
88
+ stopPropagation: jest.fn(),
89
+ stopImmediatePropagation: jest.fn(),
90
+ };
91
+
92
+ it('calls preventDefault and stopPropagation by default', () => {
93
+ stopEvent(event);
94
+
95
+ expect(event.preventDefault).toHaveBeenCalledTimes(1);
96
+ expect(event.stopPropagation).toHaveBeenCalledTimes(1);
97
+ expect(event.stopImmediatePropagation).not.toHaveBeenCalled();
98
+ });
99
+
100
+ it('completely stops the event when stopImmediatePropagation is true', () => {
101
+ stopEvent(event, { stopImmediatePropagation: true });
102
+
103
+ expect(event.preventDefault).toHaveBeenCalledTimes(1);
104
+ expect(event.stopPropagation).toHaveBeenCalledTimes(1);
105
+ expect(event.stopImmediatePropagation).toHaveBeenCalledTimes(1);
106
+ });
107
+
108
+ it('calls event stop methods set to true', () => {
109
+ stopEvent(event, {
110
+ preventDefault: false,
111
+ stopPropagation: false,
112
+ stopImmediatePropagation: true,
113
+ });
114
+
115
+ expect(event.preventDefault).not.toHaveBeenCalled();
116
+ expect(event.stopPropagation).not.toHaveBeenCalled();
117
+ expect(event.stopImmediatePropagation).toHaveBeenCalledTimes(1);
118
+ });
119
+ });