@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.
- package/README.md +1 -1
- package/dist/cjs/components/calendar/CCalendar.js +67 -65
- package/dist/cjs/components/calendar/CCalendar.js.map +1 -1
- package/dist/cjs/components/calendar/utils.d.ts +53 -2
- package/dist/cjs/components/calendar/utils.js +466 -43
- package/dist/cjs/components/calendar/utils.js.map +1 -1
- package/dist/cjs/components/date-range-picker/CDateRangePicker.js +86 -57
- package/dist/cjs/components/date-range-picker/CDateRangePicker.js.map +1 -1
- package/dist/cjs/components/date-range-picker/utils.d.ts +0 -9
- package/dist/cjs/components/date-range-picker/utils.js +0 -38
- package/dist/cjs/components/date-range-picker/utils.js.map +1 -1
- package/dist/cjs/components/dropdown/CDropdown.d.ts +32 -7
- package/dist/cjs/components/dropdown/CDropdown.js +67 -29
- package/dist/cjs/components/dropdown/CDropdown.js.map +1 -1
- package/dist/cjs/components/dropdown/CDropdownToggle.d.ts +19 -0
- package/dist/cjs/components/dropdown/CDropdownToggle.js +17 -2
- package/dist/cjs/components/dropdown/CDropdownToggle.js.map +1 -1
- package/dist/cjs/components/dropdown/utils.d.ts +2 -0
- package/dist/cjs/components/dropdown/utils.js +13 -0
- package/dist/cjs/components/dropdown/utils.js.map +1 -1
- package/dist/cjs/components/focus-trap/CFocusTrap.d.ts +108 -0
- package/dist/cjs/components/focus-trap/CFocusTrap.js +254 -0
- package/dist/cjs/components/focus-trap/CFocusTrap.js.map +1 -0
- package/dist/cjs/components/focus-trap/index.d.ts +6 -0
- package/dist/cjs/components/focus-trap/index.js +13 -0
- package/dist/cjs/components/focus-trap/index.js.map +1 -0
- package/dist/cjs/components/focus-trap/utils.d.ts +28 -0
- package/dist/cjs/components/focus-trap/utils.js +83 -0
- package/dist/cjs/components/focus-trap/utils.js.map +1 -0
- package/dist/cjs/components/index.d.ts +1 -0
- package/dist/cjs/components/index.js +70 -66
- package/dist/cjs/components/index.js.map +1 -1
- package/dist/cjs/components/modal/CModal.d.ts +2 -2
- package/dist/cjs/components/modal/CModal.js +19 -27
- package/dist/cjs/components/modal/CModal.js.map +1 -1
- package/dist/cjs/components/modal/CModalHeader.js +4 -2
- package/dist/cjs/components/modal/CModalHeader.js.map +1 -1
- package/dist/cjs/components/nav/CNavItem.d.ts +2 -2
- package/dist/cjs/components/offcanvas/COffcanvas.js +3 -2
- package/dist/cjs/components/offcanvas/COffcanvas.js.map +1 -1
- package/dist/cjs/components/picker/CPicker.js +3 -2
- package/dist/cjs/components/picker/CPicker.js.map +1 -1
- package/dist/cjs/components/time-picker/CTimePicker.d.ts +45 -1
- package/dist/cjs/components/time-picker/CTimePicker.js +64 -8
- package/dist/cjs/components/time-picker/CTimePicker.js.map +1 -1
- package/dist/cjs/components/time-picker/CTimePickerRollCol.d.ts +19 -7
- package/dist/cjs/components/time-picker/CTimePickerRollCol.js +80 -8
- package/dist/cjs/components/time-picker/CTimePickerRollCol.js.map +1 -1
- package/dist/cjs/components/time-picker/utils.d.ts +1 -1
- package/dist/cjs/composables/useDebouncedCallback.d.ts +1 -1
- package/dist/cjs/composables/useDebouncedCallback.js +1 -1
- package/dist/cjs/composables/useDebouncedCallback.js.map +1 -1
- package/dist/cjs/index.js +76 -72
- package/dist/cjs/index.js.map +1 -1
- package/dist/esm/components/calendar/CCalendar.js +67 -65
- package/dist/esm/components/calendar/CCalendar.js.map +1 -1
- package/dist/esm/components/calendar/utils.d.ts +53 -2
- package/dist/esm/components/calendar/utils.js +464 -44
- package/dist/esm/components/calendar/utils.js.map +1 -1
- package/dist/esm/components/date-range-picker/CDateRangePicker.js +86 -57
- package/dist/esm/components/date-range-picker/CDateRangePicker.js.map +1 -1
- package/dist/esm/components/date-range-picker/utils.d.ts +0 -9
- package/dist/esm/components/date-range-picker/utils.js +1 -38
- package/dist/esm/components/date-range-picker/utils.js.map +1 -1
- package/dist/esm/components/dropdown/CDropdown.d.ts +32 -7
- package/dist/esm/components/dropdown/CDropdown.js +69 -31
- package/dist/esm/components/dropdown/CDropdown.js.map +1 -1
- package/dist/esm/components/dropdown/CDropdownToggle.d.ts +19 -0
- package/dist/esm/components/dropdown/CDropdownToggle.js +17 -2
- package/dist/esm/components/dropdown/CDropdownToggle.js.map +1 -1
- package/dist/esm/components/dropdown/utils.d.ts +2 -0
- package/dist/esm/components/dropdown/utils.js +13 -1
- package/dist/esm/components/dropdown/utils.js.map +1 -1
- package/dist/esm/components/focus-trap/CFocusTrap.d.ts +108 -0
- package/dist/esm/components/focus-trap/CFocusTrap.js +252 -0
- package/dist/esm/components/focus-trap/CFocusTrap.js.map +1 -0
- package/dist/esm/components/focus-trap/index.d.ts +6 -0
- package/dist/esm/components/focus-trap/index.js +10 -0
- package/dist/esm/components/focus-trap/index.js.map +1 -0
- package/dist/esm/components/focus-trap/utils.d.ts +28 -0
- package/dist/esm/components/focus-trap/utils.js +78 -0
- package/dist/esm/components/focus-trap/utils.js.map +1 -0
- package/dist/esm/components/index.d.ts +1 -0
- package/dist/esm/components/index.js +2 -0
- package/dist/esm/components/index.js.map +1 -1
- package/dist/esm/components/modal/CModal.d.ts +2 -2
- package/dist/esm/components/modal/CModal.js +19 -27
- package/dist/esm/components/modal/CModal.js.map +1 -1
- package/dist/esm/components/modal/CModalHeader.js +4 -2
- package/dist/esm/components/modal/CModalHeader.js.map +1 -1
- package/dist/esm/components/nav/CNavItem.d.ts +2 -2
- package/dist/esm/components/offcanvas/COffcanvas.js +3 -2
- package/dist/esm/components/offcanvas/COffcanvas.js.map +1 -1
- package/dist/esm/components/picker/CPicker.js +3 -2
- package/dist/esm/components/picker/CPicker.js.map +1 -1
- package/dist/esm/components/time-picker/CTimePicker.d.ts +45 -1
- package/dist/esm/components/time-picker/CTimePicker.js +64 -8
- package/dist/esm/components/time-picker/CTimePicker.js.map +1 -1
- package/dist/esm/components/time-picker/CTimePickerRollCol.d.ts +19 -7
- package/dist/esm/components/time-picker/CTimePickerRollCol.js +80 -8
- package/dist/esm/components/time-picker/CTimePickerRollCol.js.map +1 -1
- package/dist/esm/components/time-picker/utils.d.ts +1 -1
- package/dist/esm/composables/useDebouncedCallback.d.ts +1 -1
- package/dist/esm/composables/useDebouncedCallback.js +1 -1
- package/dist/esm/composables/useDebouncedCallback.js.map +1 -1
- package/dist/esm/index.js +2 -0
- package/dist/esm/index.js.map +1 -1
- package/package.json +5 -5
- package/src/components/calendar/CCalendar.ts +61 -70
- package/src/components/calendar/utils.ts +595 -47
- package/src/components/date-range-picker/CDateRangePicker.ts +131 -82
- package/src/components/date-range-picker/utils.ts +0 -58
- package/src/components/dropdown/CDropdown.ts +119 -52
- package/src/components/dropdown/CDropdownToggle.ts +18 -3
- package/src/components/dropdown/utils.ts +21 -0
- package/src/components/focus-trap/CFocusTrap.ts +303 -0
- package/src/components/focus-trap/index.ts +10 -0
- package/src/components/focus-trap/utils.ts +90 -0
- package/src/components/index.ts +1 -0
- package/src/components/modal/CModal.ts +32 -37
- package/src/components/modal/CModalHeader.ts +5 -3
- package/src/components/nav/CNavItem.ts +1 -1
- package/src/components/offcanvas/COffcanvas.ts +40 -36
- package/src/components/picker/CPicker.ts +58 -52
- package/src/components/time-picker/CTimePicker.ts +80 -22
- package/src/components/time-picker/CTimePickerRollCol.ts +87 -9
- package/src/composables/useDebouncedCallback.ts +1 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Ref } from 'vue'
|
|
1
2
|
import type { Placement } from '@popperjs/core'
|
|
2
3
|
import type { Placements } from '../../types'
|
|
3
4
|
import type { Alignments, Breakpoints } from './types'
|
|
@@ -49,3 +50,23 @@ export const getPlacement = (
|
|
|
49
50
|
|
|
50
51
|
return _placement
|
|
51
52
|
}
|
|
53
|
+
|
|
54
|
+
export const getReferenceElement = (
|
|
55
|
+
reference: 'parent' | 'toggle' | Ref<HTMLElement | null> | HTMLElement,
|
|
56
|
+
dropdownToggleRef: Ref<HTMLElement | null>,
|
|
57
|
+
dropdownRef: Ref<HTMLElement | null>
|
|
58
|
+
): HTMLElement | null => {
|
|
59
|
+
if (reference === 'parent') {
|
|
60
|
+
return dropdownRef.value
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (reference instanceof HTMLElement) {
|
|
64
|
+
return reference
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (reference instanceof Object && 'value' in reference) {
|
|
68
|
+
return reference.value
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return dropdownToggleRef.value
|
|
72
|
+
}
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import {
|
|
2
|
+
cloneVNode,
|
|
3
|
+
defineComponent,
|
|
4
|
+
ref,
|
|
5
|
+
watch,
|
|
6
|
+
onMounted,
|
|
7
|
+
onUnmounted,
|
|
8
|
+
type Ref,
|
|
9
|
+
type PropType,
|
|
10
|
+
} from 'vue'
|
|
11
|
+
import { focusableChildren } from './utils'
|
|
12
|
+
|
|
13
|
+
const CFocusTrap = defineComponent({
|
|
14
|
+
name: 'CFocusTrap',
|
|
15
|
+
props: {
|
|
16
|
+
/**
|
|
17
|
+
* Controls whether the focus trap is active or inactive.
|
|
18
|
+
* When `true`, focus will be trapped within the child element.
|
|
19
|
+
* When `false`, normal focus behavior is restored.
|
|
20
|
+
*/
|
|
21
|
+
active: {
|
|
22
|
+
type: Boolean,
|
|
23
|
+
default: true,
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Additional container elements to include in the focus trap.
|
|
28
|
+
* Useful for floating elements like tooltips or popovers that are
|
|
29
|
+
* rendered outside the main container but should be part of the trap.
|
|
30
|
+
*/
|
|
31
|
+
additionalContainer: {
|
|
32
|
+
type: Object as PropType<Ref<HTMLElement | null>>,
|
|
33
|
+
default: undefined,
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Controls whether to focus the first selectable element or the container itself.
|
|
38
|
+
* When `true`, focuses the first tabbable element within the container.
|
|
39
|
+
* When `false`, focuses the container element directly.
|
|
40
|
+
*
|
|
41
|
+
* This is useful for containers that should receive focus themselves,
|
|
42
|
+
* such as scrollable regions or custom interactive components.
|
|
43
|
+
*/
|
|
44
|
+
focusFirstElement: {
|
|
45
|
+
type: Boolean,
|
|
46
|
+
default: false,
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Automatically restores focus to the previously focused element when the trap is deactivated.
|
|
51
|
+
* This is crucial for accessibility as it maintains the user's place in the document
|
|
52
|
+
* when returning from modal dialogs or overlay components.
|
|
53
|
+
*
|
|
54
|
+
* Recommended to be `true` for modal dialogs and popover components.
|
|
55
|
+
*/
|
|
56
|
+
restoreFocus: {
|
|
57
|
+
type: Boolean,
|
|
58
|
+
default: true,
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
emits: {
|
|
62
|
+
/**
|
|
63
|
+
* Emitted when the focus trap becomes active.
|
|
64
|
+
* Useful for triggering additional accessibility announcements or analytics.
|
|
65
|
+
*/
|
|
66
|
+
activate: () => true,
|
|
67
|
+
/**
|
|
68
|
+
* Emitted when the focus trap is deactivated.
|
|
69
|
+
* Can be used for cleanup, analytics, or triggering state changes.
|
|
70
|
+
*/
|
|
71
|
+
deactivate: () => true,
|
|
72
|
+
},
|
|
73
|
+
setup(props, { emit, slots, expose }) {
|
|
74
|
+
const containerRef = ref<HTMLElement | null>(null)
|
|
75
|
+
const prevFocusedRef = ref<HTMLElement | null>(null)
|
|
76
|
+
const isActiveRef = ref<boolean>(false)
|
|
77
|
+
const lastTabNavDirectionRef = ref<'forward' | 'backward'>('forward')
|
|
78
|
+
const tabEventSourceRef = ref<HTMLElement | null>(null)
|
|
79
|
+
|
|
80
|
+
let handleKeyDown: ((event: KeyboardEvent) => void) | null = null
|
|
81
|
+
let handleFocusIn: ((event: FocusEvent) => void) | null = null
|
|
82
|
+
|
|
83
|
+
const activateTrap = () => {
|
|
84
|
+
const container = containerRef.value
|
|
85
|
+
const additionalContainer = props.additionalContainer?.value || null
|
|
86
|
+
|
|
87
|
+
if (!container) {
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
prevFocusedRef.value = document.activeElement as HTMLElement | null
|
|
92
|
+
|
|
93
|
+
// Activating...
|
|
94
|
+
isActiveRef.value = true
|
|
95
|
+
|
|
96
|
+
// Set initial focus
|
|
97
|
+
if (props.focusFirstElement) {
|
|
98
|
+
const elements = focusableChildren(container)
|
|
99
|
+
if (elements.length > 0) {
|
|
100
|
+
elements[0].focus({ preventScroll: true })
|
|
101
|
+
} else {
|
|
102
|
+
// Fallback to container if no focusable elements
|
|
103
|
+
container.focus({ preventScroll: true })
|
|
104
|
+
}
|
|
105
|
+
} else {
|
|
106
|
+
container.focus({ preventScroll: true })
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
emit('activate')
|
|
110
|
+
|
|
111
|
+
// Create event handlers
|
|
112
|
+
handleFocusIn = (event: FocusEvent) => {
|
|
113
|
+
// Only handle focus events from tab navigation
|
|
114
|
+
if (containerRef.value !== tabEventSourceRef.value) {
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const target = event.target as Node
|
|
119
|
+
|
|
120
|
+
// Allow focus within container
|
|
121
|
+
if (target === document || target === container || container.contains(target)) {
|
|
122
|
+
return
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Allow focus within additional elements
|
|
126
|
+
if (
|
|
127
|
+
additionalContainer &&
|
|
128
|
+
(target === additionalContainer || additionalContainer.contains(target))
|
|
129
|
+
) {
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Focus escaped, bring it back
|
|
134
|
+
const elements = focusableChildren(container)
|
|
135
|
+
|
|
136
|
+
if (elements.length === 0) {
|
|
137
|
+
container.focus({ preventScroll: true })
|
|
138
|
+
} else if (lastTabNavDirectionRef.value === 'backward') {
|
|
139
|
+
elements.at(-1)?.focus({ preventScroll: true })
|
|
140
|
+
} else {
|
|
141
|
+
elements[0].focus({ preventScroll: true })
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
handleKeyDown = (event: KeyboardEvent) => {
|
|
146
|
+
if (event.key !== 'Tab') {
|
|
147
|
+
return
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
tabEventSourceRef.value = container
|
|
151
|
+
lastTabNavDirectionRef.value = event.shiftKey ? 'backward' : 'forward'
|
|
152
|
+
|
|
153
|
+
if (!additionalContainer) {
|
|
154
|
+
return
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const containerElements = focusableChildren(container)
|
|
158
|
+
const additionalElements = focusableChildren(additionalContainer)
|
|
159
|
+
|
|
160
|
+
if (containerElements.length === 0 && additionalElements.length === 0) {
|
|
161
|
+
// No focusable elements, prevent tab
|
|
162
|
+
event.preventDefault()
|
|
163
|
+
return
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const activeElement = document.activeElement as HTMLElement
|
|
167
|
+
const isInContainer = containerElements.includes(activeElement)
|
|
168
|
+
const isInAdditional = additionalElements.includes(activeElement)
|
|
169
|
+
|
|
170
|
+
// Handle tab navigation between container and additional elements
|
|
171
|
+
if (isInContainer) {
|
|
172
|
+
const index = containerElements.indexOf(activeElement)
|
|
173
|
+
|
|
174
|
+
if (
|
|
175
|
+
!event.shiftKey &&
|
|
176
|
+
index === containerElements.length - 1 &&
|
|
177
|
+
additionalElements.length > 0
|
|
178
|
+
) {
|
|
179
|
+
// Tab forward from last container element to first additional element
|
|
180
|
+
event.preventDefault()
|
|
181
|
+
additionalElements[0].focus({ preventScroll: true })
|
|
182
|
+
} else if (event.shiftKey && index === 0 && additionalElements.length > 0) {
|
|
183
|
+
// Tab backward from first container element to last additional element
|
|
184
|
+
event.preventDefault()
|
|
185
|
+
additionalElements.at(-1)?.focus({ preventScroll: true })
|
|
186
|
+
}
|
|
187
|
+
} else if (isInAdditional) {
|
|
188
|
+
const index = additionalElements.indexOf(activeElement)
|
|
189
|
+
|
|
190
|
+
if (
|
|
191
|
+
!event.shiftKey &&
|
|
192
|
+
index === additionalElements.length - 1 &&
|
|
193
|
+
containerElements.length > 0
|
|
194
|
+
) {
|
|
195
|
+
// Tab forward from last additional element to first container element
|
|
196
|
+
event.preventDefault()
|
|
197
|
+
containerElements[0].focus({ preventScroll: true })
|
|
198
|
+
} else if (event.shiftKey && index === 0 && containerElements.length > 0) {
|
|
199
|
+
// Tab backward from first additional element to last container element
|
|
200
|
+
event.preventDefault()
|
|
201
|
+
containerElements.at(-1)?.focus({ preventScroll: true })
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Add event listeners
|
|
207
|
+
container.addEventListener('keydown', handleKeyDown, true)
|
|
208
|
+
if (additionalContainer) {
|
|
209
|
+
additionalContainer.addEventListener('keydown', handleKeyDown, true)
|
|
210
|
+
}
|
|
211
|
+
document.addEventListener('focusin', handleFocusIn, true)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const deactivateTrap = () => {
|
|
215
|
+
if (!isActiveRef.value) {
|
|
216
|
+
return
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Cleanup event listeners
|
|
220
|
+
const container = containerRef.value
|
|
221
|
+
const additionalContainer = props.additionalContainer?.value || null
|
|
222
|
+
|
|
223
|
+
if (container && handleKeyDown) {
|
|
224
|
+
container.removeEventListener('keydown', handleKeyDown, true)
|
|
225
|
+
}
|
|
226
|
+
if (additionalContainer && handleKeyDown) {
|
|
227
|
+
additionalContainer.removeEventListener('keydown', handleKeyDown, true)
|
|
228
|
+
}
|
|
229
|
+
if (handleFocusIn) {
|
|
230
|
+
document.removeEventListener('focusin', handleFocusIn, true)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Restore focus
|
|
234
|
+
if (props.restoreFocus && prevFocusedRef.value?.isConnected) {
|
|
235
|
+
prevFocusedRef.value.focus({ preventScroll: true })
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
emit('deactivate')
|
|
239
|
+
isActiveRef.value = false
|
|
240
|
+
prevFocusedRef.value = null
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
watch(
|
|
244
|
+
() => props.active,
|
|
245
|
+
(newActive) => {
|
|
246
|
+
if (newActive && containerRef.value) {
|
|
247
|
+
activateTrap()
|
|
248
|
+
} else {
|
|
249
|
+
deactivateTrap()
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
{ immediate: false }
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
watch(
|
|
256
|
+
() => props.additionalContainer?.value,
|
|
257
|
+
() => {
|
|
258
|
+
if (props.active && isActiveRef.value) {
|
|
259
|
+
// Reactivate to update event listeners
|
|
260
|
+
deactivateTrap()
|
|
261
|
+
activateTrap()
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
onMounted(() => {
|
|
267
|
+
if (props.active && containerRef.value) {
|
|
268
|
+
activateTrap()
|
|
269
|
+
}
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
onUnmounted(() => {
|
|
273
|
+
deactivateTrap()
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
// Expose containerRef for parent components
|
|
277
|
+
expose({
|
|
278
|
+
containerRef,
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
return () => {
|
|
282
|
+
const vnodes = slots.default?.()
|
|
283
|
+
const vnode = vnodes?.[0]
|
|
284
|
+
if (!vnode) return null
|
|
285
|
+
|
|
286
|
+
const originalRef = (vnode.props as any)?.ref
|
|
287
|
+
|
|
288
|
+
return cloneVNode(vnode, {
|
|
289
|
+
ref: (el) => {
|
|
290
|
+
containerRef.value = el as HTMLElement | null
|
|
291
|
+
|
|
292
|
+
if (typeof originalRef === 'function') {
|
|
293
|
+
originalRef(el)
|
|
294
|
+
} else if (originalRef && typeof originalRef === 'object' && 'value' in originalRef) {
|
|
295
|
+
;(originalRef as { value: any }).value = el
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
})
|
|
299
|
+
}
|
|
300
|
+
},
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
export { CFocusTrap }
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gets all focusable child elements within a container.
|
|
3
|
+
* Uses a comprehensive selector to find elements that can receive focus.
|
|
4
|
+
* @param element - The container element to search within
|
|
5
|
+
* @returns Array of focusable HTML elements
|
|
6
|
+
*/
|
|
7
|
+
export const focusableChildren = (element: HTMLElement): HTMLElement[] => {
|
|
8
|
+
const focusableSelectors = [
|
|
9
|
+
'a[href]',
|
|
10
|
+
'button:not([disabled])',
|
|
11
|
+
'input:not([disabled])',
|
|
12
|
+
'textarea:not([disabled])',
|
|
13
|
+
'select:not([disabled])',
|
|
14
|
+
'details',
|
|
15
|
+
'[tabindex]:not([tabindex="-1"])',
|
|
16
|
+
'[contenteditable="true"]',
|
|
17
|
+
].join(',')
|
|
18
|
+
|
|
19
|
+
const elements = [...element.querySelectorAll<HTMLElement>(focusableSelectors)] as HTMLElement[]
|
|
20
|
+
|
|
21
|
+
return elements.filter((el) => !isDisabled(el) && isVisible(el))
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Checks if an element is disabled.
|
|
26
|
+
* Considers various ways an element can be disabled including CSS classes and attributes.
|
|
27
|
+
* @param element - The HTML element to check
|
|
28
|
+
* @returns True if the element is disabled, false otherwise
|
|
29
|
+
*/
|
|
30
|
+
export const isDisabled = (element: HTMLElement): boolean => {
|
|
31
|
+
if (!element || element.nodeType !== Node.ELEMENT_NODE) {
|
|
32
|
+
return true
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (element.classList.contains('disabled')) {
|
|
36
|
+
return true
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if ('disabled' in element && typeof element.disabled === 'boolean') {
|
|
40
|
+
return element.disabled
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return element.hasAttribute('disabled') && element.getAttribute('disabled') !== 'false'
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Type guard to check if an object is an Element.
|
|
48
|
+
* Handles edge cases including jQuery objects.
|
|
49
|
+
* @param object - The object to check
|
|
50
|
+
* @returns True if the object is an Element, false otherwise
|
|
51
|
+
*/
|
|
52
|
+
export const isElement = (object: unknown): object is Element => {
|
|
53
|
+
if (!object || typeof object !== 'object') {
|
|
54
|
+
return false
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return 'nodeType' in object && typeof object.nodeType === 'number'
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Checks if an element is visible in the DOM.
|
|
62
|
+
* Considers client rects and computed visibility styles, handling edge cases like details elements.
|
|
63
|
+
* @param element - The HTML element to check for visibility
|
|
64
|
+
* @returns True if the element is visible, false otherwise
|
|
65
|
+
*/
|
|
66
|
+
export const isVisible = (element: HTMLElement): boolean => {
|
|
67
|
+
if (!isElement(element) || element.getClientRects().length === 0) {
|
|
68
|
+
return false
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const elementIsVisible = getComputedStyle(element).getPropertyValue('visibility') === 'visible'
|
|
72
|
+
|
|
73
|
+
// Handle `details` element as its content may falsely appear visible when it is closed
|
|
74
|
+
const closedDetails = element.closest('details:not([open])')
|
|
75
|
+
|
|
76
|
+
if (!closedDetails) {
|
|
77
|
+
return elementIsVisible
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (closedDetails !== element) {
|
|
81
|
+
const summary = element.closest('summary')
|
|
82
|
+
|
|
83
|
+
// Check if summary is a direct child of the closed details
|
|
84
|
+
if (summary?.parentNode !== closedDetails) {
|
|
85
|
+
return false
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return elementIsVisible
|
|
90
|
+
}
|
package/src/components/index.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
|
|
14
14
|
import { CBackdrop } from '../backdrop/CBackdrop'
|
|
15
15
|
import { CConditionalTeleport } from '../conditional-teleport'
|
|
16
|
+
import { CFocusTrap } from '../focus-trap'
|
|
16
17
|
|
|
17
18
|
import { executeAfterTransition } from '../../utils/transition'
|
|
18
19
|
|
|
@@ -32,7 +33,7 @@ const CModal = defineComponent({
|
|
|
32
33
|
},
|
|
33
34
|
},
|
|
34
35
|
/**
|
|
35
|
-
* Apply a backdrop on body while
|
|
36
|
+
* Apply a backdrop on body while modal is open.
|
|
36
37
|
*
|
|
37
38
|
* @values boolean | 'static'
|
|
38
39
|
*/
|
|
@@ -162,7 +163,7 @@ const CModal = defineComponent({
|
|
|
162
163
|
() => props.visible,
|
|
163
164
|
() => {
|
|
164
165
|
visible.value = props.visible
|
|
165
|
-
}
|
|
166
|
+
}
|
|
166
167
|
)
|
|
167
168
|
|
|
168
169
|
const handleEnter = (el: RendererElement, done: () => void) => {
|
|
@@ -175,13 +176,14 @@ const CModal = defineComponent({
|
|
|
175
176
|
setTimeout(() => {
|
|
176
177
|
el.classList.add('show')
|
|
177
178
|
}, 1)
|
|
179
|
+
|
|
178
180
|
emit('show')
|
|
179
181
|
}
|
|
180
182
|
|
|
181
183
|
const handleAfterEnter = () => {
|
|
182
184
|
props.focus && modalRef.value?.focus()
|
|
183
185
|
window.addEventListener('mousedown', handleMouseDown)
|
|
184
|
-
window.addEventListener('
|
|
186
|
+
window.addEventListener('keydown', handleKeyDown)
|
|
185
187
|
}
|
|
186
188
|
|
|
187
189
|
// eslint-disable-next-line unicorn/consistent-function-scoping
|
|
@@ -195,33 +197,33 @@ const CModal = defineComponent({
|
|
|
195
197
|
}
|
|
196
198
|
|
|
197
199
|
el.classList.remove('show')
|
|
200
|
+
emit('close')
|
|
198
201
|
}
|
|
199
202
|
|
|
200
203
|
const handleAfterLeave = (el: RendererElement) => {
|
|
201
204
|
activeElementRef.value?.focus()
|
|
202
205
|
window.removeEventListener('mousedown', handleMouseDown)
|
|
203
|
-
window.removeEventListener('
|
|
206
|
+
window.removeEventListener('keydown', handleKeyDown)
|
|
204
207
|
el.style.display = 'none'
|
|
205
208
|
}
|
|
206
209
|
|
|
207
210
|
const handleDismiss = () => {
|
|
208
|
-
|
|
211
|
+
if (props.backdrop === 'static') {
|
|
212
|
+
modalRef.value.classList.add('modal-static')
|
|
213
|
+
emit('close-prevented')
|
|
214
|
+
setTimeout(() => {
|
|
215
|
+
modalRef.value.classList.remove('modal-static')
|
|
216
|
+
}, 300)
|
|
217
|
+
|
|
218
|
+
return
|
|
219
|
+
}
|
|
220
|
+
|
|
209
221
|
visible.value = false
|
|
210
222
|
}
|
|
211
223
|
|
|
212
|
-
const
|
|
213
|
-
if (
|
|
214
|
-
|
|
215
|
-
handleDismiss()
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
if (props.backdrop === 'static') {
|
|
219
|
-
modalRef.value.classList.add('modal-static')
|
|
220
|
-
emit('close-prevented')
|
|
221
|
-
setTimeout(() => {
|
|
222
|
-
modalRef.value.classList.remove('modal-static')
|
|
223
|
-
}, 300)
|
|
224
|
-
}
|
|
224
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
225
|
+
if (event.key === 'Escape' && props.keyboard) {
|
|
226
|
+
handleDismiss()
|
|
225
227
|
}
|
|
226
228
|
}
|
|
227
229
|
|
|
@@ -231,20 +233,11 @@ const CModal = defineComponent({
|
|
|
231
233
|
|
|
232
234
|
const handleMouseUp = (event: Event) => {
|
|
233
235
|
if (modalContentRef.value && !modalContentRef.value.contains(event.target as HTMLElement)) {
|
|
234
|
-
|
|
235
|
-
handleDismiss()
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
if (props.backdrop === 'static') {
|
|
239
|
-
modalRef.value.classList.add('modal-static')
|
|
240
|
-
setTimeout(() => {
|
|
241
|
-
modalRef.value.classList.remove('modal-static')
|
|
242
|
-
}, 300)
|
|
243
|
-
}
|
|
236
|
+
handleDismiss()
|
|
244
237
|
}
|
|
245
238
|
}
|
|
246
239
|
|
|
247
|
-
provide('
|
|
240
|
+
provide('visible', visible)
|
|
248
241
|
|
|
249
242
|
const modal = () =>
|
|
250
243
|
h(
|
|
@@ -276,12 +269,14 @@ const CModal = defineComponent({
|
|
|
276
269
|
},
|
|
277
270
|
],
|
|
278
271
|
},
|
|
279
|
-
h(
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
272
|
+
h(CFocusTrap, { active: props.focus }, () =>
|
|
273
|
+
h(
|
|
274
|
+
'div',
|
|
275
|
+
{ class: ['modal-content', props.contentClassName], ref: modalContentRef },
|
|
276
|
+
slots.default && slots.default()
|
|
277
|
+
)
|
|
278
|
+
)
|
|
279
|
+
)
|
|
285
280
|
)
|
|
286
281
|
|
|
287
282
|
return () =>
|
|
@@ -305,7 +300,7 @@ const CModal = defineComponent({
|
|
|
305
300
|
() =>
|
|
306
301
|
props.unmountOnClose
|
|
307
302
|
? visible.value && modal()
|
|
308
|
-
: withDirectives(modal(), [[vShow, visible.value]])
|
|
303
|
+
: withDirectives(modal(), [[vShow, visible.value]])
|
|
309
304
|
),
|
|
310
305
|
props.backdrop &&
|
|
311
306
|
h(CBackdrop, {
|
|
@@ -313,7 +308,7 @@ const CModal = defineComponent({
|
|
|
313
308
|
visible: visible.value,
|
|
314
309
|
}),
|
|
315
310
|
],
|
|
316
|
-
}
|
|
311
|
+
}
|
|
317
312
|
)
|
|
318
313
|
},
|
|
319
314
|
})
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { defineComponent, h, inject } from 'vue'
|
|
1
|
+
import { defineComponent, h, inject, Ref } from 'vue'
|
|
2
2
|
|
|
3
3
|
import { CCloseButton } from '../close-button/CCloseButton'
|
|
4
4
|
|
|
@@ -14,11 +14,13 @@ const CModalHeader = defineComponent({
|
|
|
14
14
|
},
|
|
15
15
|
},
|
|
16
16
|
setup(props, { slots }) {
|
|
17
|
-
const
|
|
17
|
+
const visible = inject<Ref<boolean>>('visible')!
|
|
18
18
|
return () =>
|
|
19
19
|
h('span', { class: 'modal-header' }, [
|
|
20
20
|
slots.default && slots.default(),
|
|
21
|
-
props.closeButton && h(CCloseButton, { onClick: () =>
|
|
21
|
+
props.closeButton && h(CCloseButton, { onClick: () => {
|
|
22
|
+
visible.value = false
|
|
23
|
+
} }, ''),
|
|
22
24
|
])
|
|
23
25
|
},
|
|
24
26
|
})
|