@@ -4,6 +4,7 @@ import { Combobox, ComboboxButton, ComboboxInput } from '@headlessui/vue'
44
55import type { Option } from ' ./Option.vue'
66import { OptionGroup , areOptionsGrouped } from ' ./OptionGroup.vue'
7+ import { GROUP_VALUE_PREFIX } from ' ./SelectableOptionGroup.vue'
78import ATag from ' ./Tag.vue'
89import AColorCircle from ' ./ColorCircle.vue'
910import OptionsPanel , { Direction } from ' ./internal/OptionsPanel.vue'
@@ -124,6 +125,15 @@ export default defineComponent({
124125 collapsedTagsLabel: {
125126 type: Function as PropType <(count : number ) => string >,
126127 default : (count : number ) => ` +${count } more `
128+ },
129+ /**
130+ * When true and the combobox is in multi-select mode with grouped options,
131+ * displays a CheckboxSelectAll in each group header to select/clear all
132+ * options in that group.
133+ */
134+ selectableGroups: {
135+ type: Boolean ,
136+ default: false
127137 }
128138 },
129139 emits: [' update:modelValue' , ' update:query' ],
@@ -146,11 +156,41 @@ export default defineComponent({
146156 }
147157 }
148158
159+ // toggle all options in a group (select all if not all selected, otherwise clear)
160+ const handleGroupToggle = (options : Option []) => {
161+ if (! isMultiSelect (props .modelValue )) return
162+
163+ const values = options .filter (o => ! o .disabled ).map (o => o .value )
164+ const currentSet = new Set (props .modelValue )
165+ const allSelected = values .every (v => currentSet .has (v ))
166+
167+ const newSelection = allSelected
168+ ? props .modelValue .filter (v => ! values .includes (v ))
169+ : [... new Set ([... props .modelValue , ... values ])]
170+
171+ emit (' update:modelValue' , newSelection )
172+ clearQuery ()
173+ }
174+
149175 const model = computed ({
150176 get : () => {
151177 return props .modelValue
152178 },
153179 set : value => {
180+ // handle keyboard selection of group headers (which have __group__ prefix)
181+ if (isMultiSelect (value ) && areOptionsGrouped (props .options )) {
182+ const groupValue = value .find (v => v .startsWith (GROUP_VALUE_PREFIX ))
183+ if (groupValue ) {
184+ // extract group title and toggle its options
185+ const title = groupValue .slice (GROUP_VALUE_PREFIX .length )
186+ const group = props .options .find (g => g .title === title )
187+ if (group ) {
188+ handleGroupToggle (group .options )
189+ }
190+ return
191+ }
192+ }
193+
154194 emit (' update:modelValue' , value )
155195 clearQuery ()
156196 }
@@ -245,6 +285,13 @@ export default defineComponent({
245285 return Math .max (0 , model .value .length - props .maxTags )
246286 })
247287
288+ const showSelectableGroups = computed (
289+ () =>
290+ props .selectableGroups &&
291+ isMultiSelect (props .modelValue ) &&
292+ areOptionsGrouped (props .options )
293+ )
294+
248295 const handleDelete = () => {
249296 if (isMultiSelect (model .value ) && ! query .value ) {
250297 model .value = model .value .slice (0 , - 1 )
@@ -279,7 +326,8 @@ export default defineComponent({
279326 handleBlur ,
280327 container ,
281328 visibleValues ,
282- hiddenCount
329+ hiddenCount ,
330+ showSelectableGroups
283331 }
284332 }
285333})
@@ -367,7 +415,9 @@ export default defineComponent({
367415 :multi =" isMultiSelect"
368416 :inline =" inline"
369417 :escape-overflow =" escapeOverflow"
370- :width =" optionsPanelWidth" >
418+ :width =" optionsPanelWidth"
419+ :selectable-groups =" showSelectableGroups"
420+ :selected-values =" isMultiSelect ? (model as string[]) : []" >
371421 <template #default >
372422 <!-- @slot `#default` slot. Takes `a-option` or `a-option-group` components without any wrappers. Use this slot to render options with extra styles or markup. Default value: `options` prop rendered as `a-option`s or `a-option-group`s -->
373423 <slot :filtered-options =" filteredOptions" ></slot >
0 commit comments