@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.
Files changed (59) 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 +68 -0
  20. package/docs/pages/component/table/usage.vue +1 -4
  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/menu/MenuItem.vue.js +1 -1
  29. package/lib/components/menu/MenuItem.vue2.js +82 -84
  30. package/lib/components/radio/theme/Radio.base.theme.js +24 -24
  31. package/lib/components/select/Select.vue.js +121 -112
  32. package/lib/components/table/Table.vue.d.ts +62 -8
  33. package/lib/components/table/Table.vue.js +194 -139
  34. package/lib/components/table/TableHeader.vue.d.ts +5 -5
  35. package/lib/components/table/TableHeader.vue.js +37 -34
  36. package/lib/components/table/TableRow.vue.d.ts +4 -0
  37. package/lib/components/table/TableRow.vue.js +3 -2
  38. package/lib/components/table/theme/TableHeader.base.theme.js +9 -9
  39. package/lib/components/table/theme/TableHeader.carbon.theme.js +1 -1
  40. package/lib/components/table/theme/TableRow.base.theme.js +3 -3
  41. package/lib/composables/useFocusTrap.d.ts +9 -4
  42. package/lib/composables/useFocusTrap.js +42 -27
  43. package/lib/index.js +1 -1
  44. package/lib/index.umd.js +4 -4
  45. package/lib/version.d.ts +1 -1
  46. package/lib/version.js +1 -1
  47. package/package.json +1 -1
  48. package/src/components/button/theme/Button.base.theme.ts +1 -1
  49. package/src/components/menu/MenuItem.vue +1 -0
  50. package/src/components/radio/theme/Radio.base.theme.ts +1 -1
  51. package/src/components/select/Select.vue +20 -5
  52. package/src/components/table/Table.vue +113 -15
  53. package/src/components/table/TableHeader.vue +7 -5
  54. package/src/components/table/TableRow.vue +1 -0
  55. package/src/components/table/theme/TableHeader.base.theme.ts +4 -3
  56. package/src/components/table/theme/TableHeader.carbon.theme.ts +0 -1
  57. package/src/components/table/theme/TableRow.base.theme.ts +2 -2
  58. package/src/composables/useFocusTrap.ts +73 -42
  59. package/src/version.ts +1 -1
package/lib/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- declare const _default: "1.13.2";
1
+ declare const _default: "1.14.1";
2
2
  export default _default;
package/lib/version.js CHANGED
@@ -1,4 +1,4 @@
1
- const e = "1.13.2";
1
+ const e = "1.14.1";
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.2",
3
+ "version": "1.14.1",
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')
@@ -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 > 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 = {
@@ -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 ?? index">
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="hasSelected ? selected === (keyProp ? (item.data as Record<string, unknown>)[keyProp] : item.index) : undefined"
267
- @click="$emit('click-row', item.data, item.index)"
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', 'justify'] as const,
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
- <slot></slot>
45
-
46
- <x-toggle-tip v-if="tooltip" :content="tooltip"/>
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"
@@ -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',
@@ -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') classes.push('justify-end')
18
- if (props.textAlign === 'center') classes.push('justify-center')
19
- if (props.textAlign === 'justify') classes.push('justify-center')
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 { 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.2'
1
+ export default '1.14.1'