@gitlab/ui 128.11.0 → 128.13.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.
@@ -53,6 +53,7 @@ const generateProps = () => makePropsConfigurable(sortKeys({
53
53
  label: makeProp(PROP_TYPE_STRING),
54
54
  labelClass: makeProp(PROP_TYPE_ARRAY_OBJECT_STRING),
55
55
  labelFor: makeProp(PROP_TYPE_STRING),
56
+ labelId: makeProp(PROP_TYPE_STRING),
56
57
  labelSize: makeProp(PROP_TYPE_STRING),
57
58
  labelSrOnly: makeProp(PROP_TYPE_BOOLEAN, false),
58
59
  tooltip: makeProp(PROP_TYPE_BOOLEAN, false),
@@ -209,7 +210,7 @@ const BFormGroup = {
209
210
  const isFieldset = !labelFor;
210
211
  let $label = h();
211
212
  const labelContent = normalizeSlot(SLOT_NAME_LABEL) || this.label;
212
- const labelId = labelContent ? safeId('_BV_label_') : null;
213
+ const labelId = labelContent ? this.labelId || safeId('_BV_label_') : null;
213
214
  if (labelContent || isHorizontal) {
214
215
  const {
215
216
  labelSize,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gitlab/ui",
3
- "version": "128.11.0",
3
+ "version": "128.13.0",
4
4
  "description": "GitLab UI Components",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -37,6 +37,7 @@
37
37
  "prebuild": "yarn build-tokens",
38
38
  "copy-fonts": "make copy-fonts",
39
39
  "build-tokens": "make tokens",
40
+ "sync-tokens": "node --env-file=.figma.env bin/figma_sync_tokens.mjs",
40
41
  "clean": "rm -r dist storybook",
41
42
  "cy:edge": "cypress run --browser edge --env grepTags=-@storybook",
42
43
  "cy:run": "cypress run --browser firefox --env grepTags=-@storybook",
@@ -131,7 +132,7 @@
131
132
  "autoprefixer": "10.4.24",
132
133
  "axe-playwright": "^2.2.2",
133
134
  "babel-loader": "^9.2.1",
134
- "cypress": "15.9.0",
135
+ "cypress": "15.10.0",
135
136
  "cypress-real-events": "^1.15.0",
136
137
  "dompurify": "^3.1.2",
137
138
  "emoji-regex": "^10.6.0",
@@ -142,12 +143,12 @@
142
143
  "jest-circus": "30.2.0",
143
144
  "jest-environment-jsdom": "30.2.0",
144
145
  "jest-image-snapshot": "^6.5.1",
145
- "merge-cobertura": "^1.0.4",
146
+ "merge-cobertura": "^1.0.5",
146
147
  "mockdate": "^3.0.5",
147
- "module-alias": "^2.2.3",
148
+ "module-alias": "^2.3.4",
148
149
  "pikaday": "^1.8.0",
149
- "playwright": "^1.58.1",
150
- "playwright-core": "^1.58.1",
150
+ "playwright": "^1.58.2",
151
+ "playwright-core": "^1.58.2",
151
152
  "postcss": "8.5.6",
152
153
  "postcss-loader": "8.2.0",
153
154
  "postcss-scss": "4.0.9",
@@ -165,7 +166,7 @@
165
166
  "start-server-and-test": "^2.1.3",
166
167
  "storybook": "^7.6.20",
167
168
  "storybook-dark-mode": "4.0.2",
168
- "style-dictionary": "^5.2.0",
169
+ "style-dictionary": "^5.3.0",
169
170
  "style-loader": "^4",
170
171
  "tailwindcss": "3.4.19",
171
172
  "vue": "2.7.16",
@@ -37,7 +37,7 @@ export default {
37
37
  default: '',
38
38
  },
39
39
  /**
40
- * Layout of label and text: 'horizontal' for inline/flex or 'vertical' for block.
40
+ * Layout of label and text: 'horizontal' for line or 'vertical' for stacked
41
41
  */
42
42
  layout: {
43
43
  type: String,
@@ -1,6 +1,7 @@
1
1
  <script>
2
2
  import isString from 'lodash/isString';
3
3
  import isPlainObject from 'lodash/isPlainObject';
4
+ import { uniqueId } from 'lodash';
4
5
  import { BFormGroup } from '../../../../vendor/bootstrap-vue/src/components/form-group/form-group';
5
6
 
6
7
  export default {
@@ -8,8 +9,11 @@ export default {
8
9
  components: {
9
10
  BFormGroup,
10
11
  },
11
- provide: {
12
- isInFormGroup: true,
12
+ provide() {
13
+ return {
14
+ // Only for internal use by @gitlab/ui components
15
+ getFormGroupInstance: () => this,
16
+ };
13
17
  },
14
18
  inheritAttrs: false,
15
19
  props: {
@@ -46,6 +50,11 @@ export default {
46
50
  default: '(optional)',
47
51
  },
48
52
  },
53
+ data() {
54
+ return {
55
+ labelId: uniqueId('gl-form-group-label-'),
56
+ };
57
+ },
49
58
  computed: {
50
59
  actualLabelClass() {
51
60
  const { labelClass } = this;
@@ -70,7 +79,12 @@ export default {
70
79
  };
71
80
  </script>
72
81
  <template>
73
- <b-form-group v-bind="$attrs" class="gl-form-group" :label-class="actualLabelClass">
82
+ <b-form-group
83
+ v-bind="$attrs"
84
+ class="gl-form-group"
85
+ :label-class="actualLabelClass"
86
+ :label-id="labelId"
87
+ >
74
88
  <template v-if="$attrs.label || $scopedSlots.label" #label>
75
89
  <slot name="label">
76
90
  {{ $attrs.label }}
@@ -1,5 +1,6 @@
1
1
  <script>
2
2
  import uniqueId from 'lodash/uniqueId';
3
+ import isBoolean from 'lodash/isBoolean';
3
4
  import {
4
5
  arrow,
5
6
  computePosition,
@@ -55,6 +56,11 @@ export default {
55
56
  GlIcon,
56
57
  },
57
58
  directives: { Outside: OutsideDirective },
59
+ inject: {
60
+ getFormGroupInstance: {
61
+ default: () => () => {},
62
+ },
63
+ },
58
64
  props: {
59
65
  toggleText: {
60
66
  type: String,
@@ -114,6 +120,14 @@ export default {
114
120
  required: false,
115
121
  default: false,
116
122
  },
123
+ /**
124
+ * Controls the validation state appearance of the component. `true` for valid, `false` for invalid, or `null` for no validation state
125
+ */
126
+ state: {
127
+ type: Boolean,
128
+ required: false,
129
+ default: null,
130
+ },
117
131
  placement: {
118
132
  type: String,
119
133
  required: false,
@@ -240,6 +254,15 @@ export default {
240
254
  isCaretOnly() {
241
255
  return !this.noCaret && !this.icon && this.hasNoVisibleToggleText;
242
256
  },
257
+ computedState() {
258
+ // If not a boolean, ensure that value is null
259
+ return isBoolean(this.state) ? this.state : null;
260
+ },
261
+ stateClass() {
262
+ if (this.computedState === true) return 'is-valid';
263
+ if (this.computedState === false) return 'is-invalid';
264
+ return null;
265
+ },
243
266
  isDefaultToggle() {
244
267
  return !this.$scopedSlots.toggle;
245
268
  },
@@ -293,21 +316,49 @@ export default {
293
316
  'gl-new-dropdown-toggle-no-caret': this.noCaret,
294
317
  'gl-new-dropdown-caret-only btn-icon': this.isCaretOnly,
295
318
  },
319
+ this.stateClass,
296
320
  ];
297
321
  },
298
322
  toggleButtonTextClasses() {
299
323
  return this.block ? 'gl-w-full' : '';
300
324
  },
325
+ // Set the aria-labelledby property with one or more ID strings
301
326
  toggleLabelledBy() {
327
+ const formGroupLabelId = this.getFormGroupInstance()?.labelId;
328
+
302
329
  if (this.isToggleCombobox) {
330
+ // Comboboxes announce label and self value when aria-labelledby is label ID.
331
+ // Tested with VoiceOver, NVDA, JAWS, Narrator and preferred browsers.
332
+ if (this.ariaLabelledby) {
333
+ return `${this.ariaLabelledby}`;
334
+ }
335
+
336
+ // Combobox inside GlFormGroup
337
+ if (formGroupLabelId) {
338
+ return `${formGroupLabelId} ${this.toggleId}`;
339
+ }
340
+
341
+ // Fallback calculated toggleId value
342
+ return this.toggleId;
343
+ }
344
+
345
+ if (!this.isToggleCombobox) {
346
+ // Disclosures or buttons with listbox require both IDs to announce correctly.
347
+ // Tested with VoiceOver, NVDA, JAWS, Narrator and preferred browsers.
303
348
  if (this.ariaLabelledby) {
304
349
  return `${this.ariaLabelledby} ${this.toggleId}`;
305
350
  }
351
+
352
+ // Disclosure or button with listbox inside GlFormGroup
353
+ if (formGroupLabelId) {
354
+ return `${formGroupLabelId} ${this.toggleId}`;
355
+ }
356
+
357
+ // Fallback calculated toggleId value
306
358
  return this.toggleId;
307
359
  }
308
360
 
309
- // For non-combobox toggles, combine IDs or use the button's own text
310
- return this.ariaLabelledby ? `${this.ariaLabelledby} ${this.toggleId}` : undefined;
361
+ return undefined;
311
362
  },
312
363
  toggleRole() {
313
364
  if (this.isToggleCombobox) {
@@ -558,7 +609,7 @@ export default {
558
609
  this.$emit(GL_DROPDOWN_SHOWN);
559
610
  } else {
560
611
  this.stopFloating();
561
- this.$emit(GL_DROPDOWN_HIDDEN);
612
+ this.$emit(GL_DROPDOWN_HIDDEN, event);
562
613
  }
563
614
 
564
615
  // this is to check whether `toggle` was prevented or not
@@ -35,6 +35,14 @@
35
35
  width: auto;
36
36
  }
37
37
  }
38
+
39
+ &.is-invalid {
40
+ @apply gl-border-control-error;
41
+
42
+ &:focus {
43
+ @apply gl-border-control-error;
44
+ }
45
+ }
38
46
  }
39
47
 
40
48
  .gl-new-dropdown-button-text {
@@ -67,8 +67,8 @@ export default {
67
67
  GlIntersectionObserver,
68
68
  },
69
69
  inject: {
70
- isInFormGroup: {
71
- default: false,
70
+ getFormGroupInstance: {
71
+ default: () => () => {},
72
72
  },
73
73
  },
74
74
  model: {
@@ -166,6 +166,14 @@ export default {
166
166
  required: false,
167
167
  default: false,
168
168
  },
169
+ /**
170
+ * Controls the validation state appearance of the component. `true` for valid, `false` for invalid, or `null` for no validation state
171
+ */
172
+ state: {
173
+ type: Boolean,
174
+ required: false,
175
+ default: null,
176
+ },
169
177
  /**
170
178
  * Set to "true" when dropdown content (items) is loading
171
179
  * It will render a small loader in the dropdown toggle and make it disabled
@@ -417,6 +425,9 @@ export default {
417
425
  // Fallback. Return a header ID or the toggle button ID.
418
426
  return this.listAriaLabelledBy || this.headerId || this.toggleIdComputed;
419
427
  },
428
+ isInFormGroup() {
429
+ return Boolean(this.getFormGroupInstance());
430
+ },
420
431
  toggleIdComputed() {
421
432
  return this.toggleId || uniqueId('dropdown-toggle-btn-');
422
433
  },
@@ -684,7 +695,7 @@ export default {
684
695
  */
685
696
  this.$emit(GL_DROPDOWN_SHOWN);
686
697
  },
687
- onHide() {
698
+ onHide(event) {
688
699
  /**
689
700
  * Emitted when dropdown is hidden
690
701
  *
@@ -692,6 +703,8 @@ export default {
692
703
  */
693
704
  this.$emit(GL_DROPDOWN_HIDDEN);
694
705
  this.nextFocusedItemIndex = null;
706
+ // Emit native blur event for form validation in FormGroup
707
+ this.$emit('blur', event);
695
708
  },
696
709
  getNextIndex(currentIndex, keyCode, totalLength) {
697
710
  // For UP: move up or wrap to end
@@ -974,6 +987,7 @@ export default {
974
987
  :has-searchable-listbox="searchable"
975
988
  :has-external-label="isInFormGroup"
976
989
  :listbox-id="listboxIdComputed"
990
+ :state="state"
977
991
  :toggle-id="toggleIdComputed"
978
992
  :toggle-text="listboxToggleText"
979
993
  :toggle-class="toggleButtonClasses"
@@ -74,6 +74,7 @@ export const generateProps = () =>
74
74
  label: makeProp(PROP_TYPE_STRING),
75
75
  labelClass: makeProp(PROP_TYPE_ARRAY_OBJECT_STRING),
76
76
  labelFor: makeProp(PROP_TYPE_STRING),
77
+ labelId: makeProp(PROP_TYPE_STRING),
77
78
  labelSize: makeProp(PROP_TYPE_STRING),
78
79
  labelSrOnly: makeProp(PROP_TYPE_BOOLEAN, false),
79
80
  tooltip: makeProp(PROP_TYPE_BOOLEAN, false),
@@ -241,7 +242,7 @@ export const BFormGroup = {
241
242
 
242
243
  let $label = h()
243
244
  const labelContent = normalizeSlot(SLOT_NAME_LABEL) || this.label
244
- const labelId = labelContent ? safeId('_BV_label_') : null
245
+ const labelId = labelContent ? this.labelId || safeId('_BV_label_') : null
245
246
  if (labelContent || isHorizontal) {
246
247
  const { labelSize, labelColProps } = this
247
248
  const labelTag = isFieldset ? 'legend' : 'label'