@coreui/react-pro 5.18.0 → 5.19.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 (46) hide show
  1. package/README.md +1 -1
  2. package/dist/cjs/components/autocomplete/CAutocomplete.d.ts +16 -2
  3. package/dist/cjs/components/autocomplete/CAutocomplete.js +99 -86
  4. package/dist/cjs/components/autocomplete/CAutocomplete.js.map +1 -1
  5. package/dist/cjs/components/calendar/utils.d.ts +0 -11
  6. package/dist/cjs/components/calendar/utils.js.map +1 -1
  7. package/dist/cjs/components/multi-select/CMultiSelect.d.ts +6 -0
  8. package/dist/cjs/components/multi-select/CMultiSelect.js +13 -12
  9. package/dist/cjs/components/multi-select/CMultiSelect.js.map +1 -1
  10. package/dist/cjs/components/multi-select/CMultiSelectOptions.js +6 -5
  11. package/dist/cjs/components/multi-select/CMultiSelectOptions.js.map +1 -1
  12. package/dist/cjs/components/multi-select/CMultiSelectSelection.js +1 -1
  13. package/dist/cjs/components/multi-select/CMultiSelectSelection.js.map +1 -1
  14. package/dist/cjs/components/multi-select/utils.d.ts +0 -2
  15. package/dist/cjs/components/multi-select/utils.js +0 -26
  16. package/dist/cjs/components/multi-select/utils.js.map +1 -1
  17. package/dist/cjs/utils/getNextSibling.js +3 -0
  18. package/dist/cjs/utils/getNextSibling.js.map +1 -1
  19. package/dist/cjs/utils/getPreviousSibling.js +3 -0
  20. package/dist/cjs/utils/getPreviousSibling.js.map +1 -1
  21. package/dist/esm/components/autocomplete/CAutocomplete.d.ts +16 -2
  22. package/dist/esm/components/autocomplete/CAutocomplete.js +99 -86
  23. package/dist/esm/components/autocomplete/CAutocomplete.js.map +1 -1
  24. package/dist/esm/components/calendar/utils.d.ts +0 -11
  25. package/dist/esm/components/calendar/utils.js.map +1 -1
  26. package/dist/esm/components/multi-select/CMultiSelect.d.ts +6 -0
  27. package/dist/esm/components/multi-select/CMultiSelect.js +14 -13
  28. package/dist/esm/components/multi-select/CMultiSelect.js.map +1 -1
  29. package/dist/esm/components/multi-select/CMultiSelectOptions.js +4 -3
  30. package/dist/esm/components/multi-select/CMultiSelectOptions.js.map +1 -1
  31. package/dist/esm/components/multi-select/CMultiSelectSelection.js +1 -1
  32. package/dist/esm/components/multi-select/CMultiSelectSelection.js.map +1 -1
  33. package/dist/esm/components/multi-select/utils.d.ts +0 -2
  34. package/dist/esm/components/multi-select/utils.js +1 -25
  35. package/dist/esm/components/multi-select/utils.js.map +1 -1
  36. package/dist/esm/utils/getNextSibling.js +3 -0
  37. package/dist/esm/utils/getNextSibling.js.map +1 -1
  38. package/dist/esm/utils/getPreviousSibling.js +3 -0
  39. package/dist/esm/utils/getPreviousSibling.js.map +1 -1
  40. package/package.json +7 -7
  41. package/src/components/autocomplete/CAutocomplete.tsx +190 -146
  42. package/src/components/calendar/utils.ts +0 -26
  43. package/src/components/multi-select/CMultiSelect.tsx +23 -4
  44. package/src/components/multi-select/CMultiSelectOptions.tsx +8 -5
  45. package/src/components/multi-select/CMultiSelectSelection.tsx +3 -3
  46. package/src/components/multi-select/utils.ts +0 -34
@@ -16,8 +16,8 @@ import PropTypes from 'prop-types'
16
16
 
17
17
  import type { Placement } from '@popperjs/core'
18
18
 
19
+ import { CConditionalPortal } from '../conditional-portal'
19
20
  import { CFormControlWrapper, CFormControlWrapperProps } from '../form/CFormControlWrapper'
20
-
21
21
  import { CAutocompleteOptions } from './CAutocompleteOptions'
22
22
 
23
23
  import { useForkedRef, usePopper } from '../../hooks'
@@ -73,6 +73,14 @@ export interface CAutocompleteProps
73
73
  */
74
74
  clearSearchOnSelect?: boolean
75
75
 
76
+ /**
77
+ * Specifies the DOM element where the dropdown should be rendered when using portal mode.
78
+ * Can be a DOM element reference, DocumentFragment, function that returns an element or null,
79
+ * or null (renders to document body). Works in conjunction with the `portal` prop to control
80
+ * where the portal content is mounted in the DOM tree.
81
+ */
82
+ container?: DocumentFragment | Element | (() => DocumentFragment | Element | null) | null
83
+
76
84
  /**
77
85
  * Toggle the disabled state for the component.
78
86
  * When `true`, the React.js autocomplete is non-interactive and appears visually disabled.
@@ -194,6 +202,16 @@ export interface CAutocompleteProps
194
202
  */
195
203
  placeholder?: string
196
204
 
205
+ /**
206
+ * Renders the autocomplete dropdown in a React portal, allowing it to break out of its
207
+ * parent container's styling constraints (like overflow, z-index, or positioning).
208
+ * When enabled, the dropdown is rendered at the document root level, ensuring it appears
209
+ * above other page elements and isn't clipped by parent containers.
210
+ *
211
+ * @default false
212
+ */
213
+ portal?: boolean
214
+
197
215
  /**
198
216
  * Toggle the readonly state for the component.
199
217
  * When `true`, users can view and interact with the dropdown but cannot
@@ -275,8 +293,6 @@ export interface CAutocompleteProps
275
293
  * Toggle the visibility of autocomplete dropdown.
276
294
  * Controls whether the dropdown is initially visible.
277
295
  * The dropdown visibility can still be toggled through user interaction.
278
- *
279
- * @default false
280
296
  */
281
297
  visible?: boolean
282
298
 
@@ -297,6 +313,7 @@ export const CAutocomplete = forwardRef<HTMLDivElement, CAutocompleteProps>(
297
313
  className,
298
314
  cleaner = false,
299
315
  clearSearchOnSelect = true,
316
+ container,
300
317
  disabled,
301
318
  feedback,
302
319
  feedbackInvalid,
@@ -317,6 +334,7 @@ export const CAutocomplete = forwardRef<HTMLDivElement, CAutocompleteProps>(
317
334
  optionsTemplate,
318
335
  optionsGroupsTemplate,
319
336
  placeholder,
337
+ portal = false,
320
338
  readOnly,
321
339
  required,
322
340
  resetSelectionOnOptionsChange = false,
@@ -329,7 +347,7 @@ export const CAutocomplete = forwardRef<HTMLDivElement, CAutocompleteProps>(
329
347
  valid,
330
348
  value,
331
349
  virtualScroller,
332
- visible = false,
350
+ visible,
333
351
  visibleItems = 10,
334
352
  ...rest
335
353
  },
@@ -380,9 +398,10 @@ export const CAutocomplete = forwardRef<HTMLDivElement, CAutocompleteProps>(
380
398
  useEffect(() => {
381
399
  if (resetSelectionOnOptionsChange) {
382
400
  handleClear()
383
- return
384
401
  }
402
+ }, [options])
385
403
 
404
+ useEffect(() => {
386
405
  if (value && typeof value === 'string') {
387
406
  handleSelect(value)
388
407
  return
@@ -407,48 +426,62 @@ export const CAutocomplete = forwardRef<HTMLDivElement, CAutocompleteProps>(
407
426
  }, [options, value])
408
427
 
409
428
  useEffect(() => {
410
- if (inputHintRef.current && showHints) {
411
- inputHintRef.current.value = hint
412
- ? `${searchValue}${getOptionLabel(hint).slice(searchValue.length)}`
413
- : ''
429
+ if (!showHints) {
430
+ return
414
431
  }
415
- }, [hint, searchValue])
432
+
433
+ const findOption =
434
+ searchValue.length > 0
435
+ ? filteredOptions.find((option) =>
436
+ getOptionLabel(option).toLowerCase().startsWith(searchValue.toLowerCase())
437
+ )
438
+ : undefined
439
+
440
+ setHint(findOption)
441
+ }, [filteredOptions, searchValue, showHints])
416
442
 
417
443
  useEffect(() => {
418
- if (visible) {
419
- handleDropdownShow()
420
- } else {
444
+ if (
445
+ !searchNoResultsLabel &&
446
+ searchValue.length > 0 &&
447
+ filteredOptions.length === 0 &&
448
+ _visible
449
+ ) {
421
450
  handleDropdownHide()
451
+ return
422
452
  }
423
- }, [visible])
424
453
 
425
- const handleKeyUp = useCallback((event: KeyboardEvent) => {
426
- if (event.key === 'Escape') {
427
- handleDropdownHide()
454
+ if (searchValue.length > 0 && filteredOptions.length > 0 && !_visible) {
455
+ handleDropdownShow()
428
456
  }
457
+ }, [filteredOptions])
429
458
 
430
- if (
431
- autoCompleteRef.current &&
432
- !autoCompleteRef.current.contains(event.target as HTMLElement)
433
- ) {
459
+ useEffect(() => {
460
+ if (visible === true) {
461
+ handleDropdownShow()
462
+ } else if (visible === false) {
434
463
  handleDropdownHide()
435
464
  }
436
- }, [])
465
+ }, [visible])
437
466
 
438
- const handleMouseUp = useCallback((event: Event) => {
439
- if (
440
- autoCompleteRef.current &&
441
- autoCompleteRef.current.contains(event.target as HTMLElement)
442
- ) {
443
- return
467
+ const handleClear = () => {
468
+ if (inputRef.current) {
469
+ inputRef.current.value = ''
444
470
  }
445
471
 
446
- handleDropdownHide()
447
- }, [])
472
+ setSearchValue('')
473
+ setSelected(null)
474
+ onChange?.(null)
475
+ }
448
476
 
449
- const handleSearch = (search: string) => {
450
- onInput?.(search)
451
- setSearchValue(search)
477
+ const handleGlobalSearch = (event: React.KeyboardEvent<HTMLDivElement>) => {
478
+ if (
479
+ isGlobalSearch(search) &&
480
+ inputRef.current &&
481
+ (event.key.length === 1 || event.key === 'Backspace' || event.key === 'Delete')
482
+ ) {
483
+ inputRef.current.focus()
484
+ }
452
485
  }
453
486
 
454
487
  const handleInputChange = (event: FormEvent<HTMLInputElement>) => {
@@ -459,29 +492,6 @@ export const CAutocomplete = forwardRef<HTMLDivElement, CAutocompleteProps>(
459
492
  onChange?.(null)
460
493
  setSelected(null)
461
494
  }
462
-
463
- const filteredOptionsInternal = filterOptions(options, value)
464
-
465
- if (!searchNoResultsLabel && value !== '' && filteredOptionsInternal.length === 0) {
466
- if (_visible) {
467
- handleDropdownHide()
468
- }
469
-
470
- return
471
- }
472
-
473
- handleDropdownShow(filteredOptionsInternal)
474
-
475
- if (showHints) {
476
- const findOption =
477
- value.length > 0
478
- ? filteredOptionsInternal.find((option) =>
479
- getOptionLabel(option).toLowerCase().startsWith(value.toLowerCase())
480
- )
481
- : undefined
482
-
483
- setHint(findOption)
484
- }
485
495
  }
486
496
 
487
497
  const handleInputKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
@@ -512,6 +522,7 @@ export const CAutocomplete = forwardRef<HTMLDivElement, CAutocompleteProps>(
512
522
  if (showHints && hint && event.key === 'Tab') {
513
523
  event.preventDefault()
514
524
  handleSelect(hint)
525
+ handleDropdownHide()
515
526
  return
516
527
  }
517
528
 
@@ -540,80 +551,38 @@ export const CAutocomplete = forwardRef<HTMLDivElement, CAutocompleteProps>(
540
551
  }
541
552
  }
542
553
 
543
- const handleClear = () => {
544
- if (inputRef.current) {
545
- inputRef.current.value = ''
554
+ const handleKeyUp = useCallback((event: KeyboardEvent) => {
555
+ if (event.key === 'Escape') {
556
+ handleDropdownHide()
546
557
  }
547
558
 
548
- setSearchValue('')
549
- setSelected(null)
550
- onChange?.(null)
551
- }
552
-
553
- const handleDropdownShow = useCallback(
554
- (filtered: Option[] = filteredOptions) => {
555
- if (disabled || readOnly || _visible) {
556
- return
557
- }
558
-
559
- if (filtered.length === 0 && searchNoResultsLabel === false) {
560
- return
561
- }
562
-
563
- if (filtered.length === 0 && allowOnlyDefinedOptions === false) {
564
- return
565
- }
566
-
567
- setVisible(true)
568
- onShow?.()
569
-
570
- window.addEventListener('mouseup', handleMouseUp)
571
- window.addEventListener('keyup', handleKeyUp)
572
-
573
- if (togglerRef.current && dropdownRef.current) {
574
- initPopper(togglerRef.current, dropdownRef.current, popperConfig)
575
- }
576
-
577
- inputRef.current?.focus()
578
- },
579
- [
580
- disabled,
581
- readOnly,
582
- _visible,
583
- filteredOptions.length,
584
- searchNoResultsLabel,
585
- allowOnlyDefinedOptions,
586
- onShow,
587
- handleMouseUp,
588
- handleKeyUp,
589
- initPopper,
590
- popperConfig,
591
- ]
592
- )
593
-
594
- const handleDropdownHide = useCallback(() => {
595
- setVisible(false)
596
- onHide?.()
597
-
598
- window.removeEventListener('mouseup', handleMouseUp)
599
- window.removeEventListener('keyup', handleKeyUp)
600
-
601
- destroyPopper()
602
- inputRef.current?.focus()
603
- }, [onHide, handleMouseUp, handleKeyUp, destroyPopper])
559
+ if (
560
+ autoCompleteRef.current &&
561
+ !autoCompleteRef.current.contains(event.target as HTMLElement)
562
+ ) {
563
+ handleDropdownHide()
564
+ }
565
+ }, [])
604
566
 
605
- const handleGlobalSearch = (event: React.KeyboardEvent<HTMLDivElement>) => {
567
+ const handleMouseUp = useCallback((event: Event) => {
606
568
  if (
607
- isGlobalSearch(search) &&
608
- inputRef.current &&
609
- (event.key.length === 1 || event.key === 'Backspace' || event.key === 'Delete')
569
+ autoCompleteRef.current &&
570
+ autoCompleteRef.current.contains(event.target as HTMLElement)
610
571
  ) {
611
- inputRef.current.focus()
572
+ return
612
573
  }
613
- }
574
+
575
+ handleDropdownHide()
576
+ }, [])
614
577
 
615
578
  const handleOptionClick = (option: Option) => {
616
579
  handleSelect(option)
580
+ handleDropdownHide()
581
+ }
582
+
583
+ const handleSearch = (search: string) => {
584
+ onInput?.(search)
585
+ setSearchValue(search)
617
586
  }
618
587
 
619
588
  const handleSelect = (option?: Option | undefined) => {
@@ -627,13 +596,73 @@ export const CAutocomplete = forwardRef<HTMLDivElement, CAutocompleteProps>(
627
596
 
628
597
  if (clearSearchOnSelect) {
629
598
  handleSearch('')
599
+ } else {
600
+ setHint('')
630
601
  }
631
602
 
632
603
  setSelected(option ?? null)
633
604
  onChange?.(option ?? null)
634
- handleDropdownHide()
635
605
  }
636
606
 
607
+ const handleDropdownShow = useCallback(() => {
608
+ if (disabled || readOnly || _visible) {
609
+ return
610
+ }
611
+
612
+ if (
613
+ !isExternalSearch(search) &&
614
+ filteredOptions.length === 0 &&
615
+ searchNoResultsLabel === false
616
+ ) {
617
+ return
618
+ }
619
+
620
+ if (portal && dropdownRef.current && togglerRef.current) {
621
+ dropdownRef.current.style.minWidth = `${(togglerRef.current as HTMLElement).offsetWidth}px`
622
+ }
623
+
624
+ setVisible(true)
625
+ onShow?.()
626
+
627
+ window.addEventListener('mouseup', handleMouseUp)
628
+ window.addEventListener('keyup', handleKeyUp)
629
+
630
+ if (togglerRef.current && dropdownRef.current) {
631
+ setTimeout(() => {
632
+ initPopper(
633
+ togglerRef.current as HTMLDivElement,
634
+ dropdownRef.current as HTMLDivElement,
635
+ popperConfig
636
+ )
637
+ }, 1) // Allow DOM updates to complete before initializing Popper
638
+ }
639
+
640
+ inputRef.current?.focus()
641
+ }, [
642
+ disabled,
643
+ readOnly,
644
+ _visible,
645
+ filteredOptions.length,
646
+ searchNoResultsLabel,
647
+ allowOnlyDefinedOptions,
648
+ onShow,
649
+ handleMouseUp,
650
+ handleKeyUp,
651
+ initPopper,
652
+ popperConfig,
653
+ ])
654
+
655
+ const handleDropdownHide = useCallback(() => {
656
+ setVisible(false)
657
+ onHide?.()
658
+
659
+ window.removeEventListener('mouseup', handleMouseUp)
660
+ window.removeEventListener('keyup', handleKeyUp)
661
+
662
+ destroyPopper()
663
+ inputRef.current?.focus()
664
+ }, [onHide, handleMouseUp, handleKeyUp, destroyPopper])
665
+
637
666
  return (
638
667
  <CFormControlWrapper
639
668
  describedby={rest['aria-describedby']}
@@ -670,10 +699,14 @@ export const CAutocomplete = forwardRef<HTMLDivElement, CAutocompleteProps>(
670
699
  {showHints && searchValue !== '' && (
671
700
  <input
672
701
  className="autocomplete-input autocomplete-input-hint"
702
+ id={id || `autocomplete-hint-${uniqueId}`}
673
703
  autoComplete="off"
674
704
  readOnly
675
705
  tabIndex={-1}
676
706
  aria-hidden="true"
707
+ value={
708
+ hint ? `${searchValue}${getOptionLabel(hint).slice(searchValue.length)}` : ''
709
+ }
677
710
  ref={inputHintRef}
678
711
  />
679
712
  )}
@@ -698,6 +731,7 @@ export const CAutocomplete = forwardRef<HTMLDivElement, CAutocompleteProps>(
698
731
  aria-autocomplete="list"
699
732
  aria-expanded={_visible}
700
733
  aria-haspopup="listbox"
734
+ {...(portal && { 'aria-owns': `autocomplete-listbox-${uniqueId}` })}
701
735
  readOnly={readOnly}
702
736
  role="combobox"
703
737
  ref={inputRef}
@@ -718,7 +752,10 @@ export const CAutocomplete = forwardRef<HTMLDivElement, CAutocompleteProps>(
718
752
  <button
719
753
  type="button"
720
754
  className="autocomplete-indicator"
721
- disabled={!(searchNoResultsLabel || filteredOptions.length > 0)}
755
+ disabled={
756
+ !(searchNoResultsLabel || filteredOptions.length > 0) &&
757
+ isExternalSearch(search)
758
+ }
722
759
  onClick={(event) => {
723
760
  event.preventDefault()
724
761
  event.stopPropagation()
@@ -732,29 +769,34 @@ export const CAutocomplete = forwardRef<HTMLDivElement, CAutocompleteProps>(
732
769
  </div>
733
770
  )}
734
771
  </div>
735
- <div
736
- className="autocomplete-dropdown"
737
- role="listbox"
738
- aria-labelledby={id || `autocomplete-${uniqueId}`}
739
- ref={dropdownRef}
740
- >
741
- <CAutocompleteOptions
742
- highlightOptionsOnSearch={highlightOptionsOnSearch}
743
- loading={loading}
744
- onOptionClick={(option: Option) =>
745
- !disabled && !readOnly && handleOptionClick(option)
746
- }
747
- options={filteredOptions}
748
- optionsMaxHeight={optionsMaxHeight}
749
- optionsTemplate={optionsTemplate}
750
- optionsGroupsTemplate={optionsGroupsTemplate}
751
- searchNoResultsLabel={searchNoResultsLabel}
752
- searchValue={searchValue}
753
- selected={selected}
754
- virtualScroller={virtualScroller}
755
- visibleItems={visibleItems}
756
- />
757
- </div>
772
+ <CConditionalPortal container={container} portal={portal}>
773
+ <div
774
+ className={classNames('autocomplete-dropdown', {
775
+ show: portal && _visible,
776
+ })}
777
+ id={`autocomplete-listbox-${uniqueId}`}
778
+ role="listbox"
779
+ aria-labelledby={id || `autocomplete-${uniqueId}`}
780
+ ref={dropdownRef}
781
+ >
782
+ <CAutocompleteOptions
783
+ highlightOptionsOnSearch={highlightOptionsOnSearch}
784
+ loading={loading}
785
+ onOptionClick={(option: Option) =>
786
+ !disabled && !readOnly && handleOptionClick(option)
787
+ }
788
+ options={filteredOptions}
789
+ optionsMaxHeight={optionsMaxHeight}
790
+ optionsTemplate={optionsTemplate}
791
+ optionsGroupsTemplate={optionsGroupsTemplate}
792
+ searchNoResultsLabel={searchNoResultsLabel}
793
+ searchValue={searchValue}
794
+ selected={selected}
795
+ virtualScroller={virtualScroller}
796
+ visibleItems={visibleItems}
797
+ />
798
+ </div>
799
+ </CConditionalPortal>
758
800
  </div>
759
801
  </CFormControlWrapper>
760
802
  )
@@ -766,6 +808,7 @@ CAutocomplete.propTypes = {
766
808
  className: PropTypes.string,
767
809
  clearSearchOnSelect: PropTypes.bool,
768
810
  cleaner: PropTypes.bool,
811
+ container: PropTypes.any,
769
812
  disabled: PropTypes.bool,
770
813
  highlightOptionsOnSearch: PropTypes.bool,
771
814
  indicator: PropTypes.bool,
@@ -780,6 +823,7 @@ CAutocomplete.propTypes = {
780
823
  optionsTemplate: PropTypes.func,
781
824
  optionsGroupsTemplate: PropTypes.func,
782
825
  placeholder: PropTypes.string,
826
+ portal: PropTypes.bool,
783
827
  required: PropTypes.bool,
784
828
  resetSelectionOnOptionsChange: PropTypes.bool,
785
829
  search: PropTypes.oneOfType([
@@ -139,32 +139,6 @@ export const getFirstAvailableDateInRange = (
139
139
  return null
140
140
  }
141
141
 
142
- /**
143
- * Returns the ISO 8601 week string (e.g., "2026W01") for a given date.
144
- *
145
- * The ISO week date system considers:
146
- * - Weeks starting on Monday
147
- * - Week 1 is the week with the first Thursday of the year
148
- *
149
- * @param {Date} date - The date for which to calculate the ISO week and year.
150
- * @returns {string} The ISO week string in the format "YYYYWww" (e.g., "2026W01").
151
- */
152
- export const getISOWeekString = (date: Date): string => {
153
- const tempDate = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()))
154
- // Set to nearest Thursday: current date + 4 - current day number (0 is Sunday)
155
- const day = tempDate.getUTCDay()
156
- const diff = day <= 4 ? 4 - day : 11 - day
157
- tempDate.setUTCDate(tempDate.getUTCDate() + diff)
158
-
159
- const yearStart = new Date(Date.UTC(tempDate.getUTCFullYear(), 0, 1))
160
- const weekNumber = Math.ceil(((tempDate.getTime() - yearStart.getTime()) / 86400000 + 1) / 7)
161
-
162
- const isoYear = tempDate.getUTCFullYear()
163
- const weekString = `W${weekNumber.toString().padStart(2, '0')}`
164
-
165
- return `${isoYear}${weekString}`
166
- }
167
-
168
142
  /**
169
143
  * Retrieves an array of month names based on locale and format.
170
144
  * @param locale - The locale string (e.g., 'en-US').
@@ -6,6 +6,7 @@ import React, {
6
6
  useState,
7
7
  useRef,
8
8
  useMemo,
9
+ useId,
9
10
  } from 'react'
10
11
 
11
12
  import classNames from 'classnames'
@@ -46,6 +47,12 @@ export interface CMultiSelectProps
46
47
  * @since 5.8.0
47
48
  */
48
49
  ariaCleanerLabel?: string
50
+ /**
51
+ * A string that provides an accessible label for the indicator button. This label is read by screen readers to describe the action associated with the button.
52
+ *
53
+ * @since 5.19.0
54
+ */
55
+ ariaIndicatorLabel?: string
49
56
  /**
50
57
  * A string of all className you want applied to the base component.
51
58
  */
@@ -217,6 +224,7 @@ export const CMultiSelect = forwardRef<HTMLDivElement, CMultiSelectProps>(
217
224
  {
218
225
  allowCreateOptions,
219
226
  ariaCleanerLabel = 'Clear all selections',
227
+ ariaIndicatorLabel = 'Toggle visibility of options menu',
220
228
  className,
221
229
  cleaner = true,
222
230
  clearSearchOnSelect,
@@ -274,6 +282,7 @@ export const CMultiSelect = forwardRef<HTMLDivElement, CMultiSelectProps>(
274
282
  const nativeSelectRef = useRef<HTMLSelectElement>(null)
275
283
  const searchRef = useRef<HTMLInputElement>(null)
276
284
  const isInitialMount = useRef(true)
285
+ const uniqueId = useId()
277
286
 
278
287
  const [searchValue, setSearchValue] = useState('')
279
288
  const [selected, setSelected] = useState<SelectedOption[]>([])
@@ -532,7 +541,7 @@ export const CMultiSelect = forwardRef<HTMLDivElement, CMultiSelectProps>(
532
541
  feedback={feedback}
533
542
  feedbackInvalid={feedbackInvalid}
534
543
  feedbackValid={feedbackValid}
535
- id={id}
544
+ id={id ?? uniqueId}
536
545
  invalid={invalid}
537
546
  label={label}
538
547
  text={text}
@@ -540,9 +549,9 @@ export const CMultiSelect = forwardRef<HTMLDivElement, CMultiSelectProps>(
540
549
  valid={valid}
541
550
  >
542
551
  <CMultiSelectNativeSelect
543
- id={id}
552
+ id={id ?? uniqueId}
544
553
  multiple={multiple}
545
- name={name}
554
+ name={name ?? uniqueId}
546
555
  options={selected}
547
556
  required={required}
548
557
  value={
@@ -574,6 +583,10 @@ export const CMultiSelect = forwardRef<HTMLDivElement, CMultiSelectProps>(
574
583
  {...(!search && !disabled && { tabIndex: 0 })}
575
584
  onClick={() => !disabled && openDropdown()}
576
585
  onKeyDown={handleTogglerKeyDown}
586
+ role="combobox"
587
+ aria-haspopup="listbox"
588
+ aria-expanded={isOpen}
589
+ {...(portal && { 'aria-owns': `multiselect-listbox-${uniqueId}` })}
577
590
  ref={dropdownRefElement}
578
591
  >
579
592
  <CMultiSelectSelection
@@ -591,6 +604,8 @@ export const CMultiSelect = forwardRef<HTMLDivElement, CMultiSelectProps>(
591
604
  type="text"
592
605
  className="form-multi-select-search"
593
606
  disabled={disabled}
607
+ id={`search${id ?? uniqueId}`}
608
+ name={`search${name ?? uniqueId}`}
594
609
  autoComplete="off"
595
610
  onChange={handleSearchChange}
596
611
  onKeyDown={handleSearchKeyDown}
@@ -632,6 +647,7 @@ export const CMultiSelect = forwardRef<HTMLDivElement, CMultiSelectProps>(
632
647
  }
633
648
  }}
634
649
  {...(disabled && { tabIndex: -1 })}
650
+ aria-label={ariaIndicatorLabel}
635
651
  ></button>
636
652
  </div>
637
653
  </div>
@@ -640,8 +656,10 @@ export const CMultiSelect = forwardRef<HTMLDivElement, CMultiSelectProps>(
640
656
  className={classNames('form-multi-select-dropdown', {
641
657
  show: portal && isOpen,
642
658
  })}
659
+ id={`multiselect-listbox-${uniqueId}`}
643
660
  onKeyDown={handleGlobalSearch}
644
- role="menu"
661
+ role="listbox"
662
+ aria-multiselectable={multiple}
645
663
  ref={dropdownMenuElement}
646
664
  >
647
665
  {multiple && selectAll && (
@@ -681,6 +699,7 @@ export const CMultiSelect = forwardRef<HTMLDivElement, CMultiSelectProps>(
681
699
  CMultiSelect.propTypes = {
682
700
  allowCreateOptions: PropTypes.bool,
683
701
  ariaCleanerLabel: PropTypes.string,
702
+ ariaIndicatorLabel: PropTypes.string,
684
703
  className: PropTypes.string,
685
704
  cleaner: PropTypes.bool,
686
705
  clearSearchOnSelect: PropTypes.bool,
@@ -5,7 +5,7 @@ import classNames from 'classnames'
5
5
  import { CElementCover } from '../element-cover'
6
6
  import { CVirtualScroller } from '../virtual-scroller'
7
7
 
8
- import { getNextSibling, getPreviousSibling } from './utils'
8
+ import { getNextSibling, getPreviousSibling } from '../../utils'
9
9
  import type { Option, OptionsGroup } from './types'
10
10
 
11
11
  export interface CMultiSelectOptionsProps extends HTMLAttributes<HTMLDivElement> {
@@ -38,12 +38,12 @@ export const CMultiSelectOptions = forwardRef<HTMLDivElement, CMultiSelectOption
38
38
  virtualScroller,
39
39
  visibleItems = 10,
40
40
  },
41
- ref,
41
+ ref
42
42
  ) => {
43
43
  const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>, option: Option) => {
44
44
  if (event.code === 'Space' || event.key === 'Enter') {
45
45
  event.preventDefault()
46
- onOptionOnClick && onOptionOnClick(option)
46
+ onOptionOnClick?.(option)
47
47
  }
48
48
 
49
49
  if (event.key === 'Down' || event.key === 'ArrowDown') {
@@ -77,6 +77,9 @@ export const CMultiSelectOptions = forwardRef<HTMLDivElement, CMultiSelectOption
77
77
  onClick={() => onOptionOnClick && onOptionOnClick(option as Option)}
78
78
  onKeyDown={(event) => handleKeyDown(event, option as Option)}
79
79
  tabIndex={0}
80
+ role="option"
81
+ aria-selected={selected.some((_option) => _option.value === option.value)}
82
+ aria-disabled={Boolean(option.disabled)}
80
83
  >
81
84
  {optionsTemplate ? optionsTemplate(option as Option) : option.label}
82
85
  </div>
@@ -84,7 +87,7 @@ export const CMultiSelectOptions = forwardRef<HTMLDivElement, CMultiSelectOption
84
87
  <div className="form-multi-select-optgroup-label" key={index}>
85
88
  {optionsGroupsTemplate ? optionsGroupsTemplate(option as OptionsGroup) : option.label}
86
89
  </div>
87
- ),
90
+ )
88
91
  )
89
92
  ) : (
90
93
  <div className="form-multi-select-options-empty">{searchNoResultsLabel}</div>
@@ -116,7 +119,7 @@ export const CMultiSelectOptions = forwardRef<HTMLDivElement, CMultiSelectOption
116
119
  {loading && <CElementCover />}
117
120
  </>
118
121
  )
119
- },
122
+ }
120
123
  )
121
124
 
122
125
  CMultiSelectOptions.propTypes = {