@indielayer/ui 1.13.1 → 1.14.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 (53) hide show
  1. package/docs/pages/component/accordion/index.vue +1 -1
  2. package/docs/pages/component/button/index.vue +1 -1
  3. package/docs/pages/component/checkbox/index.vue +1 -1
  4. package/docs/pages/component/container/index.vue +1 -1
  5. package/docs/pages/component/drawer/index.vue +1 -1
  6. package/docs/pages/component/form/index.vue +1 -1
  7. package/docs/pages/component/formGroup/index.vue +1 -1
  8. package/docs/pages/component/icon/index.vue +1 -1
  9. package/docs/pages/component/notifications/index.vue +1 -1
  10. package/docs/pages/component/pagination/index.vue +1 -1
  11. package/docs/pages/component/popover/index.vue +1 -1
  12. package/docs/pages/component/progress/index.vue +1 -1
  13. package/docs/pages/component/scroll/index.vue +1 -1
  14. package/docs/pages/component/skeleton/index.vue +1 -1
  15. package/docs/pages/component/slider/index.vue +1 -1
  16. package/docs/pages/component/spacer/index.vue +1 -1
  17. package/docs/pages/component/spinner/index.vue +1 -1
  18. package/docs/pages/component/table/index.vue +7 -0
  19. package/docs/pages/component/table/selectable.vue +67 -0
  20. package/docs/pages/component/table/usage.vue +2 -5
  21. package/docs/pages/component/table/virtual.vue +3 -0
  22. package/docs/pages/component/tag/index.vue +1 -1
  23. package/docs/pages/component/textarea/index.vue +1 -1
  24. package/docs/pages/component/toggle/index.vue +1 -1
  25. package/docs/pages/component/upload/index.vue +1 -1
  26. package/docs/search/components.json +1 -1
  27. package/lib/components/button/theme/Button.base.theme.js +21 -21
  28. package/lib/components/radio/theme/Radio.base.theme.js +24 -24
  29. package/lib/components/select/Select.vue.js +121 -112
  30. package/lib/components/table/Table.vue.d.ts +62 -8
  31. package/lib/components/table/Table.vue.js +273 -219
  32. package/lib/components/table/TableHeader.vue.d.ts +1 -1
  33. package/lib/components/table/TableHeader.vue.js +34 -32
  34. package/lib/components/table/TableRow.vue.d.ts +4 -0
  35. package/lib/components/table/TableRow.vue.js +3 -2
  36. package/lib/components/table/theme/TableHeader.base.theme.js +5 -1
  37. package/lib/components/table/theme/TableRow.base.theme.js +3 -3
  38. package/lib/composables/useFocusTrap.d.ts +9 -4
  39. package/lib/composables/useFocusTrap.js +42 -27
  40. package/lib/index.umd.js +4 -4
  41. package/lib/version.d.ts +1 -1
  42. package/lib/version.js +1 -1
  43. package/package.json +1 -1
  44. package/src/components/button/theme/Button.base.theme.ts +1 -1
  45. package/src/components/radio/theme/Radio.base.theme.ts +1 -1
  46. package/src/components/select/Select.vue +20 -5
  47. package/src/components/table/Table.vue +112 -15
  48. package/src/components/table/TableHeader.vue +3 -3
  49. package/src/components/table/TableRow.vue +1 -0
  50. package/src/components/table/theme/TableHeader.base.theme.ts +10 -4
  51. package/src/components/table/theme/TableRow.base.theme.ts +2 -2
  52. package/src/composables/useFocusTrap.ts +73 -42
  53. package/src/version.ts +1 -1
package/lib/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- declare const _default: "1.13.1";
1
+ declare const _default: "1.14.0";
2
2
  export default _default;
package/lib/version.js CHANGED
@@ -1,4 +1,4 @@
1
- const e = "1.13.1";
1
+ const e = "1.14.0";
2
2
  export {
3
3
  e as default
4
4
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indielayer/ui",
3
- "version": "1.13.1",
3
+ "version": "1.14.0",
4
4
  "description": "Indielayer UI Components with Tailwind CSS build for Vue 3",
5
5
  "author": {
6
6
  "name": "João Teixeira",
@@ -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-none inline-flex items-center justify-center font-medium whitespace-nowrap overflow-hidden align-middle active:!shadow-none border appearance-none shrink-0']
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')
@@ -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 > internalOptions.value.length - 1) next = 0
225
+ if (next > totalOptions - 1) next = 0
223
226
  while (internalOptions.value[next].disabled) {
224
- if (++next > internalOptions.value.length - 1) next = 0
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 = internalOptions.value.length - 1
239
+ if (next < 0) next = totalOptions - 1
231
240
  while (internalOptions.value[next].disabled) {
232
- if (--next < 0) next = internalOptions.value.length - 1
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 = {
@@ -92,8 +98,7 @@ const props = defineProps({
92
98
  },
93
99
  })
94
100
 
95
- const selected = defineModel<number | string>('selected')
96
- const hasSelected = computed(() => typeof selected.value !== 'undefined')
101
+ const selected = defineModel<(number | string) | (number | string)[]>('selected')
97
102
 
98
103
  type internalT = T & {
99
104
  __expanded?: boolean;
@@ -122,10 +127,6 @@ const { list, containerProps, wrapperProps } = useVirtualList(
122
127
 
123
128
  const internalItems = ref<internalT[]>([])
124
129
 
125
- watch(items, (newValue) => {
126
- if (props.expandable) internalItems.value = clone(newValue as any) as internalT[]
127
- }, { immediate: true })
128
-
129
130
  const emit = defineEmits(['update:sort', 'click-row'])
130
131
 
131
132
  function getSort(headerValue: string | undefined, sort: string[]): TableHeaderSort {
@@ -179,6 +180,81 @@ function getValue(item: any, path: string | string[] | undefined) {
179
180
  return result ?? ''
180
181
  }
181
182
 
183
+ const allKeys = computed<(number | string)[]>(() => {
184
+ if (!props.selectable) return []
185
+
186
+ return items.value.map((item, index) => props.keyProp ? (item as Record<string, unknown>)[props.keyProp] : index) as (number | string)[]
187
+ })
188
+
189
+ const allRowsSelected = computed(() => {
190
+ if (!props.selectable || props.singleSelect) return false
191
+
192
+ return Array.isArray(selected.value) && selected.value.length > 0 && allKeys.value.length > 0 && selected.value.length === allKeys.value.length
193
+ })
194
+
195
+ const someRowsSelected = computed(() => {
196
+ if (!props.selectable || props.singleSelect) return false
197
+
198
+ return Array.isArray(selected.value) && selected.value.length > 0 && allKeys.value.length > 0 && selected.value.length !== allKeys.value.length
199
+ })
200
+
201
+ function isRowSelected(rowKey: any) {
202
+ if (!props.selectable) return false
203
+ if (props.singleSelect) {
204
+ return selected.value === rowKey
205
+ } else {
206
+ return Array.isArray(selected.value) && selected.value.includes(rowKey)
207
+ }
208
+ }
209
+
210
+ function toggleRowSelection(rowKey: any) {
211
+ if (!props.selectable) return
212
+ if (props.singleSelect) {
213
+ selected.value = selected.value === rowKey ? undefined : rowKey
214
+ } else {
215
+ if (!Array.isArray(selected.value)) selected.value = []
216
+ if (selected.value.includes(rowKey)) {
217
+ selected.value = selected.value.filter((k: any) => k !== rowKey)
218
+ } else {
219
+ selected.value = [...selected.value, rowKey]
220
+ }
221
+ }
222
+ }
223
+
224
+ function toggleSelectAll() {
225
+ if (!props.selectable || props.singleSelect) return
226
+
227
+ if (allRowsSelected.value || someRowsSelected.value) {
228
+ selected.value = []
229
+ } else {
230
+ selected.value = allKeys.value
231
+ }
232
+ }
233
+
234
+ function onTableRowClick(item: any, index: number) {
235
+ if (props.selectable && props.singleSelect) {
236
+ toggleRowSelection(props.keyProp ? (item as Record<string, unknown>)[props.keyProp] : index)
237
+ }
238
+
239
+ emit('click-row', item, index)
240
+ }
241
+
242
+ watch(items, (newValue) => {
243
+ if (props.expandable) internalItems.value = clone(newValue as any) as internalT[]
244
+
245
+ if (props.selectable && props.autoClearSelected) {
246
+ if (props.singleSelect) {
247
+ if (!allKeys.value.includes(selected.value as any)) {
248
+ selected.value = undefined
249
+ }
250
+ } else {
251
+ if (Array.isArray(selected.value)) {
252
+ selected.value = selected.value.filter((k: any) => allKeys.value.includes(k))
253
+ }
254
+ }
255
+ }
256
+ }, { immediate: true })
257
+
182
258
  const { styles, classes, className } = useTheme('Table', {}, props)
183
259
  </script>
184
260
 
@@ -192,6 +268,7 @@ const { styles, classes, className } = useTheme('Table', {}, props)
192
268
 
193
269
  <div
194
270
  v-bind="wrapperProps"
271
+ class="relative"
195
272
  :class="{
196
273
  '!h-auto': props.loading
197
274
  }"
@@ -201,6 +278,16 @@ const { styles, classes, className } = useTheme('Table', {}, props)
201
278
  :class="classes.table"
202
279
  >
203
280
  <x-table-head :sticky-header="stickyHeader">
281
+ <x-table-header v-if="props.selectable && !props.singleSelect" width="48" class="!px-3 !py-2.5">
282
+ <x-checkbox
283
+ :model-value="allRowsSelected || someRowsSelected"
284
+ :indeterminate="someRowsSelected"
285
+ hide-footer
286
+ aria-label="Select all rows"
287
+ skip-form-registry
288
+ @click.prevent="toggleSelectAll"
289
+ />
290
+ </x-table-header>
204
291
  <x-table-header v-if="expandable" width="48" class="!p-0"/>
205
292
  <x-table-header
206
293
  v-for="(header, index) in headers"
@@ -259,13 +346,23 @@ const { styles, classes, className } = useTheme('Table', {}, props)
259
346
  </td>
260
347
  </tr>
261
348
  </template>
262
- <template v-for="(item, index) in list" v-else :key="keyProp ?? index">
349
+ <template v-for="(item, index) in list" v-else :key="keyProp ? (item.data as Record<string, unknown>)[keyProp] : item.index">
263
350
  <x-table-row
264
351
  :pointer="pointer"
265
352
  :striped="striped"
266
- :selected="hasSelected ? selected === (keyProp ? (item.data as Record<string, unknown>)[keyProp] : item.index) : undefined"
267
- @click="$emit('click-row', item.data, item.index)"
353
+ :selected="isRowSelected(keyProp ? (item.data as Record<string, unknown>)[keyProp] : item.index)"
354
+ :single-select="singleSelect"
355
+ @click="onTableRowClick(item.data, item.index)"
268
356
  >
357
+ <x-table-cell v-if="props.selectable && !singleSelect" width="48">
358
+ <x-checkbox
359
+ :model-value="isRowSelected(keyProp ? (item.data as Record<string, unknown>)[keyProp] : item.index)"
360
+ hide-footer
361
+ :aria-label="`Select row ${index + 1}`"
362
+ skip-form-registry
363
+ @click.prevent="toggleRowSelection(keyProp ? (item.data as Record<string, unknown>)[keyProp] : item.index)"
364
+ />
365
+ </x-table-cell>
269
366
  <x-table-cell v-if="expandable" width="48" class="!p-1">
270
367
  <button
271
368
  type="button"
@@ -311,13 +408,13 @@ const { styles, classes, className } = useTheme('Table', {}, props)
311
408
  </tr>
312
409
  </template>
313
410
  </x-table-body>
314
- <div
315
- v-if="loading"
316
- :class="classes.loadingWrapper"
317
- >
318
- <x-spinner size="lg"/>
319
- </div>
320
411
  </table>
412
+ <div
413
+ v-if="loading"
414
+ :class="classes.loadingWrapper"
415
+ >
416
+ <x-spinner size="lg"/>
417
+ </div>
321
418
  </div>
322
419
  </div>
323
420
  </template>
@@ -22,7 +22,7 @@ export type TableHeaderSort = typeof validators.sort[number]
22
22
  export type TableHeaderAlign = typeof validators.textAlign[number]
23
23
  export type TableHeaderProps = ExtractPublicPropTypes<typeof tableHeaderProps>
24
24
 
25
- type InternalClasses = 'th' | 'sortIcon'
25
+ type InternalClasses = 'th' | 'sortIcon' | 'header'
26
26
  export interface TableHeaderTheme extends ThemeComponent<TableHeaderProps, InternalClasses> {}
27
27
 
28
28
  export default { name: 'XTableHeader', validators }
@@ -40,7 +40,7 @@ const { styles, classes, className } = useTheme('TableHeader', {}, props)
40
40
 
41
41
  <template>
42
42
  <th :style="styles" :class="[className, classes.th, 'group/th']">
43
- <div class="flex items-center gap-1 select-none">
43
+ <div :class="classes.header">
44
44
  <slot></slot>
45
45
 
46
46
  <x-toggle-tip v-if="tooltip" :content="tooltip"/>
@@ -51,7 +51,7 @@ const { styles, classes, className } = useTheme('TableHeader', {}, props)
51
51
  :class="[
52
52
  classes.sortIcon,
53
53
  [sort && [1, -1].includes(sort) ? '' : 'invisible group-hover/th:visible'],
54
- [sort !== -1 && sort !== 1 ? 'text-secondary-400' : 'text-primary-700']
54
+ [sort !== -1 && sort !== 1 ? 'text-secondary-400 dark:text-secondary-500' : 'text-primary-700 dark:text-primary-400']
55
55
  ]"
56
56
  width="24"
57
57
  height="24"
@@ -7,6 +7,7 @@ const tableRowProps = {
7
7
  pointer: Boolean,
8
8
  striped: Boolean,
9
9
  selected: Boolean,
10
+ singleSelect: Boolean,
10
11
  verticalAlign: {
11
12
  type: String as PropType<'baseline' | 'bottom' | 'middle' | 'text-bottom' | 'text-top' | 'top'>,
12
13
  default: 'top',
@@ -7,10 +7,16 @@ const theme: TableHeaderTheme = {
7
7
 
8
8
  if (props.sortable) classes.push('cursor-pointer hover:text-secondary-800 dark:hover:text-secondary-300 transition-colors duration-150 ease-in-out')
9
9
 
10
- if (props.textAlign === 'left') classes.push('text-left')
11
- if (props.textAlign === 'right') classes.push('text-right')
12
- if (props.textAlign === 'center') classes.push('text-center')
13
- if (props.textAlign === 'justify') classes.push('text-justify')
10
+ return classes
11
+ },
12
+
13
+ header: ({ props }) => {
14
+ const classes = ['flex items-center gap-1 select-none']
15
+
16
+ if (props.textAlign === 'left') classes.push('justify-start')
17
+ if (props.textAlign === 'right') classes.push('justify-end')
18
+ if (props.textAlign === 'center') classes.push('justify-center')
19
+ if (props.textAlign === 'justify') classes.push('justify-center')
14
20
 
15
21
  return classes
16
22
  },
@@ -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 { ComponentPublicInstance } from 'vue'
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
- let focusable: HTMLElement[] = []
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
- async function initFocusTrap(targetRef: MaybeRef<HTMLElement | ComponentPublicInstance | null>) {
14
- targetRef = unref(targetRef)
14
+ function getEl(target: HTMLElement | ComponentPublicInstance | null): HTMLElement | null {
15
+ if (!target) return null
15
16
 
16
- if (!targetRef) return
17
+ return (target as ComponentPublicInstance).$el
18
+ ? (target as ComponentPublicInstance).$el as HTMLElement
19
+ : target as HTMLElement
20
+ }
17
21
 
18
- await nextTick()
22
+ function getFocusableElements(target: HTMLElement | ComponentPublicInstance | null) {
23
+ const el = getEl(target)
19
24
 
20
- getFocusableElements(targetRef)
25
+ if (!el) return
26
+ const elements = el.querySelectorAll(focusableQuery)
21
27
 
22
- if (firstFocusableEl) firstFocusableEl.focus()
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
- document.addEventListener('keydown', handleKeydown)
25
- observer = new MutationObserver(() => getFocusableElements(targetRef))
33
+ const handleKeydown = (event: KeyboardEvent) => {
34
+ if (event.key !== 'Tab' || focusable.value.length === 0) return
26
35
 
27
- if ((targetRef as ComponentPublicInstance).$el) observer.observe((targetRef as ComponentPublicInstance).$el as Node, { childList: true, subtree: true })
28
- else observer.observe(targetRef as Node, { childList: true, subtree: true })
29
- }
36
+ const isShiftPressed = event.shiftKey
37
+ const currentEl = document.activeElement as HTMLElement | null
30
38
 
31
- function getFocusableElements(targetRef: MaybeRef<HTMLElement | ComponentPublicInstance | null>) {
32
- if (targetRef === null) return
39
+ const firstEl = firstFocusableEl
40
+ const lastEl = lastFocusableEl
33
41
 
34
- let elements
42
+ if (!currentEl) {
43
+ event.preventDefault()
44
+ firstEl?.focus()
35
45
 
36
- if ((targetRef as ComponentPublicInstance).$el) elements = (targetRef as ComponentPublicInstance)?.$el.querySelectorAll(focusableQuery)
37
- else (targetRef as HTMLElement).querySelectorAll(focusableQuery)
46
+ return
47
+ }
38
48
 
39
- focusable = Array.from(elements || []) as HTMLElement[]
40
- firstFocusableEl = focusable[0] || null
41
- lastFocusableEl = focusable[focusable.length - 1] || null
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
- const handleKeydown = (event: KeyboardEvent) => {
45
- if (event.key === 'Tab') {
46
- const isShiftPressed = event.shiftKey
47
- const currentEl = document.activeElement as HTMLElement | null
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
- if (!currentEl) {
50
- event.preventDefault()
51
- focusable[0]?.focus()
64
+ // Clean up previous trap if any
65
+ clearFocusTrap()
66
+
67
+ prevActiveElement = document.activeElement as HTMLElement
52
68
 
53
- return
54
- }
69
+ currentTarget = unref(targetRef)
70
+ if (!currentTarget) return
55
71
 
56
- const firstEl = focusable[0]
57
- const lastEl = focusable[focusable.length - 1]
72
+ await nextTick()
73
+ getFocusableElements(currentTarget)
58
74
 
59
- if (!isShiftPressed && currentEl === lastEl) {
60
- event.preventDefault()
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
- const clearFocusTrap = () => {
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.13.1'
1
+ export default '1.14.0'