@gitlab/ui 127.1.1 → 128.0.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.
Files changed (20) hide show
  1. package/dist/components/base/form/form_group/form_group.js +3 -0
  2. package/dist/components/base/new_dropdowns/base_dropdown/base_dropdown.js +95 -20
  3. package/dist/components/base/new_dropdowns/disclosure/disclosure_dropdown.js +1 -1
  4. package/dist/components/base/new_dropdowns/listbox/listbox.js +47 -10
  5. package/package.json +1 -1
  6. package/src/components/base/form/form_checkbox/form_checkbox.vue +0 -3
  7. package/src/components/base/form/form_checkbox/form_checkbox_group.vue +148 -21
  8. package/src/components/base/form/form_group/form_group.vue +3 -0
  9. package/src/components/base/new_dropdowns/base_dropdown/base_dropdown.vue +101 -22
  10. package/src/components/base/new_dropdowns/disclosure/disclosure_dropdown.vue +3 -2
  11. package/src/components/base/new_dropdowns/listbox/listbox.vue +52 -9
  12. package/src/scss/bootstrap_vue.scss +0 -1
  13. package/src/vendor/bootstrap-vue/src/constants/components.js +0 -2
  14. package/src/vendor/bootstrap-vue/src/mixins/form-radio-check-group.js +1 -2
  15. package/src/vendor/bootstrap-vue/src/components/form-checkbox/_form-checkbox-group.scss +0 -1
  16. package/src/vendor/bootstrap-vue/src/components/form-checkbox/_form-checkbox.scss +0 -125
  17. package/src/vendor/bootstrap-vue/src/components/form-checkbox/form-checkbox-group.js +0 -42
  18. package/src/vendor/bootstrap-vue/src/components/form-checkbox/form-checkbox.js +0 -132
  19. package/src/vendor/bootstrap-vue/src/components/form-checkbox/index.js +0 -4
  20. package/src/vendor/bootstrap-vue/src/components/form-checkbox/index.scss +0 -2
@@ -8,6 +8,9 @@ var script = {
8
8
  components: {
9
9
  BFormGroup
10
10
  },
11
+ provide: {
12
+ isInFormGroup: true
13
+ },
11
14
  inheritAttrs: false,
12
15
  props: {
13
16
  /**
@@ -106,6 +106,26 @@ var script = {
106
106
  return ['menu', 'listbox', 'tree', 'grid', 'dialog', true, false].includes(value);
107
107
  }
108
108
  },
109
+ activeItemId: {
110
+ type: String,
111
+ required: false,
112
+ default: undefined
113
+ },
114
+ hasExternalLabel: {
115
+ type: Boolean,
116
+ required: false,
117
+ default: false
118
+ },
119
+ hasSearchableListbox: {
120
+ type: Boolean,
121
+ required: false,
122
+ default: false
123
+ },
124
+ isDisclosure: {
125
+ type: Boolean,
126
+ required: false,
127
+ default: false
128
+ },
109
129
  /**
110
130
  * Id that will be referenced by `aria-labelledby` attribute of the dropdown content`
111
131
  */
@@ -113,6 +133,15 @@ var script = {
113
133
  type: String,
114
134
  required: true
115
135
  },
136
+ /**
137
+ * Span Id that will be referenced by listbox `aria-labelledby` if there's an external label.
138
+ * This prop will only be defined by `GlCollapsibleListbox` when `isInFormGroup` injected is true.
139
+ */
140
+ listboxId: {
141
+ type: String,
142
+ required: false,
143
+ default: undefined
144
+ },
116
145
  /**
117
146
  * The `aria-labelledby` attribute value for the toggle `button`
118
147
  */
@@ -157,6 +186,10 @@ var script = {
157
186
  };
158
187
  },
159
188
  computed: {
189
+ ariaActiveDescendant() {
190
+ if (!this.isDisclosure && this.visible) return this.activeItemId;
191
+ return undefined;
192
+ },
160
193
  hasNoVisibleToggleText() {
161
194
  var _this$toggleText;
162
195
  return !((_this$toggleText = this.toggleText) !== null && _this$toggleText !== void 0 && _this$toggleText.length) || this.textSrOnly;
@@ -170,12 +203,47 @@ var script = {
170
203
  isCaretOnly() {
171
204
  return !this.noCaret && !this.icon && this.hasNoVisibleToggleText;
172
205
  },
173
- ariaAttributes() {
206
+ isDefaultToggle() {
207
+ return !this.$scopedSlots.toggle;
208
+ },
209
+ isToggleCombobox() {
210
+ if (this.hasSearchableListbox || this.isDisclosure) {
211
+ return false;
212
+ }
213
+ return true;
214
+ },
215
+ isToggleLabelledExternally() {
216
+ if (this.hasExternalLabel && this.toggleId) return true;
217
+ return false;
218
+ },
219
+ computedToggleId() {
220
+ if (this.isDisclosure) {
221
+ return this.isDefaultToggle ? this.toggleId : undefined;
222
+ }
223
+ if (this.isToggleLabelledExternally) {
224
+ return this.isDefaultToggle ? this.toggleId : undefined;
225
+ }
226
+ if (this.hasSearchableListbox) {
227
+ return this.toggleId;
228
+ }
229
+ return undefined;
230
+ },
231
+ computedToggleInnerId() {
232
+ if (this.isToggleCombobox && !this.isToggleLabelledExternally) {
233
+ return this.toggleId;
234
+ }
235
+ if (this.isToggleCombobox && this.isToggleLabelledExternally) {
236
+ return this.listboxId;
237
+ }
238
+ return undefined;
239
+ },
240
+ toggleAriaAttributes() {
174
241
  return {
175
- 'aria-haspopup': this.ariaHaspopup,
176
- 'aria-expanded': String(this.visible),
177
242
  'aria-controls': this.baseDropdownId,
178
- 'aria-labelledby': this.toggleLabelledBy
243
+ 'aria-expanded': String(this.visible),
244
+ 'aria-haspopup': this.ariaHaspopup,
245
+ 'aria-labelledby': this.toggleLabelledBy,
246
+ 'aria-activedescendant': this.ariaActiveDescendant
179
247
  };
180
248
  },
181
249
  toggleButtonClasses() {
@@ -191,10 +259,28 @@ var script = {
191
259
  return this.block ? 'gl-w-full' : '';
192
260
  },
193
261
  toggleLabelledBy() {
262
+ if (this.isToggleCombobox) {
263
+ if (this.ariaLabelledby) {
264
+ return `${this.ariaLabelledby} ${this.toggleId}`;
265
+ }
266
+ return this.toggleId;
267
+ }
268
+
269
+ // For non-combobox toggles, combine IDs or use the button's own text
194
270
  return this.ariaLabelledby ? `${this.ariaLabelledby} ${this.toggleId}` : undefined;
195
271
  },
196
- isDefaultToggle() {
197
- return !this.$scopedSlots.toggle;
272
+ toggleRole() {
273
+ if (this.isToggleCombobox) {
274
+ return 'combobox';
275
+ }
276
+ return undefined;
277
+ },
278
+ toggleAccessibilityAttributes() {
279
+ return {
280
+ ...this.toggleAriaAttributes,
281
+ id: this.toggleId,
282
+ role: this.toggleRole
283
+ };
198
284
  },
199
285
  toggleOptions() {
200
286
  if (this.isDefaultToggle) {
@@ -209,7 +295,8 @@ var script = {
209
295
  disabled: this.disabled,
210
296
  loading: this.loading,
211
297
  class: this.toggleButtonClasses,
212
- ...this.ariaAttributes,
298
+ role: this.toggleRole,
299
+ ...this.toggleAriaAttributes,
213
300
  listeners: {
214
301
  keydown: event => this.onKeydown(event),
215
302
  click: event => this.toggle(event)
@@ -287,18 +374,6 @@ var script = {
287
374
  };
288
375
  }
289
376
  },
290
- watch: {
291
- ariaAttributes: {
292
- deep: true,
293
- handler(ariaAttributes) {
294
- if (this.$scopedSlots.toggle) {
295
- Object.keys(ariaAttributes).forEach(key => {
296
- this.toggleElement.setAttribute(key, ariaAttributes[key]);
297
- });
298
- }
299
- }
300
- }
301
- },
302
377
  mounted() {
303
378
  this.checkToggleFocusable();
304
379
  },
@@ -538,7 +613,7 @@ var script = {
538
613
  const __vue_script__ = script;
539
614
 
540
615
  /* template */
541
- var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{class:[_vm.$options.BASE_DROPDOWN_CLASS, { '!gl-block': _vm.block }]},[_c(_vm.toggleComponent,_vm._g(_vm._b({ref:"toggle",tag:"component",attrs:{"id":_vm.toggleId,"data-testid":"base-dropdown-toggle"}},'component',_vm.toggleAttributes,false),_vm.toggleListeners),[_vm._t("toggle",function(){return [_c('span',{staticClass:"gl-new-dropdown-button-text",class:{ 'gl-sr-only': _vm.textSrOnly }},[_vm._v("\n "+_vm._s(_vm.toggleText)+"\n ")]),_vm._v(" "),(!_vm.noCaret)?_c('gl-icon',{staticClass:"gl-button-icon gl-new-dropdown-chevron",attrs:{"name":"chevron-down"}}):_vm._e()]})],2),_vm._v(" "),_c('dropdown-container',{attrs:{"positioning-strategy":_vm.positioningStrategy}},[_c('div',{directives:[{name:"outside",rawName:"v-outside.click.focusin",value:(_vm.handleClickOutside),expression:"handleClickOutside",modifiers:{"click":true,"focusin":true}}],ref:"dropdownContainer",class:_vm.$options.DROPDOWN_CONTAINER_CLASS},[_c('div',{ref:"content",staticClass:"gl-new-dropdown-panel",class:_vm.panelClasses,attrs:{"id":_vm.baseDropdownId,"data-testid":"base-dropdown-menu"},on:{"keydown":function($event){if(!$event.type.indexOf('key')&&_vm._k($event.keyCode,"esc",27,$event.key,["Esc","Escape"])){ return null; }$event.stopPropagation();$event.preventDefault();return _vm.closeAndFocus.apply(null, arguments)}}},[_c('div',{ref:"dropdownArrow",staticClass:"gl-new-dropdown-arrow"}),_vm._v(" "),_c('div',{staticClass:"gl-new-dropdown-inner"},[_vm._t("default",null,{"visible":_vm.visible})],2)])])])],1)};
616
+ var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{class:[_vm.$options.BASE_DROPDOWN_CLASS, { '!gl-block': _vm.block }]},[_c(_vm.toggleComponent,_vm._g(_vm._b({ref:"toggle",tag:"component",attrs:{"id":_vm.computedToggleId,"data-testid":"base-dropdown-toggle"}},'component',_vm.toggleAttributes,false),_vm.toggleListeners),[_vm._t("toggle",function(){return [_c('span',{staticClass:"gl-new-dropdown-button-text",class:{ 'gl-sr-only': _vm.textSrOnly },attrs:{"id":_vm.computedToggleInnerId,"data-testid":"base-dropdown-span"}},[_vm._v("\n "+_vm._s(_vm.toggleText)+"\n ")]),_vm._v(" "),(!_vm.noCaret)?_c('gl-icon',{staticClass:"gl-button-icon gl-new-dropdown-chevron",attrs:{"name":"chevron-down"}}):_vm._e()]},{"accessibilityAttributes":_vm.toggleAccessibilityAttributes})],2),_vm._v(" "),_c('dropdown-container',{attrs:{"positioning-strategy":_vm.positioningStrategy}},[_c('div',{directives:[{name:"outside",rawName:"v-outside.click.focusin",value:(_vm.handleClickOutside),expression:"handleClickOutside",modifiers:{"click":true,"focusin":true}}],ref:"dropdownContainer",class:_vm.$options.DROPDOWN_CONTAINER_CLASS},[_c('div',{ref:"content",staticClass:"gl-new-dropdown-panel",class:_vm.panelClasses,attrs:{"id":_vm.baseDropdownId,"data-testid":"base-dropdown-menu"},on:{"keydown":function($event){if(!$event.type.indexOf('key')&&_vm._k($event.keyCode,"esc",27,$event.key,["Esc","Escape"])){ return null; }$event.stopPropagation();$event.preventDefault();return _vm.closeAndFocus.apply(null, arguments)}}},[_c('div',{ref:"dropdownArrow",staticClass:"gl-new-dropdown-arrow"}),_vm._v(" "),_c('div',{staticClass:"gl-new-dropdown-inner"},[_vm._t("default",null,{"visible":_vm.visible})],2)])])])],1)};
542
617
  var __vue_staticRenderFns__ = [];
543
618
 
544
619
  /* style */
@@ -346,7 +346,7 @@ var script = {
346
346
  const __vue_script__ = script;
347
347
 
348
348
  /* template */
349
- var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('gl-base-dropdown',{ref:"baseDropdown",staticClass:"gl-disclosure-dropdown",attrs:{"aria-labelledby":_vm.toggleAriaLabelledBy,"arrow-element":_vm.$refs.disclosureArrow,"toggle-id":_vm.toggleId,"toggle-text":_vm.toggleText,"toggle-class":_vm.toggleClass,"text-sr-only":_vm.textSrOnly,"category":_vm.category,"variant":_vm.variant,"size":_vm.size,"icon":_vm.icon,"disabled":_vm.disabled,"loading":_vm.loading,"no-caret":_vm.noCaret,"placement":_vm.placement,"block":_vm.block,"offset":_vm.dropdownOffset,"fluid-width":_vm.fluidWidth,"positioning-strategy":_vm.positioningStrategy},on:_vm._d({},[_vm.$options.events.GL_DROPDOWN_SHOWN,_vm.onShow,_vm.$options.events.GL_DROPDOWN_HIDDEN,_vm.onHide,_vm.$options.events.GL_DROPDOWN_BEFORE_CLOSE,_vm.onBeforeClose,_vm.$options.events.GL_DROPDOWN_FOCUS_CONTENT,_vm.onKeydown]),scopedSlots:_vm._u([(_vm.hasCustomToggle)?{key:"toggle",fn:function(){return [_vm._t("toggle")]},proxy:true}:null],null,true)},[_vm._v(" "),_vm._t("header"),_vm._v(" "),_c(_vm.disclosureTag,{ref:"content",tag:"component",class:_vm.$options.GL_DROPDOWN_CONTENTS_CLASS,attrs:{"id":_vm.disclosureId,"aria-labelledby":_vm.listAriaLabelledBy || _vm.toggleId,"data-testid":"disclosure-content","tabindex":"-1"},on:{"keydown":_vm.onKeydown,"click":_vm.handleAutoClose}},[_vm._t("default",function(){return [_vm._l((_vm.items),function(item,index){return [(_vm.isItem(item))?[_c('gl-disclosure-dropdown-item',{key:_vm.uniqueItemId(),attrs:{"item":item},on:{"action":_vm.handleAction},scopedSlots:_vm._u([('list-item' in _vm.$scopedSlots)?{key:"list-item",fn:function(){return [_vm._t("list-item",null,{"item":item})]},proxy:true}:null],null,true)})]:[_c('gl-disclosure-dropdown-group',{key:item.name,attrs:{"bordered":index !== 0,"group":item},on:{"action":_vm.handleAction},scopedSlots:_vm._u([(_vm.$scopedSlots['group-label'])?{key:"group-label",fn:function(){return [_vm._t("group-label",null,{"group":item})]},proxy:true}:null,(_vm.$scopedSlots['list-item'])?{key:"default",fn:function(){return _vm._l((item.items),function(groupItem){return _c('gl-disclosure-dropdown-item',{key:_vm.uniqueItemId(),attrs:{"item":groupItem},on:{"action":_vm.handleAction},scopedSlots:_vm._u([{key:"list-item",fn:function(){return [_vm._t("list-item",null,{"item":groupItem})]},proxy:true}],null,true)})})},proxy:true}:null],null,true)})]]})]})],2),_vm._v(" "),_vm._t("footer")],2)};
349
+ var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('gl-base-dropdown',{ref:"baseDropdown",staticClass:"gl-disclosure-dropdown",attrs:{"aria-labelledby":_vm.toggleAriaLabelledBy,"arrow-element":_vm.$refs.disclosureArrow,"toggle-id":_vm.toggleId,"toggle-text":_vm.toggleText,"toggle-class":_vm.toggleClass,"text-sr-only":_vm.textSrOnly,"category":_vm.category,"variant":_vm.variant,"size":_vm.size,"icon":_vm.icon,"disabled":_vm.disabled,"loading":_vm.loading,"no-caret":_vm.noCaret,"placement":_vm.placement,"block":_vm.block,"offset":_vm.dropdownOffset,"fluid-width":_vm.fluidWidth,"positioning-strategy":_vm.positioningStrategy,"is-disclosure":""},on:_vm._d({},[_vm.$options.events.GL_DROPDOWN_SHOWN,_vm.onShow,_vm.$options.events.GL_DROPDOWN_HIDDEN,_vm.onHide,_vm.$options.events.GL_DROPDOWN_BEFORE_CLOSE,_vm.onBeforeClose,_vm.$options.events.GL_DROPDOWN_FOCUS_CONTENT,_vm.onKeydown]),scopedSlots:_vm._u([(_vm.hasCustomToggle)?{key:"toggle",fn:function(slotProps){return [_vm._t("toggle",null,null,slotProps)]}}:null],null,true)},[_vm._v(" "),_vm._t("header"),_vm._v(" "),_c(_vm.disclosureTag,{ref:"content",tag:"component",class:_vm.$options.GL_DROPDOWN_CONTENTS_CLASS,attrs:{"id":_vm.disclosureId,"aria-labelledby":_vm.listAriaLabelledBy || _vm.toggleId,"data-testid":"disclosure-content","tabindex":"-1"},on:{"keydown":_vm.onKeydown,"click":_vm.handleAutoClose}},[_vm._t("default",function(){return [_vm._l((_vm.items),function(item,index){return [(_vm.isItem(item))?[_c('gl-disclosure-dropdown-item',{key:_vm.uniqueItemId(),attrs:{"item":item},on:{"action":_vm.handleAction},scopedSlots:_vm._u([('list-item' in _vm.$scopedSlots)?{key:"list-item",fn:function(){return [_vm._t("list-item",null,{"item":item})]},proxy:true}:null],null,true)})]:[_c('gl-disclosure-dropdown-group',{key:item.name,attrs:{"bordered":index !== 0,"group":item},on:{"action":_vm.handleAction},scopedSlots:_vm._u([(_vm.$scopedSlots['group-label'])?{key:"group-label",fn:function(){return [_vm._t("group-label",null,{"group":item})]},proxy:true}:null,(_vm.$scopedSlots['list-item'])?{key:"default",fn:function(){return _vm._l((item.items),function(groupItem){return _c('gl-disclosure-dropdown-item',{key:_vm.uniqueItemId(),attrs:{"item":groupItem},on:{"action":_vm.handleAction},scopedSlots:_vm._u([{key:"list-item",fn:function(){return [_vm._t("list-item",null,{"item":groupItem})]},proxy:true}],null,true)})})},proxy:true}:null],null,true)})]]})]})],2),_vm._v(" "),_vm._t("footer")],2)};
350
350
  var __vue_staticRenderFns__ = [];
351
351
 
352
352
  /* style */
@@ -1,7 +1,7 @@
1
1
  import clamp from 'lodash/clamp';
2
2
  import uniqueId from 'lodash/uniqueId';
3
3
  import { logWarning, stopEvent } from '../../../../utils/utils';
4
- import { GL_DROPDOWN_SHOWN, GL_DROPDOWN_HIDDEN, POSITION_ABSOLUTE, POSITION_FIXED, GL_DROPDOWN_CONTENTS_CLASS, ARROW_UP, ENTER, ARROW_DOWN, END, HOME } from '../constants';
4
+ import { GL_DROPDOWN_SHOWN, GL_DROPDOWN_HIDDEN, GL_DROPDOWN_FOCUS_CONTENT, POSITION_ABSOLUTE, POSITION_FIXED, GL_DROPDOWN_CONTENTS_CLASS, ARROW_UP, ENTER, ARROW_DOWN, END, HOME } from '../constants';
5
5
  import { buttonCategoryOptions, dropdownVariantOptions, buttonSizeOptions, dropdownPlacements } from '../../../../utils/constants';
6
6
  import GlButton from '../../button/button';
7
7
  import GlLoadingIcon from '../../loading_icon/loading_icon';
@@ -25,7 +25,8 @@ var script = {
25
25
  HEADER_ITEMS_BORDER_CLASSES,
26
26
  events: {
27
27
  GL_DROPDOWN_SHOWN,
28
- GL_DROPDOWN_HIDDEN
28
+ GL_DROPDOWN_HIDDEN,
29
+ GL_DROPDOWN_FOCUS_CONTENT
29
30
  },
30
31
  components: {
31
32
  GlBaseDropdown,
@@ -37,6 +38,11 @@ var script = {
37
38
  GlLoadingIcon,
38
39
  GlIntersectionObserver
39
40
  },
41
+ inject: {
42
+ isInFormGroup: {
43
+ default: false
44
+ }
45
+ },
40
46
  model: {
41
47
  prop: 'selected',
42
48
  event: 'select'
@@ -184,7 +190,7 @@ var script = {
184
190
  },
185
191
  /**
186
192
  * The `aria-labelledby` attribute value for the toggle button
187
- * Provide the string of ids seperated by space
193
+ * Provide the string of IDs seperated by space
188
194
  */
189
195
  toggleAriaLabelledBy: {
190
196
  type: String,
@@ -193,7 +199,7 @@ var script = {
193
199
  },
194
200
  /**
195
201
  * The `aria-labelledby` attribute value for the list of options
196
- * Provide the string of ids seperated by space
202
+ * Provide the string of IDs seperated by space
197
203
  */
198
204
  listAriaLabelledBy: {
199
205
  type: String,
@@ -351,15 +357,42 @@ var script = {
351
357
  };
352
358
  },
353
359
  computed: {
354
- ariaLabelledByID() {
355
- if (this.searchable) {
356
- return this.searchInputId;
357
- }
360
+ /**
361
+ * Determines the `aria-labelledby` attribute value for the listbox by
362
+ * evaluating a series of conditions in priority order. The returned ID
363
+ * references the element that best describes the listbox content, with
364
+ * preference given to headers in searchable lists, followed by search
365
+ * input, form labels, and finally fallback options.
366
+ */
367
+ listboxAriaLabelledByID() {
368
+ // Listbox is labelled by closest heading, creating a meaningful relationship
369
+ if (this.headerId && this.searchable) return `${this.headerId}`;
370
+
371
+ // Listbox is labelled by the search input with role="combobox"
372
+ if (this.searchable) return this.searchInputId;
373
+
374
+ // Listbox is labelledy by the text inside an externally labelled button
375
+ if (this.isInFormGroup) return this.listboxIdComputed;
376
+
377
+ // Fallback. Return a header ID or the toggle button ID.
358
378
  return this.listAriaLabelledBy || this.headerId || this.toggleIdComputed;
359
379
  },
360
380
  toggleIdComputed() {
361
381
  return this.toggleId || uniqueId('dropdown-toggle-btn-');
362
382
  },
383
+ // Generate a custom listbox ID when inside GlFormGroup
384
+ listboxIdComputed() {
385
+ if (this.isInFormGroup) {
386
+ if (this.listAriaLabelledBy) {
387
+ return this.listAriaLabelledBy;
388
+ }
389
+ if (this.toggleId) {
390
+ return `${this.toggleId}-span`;
391
+ }
392
+ return uniqueId('dropdown-toggle-span-');
393
+ }
394
+ return undefined;
395
+ },
363
396
  listboxTag() {
364
397
  if (!this.hasItems || isOption(this.items[0])) return 'ul';
365
398
  return 'div';
@@ -567,6 +600,10 @@ var script = {
567
600
  (_this$scrollObserver = this.scrollObserver) === null || _this$scrollObserver === void 0 ? void 0 : _this$scrollObserver.disconnect();
568
601
  },
569
602
  methods: {
603
+ onFocusContent(event) {
604
+ event.preventDefault();
605
+ this.open();
606
+ },
570
607
  open() {
571
608
  this.$refs.baseDropdown.open();
572
609
  },
@@ -874,9 +911,9 @@ var script = {
874
911
  const __vue_script__ = script;
875
912
 
876
913
  /* template */
877
- var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('gl-base-dropdown',{ref:"baseDropdown",attrs:{"aria-haspopup":"listbox","aria-labelledby":_vm.toggleAriaLabelledBy,"block":_vm.block,"toggle-id":_vm.toggleIdComputed,"toggle-text":_vm.listboxToggleText,"toggle-class":_vm.toggleButtonClasses,"text-sr-only":_vm.textSrOnly,"category":_vm.category,"variant":_vm.variant,"size":_vm.size,"icon":_vm.icon,"disabled":_vm.disabled,"loading":_vm.loading,"no-caret":_vm.noCaret,"placement":_vm.placement,"offset":_vm.dropdownOffset,"fluid-width":_vm.fluidWidth,"positioning-strategy":_vm.positioningStrategy},on:_vm._d({},[_vm.$options.events.GL_DROPDOWN_SHOWN,_vm.onShow,_vm.$options.events.GL_DROPDOWN_HIDDEN,_vm.onHide]),scopedSlots:_vm._u([(_vm.hasCustomToggle)?{key:"toggle",fn:function(){return [_vm._t("toggle")]},proxy:true}:null,{key:"default",fn:function(ref){
914
+ var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('gl-base-dropdown',{ref:"baseDropdown",attrs:{"aria-haspopup":"listbox","active-item-id":_vm.activeItemId,"aria-labelledby":_vm.toggleAriaLabelledBy,"block":_vm.block,"has-searchable-listbox":_vm.searchable,"has-external-label":_vm.isInFormGroup,"listbox-id":_vm.listboxIdComputed,"toggle-id":_vm.toggleIdComputed,"toggle-text":_vm.listboxToggleText,"toggle-class":_vm.toggleButtonClasses,"text-sr-only":_vm.textSrOnly,"category":_vm.category,"variant":_vm.variant,"size":_vm.size,"icon":_vm.icon,"disabled":_vm.disabled,"loading":_vm.loading,"no-caret":_vm.noCaret,"placement":_vm.placement,"offset":_vm.dropdownOffset,"fluid-width":_vm.fluidWidth,"positioning-strategy":_vm.positioningStrategy},on:_vm._d({},[_vm.$options.events.GL_DROPDOWN_FOCUS_CONTENT,_vm.onFocusContent,_vm.$options.events.GL_DROPDOWN_SHOWN,_vm.onShow,_vm.$options.events.GL_DROPDOWN_HIDDEN,_vm.onHide]),scopedSlots:_vm._u([(_vm.hasCustomToggle)?{key:"toggle",fn:function(slotProps){return [_vm._t("toggle",null,null,slotProps)]}}:null,{key:"default",fn:function(ref){
878
915
  var visible = ref.visible;
879
- return [(_vm.headerText)?_c('div',{staticClass:"gl-flex gl-min-h-8 gl-items-center !gl-p-4",class:_vm.$options.HEADER_ITEMS_BORDER_CLASSES},[_c('div',{staticClass:"gl-grow gl-pr-2 gl-text-sm gl-font-bold gl-text-strong",attrs:{"id":_vm.headerId,"data-testid":"listbox-header-text"}},[_vm._v("\n "+_vm._s(_vm.headerText)+"\n ")]),_vm._v(" "),(_vm.showResetButton)?_c('gl-button',{staticClass:"!gl-m-0 !gl-w-auto gl-max-w-1/2 gl-flex-shrink-0 gl-text-ellipsis !gl-px-2 !gl-text-sm focus:!gl-focus-inset",attrs:{"category":"tertiary","size":"small","data-testid":"listbox-reset-button"},on:{"click":_vm.onResetButtonClicked}},[_vm._v("\n "+_vm._s(_vm.resetButtonLabel)+"\n ")]):_vm._e(),_vm._v(" "),(_vm.showSelectAllButton)?_c('gl-button',{staticClass:"!gl-m-0 !gl-w-auto gl-max-w-1/2 gl-flex-shrink-0 gl-text-ellipsis !gl-px-2 !gl-text-sm focus:!gl-focus-inset",attrs:{"category":"tertiary","size":"small","data-testid":"listbox-select-all-button"},on:{"click":_vm.onSelectAllButtonClicked}},[_vm._v("\n "+_vm._s(_vm.showSelectAllButtonLabel)+"\n ")]):_vm._e()],1):_vm._e(),_vm._v(" "),(_vm.searchable)?_c('div',{class:_vm.$options.HEADER_ITEMS_BORDER_CLASSES},[_c('gl-listbox-search-input',{ref:"searchBox",class:{ 'gl-listbox-topmost': !_vm.headerText },attrs:{"id":_vm.searchInputId,"data-testid":"listbox-search-input","role":"combobox","aria-expanded":String(visible),"aria-controls":_vm.listboxId,"aria-activedescendant":_vm.activeItemId,"aria-haspopup":"listbox","placeholder":_vm.searchPlaceholder},on:{"input":_vm.search,"keydown":[function($event){if(!$event.type.indexOf('key')&&_vm._k($event.keyCode,"enter",13,$event.key,"Enter")){ return null; }$event.preventDefault();},_vm.onKeydown]},model:{value:(_vm.searchStr),callback:function ($$v) {_vm.searchStr=$$v;},expression:"searchStr"}}),_vm._v(" "),(_vm.searching)?_c('gl-loading-icon',{staticClass:"gl-my-3",attrs:{"data-testid":"listbox-search-loader","size":"md"}}):_vm._e()],1):_vm._e(),_vm._v(" "),(_vm.showList)?_c(_vm.listboxTag,{ref:"list",tag:"component",staticClass:"gl-new-dropdown-contents gl-new-dropdown-contents-with-scrim-overlay",class:_vm.listboxClasses,attrs:{"id":_vm.listboxId,"aria-busy":_vm.isBusy,"aria-labelledby":_vm.ariaLabelledByID,"aria-multiselectable":_vm.multiple ? 'true' : undefined,"role":"listbox","tabindex":"0"},on:{"keydown":_vm.onKeydown}},[_c(_vm.itemTag,{tag:"component",staticClass:"top-scrim-wrapper",attrs:{"aria-hidden":"true","data-testid":"top-scrim"}},[_c('div',{staticClass:"top-scrim",class:{ 'top-scrim-light': !_vm.hasHeader, 'top-scrim-dark': _vm.hasHeader }})]),_vm._v(" "),_c(_vm.itemTag,{ref:"top-boundary",tag:"component",attrs:{"aria-hidden":"true"}}),_vm._v(" "),_vm._l((_vm.items),function(item,index){return [(_vm.isOption(item))?[_c('gl-listbox-item',_vm._b({key:_vm.listboxItemKey(item),attrs:{"id":_vm.generateItemId(item),"data-testid":("listbox-item-" + (item.value)),"is-highlighted":_vm.isHighlighted(item),"is-selected":_vm.isSelected(item),"is-focused":_vm.isFocused(item),"is-check-centered":_vm.isCheckCentered},on:{"select":function($event){return _vm.onSelect(item, $event)}}},'gl-listbox-item',_vm.listboxItemMoreItemsAriaAttributes(index),false),[_vm._t("list-item",function(){return [_vm._v("\n "+_vm._s(item.text)+"\n ")]},{"item":item})],2)]:[_c('gl-listbox-group',{key:item.text,class:_vm.groupClasses(index),attrs:{"name":item.text,"text-sr-only":item.textSrOnly},scopedSlots:_vm._u([(_vm.$scopedSlots['group-label'])?{key:"group-label",fn:function(){return [_vm._t("group-label",null,{"group":item})]},proxy:true}:null],null,true)},[_vm._v(" "),_vm._l((item.options),function(option){return _c('gl-listbox-item',{key:_vm.listboxItemKey(option),attrs:{"id":_vm.generateItemId(option),"data-testid":("listbox-item-" + (option.value)),"is-highlighted":_vm.isHighlighted(option),"is-selected":_vm.isSelected(option),"is-focused":_vm.isFocused(option),"is-check-centered":_vm.isCheckCentered},on:{"select":function($event){return _vm.onSelect(option, $event)}}},[_vm._t("list-item",function(){return [_vm._v("\n "+_vm._s(option.text)+"\n ")]},{"item":option})],2)})],2)]]}),_vm._v(" "),(_vm.infiniteScrollLoading)?_c(_vm.itemTag,{tag:"component"},[_c('gl-loading-icon',{staticClass:"gl-my-3",attrs:{"data-testid":"listbox-infinite-scroll-loader","size":"md"}})],1):_vm._e(),_vm._v(" "),(_vm.showIntersectionObserver)?_c('gl-intersection-observer',{on:{"appear":_vm.onIntersectionObserverAppear}}):_vm._e(),_vm._v(" "),_c(_vm.itemTag,{ref:"bottom-boundary",tag:"component",attrs:{"aria-hidden":"true"}}),_vm._v(" "),_c(_vm.itemTag,{tag:"component",staticClass:"bottom-scrim-wrapper",attrs:{"aria-hidden":"true","data-testid":"bottom-scrim"}},[_c('div',{staticClass:"bottom-scrim",class:{ '!gl-rounded-none': _vm.hasFooter }})])],2):_vm._e(),_vm._v(" "),(_vm.announceSRSearchResults)?_c('span',{staticClass:"gl-sr-only",attrs:{"data-testid":"listbox-number-of-results","aria-live":"assertive"}},[_vm._t("search-summary-sr-only",function(){return [_vm._v("\n "+_vm._s(_vm.srOnlyResultsLabel(_vm.flattenedOptions.length))+"\n ")]})],2):_vm._e(),_vm._v(" "),(_vm.isBusy)?_c('span',{staticClass:"gl-sr-only",attrs:{"aria-live":"polite","data-testid":"listbox-loading-announcement"}},[_vm._v("\n "+_vm._s(_vm.loadingAnnouncementText)+"\n ")]):(_vm.showNoResultsText)?_c('div',{staticClass:"gl-py-3 gl-pl-7 gl-pr-5 gl-text-base gl-text-subtle",attrs:{"aria-live":"assertive","data-testid":"listbox-no-results-text"}},[_vm._v("\n "+_vm._s(_vm.noResultsText)+"\n ")]):_vm._e(),_vm._v(" "),_vm._t("footer")]}}],null,true)})};
916
+ return [(_vm.headerText)?_c('div',{staticClass:"gl-flex gl-min-h-8 gl-items-center !gl-p-4",class:_vm.$options.HEADER_ITEMS_BORDER_CLASSES},[_c('div',{staticClass:"gl-grow gl-pr-2 gl-text-sm gl-font-bold gl-text-strong",attrs:{"id":_vm.headerId,"data-testid":"listbox-header-text"}},[_vm._v("\n "+_vm._s(_vm.headerText)+"\n ")]),_vm._v(" "),(_vm.showResetButton)?_c('gl-button',{staticClass:"!gl-m-0 !gl-w-auto gl-max-w-1/2 gl-flex-shrink-0 gl-text-ellipsis !gl-px-2 !gl-text-sm focus:!gl-focus-inset",attrs:{"category":"tertiary","size":"small","data-testid":"listbox-reset-button"},on:{"click":_vm.onResetButtonClicked}},[_vm._v("\n "+_vm._s(_vm.resetButtonLabel)+"\n ")]):_vm._e(),_vm._v(" "),(_vm.showSelectAllButton)?_c('gl-button',{staticClass:"!gl-m-0 !gl-w-auto gl-max-w-1/2 gl-flex-shrink-0 gl-text-ellipsis !gl-px-2 !gl-text-sm focus:!gl-focus-inset",attrs:{"category":"tertiary","size":"small","data-testid":"listbox-select-all-button"},on:{"click":_vm.onSelectAllButtonClicked}},[_vm._v("\n "+_vm._s(_vm.showSelectAllButtonLabel)+"\n ")]):_vm._e()],1):_vm._e(),_vm._v(" "),(_vm.searchable)?_c('div',{class:_vm.$options.HEADER_ITEMS_BORDER_CLASSES},[_c('gl-listbox-search-input',{ref:"searchBox",class:{ 'gl-listbox-topmost': !_vm.headerText },attrs:{"id":_vm.searchInputId,"data-testid":"listbox-search-input","role":"combobox","aria-expanded":String(visible),"aria-controls":_vm.listboxId,"aria-activedescendant":_vm.activeItemId,"aria-haspopup":"listbox","placeholder":_vm.searchPlaceholder},on:{"input":_vm.search,"keydown":[function($event){if(!$event.type.indexOf('key')&&_vm._k($event.keyCode,"enter",13,$event.key,"Enter")){ return null; }$event.preventDefault();},_vm.onKeydown]},model:{value:(_vm.searchStr),callback:function ($$v) {_vm.searchStr=$$v;},expression:"searchStr"}}),_vm._v(" "),(_vm.searching)?_c('gl-loading-icon',{staticClass:"gl-my-3",attrs:{"data-testid":"listbox-search-loader","size":"md"}}):_vm._e()],1):_vm._e(),_vm._v(" "),(_vm.showList)?_c(_vm.listboxTag,{ref:"list",tag:"component",staticClass:"gl-new-dropdown-contents gl-new-dropdown-contents-with-scrim-overlay",class:_vm.listboxClasses,attrs:{"id":_vm.listboxId,"aria-busy":_vm.isBusy,"aria-labelledby":_vm.listboxAriaLabelledByID,"aria-multiselectable":_vm.multiple ? 'true' : undefined,"role":"listbox","tabindex":"0"},on:{"keydown":_vm.onKeydown}},[_c(_vm.itemTag,{tag:"component",staticClass:"top-scrim-wrapper",attrs:{"aria-hidden":"true","data-testid":"top-scrim"}},[_c('div',{staticClass:"top-scrim",class:{ 'top-scrim-light': !_vm.hasHeader, 'top-scrim-dark': _vm.hasHeader }})]),_vm._v(" "),_c(_vm.itemTag,{ref:"top-boundary",tag:"component",attrs:{"aria-hidden":"true"}}),_vm._v(" "),_vm._l((_vm.items),function(item,index){return [(_vm.isOption(item))?[_c('gl-listbox-item',_vm._b({key:_vm.listboxItemKey(item),attrs:{"id":_vm.generateItemId(item),"data-testid":("listbox-item-" + (item.value)),"is-highlighted":_vm.isHighlighted(item),"is-selected":_vm.isSelected(item),"is-focused":_vm.isFocused(item),"is-check-centered":_vm.isCheckCentered},on:{"select":function($event){return _vm.onSelect(item, $event)}}},'gl-listbox-item',_vm.listboxItemMoreItemsAriaAttributes(index),false),[_vm._t("list-item",function(){return [_vm._v("\n "+_vm._s(item.text)+"\n ")]},{"item":item})],2)]:[_c('gl-listbox-group',{key:item.text,class:_vm.groupClasses(index),attrs:{"name":item.text,"text-sr-only":item.textSrOnly},scopedSlots:_vm._u([(_vm.$scopedSlots['group-label'])?{key:"group-label",fn:function(){return [_vm._t("group-label",null,{"group":item})]},proxy:true}:null],null,true)},[_vm._v(" "),_vm._l((item.options),function(option){return _c('gl-listbox-item',{key:_vm.listboxItemKey(option),attrs:{"id":_vm.generateItemId(option),"data-testid":("listbox-item-" + (option.value)),"is-highlighted":_vm.isHighlighted(option),"is-selected":_vm.isSelected(option),"is-focused":_vm.isFocused(option),"is-check-centered":_vm.isCheckCentered},on:{"select":function($event){return _vm.onSelect(option, $event)}}},[_vm._t("list-item",function(){return [_vm._v("\n "+_vm._s(option.text)+"\n ")]},{"item":option})],2)})],2)]]}),_vm._v(" "),(_vm.infiniteScrollLoading)?_c(_vm.itemTag,{tag:"component"},[_c('gl-loading-icon',{staticClass:"gl-my-3",attrs:{"data-testid":"listbox-infinite-scroll-loader","size":"md"}})],1):_vm._e(),_vm._v(" "),(_vm.showIntersectionObserver)?_c('gl-intersection-observer',{on:{"appear":_vm.onIntersectionObserverAppear}}):_vm._e(),_vm._v(" "),_c(_vm.itemTag,{ref:"bottom-boundary",tag:"component",attrs:{"aria-hidden":"true"}}),_vm._v(" "),_c(_vm.itemTag,{tag:"component",staticClass:"bottom-scrim-wrapper",attrs:{"aria-hidden":"true","data-testid":"bottom-scrim"}},[_c('div',{staticClass:"bottom-scrim",class:{ '!gl-rounded-none': _vm.hasFooter }})])],2):_vm._e(),_vm._v(" "),(_vm.announceSRSearchResults)?_c('span',{staticClass:"gl-sr-only",attrs:{"data-testid":"listbox-number-of-results","aria-live":"assertive"}},[_vm._t("search-summary-sr-only",function(){return [_vm._v("\n "+_vm._s(_vm.srOnlyResultsLabel(_vm.flattenedOptions.length))+"\n ")]})],2):_vm._e(),_vm._v(" "),(_vm.isBusy)?_c('span',{staticClass:"gl-sr-only",attrs:{"aria-live":"polite","data-testid":"listbox-loading-announcement"}},[_vm._v("\n "+_vm._s(_vm.loadingAnnouncementText)+"\n ")]):(_vm.showNoResultsText)?_c('div',{staticClass:"gl-py-3 gl-pl-7 gl-pr-5 gl-text-base gl-text-subtle",attrs:{"aria-live":"assertive","data-testid":"listbox-no-results-text"}},[_vm._v("\n "+_vm._s(_vm.noResultsText)+"\n ")]):_vm._e(),_vm._v(" "),_vm._t("footer")]}}],null,true)})};
880
917
  var __vue_staticRenderFns__ = [];
881
918
 
882
919
  /* style */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gitlab/ui",
3
- "version": "127.1.1",
3
+ "version": "128.0.0",
4
4
  "description": "GitLab UI Components",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -8,9 +8,6 @@ export default {
8
8
  name: 'GlFormCheckbox',
9
9
  inject: {
10
10
  getGroup: {
11
- // When we remove BFormCheckboxGroup from GlFormCheckboxGroup, we can rename
12
- // the `getBvCheckGroup` provide to `getCheckGroup`.
13
- from: 'getBvCheckGroup',
14
11
  default: () => () => null,
15
12
  },
16
13
  },
@@ -1,44 +1,171 @@
1
1
  <script>
2
+ import uniqueId from 'lodash/uniqueId';
3
+ import isBoolean from 'lodash/isBoolean';
4
+ import omit from 'lodash/omit';
5
+ import pick from 'lodash/pick';
6
+ import { looseEqual } from '../../../../vendor/bootstrap-vue/src/utils/loose-equal';
2
7
  import { formOptionsMixin } from '../../../../vendor/bootstrap-vue/src/mixins/form-options';
3
- import { BFormCheckboxGroup } from '../../../../vendor/bootstrap-vue/src/components/form-checkbox/form-checkbox-group';
4
8
  import { SafeHtmlDirective as SafeHtml } from '../../../../directives/safe_html/safe_html';
5
9
  import GlFormCheckbox from './form_checkbox.vue';
6
10
 
11
+ // Attributes to pass down to checks/radios instead of applying them to the group
12
+ const PASS_DOWN_ATTRS = ['aria-describedby', 'aria-labelledby'];
13
+
7
14
  export default {
8
15
  name: 'GlFormCheckboxGroup',
9
- components: { BFormCheckboxGroup, GlFormCheckbox },
16
+ components: { GlFormCheckbox },
10
17
  directives: {
11
18
  SafeHtml,
12
19
  },
13
20
  mixins: [formOptionsMixin],
21
+ provide() {
22
+ return {
23
+ getGroup: () => this,
24
+ };
25
+ },
14
26
  inheritAttrs: false,
15
27
  model: {
16
28
  prop: 'checked',
17
29
  event: 'input',
18
30
  },
31
+ props: {
32
+ /**
33
+ * Used to set the `id` attribute on the rendered content, and used as the base to generate any additional element IDs as needed.
34
+ */
35
+ id: {
36
+ type: String,
37
+ required: false,
38
+ default: undefined,
39
+ },
40
+ /**
41
+ * The current value of the checkbox.
42
+ */
43
+ checked: {
44
+ type: Array,
45
+ required: false,
46
+ default: () => [],
47
+ },
48
+ /**
49
+ * Array of items to render in the component
50
+ */
51
+ options: {
52
+ type: Array,
53
+ required: false,
54
+ default: () => [],
55
+ },
56
+ /**
57
+ * When set to `true`, disables the component's functionality and places it in a disabled state.
58
+ */
59
+ disabled: {
60
+ type: Boolean,
61
+ required: false,
62
+ default: false,
63
+ },
64
+ /**
65
+ * Sets the value of the `name` attribute on the form control.
66
+ */
67
+ name: {
68
+ type: String,
69
+ required: false,
70
+ default: undefined,
71
+ },
72
+ /**
73
+ * Adds the `required` attribute to the form control.
74
+ */
75
+ required: {
76
+ type: Boolean,
77
+ required: false,
78
+ default: false,
79
+ },
80
+ /**
81
+ * Controls the validation state appearance of the component. `true` for valid, `false` for invalid, or `null` for no validation state.
82
+ */
83
+ state: {
84
+ type: Boolean,
85
+ required: false,
86
+ default: null,
87
+ },
88
+ /**
89
+ * Optional value to set for the 'aria-invalid' attribute. Supported values are 'true' and 'false'. If not set, the 'state' prop will dictate the value
90
+ */
91
+ ariaInvalid: {
92
+ type: [Boolean, String],
93
+ required: false,
94
+ default: false,
95
+ },
96
+ },
97
+ data() {
98
+ return {
99
+ internalId: this.id ? this.id : uniqueId('gitlab_ui_checkbox_group_'),
100
+ localChecked: this.checked,
101
+ };
102
+ },
103
+ computed: {
104
+ computedState() {
105
+ return isBoolean(this.state) ? this.state : null;
106
+ },
107
+ stateClass() {
108
+ if (this.computedState === true) return 'is-valid';
109
+ if (this.computedState === false) return 'is-invalid';
110
+ return null;
111
+ },
112
+ computedAriaInvalid() {
113
+ const { ariaInvalid } = this;
114
+ if (ariaInvalid === true || ariaInvalid === 'true' || ariaInvalid === '') {
115
+ return 'true';
116
+ }
117
+ return this.computedState === false ? 'true' : ariaInvalid;
118
+ },
119
+ computedAttrs() {
120
+ return {
121
+ ...omit(this.$attrs, PASS_DOWN_ATTRS),
122
+ id: this.internalId,
123
+ 'aria-invalid': this.computedAriaInvalid,
124
+ 'aria-required': this.required || null,
125
+ };
126
+ },
127
+ passDownAttrs() {
128
+ return pick(this.$attrs, PASS_DOWN_ATTRS);
129
+ },
130
+ groupName() {
131
+ // Checks/Radios tied to the same model must have the same name,
132
+ // especially for ARIA accessibility
133
+ return this.name || this.internalId;
134
+ },
135
+ },
136
+ watch: {
137
+ checked(newValue) {
138
+ if (!looseEqual(newValue, this.localChecked)) {
139
+ this.localChecked = newValue;
140
+ }
141
+ },
142
+ localChecked(newValue, oldValue) {
143
+ if (!looseEqual(newValue, oldValue)) {
144
+ this.$emit('input', newValue);
145
+ }
146
+ },
147
+ },
19
148
  };
20
149
  </script>
21
150
 
22
151
  <template>
23
- <div>
24
- <b-form-checkbox-group
25
- v-bind="$attrs"
26
- class="gl-form-checkbox-group"
27
- stacked
28
- @change="$emit('change', $event)"
29
- @input="$emit('input', $event)"
152
+ <div
153
+ v-bind="computedAttrs"
154
+ role="group"
155
+ tabindex="-1"
156
+ class="gl-form-checkbox-group gl-outline-none"
157
+ >
158
+ <slot name="first"></slot>
159
+ <gl-form-checkbox
160
+ v-for="(option, idx) in formOptions"
161
+ v-bind="passDownAttrs"
162
+ :key="idx"
163
+ :value="option.value"
164
+ :disabled="option.disabled"
30
165
  >
31
- <slot name="first"></slot>
32
- <gl-form-checkbox
33
- v-for="(option, idx) in formOptions"
34
- :key="idx"
35
- :value="option.value"
36
- :disabled="option.disabled"
37
- >
38
- <span v-if="option.html" v-safe-html="option.html"></span>
39
- <span v-else>{{ option.text }}</span>
40
- </gl-form-checkbox>
41
- <slot></slot>
42
- </b-form-checkbox-group>
166
+ <span v-if="option.html" v-safe-html="option.html"></span>
167
+ <span v-else>{{ option.text }}</span>
168
+ </gl-form-checkbox>
169
+ <slot></slot>
43
170
  </div>
44
171
  </template>
@@ -8,6 +8,9 @@ export default {
8
8
  components: {
9
9
  BFormGroup,
10
10
  },
11
+ provide: {
12
+ isInFormGroup: true,
13
+ },
11
14
  inheritAttrs: false,
12
15
  props: {
13
16
  /**
@@ -138,6 +138,26 @@ export default {
138
138
  return ['menu', 'listbox', 'tree', 'grid', 'dialog', true, false].includes(value);
139
139
  },
140
140
  },
141
+ activeItemId: {
142
+ type: String,
143
+ required: false,
144
+ default: undefined,
145
+ },
146
+ hasExternalLabel: {
147
+ type: Boolean,
148
+ required: false,
149
+ default: false,
150
+ },
151
+ hasSearchableListbox: {
152
+ type: Boolean,
153
+ required: false,
154
+ default: false,
155
+ },
156
+ isDisclosure: {
157
+ type: Boolean,
158
+ required: false,
159
+ default: false,
160
+ },
141
161
  /**
142
162
  * Id that will be referenced by `aria-labelledby` attribute of the dropdown content`
143
163
  */
@@ -145,6 +165,15 @@ export default {
145
165
  type: String,
146
166
  required: true,
147
167
  },
168
+ /**
169
+ * Span Id that will be referenced by listbox `aria-labelledby` if there's an external label.
170
+ * This prop will only be defined by `GlCollapsibleListbox` when `isInFormGroup` injected is true.
171
+ */
172
+ listboxId: {
173
+ type: String,
174
+ required: false,
175
+ default: undefined,
176
+ },
148
177
  /**
149
178
  * The `aria-labelledby` attribute value for the toggle `button`
150
179
  */
@@ -187,6 +216,10 @@ export default {
187
216
  };
188
217
  },
189
218
  computed: {
219
+ ariaActiveDescendant() {
220
+ if (!this.isDisclosure && this.visible) return this.activeItemId;
221
+ return undefined;
222
+ },
190
223
  hasNoVisibleToggleText() {
191
224
  return !this.toggleText?.length || this.textSrOnly;
192
225
  },
@@ -199,12 +232,47 @@ export default {
199
232
  isCaretOnly() {
200
233
  return !this.noCaret && !this.icon && this.hasNoVisibleToggleText;
201
234
  },
202
- ariaAttributes() {
235
+ isDefaultToggle() {
236
+ return !this.$scopedSlots.toggle;
237
+ },
238
+ isToggleCombobox() {
239
+ if (this.hasSearchableListbox || this.isDisclosure) {
240
+ return false;
241
+ }
242
+ return true;
243
+ },
244
+ isToggleLabelledExternally() {
245
+ if (this.hasExternalLabel && this.toggleId) return true;
246
+ return false;
247
+ },
248
+ computedToggleId() {
249
+ if (this.isDisclosure) {
250
+ return this.isDefaultToggle ? this.toggleId : undefined;
251
+ }
252
+ if (this.isToggleLabelledExternally) {
253
+ return this.isDefaultToggle ? this.toggleId : undefined;
254
+ }
255
+ if (this.hasSearchableListbox) {
256
+ return this.toggleId;
257
+ }
258
+ return undefined;
259
+ },
260
+ computedToggleInnerId() {
261
+ if (this.isToggleCombobox && !this.isToggleLabelledExternally) {
262
+ return this.toggleId;
263
+ }
264
+ if (this.isToggleCombobox && this.isToggleLabelledExternally) {
265
+ return this.listboxId;
266
+ }
267
+ return undefined;
268
+ },
269
+ toggleAriaAttributes() {
203
270
  return {
204
- 'aria-haspopup': this.ariaHaspopup,
205
- 'aria-expanded': String(this.visible),
206
271
  'aria-controls': this.baseDropdownId,
272
+ 'aria-expanded': String(this.visible),
273
+ 'aria-haspopup': this.ariaHaspopup,
207
274
  'aria-labelledby': this.toggleLabelledBy,
275
+ 'aria-activedescendant': this.ariaActiveDescendant,
208
276
  };
209
277
  },
210
278
  toggleButtonClasses() {
@@ -223,10 +291,28 @@ export default {
223
291
  return this.block ? 'gl-w-full' : '';
224
292
  },
225
293
  toggleLabelledBy() {
294
+ if (this.isToggleCombobox) {
295
+ if (this.ariaLabelledby) {
296
+ return `${this.ariaLabelledby} ${this.toggleId}`;
297
+ }
298
+ return this.toggleId;
299
+ }
300
+
301
+ // For non-combobox toggles, combine IDs or use the button's own text
226
302
  return this.ariaLabelledby ? `${this.ariaLabelledby} ${this.toggleId}` : undefined;
227
303
  },
228
- isDefaultToggle() {
229
- return !this.$scopedSlots.toggle;
304
+ toggleRole() {
305
+ if (this.isToggleCombobox) {
306
+ return 'combobox';
307
+ }
308
+ return undefined;
309
+ },
310
+ toggleAccessibilityAttributes() {
311
+ return {
312
+ ...this.toggleAriaAttributes,
313
+ id: this.toggleId,
314
+ role: this.toggleRole,
315
+ };
230
316
  },
231
317
  toggleOptions() {
232
318
  if (this.isDefaultToggle) {
@@ -241,7 +327,8 @@ export default {
241
327
  disabled: this.disabled,
242
328
  loading: this.loading,
243
329
  class: this.toggleButtonClasses,
244
- ...this.ariaAttributes,
330
+ role: this.toggleRole,
331
+ ...this.toggleAriaAttributes,
245
332
  listeners: {
246
333
  keydown: (event) => this.onKeydown(event),
247
334
  click: (event) => this.toggle(event),
@@ -264,7 +351,6 @@ export default {
264
351
 
265
352
  toggleAttributes() {
266
353
  const { listeners, is, ...attributes } = this.toggleOptions;
267
-
268
354
  return attributes;
269
355
  },
270
356
  toggleComponent() {
@@ -325,18 +411,6 @@ export default {
325
411
  };
326
412
  },
327
413
  },
328
- watch: {
329
- ariaAttributes: {
330
- deep: true,
331
- handler(ariaAttributes) {
332
- if (this.$scopedSlots.toggle) {
333
- Object.keys(ariaAttributes).forEach((key) => {
334
- this.toggleElement.setAttribute(key, ariaAttributes[key]);
335
- });
336
- }
337
- },
338
- },
339
- },
340
414
  mounted() {
341
415
  this.checkToggleFocusable();
342
416
  },
@@ -593,14 +667,19 @@ export default {
593
667
  <component
594
668
  :is="toggleComponent"
595
669
  v-bind="toggleAttributes"
596
- :id="toggleId"
670
+ :id="computedToggleId"
597
671
  ref="toggle"
598
672
  data-testid="base-dropdown-toggle"
599
673
  v-on="toggleListeners"
600
674
  >
601
675
  <!-- @slot Custom toggle button content -->
602
- <slot name="toggle">
603
- <span class="gl-new-dropdown-button-text" :class="{ 'gl-sr-only': textSrOnly }">
676
+ <slot name="toggle" :accessibility-attributes="toggleAccessibilityAttributes">
677
+ <span
678
+ :id="computedToggleInnerId"
679
+ class="gl-new-dropdown-button-text"
680
+ :class="{ 'gl-sr-only': textSrOnly }"
681
+ data-testid="base-dropdown-span"
682
+ >
604
683
  {{ toggleText }}
605
684
  </span>
606
685
  <gl-icon
@@ -389,14 +389,15 @@ export default {
389
389
  :fluid-width="fluidWidth"
390
390
  :positioning-strategy="positioningStrategy"
391
391
  class="gl-disclosure-dropdown"
392
+ is-disclosure
392
393
  @[$options.events.GL_DROPDOWN_SHOWN]="onShow"
393
394
  @[$options.events.GL_DROPDOWN_HIDDEN]="onHide"
394
395
  @[$options.events.GL_DROPDOWN_BEFORE_CLOSE]="onBeforeClose"
395
396
  @[$options.events.GL_DROPDOWN_FOCUS_CONTENT]="onKeydown"
396
397
  >
397
- <template v-if="hasCustomToggle" #toggle>
398
+ <template v-if="hasCustomToggle" #toggle="slotProps">
398
399
  <!-- @slot Custom toggle content -->
399
- <slot name="toggle"></slot>
400
+ <slot name="toggle" v-bind="slotProps"></slot>
400
401
  </template>
401
402
 
402
403
  <!-- @slot Content to display in dropdown header -->
@@ -11,6 +11,7 @@ import {
11
11
  ARROW_DOWN,
12
12
  ARROW_UP,
13
13
  GL_DROPDOWN_CONTENTS_CLASS,
14
+ GL_DROPDOWN_FOCUS_CONTENT,
14
15
  POSITION_ABSOLUTE,
15
16
  POSITION_FIXED,
16
17
  } from '../constants';
@@ -53,6 +54,7 @@ export default {
53
54
  events: {
54
55
  GL_DROPDOWN_SHOWN,
55
56
  GL_DROPDOWN_HIDDEN,
57
+ GL_DROPDOWN_FOCUS_CONTENT,
56
58
  },
57
59
  components: {
58
60
  GlBaseDropdown,
@@ -64,6 +66,11 @@ export default {
64
66
  GlLoadingIcon,
65
67
  GlIntersectionObserver,
66
68
  },
69
+ inject: {
70
+ isInFormGroup: {
71
+ default: false,
72
+ },
73
+ },
67
74
  model: {
68
75
  prop: 'selected',
69
76
  event: 'select',
@@ -211,7 +218,7 @@ export default {
211
218
  },
212
219
  /**
213
220
  * The `aria-labelledby` attribute value for the toggle button
214
- * Provide the string of ids seperated by space
221
+ * Provide the string of IDs seperated by space
215
222
  */
216
223
  toggleAriaLabelledBy: {
217
224
  type: String,
@@ -220,7 +227,7 @@ export default {
220
227
  },
221
228
  /**
222
229
  * The `aria-labelledby` attribute value for the list of options
223
- * Provide the string of ids seperated by space
230
+ * Provide the string of IDs seperated by space
224
231
  */
225
232
  listAriaLabelledBy: {
226
233
  type: String,
@@ -382,15 +389,42 @@ export default {
382
389
  };
383
390
  },
384
391
  computed: {
385
- ariaLabelledByID() {
386
- if (this.searchable) {
387
- return this.searchInputId;
388
- }
392
+ /**
393
+ * Determines the `aria-labelledby` attribute value for the listbox by
394
+ * evaluating a series of conditions in priority order. The returned ID
395
+ * references the element that best describes the listbox content, with
396
+ * preference given to headers in searchable lists, followed by search
397
+ * input, form labels, and finally fallback options.
398
+ */
399
+ listboxAriaLabelledByID() {
400
+ // Listbox is labelled by closest heading, creating a meaningful relationship
401
+ if (this.headerId && this.searchable) return `${this.headerId}`;
402
+
403
+ // Listbox is labelled by the search input with role="combobox"
404
+ if (this.searchable) return this.searchInputId;
405
+
406
+ // Listbox is labelledy by the text inside an externally labelled button
407
+ if (this.isInFormGroup) return this.listboxIdComputed;
408
+
409
+ // Fallback. Return a header ID or the toggle button ID.
389
410
  return this.listAriaLabelledBy || this.headerId || this.toggleIdComputed;
390
411
  },
391
412
  toggleIdComputed() {
392
413
  return this.toggleId || uniqueId('dropdown-toggle-btn-');
393
414
  },
415
+ // Generate a custom listbox ID when inside GlFormGroup
416
+ listboxIdComputed() {
417
+ if (this.isInFormGroup) {
418
+ if (this.listAriaLabelledBy) {
419
+ return this.listAriaLabelledBy;
420
+ }
421
+ if (this.toggleId) {
422
+ return `${this.toggleId}-span`;
423
+ }
424
+ return uniqueId('dropdown-toggle-span-');
425
+ }
426
+ return undefined;
427
+ },
394
428
  listboxTag() {
395
429
  if (!this.hasItems || isOption(this.items[0])) return 'ul';
396
430
  return 'div';
@@ -606,6 +640,10 @@ export default {
606
640
  this.scrollObserver?.disconnect();
607
641
  },
608
642
  methods: {
643
+ onFocusContent(event) {
644
+ event.preventDefault();
645
+ this.open();
646
+ },
609
647
  open() {
610
648
  this.$refs.baseDropdown.open();
611
649
  },
@@ -922,8 +960,12 @@ export default {
922
960
  <gl-base-dropdown
923
961
  ref="baseDropdown"
924
962
  aria-haspopup="listbox"
963
+ :active-item-id="activeItemId"
925
964
  :aria-labelledby="toggleAriaLabelledBy"
926
965
  :block="block"
966
+ :has-searchable-listbox="searchable"
967
+ :has-external-label="isInFormGroup"
968
+ :listbox-id="listboxIdComputed"
927
969
  :toggle-id="toggleIdComputed"
928
970
  :toggle-text="listboxToggleText"
929
971
  :toggle-class="toggleButtonClasses"
@@ -939,12 +981,13 @@ export default {
939
981
  :offset="dropdownOffset"
940
982
  :fluid-width="fluidWidth"
941
983
  :positioning-strategy="positioningStrategy"
984
+ @[$options.events.GL_DROPDOWN_FOCUS_CONTENT]="onFocusContent"
942
985
  @[$options.events.GL_DROPDOWN_SHOWN]="onShow"
943
986
  @[$options.events.GL_DROPDOWN_HIDDEN]="onHide"
944
987
  >
945
- <template v-if="hasCustomToggle" #toggle>
988
+ <template v-if="hasCustomToggle" #toggle="slotProps">
946
989
  <!-- @slot Custom toggle content -->
947
- <slot name="toggle"></slot>
990
+ <slot name="toggle" v-bind="slotProps"></slot>
948
991
  </template>
949
992
 
950
993
  <template #default="{ visible }">
@@ -1013,7 +1056,7 @@ export default {
1013
1056
  :id="listboxId"
1014
1057
  ref="list"
1015
1058
  :aria-busy="isBusy"
1016
- :aria-labelledby="ariaLabelledByID"
1059
+ :aria-labelledby="listboxAriaLabelledByID"
1017
1060
  :aria-multiselectable="multiple ? 'true' : undefined"
1018
1061
  role="listbox"
1019
1062
  class="gl-new-dropdown-contents gl-new-dropdown-contents-with-scrim-overlay"
@@ -7,7 +7,6 @@ $bv-enable-popover-variants: false;
7
7
  @import '../vendor/bootstrap-vue/src/utilities.scss';
8
8
 
9
9
  @import '../vendor/bootstrap-vue/src/components/dropdown/index.scss';
10
- @import '../vendor/bootstrap-vue/src/components/form-checkbox/index.scss';
11
10
  @import '../vendor/bootstrap-vue/src/components/form-input/index.scss';
12
11
  @import '../vendor/bootstrap-vue/src/components/form-radio/index.scss';
13
12
  @import '../vendor/bootstrap-vue/src/components/modal/index.scss';
@@ -11,8 +11,6 @@ export const NAME_DROPDOWN_ITEM = 'BDropdownItem'
11
11
  export const NAME_DROPDOWN_ITEM_BUTTON = 'BDropdownItemButton'
12
12
  export const NAME_DROPDOWN_TEXT = 'BDropdownText'
13
13
  export const NAME_FORM = 'BForm'
14
- export const NAME_FORM_CHECKBOX = 'BFormCheckbox'
15
- export const NAME_FORM_CHECKBOX_GROUP = 'BFormCheckboxGroup'
16
14
  export const NAME_FORM_GROUP = 'BFormGroup'
17
15
  export const NAME_FORM_INVALID_FEEDBACK = 'BFormInvalidFeedback'
18
16
  export const NAME_FORM_RADIO = 'BFormRadio'
@@ -6,7 +6,6 @@ import { looseEqual } from '../utils/loose-equal'
6
6
  import { makeModelMixin } from '../utils/model'
7
7
  import { omit, pick, sortKeys } from '../utils/object'
8
8
  import { makeProp, makePropsConfigurable } from '../utils/props'
9
- import { BFormCheckbox } from '../components/form-checkbox/form-checkbox'
10
9
  import { BFormRadio } from '../components/form-radio/form-radio'
11
10
  import { formControlMixin, props as formControlProps } from './form-control'
12
11
  import { formCustomMixin, props as formCustomProps } from './form-custom'
@@ -99,7 +98,7 @@ export const formRadioCheckGroupMixin = extend({
99
98
  render(h) {
100
99
  const { isRadioGroup } = this
101
100
  const attrs = pick(this.$attrs, PASS_DOWN_ATTRS)
102
- const optionComponent = isRadioGroup ? BFormRadio : BFormCheckbox
101
+ const optionComponent = BFormRadio
103
102
 
104
103
  const $inputs = this.formOptions.map((option, index) => {
105
104
  const key = `BV_option_${index}`
@@ -1,125 +0,0 @@
1
- // Adds control sizing to Bootstrap custom checkbox/switch inputs
2
-
3
- .custom-checkbox.b-custom-control-lg,
4
- .input-group-lg .custom-checkbox {
5
- font-size: $font-size-lg;
6
- line-height: $line-height-lg;
7
- padding-left: $b-custom-control-gutter-lg + $b-custom-control-indicator-size-lg;
8
-
9
- .custom-control-label::before {
10
- top: ($font-size-lg * $line-height-lg - $b-custom-control-indicator-size-lg) * 0.5;
11
- left: -($b-custom-control-gutter-lg + $b-custom-control-indicator-size-lg);
12
- width: $b-custom-control-indicator-size-lg;
13
- height: $b-custom-control-indicator-size-lg;
14
- @include border-radius($b-custom-checkbox-indicator-border-radius-lg);
15
- }
16
-
17
- .custom-control-label::after {
18
- top: ($font-size-lg * $line-height-lg - $b-custom-control-indicator-size-lg) * 0.5;
19
- left: -($b-custom-control-gutter-lg + $b-custom-control-indicator-size-lg);
20
- width: $b-custom-control-indicator-size-lg;
21
- height: $b-custom-control-indicator-size-lg;
22
- background-size: $b-custom-control-indicator-bg-size-lg;
23
- }
24
- }
25
-
26
- .custom-checkbox.b-custom-control-sm,
27
- .input-group-sm .custom-checkbox {
28
- font-size: $font-size-sm;
29
- line-height: $line-height-sm;
30
- padding-left: $b-custom-control-gutter-sm + $b-custom-control-indicator-size-sm;
31
-
32
- .custom-control-label::before {
33
- top: ($font-size-sm * $line-height-sm - $b-custom-control-indicator-size-sm) * 0.5;
34
- left: -($b-custom-control-gutter-sm + $b-custom-control-indicator-size-sm);
35
- width: $b-custom-control-indicator-size-sm;
36
- height: $b-custom-control-indicator-size-sm;
37
- @include border-radius($b-custom-checkbox-indicator-border-radius-sm);
38
- }
39
-
40
- .custom-control-label::after {
41
- top: ($font-size-sm * $line-height-sm - $b-custom-control-indicator-size-sm) * 0.5;
42
- left: -($b-custom-control-gutter-sm + $b-custom-control-indicator-size-sm);
43
- width: $b-custom-control-indicator-size-sm;
44
- height: $b-custom-control-indicator-size-sm;
45
- background-size: $b-custom-control-indicator-bg-size-sm;
46
- }
47
- }
48
-
49
- .custom-switch.b-custom-control-lg,
50
- .input-group-lg .custom-switch {
51
- padding-left: $b-custom-switch-width-lg + $b-custom-control-gutter-lg;
52
-
53
- .custom-control-label {
54
- font-size: $font-size-lg;
55
- line-height: $line-height-lg;
56
-
57
- &::before {
58
- top: ($font-size-lg * $line-height-lg - $b-custom-control-indicator-size-lg) * 0.5;
59
- height: $b-custom-control-indicator-size-lg;
60
- left: -($b-custom-switch-width-lg + $b-custom-control-gutter-lg);
61
- width: $b-custom-switch-width-lg;
62
- border-radius: $b-custom-switch-indicator-border-radius-lg;
63
- }
64
-
65
- &::after {
66
- top: calc(
67
- #{(($font-size-lg * $line-height-lg - $b-custom-control-indicator-size-lg) * 0.5)} + #{$custom-control-indicator-border-width *
68
- 2}
69
- );
70
- left: calc(
71
- #{- ($b-custom-switch-width-lg + $b-custom-control-gutter-lg)} + #{$custom-control-indicator-border-width *
72
- 2}
73
- );
74
- width: $b-custom-switch-indicator-size-lg;
75
- height: $b-custom-switch-indicator-size-lg;
76
- border-radius: $b-custom-switch-indicator-border-radius-lg;
77
- background-size: $b-custom-control-indicator-bg-size-lg;
78
- }
79
- }
80
-
81
- .custom-control-input:checked ~ .custom-control-label {
82
- &::after {
83
- transform: translateX($b-custom-switch-width-lg - $b-custom-control-indicator-size-lg);
84
- }
85
- }
86
- }
87
-
88
- .custom-switch.b-custom-control-sm,
89
- .input-group-sm .custom-switch {
90
- padding-left: $b-custom-switch-width-sm + $b-custom-control-gutter-sm;
91
-
92
- .custom-control-label {
93
- font-size: $font-size-sm;
94
- line-height: $line-height-sm;
95
-
96
- &::before {
97
- top: ($font-size-sm * $line-height-sm - $b-custom-control-indicator-size-sm) * 0.5;
98
- left: -($b-custom-switch-width-sm + $b-custom-control-gutter-sm);
99
- width: $b-custom-switch-width-sm;
100
- height: $b-custom-control-indicator-size-sm;
101
- border-radius: $b-custom-switch-indicator-border-radius-sm;
102
- }
103
-
104
- &::after {
105
- top: calc(
106
- #{(($font-size-sm * $line-height-sm - $b-custom-control-indicator-size-sm) * 0.5)} + #{$custom-control-indicator-border-width *
107
- 2}
108
- );
109
- left: calc(
110
- #{- ($b-custom-switch-width-sm + $b-custom-control-gutter-sm)} + #{$custom-control-indicator-border-width *
111
- 2}
112
- );
113
- width: $b-custom-switch-indicator-size-sm;
114
- height: $b-custom-switch-indicator-size-sm;
115
- border-radius: $b-custom-switch-indicator-border-radius-sm;
116
- background-size: $b-custom-control-indicator-bg-size-sm;
117
- }
118
- }
119
-
120
- .custom-control-input:checked ~ .custom-control-label {
121
- &::after {
122
- transform: translateX($b-custom-switch-width-sm - $b-custom-control-indicator-size-sm);
123
- }
124
- }
125
- }
@@ -1,42 +0,0 @@
1
- import { extend } from '../../vue'
2
- import { NAME_FORM_CHECKBOX_GROUP } from '../../constants/components'
3
- import { PROP_TYPE_ARRAY, PROP_TYPE_BOOLEAN } from '../../constants/props'
4
- import { sortKeys } from '../../utils/object'
5
- import { makeProp, makePropsConfigurable } from '../../utils/props'
6
- import {
7
- MODEL_PROP_NAME,
8
- formRadioCheckGroupMixin,
9
- props as formRadioCheckGroupProps
10
- } from '../../mixins/form-radio-check-group'
11
-
12
- // --- Props ---
13
-
14
- export const props = makePropsConfigurable(
15
- sortKeys({
16
- ...formRadioCheckGroupProps,
17
- [MODEL_PROP_NAME]: makeProp(PROP_TYPE_ARRAY, []),
18
- // Custom switch styling
19
- switches: makeProp(PROP_TYPE_BOOLEAN, false)
20
- }),
21
- NAME_FORM_CHECKBOX_GROUP
22
- )
23
-
24
- // --- Main component ---
25
-
26
- // @vue/component
27
- export const BFormCheckboxGroup = /*#__PURE__*/ extend({
28
- name: NAME_FORM_CHECKBOX_GROUP,
29
- // Includes render function
30
- mixins: [formRadioCheckGroupMixin],
31
- provide() {
32
- return {
33
- getBvCheckGroup: () => this
34
- }
35
- },
36
- props,
37
- computed: {
38
- isRadioGroup() {
39
- return false
40
- }
41
- }
42
- })
@@ -1,132 +0,0 @@
1
- import { extend } from '../../vue'
2
- import { NAME_FORM_CHECKBOX } from '../../constants/components'
3
- import { EVENT_NAME_CHANGE, MODEL_EVENT_NAME_PREFIX } from '../../constants/events'
4
- import { PROP_TYPE_ANY, PROP_TYPE_BOOLEAN } from '../../constants/props'
5
- import { isArray } from '../../utils/inspect'
6
- import { looseEqual } from '../../utils/loose-equal'
7
- import { looseIndexOf } from '../../utils/loose-index-of'
8
- import { sortKeys } from '../../utils/object'
9
- import { makeProp, makePropsConfigurable } from '../../utils/props'
10
- import {
11
- MODEL_EVENT_NAME,
12
- formRadioCheckMixin,
13
- props as formRadioCheckProps
14
- } from '../../mixins/form-radio-check'
15
-
16
- // --- Constants ---
17
-
18
- const MODEL_PROP_NAME_INDETERMINATE = 'indeterminate'
19
- const MODEL_EVENT_NAME_INDETERMINATE = MODEL_EVENT_NAME_PREFIX + MODEL_PROP_NAME_INDETERMINATE
20
-
21
- // --- Props ---
22
-
23
- export const props = makePropsConfigurable(
24
- sortKeys({
25
- ...formRadioCheckProps,
26
- // Not applicable in multi-check mode
27
- [MODEL_PROP_NAME_INDETERMINATE]: makeProp(PROP_TYPE_BOOLEAN, false),
28
- // Custom switch styling
29
- switch: makeProp(PROP_TYPE_BOOLEAN, false),
30
- // Not applicable in multi-check mode
31
- uncheckedValue: makeProp(PROP_TYPE_ANY, false),
32
- value: makeProp(PROP_TYPE_ANY, true)
33
- }),
34
- NAME_FORM_CHECKBOX
35
- )
36
-
37
- // --- Main component ---
38
-
39
- // @vue/component
40
- export const BFormCheckbox = /*#__PURE__*/ extend({
41
- name: NAME_FORM_CHECKBOX,
42
- mixins: [formRadioCheckMixin],
43
- inject: {
44
- getBvGroup: {
45
- from: 'getBvCheckGroup',
46
- default: () => () => null
47
- }
48
- },
49
- props,
50
- computed: {
51
- bvGroup() {
52
- return this.getBvGroup()
53
- },
54
- isChecked() {
55
- const { value, computedLocalChecked: checked } = this
56
- return isArray(checked) ? looseIndexOf(checked, value) > -1 : looseEqual(checked, value)
57
- },
58
- isRadio() {
59
- return false
60
- }
61
- },
62
- watch: {
63
- [MODEL_PROP_NAME_INDETERMINATE](newValue, oldValue) {
64
- if (!looseEqual(newValue, oldValue)) {
65
- this.setIndeterminate(newValue)
66
- }
67
- }
68
- },
69
- mounted() {
70
- // Set initial indeterminate state
71
- this.setIndeterminate(this[MODEL_PROP_NAME_INDETERMINATE])
72
- },
73
- methods: {
74
- computedLocalCheckedWatcher(newValue, oldValue) {
75
- if (!looseEqual(newValue, oldValue)) {
76
- this.$emit(MODEL_EVENT_NAME, newValue)
77
-
78
- const $input = this.$refs.input
79
- if ($input) {
80
- this.$emit(MODEL_EVENT_NAME_INDETERMINATE, $input.indeterminate)
81
- }
82
- }
83
- },
84
-
85
- handleChange({ target: { checked, indeterminate } }) {
86
- const { value, uncheckedValue } = this
87
-
88
- // Update `computedLocalChecked`
89
- let localChecked = this.computedLocalChecked
90
- if (isArray(localChecked)) {
91
- const index = looseIndexOf(localChecked, value)
92
- if (checked && index < 0) {
93
- // Add value to array
94
- localChecked = localChecked.concat(value)
95
- } else if (!checked && index > -1) {
96
- // Remove value from array
97
- localChecked = localChecked.slice(0, index).concat(localChecked.slice(index + 1))
98
- }
99
- } else {
100
- localChecked = checked ? value : uncheckedValue
101
- }
102
- this.computedLocalChecked = localChecked
103
-
104
- // Fire events in a `$nextTick()` to ensure the `v-model` is updated
105
- this.$nextTick(() => {
106
- // Change is only emitted on user interaction
107
- this.$emit(EVENT_NAME_CHANGE, localChecked)
108
-
109
- // If this is a child of a group, we emit a change event on it as well
110
- if (this.isGroup) {
111
- this.bvGroup.$emit(EVENT_NAME_CHANGE, localChecked)
112
- }
113
-
114
- this.$emit(MODEL_EVENT_NAME_INDETERMINATE, indeterminate)
115
- })
116
- },
117
-
118
- setIndeterminate(state) {
119
- // Indeterminate only supported in single checkbox mode
120
- if (isArray(this.computedLocalChecked)) {
121
- state = false
122
- }
123
-
124
- const $input = this.$refs.input
125
- if ($input) {
126
- $input.indeterminate = state
127
- // Emit update event to prop
128
- this.$emit(MODEL_EVENT_NAME_INDETERMINATE, state)
129
- }
130
- }
131
- }
132
- })
@@ -1,4 +0,0 @@
1
- import { BFormCheckbox } from './form-checkbox'
2
- import { BFormCheckboxGroup } from './form-checkbox-group'
3
-
4
- export { BFormCheckbox, BFormCheckboxGroup }
@@ -1,2 +0,0 @@
1
- @import "form-checkbox";
2
- @import "form-checkbox-group";