@indielayer/ui 1.13.2 → 1.14.1
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/docs/pages/component/accordion/index.vue +1 -1
- package/docs/pages/component/button/index.vue +1 -1
- package/docs/pages/component/checkbox/index.vue +1 -1
- package/docs/pages/component/container/index.vue +1 -1
- package/docs/pages/component/drawer/index.vue +1 -1
- package/docs/pages/component/form/index.vue +1 -1
- package/docs/pages/component/formGroup/index.vue +1 -1
- package/docs/pages/component/icon/index.vue +1 -1
- package/docs/pages/component/notifications/index.vue +1 -1
- package/docs/pages/component/pagination/index.vue +1 -1
- package/docs/pages/component/popover/index.vue +1 -1
- package/docs/pages/component/progress/index.vue +1 -1
- package/docs/pages/component/scroll/index.vue +1 -1
- package/docs/pages/component/skeleton/index.vue +1 -1
- package/docs/pages/component/slider/index.vue +1 -1
- package/docs/pages/component/spacer/index.vue +1 -1
- package/docs/pages/component/spinner/index.vue +1 -1
- package/docs/pages/component/table/index.vue +7 -0
- package/docs/pages/component/table/selectable.vue +68 -0
- package/docs/pages/component/table/usage.vue +1 -4
- package/docs/pages/component/table/virtual.vue +3 -0
- package/docs/pages/component/tag/index.vue +1 -1
- package/docs/pages/component/textarea/index.vue +1 -1
- package/docs/pages/component/toggle/index.vue +1 -1
- package/docs/pages/component/upload/index.vue +1 -1
- package/docs/search/components.json +1 -1
- package/lib/components/button/theme/Button.base.theme.js +21 -21
- package/lib/components/menu/MenuItem.vue.js +1 -1
- package/lib/components/menu/MenuItem.vue2.js +82 -84
- package/lib/components/radio/theme/Radio.base.theme.js +24 -24
- package/lib/components/select/Select.vue.js +121 -112
- package/lib/components/table/Table.vue.d.ts +62 -8
- package/lib/components/table/Table.vue.js +194 -139
- package/lib/components/table/TableHeader.vue.d.ts +5 -5
- package/lib/components/table/TableHeader.vue.js +37 -34
- package/lib/components/table/TableRow.vue.d.ts +4 -0
- package/lib/components/table/TableRow.vue.js +3 -2
- package/lib/components/table/theme/TableHeader.base.theme.js +9 -9
- package/lib/components/table/theme/TableHeader.carbon.theme.js +1 -1
- package/lib/components/table/theme/TableRow.base.theme.js +3 -3
- package/lib/composables/useFocusTrap.d.ts +9 -4
- package/lib/composables/useFocusTrap.js +42 -27
- package/lib/index.js +1 -1
- package/lib/index.umd.js +4 -4
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +1 -1
- package/src/components/button/theme/Button.base.theme.ts +1 -1
- package/src/components/menu/MenuItem.vue +1 -0
- package/src/components/radio/theme/Radio.base.theme.ts +1 -1
- package/src/components/select/Select.vue +20 -5
- package/src/components/table/Table.vue +113 -15
- package/src/components/table/TableHeader.vue +7 -5
- package/src/components/table/TableRow.vue +1 -0
- package/src/components/table/theme/TableHeader.base.theme.ts +4 -3
- package/src/components/table/theme/TableHeader.carbon.theme.ts +0 -1
- package/src/components/table/theme/TableRow.base.theme.ts +2 -2
- package/src/composables/useFocusTrap.ts +73 -42
- package/src/version.ts +1 -1
package/lib/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare const _default: "1.
|
|
1
|
+
declare const _default: "1.14.1";
|
|
2
2
|
export default _default;
|
package/lib/version.js
CHANGED
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@ import type { ButtonTheme } from '../Button.vue'
|
|
|
3
3
|
const theme: ButtonTheme = {
|
|
4
4
|
classes: {
|
|
5
5
|
wrapper({ props, slots, data }) {
|
|
6
|
-
const classes = ['relative transition duration-150 focus:outline-
|
|
6
|
+
const classes = ['relative transition duration-150 focus-visible:outline-secondary-300 outline-transparent outline outline-1 outline-offset-2 inline-flex items-center justify-center font-medium whitespace-nowrap overflow-hidden align-middle active:!shadow-none border appearance-none shrink-0']
|
|
7
7
|
|
|
8
8
|
// radius
|
|
9
9
|
if (!data.isButtonGroup) classes.push(props.rounded ? 'rounded-full' : 'rounded-md')
|
|
@@ -60,6 +60,7 @@ import { useTheme, type ThemeComponent } from '../../composables/useTheme'
|
|
|
60
60
|
|
|
61
61
|
import XIcon from '../../components/icon/Icon.vue'
|
|
62
62
|
import XSpinner from '../../components/spinner/Spinner.vue'
|
|
63
|
+
import XCheckbox from '../../components/checkbox/Checkbox.vue'
|
|
63
64
|
|
|
64
65
|
import type { MenuArrayItem } from './Menu.vue'
|
|
65
66
|
|
|
@@ -11,7 +11,7 @@ const theme: RadioTheme = {
|
|
|
11
11
|
},
|
|
12
12
|
|
|
13
13
|
circle: ({ props }) => {
|
|
14
|
-
const classes = ['rounded-full flex justify-center items-center shrink-0 border outline-offset-2 outline-slate-300 dark:outline-slate-500 group-focus:outline-1 group-focus:outline']
|
|
14
|
+
const classes = ['rounded-full flex justify-center items-center shrink-0 border outline-offset-2 outline-slate-300 dark:outline-slate-500 group-focus-visible:outline-1 group-focus-visible:outline']
|
|
15
15
|
|
|
16
16
|
if (props.size === 'lg') classes.push('h-5 w-5')
|
|
17
17
|
else if (props.size === 'xl') classes.push('h-6 w-6')
|
|
@@ -216,20 +216,35 @@ function findSelectableIndex(start: number | undefined, direction = 'down') {
|
|
|
216
216
|
start = direction === 'down' ? -1 : 1
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
+
const totalOptions = internalOptions.value.length
|
|
220
|
+
let checked = 0
|
|
221
|
+
|
|
219
222
|
if (direction === 'down') {
|
|
220
223
|
let next = start + 1
|
|
221
224
|
|
|
222
|
-
if (next >
|
|
225
|
+
if (next > totalOptions - 1) next = 0
|
|
223
226
|
while (internalOptions.value[next].disabled) {
|
|
224
|
-
if (++next >
|
|
227
|
+
if (++next > totalOptions - 1) next = 0
|
|
228
|
+
if (++checked >= totalOptions) {
|
|
229
|
+
// All options are disabled, break to avoid infinite loop
|
|
230
|
+
selectedIndex.value = undefined
|
|
231
|
+
|
|
232
|
+
return
|
|
233
|
+
}
|
|
225
234
|
}
|
|
226
235
|
selectedIndex.value = next
|
|
227
236
|
} else {
|
|
228
237
|
let next = start - 1
|
|
229
238
|
|
|
230
|
-
if (next < 0) next =
|
|
239
|
+
if (next < 0) next = totalOptions - 1
|
|
231
240
|
while (internalOptions.value[next].disabled) {
|
|
232
|
-
if (--next < 0) next =
|
|
241
|
+
if (--next < 0) next = totalOptions - 1
|
|
242
|
+
if (++checked >= totalOptions) {
|
|
243
|
+
// All options are disabled, break to avoid infinite loop
|
|
244
|
+
selectedIndex.value = undefined
|
|
245
|
+
|
|
246
|
+
return
|
|
247
|
+
}
|
|
233
248
|
}
|
|
234
249
|
selectedIndex.value = next
|
|
235
250
|
}
|
|
@@ -286,7 +301,7 @@ function isEmpty(value: string | number | []) {
|
|
|
286
301
|
function handleRemove(e: Event, value: string) {
|
|
287
302
|
e.stopPropagation()
|
|
288
303
|
|
|
289
|
-
if (isDisabled.value) return
|
|
304
|
+
if (isDisabled.value || !Array.isArray(selected.value)) return
|
|
290
305
|
|
|
291
306
|
// find value in selected and remove it
|
|
292
307
|
const index = selected.value.indexOf(value)
|
|
@@ -44,6 +44,12 @@ const tableProps = {
|
|
|
44
44
|
default: 5,
|
|
45
45
|
},
|
|
46
46
|
keyProp: String,
|
|
47
|
+
selectable: Boolean,
|
|
48
|
+
singleSelect: Boolean,
|
|
49
|
+
autoClearSelected: {
|
|
50
|
+
type: Boolean,
|
|
51
|
+
default: true,
|
|
52
|
+
},
|
|
47
53
|
}
|
|
48
54
|
|
|
49
55
|
export type TableHeader = {
|
|
@@ -81,6 +87,7 @@ import XSkeleton from '../skeleton/Skeleton.vue'
|
|
|
81
87
|
import type { SkeletonShape } from '../skeleton/Skeleton.vue'
|
|
82
88
|
|
|
83
89
|
import XIcon from '../icon/Icon.vue'
|
|
90
|
+
import XCheckbox from '../checkbox/Checkbox.vue'
|
|
84
91
|
|
|
85
92
|
import { chevronDownIcon } from '../../common/icons'
|
|
86
93
|
|
|
@@ -92,8 +99,7 @@ const props = defineProps({
|
|
|
92
99
|
},
|
|
93
100
|
})
|
|
94
101
|
|
|
95
|
-
const selected = defineModel<number | string>('selected')
|
|
96
|
-
const hasSelected = computed(() => typeof selected.value !== 'undefined')
|
|
102
|
+
const selected = defineModel<(number | string) | (number | string)[]>('selected')
|
|
97
103
|
|
|
98
104
|
type internalT = T & {
|
|
99
105
|
__expanded?: boolean;
|
|
@@ -122,10 +128,6 @@ const { list, containerProps, wrapperProps } = useVirtualList(
|
|
|
122
128
|
|
|
123
129
|
const internalItems = ref<internalT[]>([])
|
|
124
130
|
|
|
125
|
-
watch(items, (newValue) => {
|
|
126
|
-
if (props.expandable) internalItems.value = clone(newValue as any) as internalT[]
|
|
127
|
-
}, { immediate: true })
|
|
128
|
-
|
|
129
131
|
const emit = defineEmits(['update:sort', 'click-row'])
|
|
130
132
|
|
|
131
133
|
function getSort(headerValue: string | undefined, sort: string[]): TableHeaderSort {
|
|
@@ -179,6 +181,81 @@ function getValue(item: any, path: string | string[] | undefined) {
|
|
|
179
181
|
return result ?? ''
|
|
180
182
|
}
|
|
181
183
|
|
|
184
|
+
const allKeys = computed<(number | string)[]>(() => {
|
|
185
|
+
if (!props.selectable) return []
|
|
186
|
+
|
|
187
|
+
return items.value.map((item, index) => props.keyProp ? (item as Record<string, unknown>)[props.keyProp] : index) as (number | string)[]
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
const allRowsSelected = computed(() => {
|
|
191
|
+
if (!props.selectable || props.singleSelect) return false
|
|
192
|
+
|
|
193
|
+
return Array.isArray(selected.value) && selected.value.length > 0 && allKeys.value.length > 0 && selected.value.length === allKeys.value.length
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
const someRowsSelected = computed(() => {
|
|
197
|
+
if (!props.selectable || props.singleSelect) return false
|
|
198
|
+
|
|
199
|
+
return Array.isArray(selected.value) && selected.value.length > 0 && allKeys.value.length > 0 && selected.value.length !== allKeys.value.length
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
function isRowSelected(rowKey: any) {
|
|
203
|
+
if (!props.selectable) return false
|
|
204
|
+
if (props.singleSelect) {
|
|
205
|
+
return selected.value === rowKey
|
|
206
|
+
} else {
|
|
207
|
+
return Array.isArray(selected.value) && selected.value.includes(rowKey)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function toggleRowSelection(rowKey: any) {
|
|
212
|
+
if (!props.selectable) return
|
|
213
|
+
if (props.singleSelect) {
|
|
214
|
+
selected.value = selected.value === rowKey ? undefined : rowKey
|
|
215
|
+
} else {
|
|
216
|
+
if (!Array.isArray(selected.value)) selected.value = []
|
|
217
|
+
if (selected.value.includes(rowKey)) {
|
|
218
|
+
selected.value = selected.value.filter((k: any) => k !== rowKey)
|
|
219
|
+
} else {
|
|
220
|
+
selected.value = [...selected.value, rowKey]
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function toggleSelectAll() {
|
|
226
|
+
if (!props.selectable || props.singleSelect) return
|
|
227
|
+
|
|
228
|
+
if (allRowsSelected.value || someRowsSelected.value) {
|
|
229
|
+
selected.value = []
|
|
230
|
+
} else {
|
|
231
|
+
selected.value = allKeys.value
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function onTableRowClick(item: any, index: number) {
|
|
236
|
+
if (props.selectable && props.singleSelect) {
|
|
237
|
+
toggleRowSelection(props.keyProp ? (item as Record<string, unknown>)[props.keyProp] : index)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
emit('click-row', item, index)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
watch(items, (newValue) => {
|
|
244
|
+
if (props.expandable) internalItems.value = clone(newValue as any) as internalT[]
|
|
245
|
+
|
|
246
|
+
if (props.selectable && props.autoClearSelected) {
|
|
247
|
+
if (props.singleSelect) {
|
|
248
|
+
if (!allKeys.value.includes(selected.value as any)) {
|
|
249
|
+
selected.value = undefined
|
|
250
|
+
}
|
|
251
|
+
} else {
|
|
252
|
+
if (Array.isArray(selected.value)) {
|
|
253
|
+
selected.value = selected.value.filter((k: any) => allKeys.value.includes(k))
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}, { immediate: true })
|
|
258
|
+
|
|
182
259
|
const { styles, classes, className } = useTheme('Table', {}, props)
|
|
183
260
|
</script>
|
|
184
261
|
|
|
@@ -192,6 +269,7 @@ const { styles, classes, className } = useTheme('Table', {}, props)
|
|
|
192
269
|
|
|
193
270
|
<div
|
|
194
271
|
v-bind="wrapperProps"
|
|
272
|
+
class="relative"
|
|
195
273
|
:class="{
|
|
196
274
|
'!h-auto': props.loading
|
|
197
275
|
}"
|
|
@@ -201,6 +279,16 @@ const { styles, classes, className } = useTheme('Table', {}, props)
|
|
|
201
279
|
:class="classes.table"
|
|
202
280
|
>
|
|
203
281
|
<x-table-head :sticky-header="stickyHeader">
|
|
282
|
+
<x-table-header v-if="props.selectable && !props.singleSelect" width="40" class="!pl-3.5 !pr-0.5 !py-2.5 cursor-pointer" @click="toggleSelectAll">
|
|
283
|
+
<x-checkbox
|
|
284
|
+
:model-value="allRowsSelected || someRowsSelected"
|
|
285
|
+
:indeterminate="someRowsSelected"
|
|
286
|
+
hide-footer
|
|
287
|
+
aria-label="Select all rows"
|
|
288
|
+
skip-form-registry
|
|
289
|
+
@click.prevent.stop="toggleSelectAll"
|
|
290
|
+
/>
|
|
291
|
+
</x-table-header>
|
|
204
292
|
<x-table-header v-if="expandable" width="48" class="!p-0"/>
|
|
205
293
|
<x-table-header
|
|
206
294
|
v-for="(header, index) in headers"
|
|
@@ -259,13 +347,23 @@ const { styles, classes, className } = useTheme('Table', {}, props)
|
|
|
259
347
|
</td>
|
|
260
348
|
</tr>
|
|
261
349
|
</template>
|
|
262
|
-
<template v-for="(item, index) in list" v-else :key="keyProp
|
|
350
|
+
<template v-for="(item, index) in list" v-else :key="keyProp ? (item.data as Record<string, unknown>)[keyProp] : item.index">
|
|
263
351
|
<x-table-row
|
|
264
352
|
:pointer="pointer"
|
|
265
353
|
:striped="striped"
|
|
266
|
-
:selected="
|
|
267
|
-
|
|
354
|
+
:selected="isRowSelected(keyProp ? (item.data as Record<string, unknown>)[keyProp] : item.index)"
|
|
355
|
+
:single-select="singleSelect"
|
|
356
|
+
@click="onTableRowClick(item.data, item.index)"
|
|
268
357
|
>
|
|
358
|
+
<x-table-cell v-if="props.selectable && !singleSelect" width="40" class="!pl-3.5 !pr-0.5 cursor-pointer" @click.stop="toggleRowSelection(keyProp ? (item.data as Record<string, unknown>)[keyProp] : item.index)">
|
|
359
|
+
<x-checkbox
|
|
360
|
+
:model-value="isRowSelected(keyProp ? (item.data as Record<string, unknown>)[keyProp] : item.index)"
|
|
361
|
+
hide-footer
|
|
362
|
+
:aria-label="`Select row ${index + 1}`"
|
|
363
|
+
skip-form-registry
|
|
364
|
+
@click.prevent.stop="toggleRowSelection(keyProp ? (item.data as Record<string, unknown>)[keyProp] : item.index)"
|
|
365
|
+
/>
|
|
366
|
+
</x-table-cell>
|
|
269
367
|
<x-table-cell v-if="expandable" width="48" class="!p-1">
|
|
270
368
|
<button
|
|
271
369
|
type="button"
|
|
@@ -311,13 +409,13 @@ const { styles, classes, className } = useTheme('Table', {}, props)
|
|
|
311
409
|
</tr>
|
|
312
410
|
</template>
|
|
313
411
|
</x-table-body>
|
|
314
|
-
<div
|
|
315
|
-
v-if="loading"
|
|
316
|
-
:class="classes.loadingWrapper"
|
|
317
|
-
>
|
|
318
|
-
<x-spinner size="lg"/>
|
|
319
|
-
</div>
|
|
320
412
|
</table>
|
|
413
|
+
<div
|
|
414
|
+
v-if="loading"
|
|
415
|
+
:class="classes.loadingWrapper"
|
|
416
|
+
>
|
|
417
|
+
<x-spinner size="lg"/>
|
|
418
|
+
</div>
|
|
321
419
|
</div>
|
|
322
420
|
</div>
|
|
323
421
|
</template>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
const validators = {
|
|
3
3
|
sort: [1, -1, undefined] as const,
|
|
4
|
-
textAlign: ['left', 'center', 'right'
|
|
4
|
+
textAlign: ['left', 'center', 'right'] as const,
|
|
5
5
|
}
|
|
6
6
|
|
|
7
7
|
const tableHeaderProps = {
|
|
@@ -41,17 +41,19 @@ const { styles, classes, className } = useTheme('TableHeader', {}, props)
|
|
|
41
41
|
<template>
|
|
42
42
|
<th :style="styles" :class="[className, classes.th, 'group/th']">
|
|
43
43
|
<div :class="classes.header">
|
|
44
|
-
<
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
<div class="flex items-center gap-1">
|
|
45
|
+
<slot></slot>
|
|
46
|
+
<x-toggle-tip v-if="tooltip" :content="tooltip"/>
|
|
47
|
+
</div>
|
|
47
48
|
|
|
48
49
|
<svg
|
|
49
50
|
v-if="sortable"
|
|
50
51
|
class="shrink-0"
|
|
51
52
|
:class="[
|
|
52
53
|
classes.sortIcon,
|
|
54
|
+
textAlign === 'right' ? '-mr-4 -translate-x-4' : '-ml-4 translate-x-4',
|
|
53
55
|
[sort && [1, -1].includes(sort) ? '' : 'invisible group-hover/th:visible'],
|
|
54
|
-
[sort !== -1 && sort !== 1 ? 'text-secondary-400' : 'text-primary-700']
|
|
56
|
+
[sort !== -1 && sort !== 1 ? 'text-secondary-400 dark:text-secondary-500' : 'text-primary-700 dark:text-primary-400']
|
|
55
57
|
]"
|
|
56
58
|
width="24"
|
|
57
59
|
height="24"
|
|
@@ -14,9 +14,10 @@ const theme: TableHeaderTheme = {
|
|
|
14
14
|
const classes = ['flex items-center gap-1 select-none']
|
|
15
15
|
|
|
16
16
|
if (props.textAlign === 'left') classes.push('justify-start')
|
|
17
|
-
if (props.textAlign === 'right')
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
else if (props.textAlign === 'right') {
|
|
18
|
+
if (props.sortable) classes.push('flex-row-reverse')
|
|
19
|
+
else classes.push('justify-end')
|
|
20
|
+
} else if (props.textAlign === 'center') classes.push('justify-center')
|
|
20
21
|
|
|
21
22
|
return classes
|
|
22
23
|
},
|
|
@@ -10,7 +10,6 @@ const theme: TableHeaderTheme = {
|
|
|
10
10
|
if (props.textAlign === 'left') classes.push('text-left')
|
|
11
11
|
if (props.textAlign === 'right') classes.push('text-right')
|
|
12
12
|
if (props.textAlign === 'center') classes.push('text-center')
|
|
13
|
-
if (props.textAlign === 'justify') classes.push('text-justify')
|
|
14
13
|
|
|
15
14
|
return classes
|
|
16
15
|
},
|
|
@@ -5,8 +5,8 @@ const theme: TableRowTheme = {
|
|
|
5
5
|
row: ({ props }) => {
|
|
6
6
|
const classes = []
|
|
7
7
|
|
|
8
|
-
if (props.selected) {
|
|
9
|
-
classes.push('shadow-[inset_2px_0] shadow-primary-500')
|
|
8
|
+
if (props.selected && props.singleSelect) {
|
|
9
|
+
classes.push('shadow-[inset_2px_0] shadow-primary-500 !bg-secondary-50 dark:!bg-secondary-600')
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
if (props.striped) {
|
|
@@ -1,83 +1,114 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import { onUnmounted, type MaybeRef, unref, nextTick } from 'vue'
|
|
1
|
+
import { onUnmounted, unref, nextTick, watch, ref, type Ref, type ComponentPublicInstance } from 'vue'
|
|
3
2
|
|
|
4
3
|
const focusableQuery = 'button:not([tabindex="-1"]), [href], input, select, textarea, li, a, [tabindex]:not([tabindex="-1"])'
|
|
5
4
|
|
|
6
5
|
export function useFocusTrap() {
|
|
7
|
-
|
|
6
|
+
const focusable = ref<HTMLElement[]>([])
|
|
8
7
|
let observer: MutationObserver | null = null
|
|
9
8
|
|
|
10
9
|
let firstFocusableEl: HTMLElement | null = null
|
|
11
10
|
let lastFocusableEl: HTMLElement | null = null
|
|
11
|
+
let prevActiveElement: HTMLElement | null = null
|
|
12
|
+
let currentTarget: HTMLElement | ComponentPublicInstance | null = null
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
function getEl(target: HTMLElement | ComponentPublicInstance | null): HTMLElement | null {
|
|
15
|
+
if (!target) return null
|
|
15
16
|
|
|
16
|
-
|
|
17
|
+
return (target as ComponentPublicInstance).$el
|
|
18
|
+
? (target as ComponentPublicInstance).$el as HTMLElement
|
|
19
|
+
: target as HTMLElement
|
|
20
|
+
}
|
|
17
21
|
|
|
18
|
-
|
|
22
|
+
function getFocusableElements(target: HTMLElement | ComponentPublicInstance | null) {
|
|
23
|
+
const el = getEl(target)
|
|
19
24
|
|
|
20
|
-
|
|
25
|
+
if (!el) return
|
|
26
|
+
const elements = el.querySelectorAll(focusableQuery)
|
|
21
27
|
|
|
22
|
-
|
|
28
|
+
focusable.value = Array.from(elements) as HTMLElement[]
|
|
29
|
+
firstFocusableEl = focusable.value[0] || null
|
|
30
|
+
lastFocusableEl = focusable.value[focusable.value.length - 1] || null
|
|
31
|
+
}
|
|
23
32
|
|
|
24
|
-
|
|
25
|
-
|
|
33
|
+
const handleKeydown = (event: KeyboardEvent) => {
|
|
34
|
+
if (event.key !== 'Tab' || focusable.value.length === 0) return
|
|
26
35
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
36
|
+
const isShiftPressed = event.shiftKey
|
|
37
|
+
const currentEl = document.activeElement as HTMLElement | null
|
|
30
38
|
|
|
31
|
-
|
|
32
|
-
|
|
39
|
+
const firstEl = firstFocusableEl
|
|
40
|
+
const lastEl = lastFocusableEl
|
|
33
41
|
|
|
34
|
-
|
|
42
|
+
if (!currentEl) {
|
|
43
|
+
event.preventDefault()
|
|
44
|
+
firstEl?.focus()
|
|
35
45
|
|
|
36
|
-
|
|
37
|
-
|
|
46
|
+
return
|
|
47
|
+
}
|
|
38
48
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
49
|
+
if (!isShiftPressed && currentEl === lastEl) {
|
|
50
|
+
event.preventDefault()
|
|
51
|
+
firstEl?.focus()
|
|
52
|
+
} else if (isShiftPressed && currentEl === firstEl) {
|
|
53
|
+
event.preventDefault()
|
|
54
|
+
lastEl?.focus()
|
|
55
|
+
}
|
|
42
56
|
}
|
|
43
57
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
58
|
+
async function initFocusTrap(
|
|
59
|
+
targetRef: Ref<HTMLElement | ComponentPublicInstance | null> | HTMLElement | ComponentPublicInstance | null,
|
|
60
|
+
options?: { initialFocusIndex?: number; returnFocusOnClear?: boolean; },
|
|
61
|
+
) {
|
|
62
|
+
if (typeof window === 'undefined') return
|
|
48
63
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
64
|
+
// Clean up previous trap if any
|
|
65
|
+
clearFocusTrap()
|
|
66
|
+
|
|
67
|
+
prevActiveElement = document.activeElement as HTMLElement
|
|
52
68
|
|
|
53
|
-
|
|
54
|
-
|
|
69
|
+
currentTarget = unref(targetRef)
|
|
70
|
+
if (!currentTarget) return
|
|
55
71
|
|
|
56
|
-
|
|
57
|
-
|
|
72
|
+
await nextTick()
|
|
73
|
+
getFocusableElements(currentTarget)
|
|
58
74
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
firstEl?.focus()
|
|
62
|
-
} else if (isShiftPressed && currentEl === firstEl) {
|
|
63
|
-
event.preventDefault()
|
|
64
|
-
lastEl?.focus()
|
|
65
|
-
}
|
|
75
|
+
// Focus initial element
|
|
76
|
+
const idx = options?.initialFocusIndex ?? 0
|
|
66
77
|
|
|
78
|
+
focusable.value[idx]?.focus()
|
|
79
|
+
|
|
80
|
+
document.addEventListener('keydown', handleKeydown)
|
|
81
|
+
observer = new MutationObserver(() => getFocusableElements(currentTarget))
|
|
82
|
+
const el = getEl(currentTarget)
|
|
83
|
+
|
|
84
|
+
if (el) observer.observe(el, { childList: true, subtree: true })
|
|
85
|
+
|
|
86
|
+
// If targetRef is a Ref, watch for changes
|
|
87
|
+
if (typeof targetRef === 'object' && targetRef !== null && 'value' in targetRef) {
|
|
88
|
+
watch(targetRef, (newVal) => {
|
|
89
|
+
clearFocusTrap()
|
|
90
|
+
if (newVal !== null) initFocusTrap(targetRef, options)
|
|
91
|
+
})
|
|
67
92
|
}
|
|
68
93
|
}
|
|
69
94
|
|
|
70
|
-
|
|
95
|
+
function clearFocusTrap(options?: { returnFocus?: boolean; }) {
|
|
71
96
|
document.removeEventListener('keydown', handleKeydown)
|
|
72
97
|
observer?.disconnect()
|
|
98
|
+
observer = null
|
|
99
|
+
if (options?.returnFocus && prevActiveElement) {
|
|
100
|
+
prevActiveElement.focus()
|
|
101
|
+
}
|
|
102
|
+
currentTarget = null
|
|
73
103
|
}
|
|
74
104
|
|
|
75
105
|
onUnmounted(() => {
|
|
76
|
-
clearFocusTrap()
|
|
106
|
+
clearFocusTrap({ returnFocus: true })
|
|
77
107
|
})
|
|
78
108
|
|
|
79
109
|
return {
|
|
80
110
|
initFocusTrap,
|
|
81
111
|
clearFocusTrap,
|
|
112
|
+
focusable, // expose for advanced use
|
|
82
113
|
}
|
|
83
114
|
}
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export default '1.
|
|
1
|
+
export default '1.14.1'
|