@coreui/vue-pro 5.14.0 → 5.15.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 (106) hide show
  1. package/README.md +1 -1
  2. package/dist/cjs/components/calendar/CCalendar.js +61 -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.js +22 -13
  13. package/dist/cjs/components/dropdown/CDropdown.js.map +1 -1
  14. package/dist/cjs/components/dropdown/CDropdownToggle.js +7 -1
  15. package/dist/cjs/components/dropdown/CDropdownToggle.js.map +1 -1
  16. package/dist/cjs/components/focus-trap/CFocusTrap.d.ts +108 -0
  17. package/dist/cjs/components/focus-trap/CFocusTrap.js +254 -0
  18. package/dist/cjs/components/focus-trap/CFocusTrap.js.map +1 -0
  19. package/dist/cjs/components/focus-trap/index.d.ts +6 -0
  20. package/dist/cjs/components/focus-trap/index.js +13 -0
  21. package/dist/cjs/components/focus-trap/index.js.map +1 -0
  22. package/dist/cjs/components/focus-trap/utils.d.ts +28 -0
  23. package/dist/cjs/components/focus-trap/utils.js +83 -0
  24. package/dist/cjs/components/focus-trap/utils.js.map +1 -0
  25. package/dist/cjs/components/index.d.ts +1 -0
  26. package/dist/cjs/components/index.js +70 -66
  27. package/dist/cjs/components/index.js.map +1 -1
  28. package/dist/cjs/components/modal/CModal.d.ts +2 -2
  29. package/dist/cjs/components/modal/CModal.js +19 -27
  30. package/dist/cjs/components/modal/CModal.js.map +1 -1
  31. package/dist/cjs/components/modal/CModalHeader.js +4 -2
  32. package/dist/cjs/components/modal/CModalHeader.js.map +1 -1
  33. package/dist/cjs/components/offcanvas/COffcanvas.js +3 -2
  34. package/dist/cjs/components/offcanvas/COffcanvas.js.map +1 -1
  35. package/dist/cjs/components/picker/CPicker.js +3 -2
  36. package/dist/cjs/components/picker/CPicker.js.map +1 -1
  37. package/dist/cjs/components/time-picker/CTimePicker.d.ts +1 -1
  38. package/dist/cjs/components/time-picker/CTimePicker.js +1 -1
  39. package/dist/cjs/components/time-picker/CTimePicker.js.map +1 -1
  40. package/dist/cjs/components/time-picker/utils.d.ts +1 -1
  41. package/dist/cjs/composables/useDebouncedCallback.d.ts +1 -1
  42. package/dist/cjs/composables/useDebouncedCallback.js +1 -1
  43. package/dist/cjs/composables/useDebouncedCallback.js.map +1 -1
  44. package/dist/cjs/index.js +76 -72
  45. package/dist/cjs/index.js.map +1 -1
  46. package/dist/esm/components/calendar/CCalendar.js +61 -65
  47. package/dist/esm/components/calendar/CCalendar.js.map +1 -1
  48. package/dist/esm/components/calendar/utils.d.ts +53 -2
  49. package/dist/esm/components/calendar/utils.js +464 -44
  50. package/dist/esm/components/calendar/utils.js.map +1 -1
  51. package/dist/esm/components/date-range-picker/CDateRangePicker.js +86 -57
  52. package/dist/esm/components/date-range-picker/CDateRangePicker.js.map +1 -1
  53. package/dist/esm/components/date-range-picker/utils.d.ts +0 -9
  54. package/dist/esm/components/date-range-picker/utils.js +1 -38
  55. package/dist/esm/components/date-range-picker/utils.js.map +1 -1
  56. package/dist/esm/components/dropdown/CDropdown.js +23 -14
  57. package/dist/esm/components/dropdown/CDropdown.js.map +1 -1
  58. package/dist/esm/components/dropdown/CDropdownToggle.js +7 -1
  59. package/dist/esm/components/dropdown/CDropdownToggle.js.map +1 -1
  60. package/dist/esm/components/focus-trap/CFocusTrap.d.ts +108 -0
  61. package/dist/esm/components/focus-trap/CFocusTrap.js +252 -0
  62. package/dist/esm/components/focus-trap/CFocusTrap.js.map +1 -0
  63. package/dist/esm/components/focus-trap/index.d.ts +6 -0
  64. package/dist/esm/components/focus-trap/index.js +10 -0
  65. package/dist/esm/components/focus-trap/index.js.map +1 -0
  66. package/dist/esm/components/focus-trap/utils.d.ts +28 -0
  67. package/dist/esm/components/focus-trap/utils.js +78 -0
  68. package/dist/esm/components/focus-trap/utils.js.map +1 -0
  69. package/dist/esm/components/index.d.ts +1 -0
  70. package/dist/esm/components/index.js +2 -0
  71. package/dist/esm/components/index.js.map +1 -1
  72. package/dist/esm/components/modal/CModal.d.ts +2 -2
  73. package/dist/esm/components/modal/CModal.js +19 -27
  74. package/dist/esm/components/modal/CModal.js.map +1 -1
  75. package/dist/esm/components/modal/CModalHeader.js +4 -2
  76. package/dist/esm/components/modal/CModalHeader.js.map +1 -1
  77. package/dist/esm/components/offcanvas/COffcanvas.js +3 -2
  78. package/dist/esm/components/offcanvas/COffcanvas.js.map +1 -1
  79. package/dist/esm/components/picker/CPicker.js +3 -2
  80. package/dist/esm/components/picker/CPicker.js.map +1 -1
  81. package/dist/esm/components/time-picker/CTimePicker.d.ts +1 -1
  82. package/dist/esm/components/time-picker/CTimePicker.js +1 -1
  83. package/dist/esm/components/time-picker/CTimePicker.js.map +1 -1
  84. package/dist/esm/components/time-picker/utils.d.ts +1 -1
  85. package/dist/esm/composables/useDebouncedCallback.d.ts +1 -1
  86. package/dist/esm/composables/useDebouncedCallback.js +1 -1
  87. package/dist/esm/composables/useDebouncedCallback.js.map +1 -1
  88. package/dist/esm/index.js +2 -0
  89. package/dist/esm/index.js.map +1 -1
  90. package/package.json +4 -4
  91. package/src/components/calendar/CCalendar.ts +55 -70
  92. package/src/components/calendar/utils.ts +595 -47
  93. package/src/components/date-range-picker/CDateRangePicker.ts +131 -82
  94. package/src/components/date-range-picker/utils.ts +0 -58
  95. package/src/components/dropdown/CDropdown.ts +34 -23
  96. package/src/components/dropdown/CDropdownToggle.ts +8 -2
  97. package/src/components/focus-trap/CFocusTrap.ts +303 -0
  98. package/src/components/focus-trap/index.ts +10 -0
  99. package/src/components/focus-trap/utils.ts +90 -0
  100. package/src/components/index.ts +1 -0
  101. package/src/components/modal/CModal.ts +32 -37
  102. package/src/components/modal/CModalHeader.ts +5 -3
  103. package/src/components/offcanvas/COffcanvas.ts +40 -36
  104. package/src/components/picker/CPicker.ts +58 -52
  105. package/src/components/time-picker/CTimePicker.ts +12 -13
  106. package/src/composables/useDebouncedCallback.ts +1 -1
@@ -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,10 @@
1
+ import { App } from 'vue'
2
+ import { CFocusTrap } from './CFocusTrap'
3
+
4
+ const CFocusTrapPlugin = {
5
+ install: (app: App): void => {
6
+ app.component(CFocusTrap.name as string, CFocusTrap)
7
+ },
8
+ }
9
+
10
+ export { CFocusTrapPlugin, 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
+ }
@@ -18,6 +18,7 @@ export * from './date-picker'
18
18
  export * from './date-range-picker'
19
19
  export * from './dropdown'
20
20
  export * from './element-cover'
21
+ export * from './focus-trap'
21
22
  export * from './footer'
22
23
  export * from './form'
23
24
  export * from './grid'
@@ -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 offcanvas is open.
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('keyup', handleKeyUp)
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('keyup', handleKeyUp)
206
+ window.removeEventListener('keydown', handleKeyDown)
204
207
  el.style.display = 'none'
205
208
  }
206
209
 
207
210
  const handleDismiss = () => {
208
- emit('close')
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 handleKeyUp = (event: KeyboardEvent) => {
213
- if (modalContentRef.value && !modalContentRef.value.contains(event.target as HTMLElement)) {
214
- if (props.backdrop !== 'static' && event.key === 'Escape' && props.keyboard) {
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
- if (props.backdrop !== 'static') {
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('handleDismiss', handleDismiss)
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
- 'div',
281
- { class: ['modal-content', props.contentClassName], ref: modalContentRef },
282
- slots.default && slots.default(),
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 handleDismiss = inject('handleDismiss') as () => void
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: () => handleDismiss() }, ''),
21
+ props.closeButton && h(CCloseButton, { onClick: () => {
22
+ visible.value = false
23
+ } }, ''),
22
24
  ])
23
25
  },
24
26
  })
@@ -1,6 +1,7 @@
1
1
  import { defineComponent, h, ref, RendererElement, Transition, watch, withDirectives } from 'vue'
2
2
 
3
3
  import { CBackdrop } from '../backdrop'
4
+ import { CFocusTrap } from '../focus-trap'
4
5
 
5
6
  import { vVisible } from '../../directives/v-c-visible'
6
7
  import { executeAfterTransition } from '../../utils/transition'
@@ -103,7 +104,7 @@ const COffcanvas = defineComponent({
103
104
  () => props.visible,
104
105
  () => {
105
106
  visible.value = props.visible
106
- },
107
+ }
107
108
  )
108
109
 
109
110
  watch(visible, () => {
@@ -161,41 +162,44 @@ const COffcanvas = defineComponent({
161
162
  }
162
163
 
163
164
  return () => [
164
- h(
165
- Transition,
166
- {
167
- appear: visible.value,
168
- css: false,
169
- onEnter: (el, done) => handleEnter(el, done),
170
- onAfterEnter: () => handleAfterEnter(),
171
- onLeave: (el, done) => handleLeave(el, done),
172
- onAfterLeave: (el) => handleAfterLeave(el),
173
- },
174
- () =>
175
- withDirectives(
176
- h(
177
- 'div',
178
- {
179
- ...attrs,
180
- class: [
181
- {
182
- [`offcanvas${
183
- typeof props.responsive === 'boolean' ? '' : '-' + props.responsive
184
- }`]: props.responsive,
185
- [`offcanvas-${props.placement}`]: props.placement,
186
- },
187
- attrs.class,
188
- ],
189
- onKeydown: (event: KeyboardEvent) => handleKeyDown(event),
190
- ref: offcanvasRef,
191
- role: 'dialog',
192
- tabindex: -1,
193
- ...(props.dark && { 'data-coreui-theme': 'dark' }),
194
- },
195
- slots.default && slots.default(),
196
- ),
197
- [[vVisible, props.visible]],
198
- ),
165
+ h(CFocusTrap, { active: visible.value && Boolean(props.backdrop) }, () =>
166
+ h(
167
+ Transition,
168
+ {
169
+ appear: visible.value,
170
+ css: false,
171
+ onEnter: (el, done) => handleEnter(el, done),
172
+ onAfterEnter: () => handleAfterEnter(),
173
+ onLeave: (el, done) => handleLeave(el, done),
174
+ onAfterLeave: (el) => handleAfterLeave(el),
175
+ },
176
+ () =>
177
+ withDirectives(
178
+ h(
179
+ 'div',
180
+ {
181
+ ...attrs,
182
+ class: [
183
+ {
184
+ [`offcanvas${
185
+ typeof props.responsive === 'boolean' ? '' : '-' + props.responsive
186
+ }`]: props.responsive,
187
+ [`offcanvas-${props.placement}`]: props.placement,
188
+ },
189
+ attrs.class,
190
+ ],
191
+ onKeydown: (event: KeyboardEvent) => handleKeyDown(event),
192
+ ref: offcanvasRef,
193
+ role: 'dialog',
194
+ tabindex: -1,
195
+ ...(props.dark && { 'data-coreui-theme': 'dark' }),
196
+ },
197
+ slots.default && slots.default()
198
+ ),
199
+
200
+ [[vVisible, props.visible]]
201
+ )
202
+ )
199
203
  ),
200
204
  props.backdrop &&
201
205
  h(CBackdrop, {