@coreui/vue-pro 5.14.0 → 5.16.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 (127) hide show
  1. package/README.md +1 -1
  2. package/dist/cjs/components/calendar/CCalendar.js +67 -65
  3. package/dist/cjs/components/calendar/CCalendar.js.map +1 -1
  4. package/dist/cjs/components/calendar/utils.d.ts +53 -2
  5. package/dist/cjs/components/calendar/utils.js +466 -43
  6. package/dist/cjs/components/calendar/utils.js.map +1 -1
  7. package/dist/cjs/components/date-range-picker/CDateRangePicker.js +86 -57
  8. package/dist/cjs/components/date-range-picker/CDateRangePicker.js.map +1 -1
  9. package/dist/cjs/components/date-range-picker/utils.d.ts +0 -9
  10. package/dist/cjs/components/date-range-picker/utils.js +0 -38
  11. package/dist/cjs/components/date-range-picker/utils.js.map +1 -1
  12. package/dist/cjs/components/dropdown/CDropdown.d.ts +32 -7
  13. package/dist/cjs/components/dropdown/CDropdown.js +67 -29
  14. package/dist/cjs/components/dropdown/CDropdown.js.map +1 -1
  15. package/dist/cjs/components/dropdown/CDropdownToggle.d.ts +19 -0
  16. package/dist/cjs/components/dropdown/CDropdownToggle.js +17 -2
  17. package/dist/cjs/components/dropdown/CDropdownToggle.js.map +1 -1
  18. package/dist/cjs/components/dropdown/utils.d.ts +2 -0
  19. package/dist/cjs/components/dropdown/utils.js +13 -0
  20. package/dist/cjs/components/dropdown/utils.js.map +1 -1
  21. package/dist/cjs/components/focus-trap/CFocusTrap.d.ts +108 -0
  22. package/dist/cjs/components/focus-trap/CFocusTrap.js +254 -0
  23. package/dist/cjs/components/focus-trap/CFocusTrap.js.map +1 -0
  24. package/dist/cjs/components/focus-trap/index.d.ts +6 -0
  25. package/dist/cjs/components/focus-trap/index.js +13 -0
  26. package/dist/cjs/components/focus-trap/index.js.map +1 -0
  27. package/dist/cjs/components/focus-trap/utils.d.ts +28 -0
  28. package/dist/cjs/components/focus-trap/utils.js +83 -0
  29. package/dist/cjs/components/focus-trap/utils.js.map +1 -0
  30. package/dist/cjs/components/index.d.ts +1 -0
  31. package/dist/cjs/components/index.js +70 -66
  32. package/dist/cjs/components/index.js.map +1 -1
  33. package/dist/cjs/components/modal/CModal.d.ts +2 -2
  34. package/dist/cjs/components/modal/CModal.js +19 -27
  35. package/dist/cjs/components/modal/CModal.js.map +1 -1
  36. package/dist/cjs/components/modal/CModalHeader.js +4 -2
  37. package/dist/cjs/components/modal/CModalHeader.js.map +1 -1
  38. package/dist/cjs/components/nav/CNavItem.d.ts +2 -2
  39. package/dist/cjs/components/offcanvas/COffcanvas.js +3 -2
  40. package/dist/cjs/components/offcanvas/COffcanvas.js.map +1 -1
  41. package/dist/cjs/components/picker/CPicker.js +3 -2
  42. package/dist/cjs/components/picker/CPicker.js.map +1 -1
  43. package/dist/cjs/components/time-picker/CTimePicker.d.ts +45 -1
  44. package/dist/cjs/components/time-picker/CTimePicker.js +64 -8
  45. package/dist/cjs/components/time-picker/CTimePicker.js.map +1 -1
  46. package/dist/cjs/components/time-picker/CTimePickerRollCol.d.ts +19 -7
  47. package/dist/cjs/components/time-picker/CTimePickerRollCol.js +80 -8
  48. package/dist/cjs/components/time-picker/CTimePickerRollCol.js.map +1 -1
  49. package/dist/cjs/components/time-picker/utils.d.ts +1 -1
  50. package/dist/cjs/composables/useDebouncedCallback.d.ts +1 -1
  51. package/dist/cjs/composables/useDebouncedCallback.js +1 -1
  52. package/dist/cjs/composables/useDebouncedCallback.js.map +1 -1
  53. package/dist/cjs/index.js +76 -72
  54. package/dist/cjs/index.js.map +1 -1
  55. package/dist/esm/components/calendar/CCalendar.js +67 -65
  56. package/dist/esm/components/calendar/CCalendar.js.map +1 -1
  57. package/dist/esm/components/calendar/utils.d.ts +53 -2
  58. package/dist/esm/components/calendar/utils.js +464 -44
  59. package/dist/esm/components/calendar/utils.js.map +1 -1
  60. package/dist/esm/components/date-range-picker/CDateRangePicker.js +86 -57
  61. package/dist/esm/components/date-range-picker/CDateRangePicker.js.map +1 -1
  62. package/dist/esm/components/date-range-picker/utils.d.ts +0 -9
  63. package/dist/esm/components/date-range-picker/utils.js +1 -38
  64. package/dist/esm/components/date-range-picker/utils.js.map +1 -1
  65. package/dist/esm/components/dropdown/CDropdown.d.ts +32 -7
  66. package/dist/esm/components/dropdown/CDropdown.js +69 -31
  67. package/dist/esm/components/dropdown/CDropdown.js.map +1 -1
  68. package/dist/esm/components/dropdown/CDropdownToggle.d.ts +19 -0
  69. package/dist/esm/components/dropdown/CDropdownToggle.js +17 -2
  70. package/dist/esm/components/dropdown/CDropdownToggle.js.map +1 -1
  71. package/dist/esm/components/dropdown/utils.d.ts +2 -0
  72. package/dist/esm/components/dropdown/utils.js +13 -1
  73. package/dist/esm/components/dropdown/utils.js.map +1 -1
  74. package/dist/esm/components/focus-trap/CFocusTrap.d.ts +108 -0
  75. package/dist/esm/components/focus-trap/CFocusTrap.js +252 -0
  76. package/dist/esm/components/focus-trap/CFocusTrap.js.map +1 -0
  77. package/dist/esm/components/focus-trap/index.d.ts +6 -0
  78. package/dist/esm/components/focus-trap/index.js +10 -0
  79. package/dist/esm/components/focus-trap/index.js.map +1 -0
  80. package/dist/esm/components/focus-trap/utils.d.ts +28 -0
  81. package/dist/esm/components/focus-trap/utils.js +78 -0
  82. package/dist/esm/components/focus-trap/utils.js.map +1 -0
  83. package/dist/esm/components/index.d.ts +1 -0
  84. package/dist/esm/components/index.js +2 -0
  85. package/dist/esm/components/index.js.map +1 -1
  86. package/dist/esm/components/modal/CModal.d.ts +2 -2
  87. package/dist/esm/components/modal/CModal.js +19 -27
  88. package/dist/esm/components/modal/CModal.js.map +1 -1
  89. package/dist/esm/components/modal/CModalHeader.js +4 -2
  90. package/dist/esm/components/modal/CModalHeader.js.map +1 -1
  91. package/dist/esm/components/nav/CNavItem.d.ts +2 -2
  92. package/dist/esm/components/offcanvas/COffcanvas.js +3 -2
  93. package/dist/esm/components/offcanvas/COffcanvas.js.map +1 -1
  94. package/dist/esm/components/picker/CPicker.js +3 -2
  95. package/dist/esm/components/picker/CPicker.js.map +1 -1
  96. package/dist/esm/components/time-picker/CTimePicker.d.ts +45 -1
  97. package/dist/esm/components/time-picker/CTimePicker.js +64 -8
  98. package/dist/esm/components/time-picker/CTimePicker.js.map +1 -1
  99. package/dist/esm/components/time-picker/CTimePickerRollCol.d.ts +19 -7
  100. package/dist/esm/components/time-picker/CTimePickerRollCol.js +80 -8
  101. package/dist/esm/components/time-picker/CTimePickerRollCol.js.map +1 -1
  102. package/dist/esm/components/time-picker/utils.d.ts +1 -1
  103. package/dist/esm/composables/useDebouncedCallback.d.ts +1 -1
  104. package/dist/esm/composables/useDebouncedCallback.js +1 -1
  105. package/dist/esm/composables/useDebouncedCallback.js.map +1 -1
  106. package/dist/esm/index.js +2 -0
  107. package/dist/esm/index.js.map +1 -1
  108. package/package.json +5 -5
  109. package/src/components/calendar/CCalendar.ts +61 -70
  110. package/src/components/calendar/utils.ts +595 -47
  111. package/src/components/date-range-picker/CDateRangePicker.ts +131 -82
  112. package/src/components/date-range-picker/utils.ts +0 -58
  113. package/src/components/dropdown/CDropdown.ts +119 -52
  114. package/src/components/dropdown/CDropdownToggle.ts +18 -3
  115. package/src/components/dropdown/utils.ts +21 -0
  116. package/src/components/focus-trap/CFocusTrap.ts +303 -0
  117. package/src/components/focus-trap/index.ts +10 -0
  118. package/src/components/focus-trap/utils.ts +90 -0
  119. package/src/components/index.ts +1 -0
  120. package/src/components/modal/CModal.ts +32 -37
  121. package/src/components/modal/CModalHeader.ts +5 -3
  122. package/src/components/nav/CNavItem.ts +1 -1
  123. package/src/components/offcanvas/COffcanvas.ts +40 -36
  124. package/src/components/picker/CPicker.ts +58 -52
  125. package/src/components/time-picker/CTimePicker.ts +80 -22
  126. package/src/components/time-picker/CTimePickerRollCol.ts +87 -9
  127. package/src/composables/useDebouncedCallback.ts +1 -1
@@ -1,13 +1,18 @@
1
- import { defineComponent, h, onMounted, PropType, ref, watch } from 'vue'
1
+ import { defineComponent, h, onMounted, PropType, Ref, ref, watch } from 'vue'
2
2
 
3
3
  import { CButton } from '../button'
4
4
  import { CCalendar } from '../calendar'
5
- import { convertToDateObject } from '../calendar/utils'
5
+ import {
6
+ convertToDateObject,
7
+ getDateBySelectionType,
8
+ getLocalDateFromString,
9
+ isDateDisabled,
10
+ } from '../calendar/utils'
6
11
  import { CFormControlWrapper } from './../form/CFormControlWrapper'
7
12
  import { CPicker } from '../picker'
8
13
  import { CTimePicker } from '../time-picker'
9
14
 
10
- import { getInputIdOrName, getLocalDateFromString } from './utils'
15
+ import { getInputIdOrName } from './utils'
11
16
 
12
17
  import { useDebouncedCallback } from '../../composables'
13
18
  import { Color } from '../props'
@@ -649,9 +654,17 @@ const CDateRangePicker = defineComponent({
649
654
 
650
655
  const formatDate = (date: Date | string) => {
651
656
  if (props.inputDateFormat) {
652
- return props.inputDateFormat(
657
+ const convertedDate =
653
658
  date instanceof Date ? new Date(date) : convertToDateObject(date, props.selectionType)
654
- )
659
+
660
+ if (
661
+ !convertedDate ||
662
+ (convertedDate instanceof Date && Number.isNaN(convertedDate.getTime()))
663
+ ) {
664
+ return ''
665
+ }
666
+
667
+ return props.inputDateFormat(convertedDate)
655
668
  }
656
669
 
657
670
  if (props.selectionType !== 'day') {
@@ -659,13 +672,20 @@ const CDateRangePicker = defineComponent({
659
672
  }
660
673
 
661
674
  const _date = new Date(date)
675
+ if (Number.isNaN(_date.getTime())) {
676
+ return ''
677
+ }
662
678
 
663
679
  return props.timepicker
664
680
  ? _date.toLocaleString(props.locale)
665
681
  : _date.toLocaleDateString(props.locale)
666
682
  }
667
683
 
668
- const setInputValue = (date: Date | string | null) => {
684
+ const setInputValue = (date: Date | string | null, value?: Ref<HTMLInputElement>) => {
685
+ if (date instanceof Date && isNaN(date.getTime())) {
686
+ return value?.value
687
+ }
688
+
669
689
  if (date) {
670
690
  return formatDate(date)
671
691
  }
@@ -750,24 +770,118 @@ const CDateRangePicker = defineComponent({
750
770
  }
751
771
 
752
772
  const handleOnChange = (value: string, input: string) => {
753
- const date = props.inputDateParse
754
- ? props.inputDateParse(value)
755
- : getLocalDateFromString(value, props.locale, props.timepicker)
773
+ let date: Date | string | null = null
774
+
775
+ if (props.inputDateParse) {
776
+ date = props.inputDateParse(value)
777
+ } else if (props.selectionType === 'day') {
778
+ date = getLocalDateFromString(value, props.locale, props.timepicker)
779
+ } else {
780
+ date = convertToDateObject(value, props.selectionType)
781
+ }
782
+
756
783
  if (date instanceof Date && date.getTime()) {
784
+ if (
785
+ isDateDisabled(
786
+ date,
787
+ props.minDate ? convertToDateObject(props.minDate, props.selectionType) : null,
788
+ props.maxDate ? convertToDateObject(props.maxDate, props.selectionType) : null,
789
+ props.disabledDates
790
+ )
791
+ ) {
792
+ return // Don't update if date is disabled
793
+ }
794
+
757
795
  calendarDate.value = date
758
796
  }
759
797
 
798
+ let formatedDate: Date | string | null = date ?? null
799
+
800
+ if (date instanceof Date && date.getTime() && props.selectionType !== 'day') {
801
+ formatedDate = getDateBySelectionType(date, props.selectionType)
802
+ }
803
+
760
804
  if (input === 'start') {
761
- startDate.value = date ?? null
762
- emit('start-date-change', date ?? null)
763
- emit('update:start-date', date ?? null)
805
+ startDate.value = formatedDate ?? null
806
+ emit('start-date-change', formatedDate ?? null)
807
+ emit('update:start-date', formatedDate ?? null)
764
808
  } else {
765
- endDate.value = date ?? null
766
- emit('end-date-change', date ?? null)
767
- emit('update:end-date', date ?? null)
809
+ endDate.value = formatedDate ?? null
810
+ emit('end-date-change', formatedDate ?? null)
811
+ emit('update:end-date', formatedDate ?? null)
768
812
  }
769
813
  }
770
814
 
815
+ const createInputElement = (position: 'start' | 'end') => {
816
+ const isStart = position === 'start'
817
+ const hoverValue = isStart ? inputStartHoverValue.value : inputEndHoverValue.value
818
+ const dateValue = isStart ? startDate.value : endDate.value
819
+ const inputRef = isStart ? inputStartRef : inputEndRef
820
+
821
+ return h('input', {
822
+ autocomplete: 'off',
823
+ class: [
824
+ 'date-picker-input',
825
+ {
826
+ hover: hoverValue,
827
+ },
828
+ ],
829
+ disabled: props.disabled,
830
+ ...(props.id && { id: getInputIdOrName(props.id, props.range, position) }),
831
+ ...(props.name && { name: getInputIdOrName(props.name, props.range, position) }),
832
+ ...(props.id &&
833
+ !Array.isArray(props.id) &&
834
+ !props.name && {
835
+ name: isStart
836
+ ? props.range
837
+ ? `${props.id}-start-date`
838
+ : `${props.id}-date`
839
+ : `${props.id}-end-date`,
840
+ }), // TODO: remove in v6
841
+ onClick: () => {
842
+ selectEndDate.value = !isStart
843
+ },
844
+ onChange: (event: Event) =>
845
+ handleOnChange((event.target as HTMLInputElement).value, position),
846
+ onInput: useDebouncedCallback(
847
+ (event: Event) => handleOnChange((event.target as HTMLInputElement).value, position),
848
+ props.inputOnChangeDelay
849
+ ),
850
+ placeholder: Array.isArray(props.placeholder)
851
+ ? props.placeholder[isStart ? 0 : 1]
852
+ : props.placeholder,
853
+ readonly: props.inputReadOnly || typeof props.format === 'string',
854
+ required: props.required,
855
+ ref: inputRef,
856
+ value: setInputValue(dateValue, inputRef.value),
857
+ })
858
+ }
859
+
860
+ const createPreviewInput = (position: 'start' | 'end') => {
861
+ const isStart = position === 'start'
862
+ const hoverValue = isStart ? inputStartHoverValue.value : inputEndHoverValue.value
863
+
864
+ return (
865
+ hoverValue &&
866
+ h('input', {
867
+ class: 'date-picker-input date-picker-input-preview',
868
+ disabled: props.disabled,
869
+ readonly: true,
870
+ tabIndex: -1,
871
+ value: hoverValue ? setInputValue(hoverValue) : '',
872
+ })
873
+ )
874
+ }
875
+
876
+ const createInputWithWrapper = (position: 'start' | 'end') => {
877
+ return props.previewDateOnHover
878
+ ? h('div', { class: 'date-picker-input-wrapper' }, [
879
+ createInputElement(position),
880
+ createPreviewInput(position),
881
+ ])
882
+ : createInputElement(position)
883
+ }
884
+
771
885
  const InputGroup = () =>
772
886
  h(
773
887
  'div',
@@ -775,74 +889,9 @@ const CDateRangePicker = defineComponent({
775
889
  class: 'date-picker-input-group',
776
890
  },
777
891
  [
778
- h('input', {
779
- autocomplete: 'off',
780
- class: [
781
- 'date-picker-input',
782
- {
783
- hover: inputStartHoverValue.value,
784
- },
785
- ],
786
- disabled: props.disabled,
787
- ...(props.id && { id: getInputIdOrName(props.id, props.range, 'start') }),
788
- ...(props.name && { name: getInputIdOrName(props.name, props.range, 'start') }),
789
- ...(props.id &&
790
- !Array.isArray(props.id) &&
791
- !props.name && { name: props.range ? `${props.id}-start-date` : `${props.id}-date` }), // TODO: remove in v6
792
- onClick: () => {
793
- selectEndDate.value = false
794
- },
795
- onChange: (event: Event) =>
796
- handleOnChange((event.target as HTMLInputElement).value, 'start'),
797
- onInput: (event: Event) =>
798
- useDebouncedCallback(
799
- () => handleOnChange((event.target as HTMLInputElement).value, 'start'),
800
- props.inputOnChangeDelay
801
- ),
802
- placeholder: Array.isArray(props.placeholder)
803
- ? props.placeholder[0]
804
- : props.placeholder,
805
- readonly: props.inputReadOnly || typeof props.format === 'string',
806
- required: props.required,
807
- ref: inputStartRef,
808
- value: inputStartHoverValue.value
809
- ? setInputValue(inputStartHoverValue.value)
810
- : setInputValue(startDate.value),
811
- }),
892
+ createInputWithWrapper('start'),
812
893
  props.range && props.separator !== false && h('div', { class: 'date-picker-separator' }),
813
- props.range &&
814
- h('input', {
815
- autocomplete: 'off',
816
- class: [
817
- 'date-picker-input',
818
- {
819
- hover: inputEndHoverValue.value,
820
- },
821
- ],
822
- disabled: props.disabled,
823
- ...(props.id && { id: getInputIdOrName(props.id, props.range, 'end') }),
824
- ...(props.name && { name: getInputIdOrName(props.name, props.range, 'end') }),
825
- ...(props.id &&
826
- !Array.isArray(props.id) &&
827
- !props.name && { name: `${props.id}-end-date` }), // TODO: remove in v6
828
- onClick: () => {
829
- selectEndDate.value = true
830
- },
831
- onChange: (event: Event) =>
832
- handleOnChange((event.target as HTMLInputElement).value, 'end'),
833
- onInput: (event: Event) =>
834
- useDebouncedCallback(
835
- () => handleOnChange((event.target as HTMLInputElement).value, 'end'),
836
- props.inputOnChangeDelay
837
- ),
838
- placeholder: props.placeholder[1],
839
- readonly: props.inputReadOnly || typeof props.format === 'string',
840
- required: props.required,
841
- ref: inputEndRef,
842
- value: inputEndHoverValue.value
843
- ? setInputValue(inputEndHoverValue.value)
844
- : setInputValue(endDate.value),
845
- }),
894
+ props.range && createInputWithWrapper('end'),
846
895
  props.indicator &&
847
896
  h('div', {
848
897
  class: 'date-picker-indicator',
@@ -21,61 +21,3 @@ export const getInputIdOrName = (
21
21
 
22
22
  return attribute
23
23
  }
24
-
25
- /**
26
- * Parses a date string into a Date object based on the provided locale and time inclusion.
27
- *
28
- * @param dateString - The date string to parse.
29
- * @param locale - The locale to use for parsing the date string.
30
- * @param includeTime - Optional. Determines whether to include time in the parsed Date object.
31
- * @returns A Date object representing the parsed date and time, or `undefined` if parsing fails.
32
- */
33
- export const getLocalDateFromString = (
34
- string: string,
35
- locale: string,
36
- time?: boolean
37
- ): Date | undefined => {
38
- const date = new Date(2013, 11, 31, 17, 19, 22)
39
- let regex = time ? date.toLocaleString(locale) : date.toLocaleDateString(locale)
40
- regex = regex
41
- .replace('2013', '(?<year>[0-9]{2,4})')
42
- .replace('12', '(?<month>[0-9]{1,2})')
43
- .replace('31', '(?<day>[0-9]{1,2})')
44
-
45
- if (time) {
46
- regex = regex
47
- .replace('5', '(?<hour>[0-9]{1,2})')
48
- .replace('17', '(?<hour>[0-9]{1,2})')
49
- .replace('19', '(?<minute>[0-9]{1,2})')
50
- .replace('22', '(?<second>[0-9]{1,2})')
51
- .replace('PM', '(?<ampm>[A-Z]{2})')
52
- }
53
-
54
- const rgx = new RegExp(`${regex}`)
55
- const partials = string.match(rgx)
56
-
57
- if (partials === null) return
58
-
59
- const newDate =
60
- partials.groups &&
61
- (time
62
- ? new Date(
63
- Number(partials.groups['year']),
64
- Number(partials.groups['month']) - 1,
65
- Number(partials.groups['day']),
66
- partials.groups['ampm']
67
- ? partials.groups['ampm'] === 'PM'
68
- ? Number(partials.groups['hour']) + 12
69
- : Number(partials.groups['hour'])
70
- : Number(partials.groups['hour']),
71
- Number(partials.groups['minute']),
72
- Number(partials.groups['second'])
73
- )
74
- : new Date(
75
- Number(partials.groups['year']),
76
- Number(partials.groups['month']) - 1,
77
- Number(partials.groups['day'])
78
- ))
79
-
80
- return newDate
81
- }
@@ -1,4 +1,15 @@
1
- import { defineComponent, h, ref, provide, watch, PropType } from 'vue'
1
+ import {
2
+ computed,
3
+ defineComponent,
4
+ h,
5
+ nextTick,
6
+ onUnmounted,
7
+ provide,
8
+ PropType,
9
+ ref,
10
+ Ref,
11
+ watch,
12
+ } from 'vue'
2
13
  import type { Placement } from '@popperjs/core'
3
14
 
4
15
  import { usePopper } from '../../composables'
@@ -6,7 +17,8 @@ import type { Triggers } from '../../types'
6
17
  import { getNextActiveElement, isRTL } from '../../utils'
7
18
 
8
19
  import type { Alignments } from './types'
9
- import { getPlacement } from './utils'
20
+ import { getPlacement, getReferenceElement } from './utils'
21
+ import { CFocusTrap } from '../focus-trap'
10
22
 
11
23
  const CDropdown = defineComponent({
12
24
  name: 'CDropdown',
@@ -53,7 +65,7 @@ const CDropdown = defineComponent({
53
65
  * - `'outside'` - the dropdown will be closed (only) by clicking outside the dropdown menu.
54
66
  */
55
67
  autoClose: {
56
- type: [Boolean, String],
68
+ type: [Boolean, String] as PropType<boolean | 'inside' | 'outside'>,
57
69
  default: true,
58
70
  validator: (value: boolean | string) => {
59
71
  return typeof value === 'boolean' || ['inside', 'outside'].includes(value)
@@ -112,6 +124,21 @@ const CDropdown = defineComponent({
112
124
  type: Boolean,
113
125
  default: true,
114
126
  },
127
+ /**
128
+ * Sets the reference element for positioning the Vue Dropdown Menu.
129
+ * - `toggle` - The Vue Dropdown Toggle button (default).
130
+ * - `parent` - The Vue Dropdown wrapper element.
131
+ * - `HTMLElement` - A custom HTML element.
132
+ * - `Ref` - A custom reference element.
133
+ *
134
+ * @since 5.7.0
135
+ */
136
+ reference: {
137
+ type: [String, Object] as PropType<
138
+ 'parent' | 'toggle' | HTMLElement | Ref<HTMLElement | null>
139
+ >,
140
+ default: 'toggle',
141
+ },
115
142
  /**
116
143
  * Generates dropdown menu using Teleport.
117
144
  *
@@ -156,14 +183,16 @@ const CDropdown = defineComponent({
156
183
  'show',
157
184
  ],
158
185
  setup(props, { slots, emit }) {
159
- const dropdownToggleRef = ref()
160
- const dropdownMenuRef = ref()
186
+ const dropdownRef = ref<HTMLElement | null>(null)
187
+ const dropdownMenuRef = ref<HTMLElement | null>(null)
188
+ const dropdownToggleRef = ref<HTMLElement | null>(null)
189
+ const pendingKeyDownEventRef = ref<KeyboardEvent | null>(null)
161
190
  const popper = ref(typeof props.alignment === 'object' ? false : props.popper)
162
191
  const visible = ref(props.visible)
163
192
 
164
193
  const { initPopper, destroyPopper } = usePopper()
165
194
 
166
- const popperConfig = {
195
+ const popperConfig = computed(() => ({
167
196
  modifiers: [
168
197
  {
169
198
  name: 'offset',
@@ -176,37 +205,62 @@ const CDropdown = defineComponent({
176
205
  props.placement,
177
206
  props.direction,
178
207
  props.alignment,
179
- isRTL(dropdownMenuRef.value),
208
+ isRTL(dropdownMenuRef.value)
180
209
  ) as Placement,
181
- }
210
+ }))
182
211
 
183
212
  watch(
184
213
  () => props.visible,
185
214
  () => {
186
215
  visible.value = props.visible
187
- },
216
+ }
188
217
  )
189
218
 
190
219
  watch(visible, () => {
191
220
  if (visible.value && dropdownToggleRef.value && dropdownMenuRef.value) {
192
- popper.value && initPopper(dropdownToggleRef.value, dropdownMenuRef.value, popperConfig)
193
- window.addEventListener('mouseup', handleMouseUp)
221
+ const referenceElement = getReferenceElement(
222
+ props.reference,
223
+ dropdownToggleRef,
224
+ dropdownRef
225
+ )
226
+ if (referenceElement && popper.value) {
227
+ initPopper(referenceElement, dropdownMenuRef.value, popperConfig.value)
228
+ }
229
+
230
+ window.addEventListener('click', handleClick)
194
231
  window.addEventListener('keyup', handleKeyup)
195
232
  dropdownToggleRef.value.addEventListener('keydown', handleKeydown)
196
233
  dropdownMenuRef.value.addEventListener('keydown', handleKeydown)
234
+
235
+ if (pendingKeyDownEventRef.value) {
236
+ nextTick(() => {
237
+ handleKeydown(pendingKeyDownEventRef.value as KeyboardEvent)
238
+ pendingKeyDownEventRef.value = null
239
+ })
240
+ }
241
+
197
242
  emit('show')
198
243
  return
199
244
  }
200
245
 
201
- popper.value && destroyPopper()
202
- window.removeEventListener('mouseup', handleMouseUp)
246
+ if (popper.value) {
247
+ destroyPopper()
248
+ }
249
+
250
+ window.removeEventListener('click', handleClick)
203
251
  window.removeEventListener('keyup', handleKeyup)
252
+ dropdownMenuRef.value && dropdownMenuRef.value.removeEventListener('keydown', handleKeydown)
204
253
  dropdownToggleRef.value &&
205
254
  dropdownToggleRef.value.removeEventListener('keydown', handleKeydown)
206
- dropdownMenuRef.value && dropdownMenuRef.value.removeEventListener('keydown', handleKeydown)
207
255
  emit('hide')
208
256
  })
209
257
 
258
+ onUnmounted(() => {
259
+ dropdownToggleRef.value &&
260
+ dropdownToggleRef.value.removeEventListener('keydown', handleKeydown)
261
+ dropdownMenuRef.value && dropdownMenuRef.value.removeEventListener('keydown', handleKeydown)
262
+ })
263
+
210
264
  provide('config', {
211
265
  alignment: props.alignment,
212
266
  container: props.container,
@@ -219,18 +273,14 @@ const CDropdown = defineComponent({
219
273
  provide('visible', visible)
220
274
  provide('dropdownToggleRef', dropdownToggleRef)
221
275
  provide('dropdownMenuRef', dropdownMenuRef)
276
+ provide('pendingKeyDownEventRef', pendingKeyDownEventRef)
222
277
 
223
278
  const handleKeydown = (event: KeyboardEvent) => {
224
- if (
225
- visible.value &&
226
- dropdownMenuRef.value &&
227
- (event.key === 'ArrowDown' || event.key === 'ArrowUp')
228
- ) {
279
+ if (dropdownMenuRef.value && (event.key === 'ArrowDown' || event.key === 'ArrowUp')) {
229
280
  event.preventDefault()
230
281
  const target = event.target as HTMLElement
231
- // eslint-disable-next-line unicorn/prefer-spread
232
282
  const items: HTMLElement[] = Array.from(
233
- dropdownMenuRef.value.querySelectorAll('.dropdown-item:not(.disabled):not(:disabled)'),
283
+ dropdownMenuRef.value.querySelectorAll('.dropdown-item:not(.disabled):not(:disabled)')
234
284
  )
235
285
  getNextActiveElement(items, target, event.key === 'ArrowDown', true).focus()
236
286
  }
@@ -243,67 +293,84 @@ const CDropdown = defineComponent({
243
293
 
244
294
  if (event.key === 'Escape') {
245
295
  setVisible(false)
296
+ dropdownToggleRef.value?.focus()
246
297
  }
247
298
  }
248
299
 
249
- const handleMouseUp = (event: Event) => {
300
+ const handleClick = (event: Event) => {
250
301
  if (!dropdownToggleRef.value || !dropdownMenuRef.value) {
251
302
  return
252
303
  }
253
304
 
254
- if (dropdownToggleRef.value.contains(event.target as HTMLElement)) {
305
+ if ((event as MouseEvent).button === 2) {
306
+ return
307
+ }
308
+
309
+ const composedPath = event.composedPath()
310
+ const isOnToggle = composedPath.includes(dropdownToggleRef.value)
311
+ const isOnMenu = composedPath.includes(dropdownMenuRef.value)
312
+
313
+ if (isOnToggle) {
314
+ return
315
+ }
316
+
317
+ const target = event.target as HTMLElement | null
318
+ const FORM_TAG_RE = /^(input|select|option|textarea|form|button|label)$/i
319
+
320
+ if (isOnMenu && target && FORM_TAG_RE.test(target.tagName)) {
255
321
  return
256
322
  }
257
323
 
258
324
  if (
259
325
  props.autoClose === true ||
260
- (props.autoClose === 'inside' &&
261
- dropdownMenuRef.value.contains(event.target as HTMLElement)) ||
262
- (props.autoClose === 'outside' &&
263
- !dropdownMenuRef.value.contains(event.target as HTMLElement))
326
+ (props.autoClose === 'inside' && isOnMenu) ||
327
+ (props.autoClose === 'outside' && !isOnMenu)
264
328
  ) {
265
329
  setVisible(false)
266
- return
267
330
  }
268
331
  }
269
332
 
270
- const setVisible = (_visible?: boolean) => {
333
+ const setVisible = (_visible?: boolean, event?: KeyboardEvent) => {
271
334
  if (props.disabled) {
272
335
  return
273
336
  }
274
337
 
275
- if (typeof _visible == 'boolean') {
338
+ if (typeof _visible === 'boolean') {
339
+ if (event) {
340
+ pendingKeyDownEventRef.value = event || null
341
+ }
342
+
276
343
  visible.value = _visible
277
- return
278
- }
279
344
 
280
- if (visible.value === true) {
281
- visible.value = false
282
345
  return
283
346
  }
284
-
285
- visible.value = true
286
347
  }
287
348
 
288
349
  provide('setVisible', setVisible)
289
350
 
290
351
  return () =>
291
- props.variant === 'input-group'
292
- ? [slots.default && slots.default()]
293
- : h(
294
- 'div',
295
- {
296
- class: [
297
- props.variant === 'nav-item' ? 'nav-item dropdown' : props.variant,
298
- props.direction === 'center'
299
- ? 'dropdown-center'
300
- : props.direction === 'dropup-center'
301
- ? 'dropup dropup-center'
302
- : props.direction,
303
- ],
304
- },
305
- slots.default && slots.default(),
306
- )
352
+ h(
353
+ CFocusTrap,
354
+ { active: props.teleport && visible.value, additionalContainer: dropdownMenuRef },
355
+ () =>
356
+ props.variant === 'input-group'
357
+ ? [slots.default && slots.default()]
358
+ : h(
359
+ 'div',
360
+ {
361
+ class: [
362
+ props.variant === 'nav-item' ? 'nav-item dropdown' : props.variant,
363
+ props.direction === 'center'
364
+ ? 'dropdown-center'
365
+ : props.direction === 'dropup-center'
366
+ ? 'dropup dropup-center'
367
+ : props.direction,
368
+ ],
369
+ ref: dropdownRef,
370
+ },
371
+ slots.default && slots.default()
372
+ )
373
+ )
307
374
  },
308
375
  })
309
376
 
@@ -74,6 +74,15 @@ const CDropdownToggle = defineComponent({
74
74
  * Similarly, create split button dropdowns with virtually the same markup as single button dropdowns, but with the addition of `.dropdown-toggle-split` className for proper spacing around the dropdown caret.
75
75
  */
76
76
  split: Boolean,
77
+ /**
78
+ * Screen reader label for split button dropdown toggle.
79
+ *
80
+ * @since 5.7.0
81
+ */
82
+ splitLabel: {
83
+ type: String,
84
+ default: 'Toggle Dropdown',
85
+ },
77
86
  /**
78
87
  * Sets which event handlers you’d like provided to your toggle prop. You can specify one trigger or an array of them.
79
88
  *
@@ -100,7 +109,7 @@ const CDropdownToggle = defineComponent({
100
109
  const dropdownToggleRef = inject('dropdownToggleRef') as Ref<HTMLElement>
101
110
  const dropdownVariant = inject('variant') as string
102
111
  const visible = inject('visible') as Ref<boolean>
103
- const setVisible = inject('setVisible') as (_visible?: boolean) => void
112
+ const setVisible = inject('setVisible') as (_visible?: boolean, event?: KeyboardEvent) => void
104
113
 
105
114
  const triggers = {
106
115
  ...((props.trigger === 'click' || props.trigger.includes('click')) && {
@@ -110,7 +119,7 @@ const CDropdownToggle = defineComponent({
110
119
  return
111
120
  }
112
121
 
113
- setVisible()
122
+ setVisible(!visible.value)
114
123
  },
115
124
  }),
116
125
  ...((props.trigger === 'focus' || props.trigger.includes('focus')) && {
@@ -128,6 +137,12 @@ const CDropdownToggle = defineComponent({
128
137
  setVisible(false)
129
138
  },
130
139
  }),
140
+ onkeydown: (event: KeyboardEvent) => {
141
+ if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
142
+ event.preventDefault()
143
+ setVisible(true, event)
144
+ }
145
+ }
131
146
  }
132
147
 
133
148
  const togglerProps = computed(() => {
@@ -188,7 +203,7 @@ const CDropdownToggle = defineComponent({
188
203
  },
189
204
  () =>
190
205
  props.split
191
- ? h('span', { class: 'visually-hidden' }, 'Toggle Dropdown')
206
+ ? h('span', { class: 'visually-hidden' }, props.splitLabel)
192
207
  : slots.default && slots.default(),
193
208
  )
194
209
  },