@gitlab/ui 55.2.1 → 55.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gitlab/ui",
3
- "version": "55.2.1",
3
+ "version": "55.3.1",
4
4
  "description": "GitLab UI Components",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -0,0 +1,2 @@
1
+ export const DISCLOSURE_DROPDOWN_ITEM_NAME = 'GlDisclosureDropdownItem';
2
+ export const DISCLOSURE_DROPDOWN_GROUP_NAME = 'GlDisclosureDropdownGroup';
@@ -22,6 +22,7 @@ describe('GlDisclosureDropdown', () => {
22
22
  const buildWrapper = (propsData, slots = {}) => {
23
23
  wrapper = mount(GlDisclosureDropdown, {
24
24
  propsData,
25
+ components: { GlDisclosureDropdownItem, GlDisclosureDropdownGroup },
25
26
  slots,
26
27
  attachTo: document.body,
27
28
  });
@@ -216,23 +217,65 @@ describe('GlDisclosureDropdown', () => {
216
217
  });
217
218
  });
218
219
 
219
- describe('disclosure options', () => {
220
- it('should render the `ul` as content tag and not add `role` attribute when it is a list of items only', () => {
220
+ describe('disclosure tag', () => {
221
+ it('should render `ul` as content tag when items is a list of items', () => {
221
222
  buildWrapper({ items: mockItems });
222
223
  expect(findDisclosureContent().element.tagName).toBe('UL');
223
- expect(findDisclosureContent().attributes('role')).toBeUndefined();
224
224
  });
225
225
 
226
- it('should render the `div` as content tag and add `role` attribute when it is a list of groups', () => {
226
+ it('should render `ul` as content tag when items is a list of groups', () => {
227
227
  buildWrapper({ items: mockGroups });
228
+ expect(findDisclosureContent().element.tagName).toBe('UL');
229
+ });
230
+
231
+ it('should render `ul` as content tag when default slot contains only groups', () => {
232
+ const slots = {
233
+ default: `
234
+ <gl-disclosure-dropdown-group>
235
+ <gl-disclosure-dropdown-item>Item</gl-disclosure-dropdown-item>
236
+ <gl-disclosure-dropdown-item>Item</gl-disclosure-dropdown-item>
237
+ </gl-disclosure-dropdown-group>
238
+ <gl-disclosure-dropdown-group>
239
+ <gl-disclosure-dropdown-item>Item</gl-disclosure-dropdown-item>
240
+ </gl-disclosure-dropdown-group>
241
+ `,
242
+ };
243
+
244
+ buildWrapper({}, slots);
245
+ expect(findDisclosureContent().element.tagName).toBe('UL');
246
+ });
247
+
248
+ it('should render `ul` as content tag when default slot contains only items', () => {
249
+ const slots = {
250
+ default: `
251
+ <gl-disclosure-dropdown-item>Item</gl-disclosure-dropdown-item>
252
+ <gl-disclosure-dropdown-item>Item</gl-disclosure-dropdown-item>
253
+ <gl-disclosure-dropdown-item>Item</gl-disclosure-dropdown-item>
254
+ `,
255
+ };
256
+
257
+ buildWrapper({}, slots);
258
+ expect(findDisclosureContent().element.tagName).toBe('UL');
259
+ });
260
+
261
+ it('should render `div` as content tag when default slot does not contain valid list item', () => {
262
+ const slots = {
263
+ default: `
264
+ <div>Item</div>
265
+ <gl-disclosure-dropdown-group>
266
+ <gl-disclosure-dropdown-item>Item</gl-disclosure-dropdown-item>
267
+ <gl-disclosure-dropdown-item>Item</gl-disclosure-dropdown-item>
268
+ </gl-disclosure-dropdown-group>
269
+ `,
270
+ };
271
+
272
+ buildWrapper({}, slots);
228
273
  expect(findDisclosureContent().element.tagName).toBe('DIV');
229
- expect(findDisclosureContent().attributes('role')).toBe('group');
230
274
  });
231
275
 
232
- it('should render the `div` as content tag and NOT add `role` otherwise', () => {
233
- buildWrapper({ items: null }, { default: 'Some other content' });
276
+ it('should render `div` as content tag when slot is not a list item', () => {
277
+ buildWrapper({}, { default: 'Some other content' });
234
278
  expect(findDisclosureContent().element.tagName).toBe('DIV');
235
- expect(findDisclosureContent().attributes('role')).toBeUndefined();
236
279
  });
237
280
  });
238
281
  });
@@ -52,13 +52,16 @@ function openDisclosure(component) {
52
52
  });
53
53
  }
54
54
 
55
- const template = (content, { bindingOverrides = {} } = {}) => `
55
+ const template = (content, { bindingOverrides = {} } = {}, after) => `
56
+ <div>
56
57
  <gl-disclosure-dropdown
57
58
  ref="disclosure"
58
59
  ${makeBindings(bindingOverrides)}
59
60
  >
60
61
  ${content || ''}
61
62
  </gl-disclosure-dropdown>
63
+ ${after || ''}
64
+ </div>
62
65
  `;
63
66
 
64
67
  const TOGGLE_ID = 'custom-toggle-id';
@@ -217,15 +220,14 @@ CustomGroupsAndItems.args = {
217
220
  CustomGroupsAndItems.decorators = [makeContainer({ height: '200px' })];
218
221
 
219
222
  export const CustomGroupsItemsAndToggle = makeGroupedExample({
220
- template: template(`
221
- <template #toggle>
222
- <span class="gl-sr-only">
223
- Orange Fox user's menu
224
- </span>
225
- <gl-avatar :size="32" entity-name="Orange Fox" aria-hidden="true"></gl-avatar>
226
- </template>
227
-
228
- <div role="group">
223
+ template: template(
224
+ `
225
+ <template #toggle>
226
+ <span class="gl-sr-only">
227
+ Orange Fox user's menu
228
+ </span>
229
+ <gl-avatar :size="32" entity-name="Orange Fox" aria-hidden="true"></gl-avatar>
230
+ </template>
229
231
  <gl-disclosure-dropdown-group>
230
232
  <gl-disclosure-dropdown-item>
231
233
  <span class="gl-display-flex gl-flex-direction-column">
@@ -256,15 +258,18 @@ export const CustomGroupsItemsAndToggle = makeGroupedExample({
256
258
  <gl-toggle label="New navigation" label-position="left" v-model="newNavigation"/>
257
259
  </gl-disclosure-dropdown-item>
258
260
  <gl-disclosure-dropdown-item @action="toggleModalVisibility(true)">
259
- <a>Provide feedback</a>
261
+ Provide feedback
260
262
  </gl-disclosure-dropdown-item>
261
263
  </gl-disclosure-dropdown-group>
262
264
  <gl-disclosure-dropdown-group bordered :group="$options.groups[1]"> </gl-disclosure-dropdown-group>
265
+ `,
266
+ {},
267
+ `
263
268
  <gl-modal :visible="feedBackModalVisible" @change="toggleModalVisibility" modal-id="feedbackModal" size="sm">
264
269
  <textarea class="gl-w-full">Tell us what you think!</textarea>
265
270
  </gl-modal>
266
- </div>
267
- `),
271
+ `
272
+ ),
268
273
  data() {
269
274
  return {
270
275
  newNavigation: true,
@@ -19,7 +19,7 @@ import {
19
19
  import GlBaseDropdown from '../base_dropdown/base_dropdown.vue';
20
20
  import GlDisclosureDropdownItem, { ITEM_CLASS } from './disclosure_dropdown_item.vue';
21
21
  import GlDisclosureDropdownGroup from './disclosure_dropdown_group.vue';
22
- import { itemsValidator, isItem, isAllItems, isAllGroups } from './utils';
22
+ import { itemsValidator, isItem, hasOnlyListItems } from './utils';
23
23
 
24
24
  export default {
25
25
  events: {
@@ -169,22 +169,11 @@ export default {
169
169
  };
170
170
  },
171
171
  computed: {
172
- disclosureOptions() {
173
- if (this.items) {
174
- if (isAllItems(this.items)) {
175
- return {
176
- tag: 'ul',
177
- };
178
- }
179
-
180
- if (isAllGroups(this.items))
181
- return {
182
- tag: 'div',
183
- role: 'group',
184
- };
172
+ disclosureTag() {
173
+ if (this.items?.length || hasOnlyListItems(this.$scopedSlots)) {
174
+ return 'ul';
185
175
  }
186
-
187
- return { tag: 'div' };
176
+ return 'div';
188
177
  },
189
178
  hasCustomToggle() {
190
179
  return Boolean(this.$scopedSlots.toggle);
@@ -305,10 +294,9 @@ export default {
305
294
  <slot name="header"></slot>
306
295
 
307
296
  <component
308
- :is="disclosureOptions.tag"
297
+ :is="disclosureTag"
309
298
  :id="disclosureId"
310
299
  ref="content"
311
- :role="disclosureOptions.role"
312
300
  :aria-labelledby="listAriaLabelledBy || toggleId"
313
301
  data-testid="disclosure-content"
314
302
  class="gl-new-dropdown-contents"
@@ -2,10 +2,12 @@
2
2
  import { uniqueId } from 'lodash';
3
3
  import GlDisclosureDropdownItem from './disclosure_dropdown_item.vue';
4
4
  import { isGroup } from './utils';
5
+ import { DISCLOSURE_DROPDOWN_GROUP_NAME } from './constants';
5
6
 
6
7
  export const GROUP_TOP_BORDER_CLASSES = 'gl-border-t gl-pt-2 gl-mt-2';
7
8
 
8
9
  export default {
10
+ name: DISCLOSURE_DROPDOWN_GROUP_NAME,
9
11
  components: {
10
12
  GlDisclosureDropdownItem,
11
13
  },
@@ -52,7 +54,7 @@ export default {
52
54
  </script>
53
55
 
54
56
  <template>
55
- <div :class="borderClass">
57
+ <li :class="borderClass">
56
58
  <div
57
59
  v-if="showHeader"
58
60
  :id="nameId"
@@ -73,5 +75,5 @@ export default {
73
75
  </gl-disclosure-dropdown-item>
74
76
  </slot>
75
77
  </ul>
76
- </div>
78
+ </li>
77
79
  </template>
@@ -2,10 +2,12 @@
2
2
  import { ENTER, SPACE } from '../constants';
3
3
  import { stopEvent } from '../../../../utils/utils';
4
4
  import { isItem } from './utils';
5
+ import { DISCLOSURE_DROPDOWN_ITEM_NAME } from './constants';
5
6
 
6
7
  export const ITEM_CLASS = 'gl-new-dropdown-item';
7
8
 
8
9
  export default {
10
+ name: DISCLOSURE_DROPDOWN_ITEM_NAME,
9
11
  ITEM_CLASS,
10
12
  props: {
11
13
  item: {
@@ -1,3 +1,6 @@
1
+ import { isFunction } from 'lodash';
2
+ import { DISCLOSURE_DROPDOWN_ITEM_NAME, DISCLOSURE_DROPDOWN_GROUP_NAME } from './constants';
3
+
1
4
  const itemValidator = (item) => item?.text?.length > 0 && !Array.isArray(item?.items);
2
5
 
3
6
  const isItem = (item) => Boolean(item) && itemValidator(item);
@@ -10,8 +13,30 @@ const isGroup = (group) =>
10
13
 
11
14
  const itemsValidator = (items) => items.every(isItem) || items.every(isGroup);
12
15
 
13
- const isAllItems = (items) => items.every(isItem);
16
+ const isListItem = (tag) =>
17
+ ['gl-disclosure-dropdown-group', 'gl-disclosure-dropdown-item', 'li'].includes(tag);
18
+
19
+ const isValidSlotTagVue2 = (vNode) =>
20
+ Boolean(vNode) && isListItem(vNode.componentOptions?.tag || vNode.tag);
21
+
22
+ const isValidSlotTag = (vNode) => {
23
+ return (
24
+ [DISCLOSURE_DROPDOWN_ITEM_NAME, DISCLOSURE_DROPDOWN_GROUP_NAME].includes(vNode.type?.name) ||
25
+ vNode.type === 'li'
26
+ );
27
+ };
14
28
 
15
- const isAllGroups = (items) => items.every(isGroup);
29
+ const hasOnlyListItems = ({ default: defaultSlot }) => {
30
+ if (!isFunction(defaultSlot)) {
31
+ return false;
32
+ }
33
+ const nodes = defaultSlot();
34
+ return (
35
+ Array.isArray(nodes) &&
36
+ nodes.filter((vNode) => vNode.tag).length &&
37
+ (nodes.filter((vNode) => vNode.tag).every(isValidSlotTagVue2) ||
38
+ nodes.filter((vNode) => vNode.tag).every(isValidSlotTag))
39
+ );
40
+ };
16
41
 
17
- export { itemsValidator, isItem, isGroup, isAllItems, isAllGroups };
42
+ export { itemsValidator, isItem, isGroup, hasOnlyListItems };
@@ -44,7 +44,7 @@
44
44
  }
45
45
 
46
46
  .gl-toggle-label,
47
- .gl-help-label {
47
+ .gl-description-label {
48
48
  @include gl-text-gray-500;
49
49
  }
50
50
  }
@@ -73,7 +73,6 @@
73
73
  }
74
74
 
75
75
  .gl-toggle-label {
76
- @include gl-mb-3;
77
76
  @include gl-font-weight-bold;
78
77
  }
79
78
 
@@ -8,6 +8,7 @@ describe('toggle', () => {
8
8
  let wrapper;
9
9
 
10
10
  const label = 'toggle label';
11
+ const descriptionText = 'description text';
11
12
  const helpText = 'help text';
12
13
 
13
14
  const createWrapper = (props = {}, options = {}) => {
@@ -21,6 +22,7 @@ describe('toggle', () => {
21
22
  };
22
23
 
23
24
  const findButton = () => wrapper.find('button');
25
+ const findDescriptionElement = () => wrapper.find('[data-testid="toggle-description"]');
24
26
  const findHelpElement = () => wrapper.find('[data-testid="toggle-help"]');
25
27
 
26
28
  it('has role=switch', () => {
@@ -85,6 +87,28 @@ describe('toggle', () => {
85
87
  });
86
88
  });
87
89
 
90
+ describe.each`
91
+ state | description | props | options
92
+ ${'with description'} | ${descriptionText} | ${{ description: descriptionText }} | ${undefined}
93
+ ${'with description in slot'} | ${descriptionText} | ${undefined} | ${{ slots: { description: descriptionText } }}
94
+ ${'without description'} | ${undefined} | ${undefined} | ${undefined}
95
+ ${'with description and labelPosition left'} | ${undefined} | ${{ desciption: descriptionText, labelPosition: toggleLabelPosition.left }} | ${undefined}
96
+ `('$state', ({ description, props, options }) => {
97
+ beforeEach(() => {
98
+ createWrapper(props, options);
99
+ });
100
+
101
+ if (description) {
102
+ it('shows description', () => {
103
+ expect(findDescriptionElement().text()).toBe(description);
104
+ });
105
+ } else {
106
+ it('does not show description', () => {
107
+ expect(findDescriptionElement().exists()).toBe(false);
108
+ });
109
+ }
110
+ });
111
+
88
112
  describe.each`
89
113
  state | help | props | options | getAriaDescribedBy
90
114
  ${'with help'} | ${helpText} | ${{ help: helpText }} | ${undefined} | ${() => findHelpElement().attributes('id')}
@@ -5,7 +5,9 @@ import readme from './toggle.md';
5
5
 
6
6
  const defaultValue = (prop) => GlToggle.props[prop].default;
7
7
 
8
- const longHelp = `This is a toggle component with a long help message.
8
+ const withDescription = 'A dark color theme that is easier on the eyes.';
9
+
10
+ const longHelp = `This is a toggle component with a long help message.
9
11
  You can notice how the text wraps when the width of the container
10
12
  is not enough to fix the entire text.`;
11
13
 
@@ -15,7 +17,8 @@ const generateProps = ({
15
17
  isLoading = defaultValue('isLoading'),
16
18
  label = 'Dark mode',
17
19
  labelId = 'dark-mode-toggle',
18
- help = 'Toggle dark mode for the website',
20
+ description = '',
21
+ help = 'Toggle dark mode for the website.',
19
22
  labelPosition = defaultValue('labelPosition'),
20
23
  } = {}) => ({
21
24
  value,
@@ -23,6 +26,7 @@ const generateProps = ({
23
26
  isLoading,
24
27
  label,
25
28
  labelId,
29
+ description,
26
30
  help,
27
31
  labelPosition,
28
32
  });
@@ -35,6 +39,7 @@ const Template = (args, { argTypes }) => ({
35
39
  <gl-toggle
36
40
  v-model="value"
37
41
  :disabled="disabled"
42
+ :description="description"
38
43
  :help="help"
39
44
  :label-id="labelId"
40
45
  :is-loading="isLoading"
@@ -47,11 +52,21 @@ const Template = (args, { argTypes }) => ({
47
52
  export const Default = Template.bind({});
48
53
  Default.args = generateProps();
49
54
 
55
+ export const WithDescription = Template.bind({});
56
+ WithDescription.args = generateProps({
57
+ description: withDescription,
58
+ });
59
+
50
60
  export const WithLongHelp = Template.bind({});
51
61
  WithLongHelp.args = generateProps({
52
62
  help: longHelp,
53
63
  });
54
64
 
65
+ export const LabelPositionLeft = Template.bind({});
66
+ LabelPositionLeft.args = generateProps({
67
+ labelPosition: 'left',
68
+ });
69
+
55
70
  export default {
56
71
  title: 'base/toggle',
57
72
  component: GlToggle,
@@ -71,6 +86,9 @@ export default {
71
86
  label: {
72
87
  control: 'text',
73
88
  },
89
+ description: {
90
+ control: 'text',
91
+ },
74
92
  help: {
75
93
  control: 'text',
76
94
  },
@@ -55,6 +55,14 @@ export default {
55
55
  type: String,
56
56
  required: true,
57
57
  },
58
+ /**
59
+ * The toggle's description.
60
+ */
61
+ description: {
62
+ type: String,
63
+ required: false,
64
+ default: undefined,
65
+ },
58
66
  /**
59
67
  * A help text to be shown below the toggle.
60
68
  */
@@ -81,10 +89,20 @@ export default {
81
89
  };
82
90
  },
83
91
  computed: {
92
+ shouldRenderDescription() {
93
+ // eslint-disable-next-line @gitlab/vue-prefer-dollar-scopedslots
94
+ return Boolean(this.$scopedSlots.description || this.description) && this.isVerticalLayout;
95
+ },
84
96
  shouldRenderHelp() {
85
97
  // eslint-disable-next-line @gitlab/vue-prefer-dollar-scopedslots
86
98
  return Boolean(this.$slots.help || this.help) && this.isVerticalLayout;
87
99
  },
100
+ toggleClasses() {
101
+ return [
102
+ { 'gl-sr-only': this.labelPosition === 'hidden' },
103
+ this.shouldRenderDescription ? 'gl-mb-2' : 'gl-mb-3',
104
+ ];
105
+ },
88
106
  icon() {
89
107
  return this.value ? 'mobile-issue-close' : 'close';
90
108
  },
@@ -132,13 +150,21 @@ export default {
132
150
  >
133
151
  <span
134
152
  :id="labelId"
135
- :class="{ 'gl-sr-only': labelPosition === 'hidden' }"
153
+ :class="toggleClasses"
136
154
  class="gl-toggle-label gl-flex-shrink-0"
137
155
  data-testid="toggle-label"
138
156
  >
139
157
  <!-- @slot The toggle's label. -->
140
158
  <slot name="label">{{ label }}</slot>
141
159
  </span>
160
+ <span
161
+ v-if="shouldRenderDescription"
162
+ class="gl-description-label gl-mb-3"
163
+ data-testid="toggle-description"
164
+ >
165
+ <!-- @slot A description text to be shown below the label. -->
166
+ <slot name="description">{{ description }}</slot>
167
+ </span>
142
168
  <input v-if="name" :name="name" :value="value" type="hidden" />
143
169
  <button
144
170
  role="switch"