frappe-ui 1.0.0-beta.72 → 1.0.0-beta.73
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/package.json +1 -1
- package/src/components/DatePicker/DatePicker.vue +3 -1
- package/src/components/DatePicker/DateRangePicker.vue +3 -1
- package/src/components/DatePicker/DateTimePicker.vue +3 -1
- package/src/components/MultiSelect/MultiSelect.api.md +1 -1
- package/src/components/MultiSelect/MultiSelect.md +3 -1
- package/src/components/MultiSelect/types.ts +5 -1
- package/src/components/TabButtons/TabButtons.md +27 -0
- package/src/components/TabButtons/TabButtons.vue +26 -3
- package/src/components/TabButtons/index.ts +1 -0
- package/src/components/TabButtons/types.ts +10 -0
- package/src/components/Tabs/Tabs.vue +35 -0
- package/src/components/TimePicker/TimePicker.vue +18 -4
- package/src/components/shared/picker/PickerShell.vue +35 -14
- package/src/molecules/list/List.vue +11 -4
- package/src/molecules/list/ListRows.vue +12 -6
package/package.json
CHANGED
|
@@ -114,7 +114,9 @@ defineSlots<DatePickerSlots>()
|
|
|
114
114
|
// ── Popover open state ───────────────────────────────────────────────────────
|
|
115
115
|
|
|
116
116
|
const shellRef = ref<PickerShellExposed | null>(null)
|
|
117
|
-
|
|
117
|
+
// Seeded from the prop, so a parent that mounts the picker with `open` already
|
|
118
|
+
// true gets an open panel. The watch below only sees later changes.
|
|
119
|
+
const isOpen = ref(props.open === true)
|
|
118
120
|
|
|
119
121
|
watch(
|
|
120
122
|
() => props.open,
|
|
@@ -122,7 +122,9 @@ defineSlots<DateRangePickerSlots>()
|
|
|
122
122
|
// ── Popover open state ───────────────────────────────────────────────────────
|
|
123
123
|
|
|
124
124
|
const shellRef = ref<PickerShellExposed | null>(null)
|
|
125
|
-
|
|
125
|
+
// Seeded from the prop, so a parent that mounts the picker with `open` already
|
|
126
|
+
// true gets an open panel. The watch below only sees later changes.
|
|
127
|
+
const isOpen = ref(props.open === true)
|
|
126
128
|
|
|
127
129
|
watch(
|
|
128
130
|
() => props.open,
|
|
@@ -134,7 +134,9 @@ defineSlots<DateTimePickerSlots>()
|
|
|
134
134
|
// ── Popover open state ───────────────────────────────────────────────────────
|
|
135
135
|
|
|
136
136
|
const shellRef = ref<PickerShellExposed | null>(null)
|
|
137
|
-
|
|
137
|
+
// Seeded from the prop, so a parent that mounts the picker with `open` already
|
|
138
|
+
// true gets an open panel. The watch below only sees later changes.
|
|
139
|
+
const isOpen = ref(props.open === true)
|
|
138
140
|
|
|
139
141
|
watch(
|
|
140
142
|
() => props.open,
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
},
|
|
71
71
|
{
|
|
72
72
|
name: 'loading',
|
|
73
|
-
description: '
|
|
73
|
+
description: 'Marks a fetch as in flight. With the search row showing, a spinner appears\nin it and the options you passed stay selectable; under `hide-search` a\nloading row replaces the results instead.',
|
|
74
74
|
required: false,
|
|
75
75
|
type: 'boolean',
|
|
76
76
|
default: 'false'
|
|
@@ -37,7 +37,9 @@ Use `#search-prefix` and `#search-suffix` to add content around the popover's se
|
|
|
37
37
|
<ComponentPreview name="MultiSelect-SearchSlots" />
|
|
38
38
|
|
|
39
39
|
## Server Search
|
|
40
|
-
Fetch options from a server as the user types. Bind `v-model:query`, debounce the request, and feed the results back into `:options`.
|
|
40
|
+
Fetch options from a server as the user types. Bind `v-model:query`, debounce the request, and feed the results back into `:options`. Four things to watch for: pass `:filterable="false"` so the client doesn't substring-filter what the server already matched, drop stale responses with a request id so a slower earlier query can't overwrite the latest results, merge currently-selected items into the options array so chips stay resolvable after the query narrows the list, and clear the query yourself when the popover opens — see the note below on who owns it.
|
|
41
|
+
|
|
42
|
+
The `:loading` prop shows a spinner in the search row and leaves the options you passed rendering and selectable, so the user can go on picking from the previous results while the next ones arrive. Under `hide-search` there is no search row to put the spinner in, so a loading row replaces the results instead. Either way the empty state is suppressed while loading, so an in-flight fetch never reads as "No results". The popover carries a bare `data-loading` attribute while the fetch is open, so you can style the wait yourself.
|
|
41
43
|
|
|
42
44
|
<ComponentPreview name="MultiSelect-AsyncOptions" />
|
|
43
45
|
|
|
@@ -90,7 +90,11 @@ export interface MultiSelectProps extends InputLabelingProps {
|
|
|
90
90
|
/** Hides the in-popover search input. */
|
|
91
91
|
hideSearch?: boolean
|
|
92
92
|
|
|
93
|
-
/**
|
|
93
|
+
/**
|
|
94
|
+
* Marks a fetch as in flight. With the search row showing, a spinner appears
|
|
95
|
+
* in it and the options you passed stay selectable; under `hide-search` a
|
|
96
|
+
* loading row replaces the results instead.
|
|
97
|
+
*/
|
|
94
98
|
loading?: boolean
|
|
95
99
|
|
|
96
100
|
/**
|
|
@@ -64,6 +64,33 @@ items.
|
|
|
64
64
|
|
|
65
65
|
<ComponentPreview name="TabButtons-PrefixSuffix" />
|
|
66
66
|
|
|
67
|
+
## Template ref
|
|
68
|
+
|
|
69
|
+
`focus()` is the one method every focusable control in the library exposes. It
|
|
70
|
+
moves focus to the selected option, or to the first enabled one when nothing is
|
|
71
|
+
selected — the same tab a `Tab` press reaches, since the group is one tabstop.
|
|
72
|
+
It takes the native `FocusOptions`, so `focus({ preventScroll: true })` moves
|
|
73
|
+
focus without scrolling the tab into view.
|
|
74
|
+
|
|
75
|
+
Disabled options are skipped. A group with no options, or with every option
|
|
76
|
+
disabled, has nothing to focus and the call does nothing.
|
|
77
|
+
|
|
78
|
+
```vue
|
|
79
|
+
<script setup lang="ts">
|
|
80
|
+
import { useTemplateRef } from 'vue'
|
|
81
|
+
|
|
82
|
+
const view = useTemplateRef('view')
|
|
83
|
+
|
|
84
|
+
function reset() {
|
|
85
|
+
view.value?.focus()
|
|
86
|
+
}
|
|
87
|
+
</script>
|
|
88
|
+
|
|
89
|
+
<template>
|
|
90
|
+
<TabButtons ref="view" v-model="value" :options="options" />
|
|
91
|
+
</template>
|
|
92
|
+
```
|
|
93
|
+
|
|
67
94
|
## Styling a single tab
|
|
68
95
|
|
|
69
96
|
Each tab exposes data-attribute hooks for styling:
|
|
@@ -26,6 +26,7 @@ import type { BrowserTabBase } from '../shared/tabs/pillTypes'
|
|
|
26
26
|
import type {
|
|
27
27
|
TabButton,
|
|
28
28
|
TabButtonsEmits,
|
|
29
|
+
TabButtonsExposed,
|
|
29
30
|
TabButtonsProps,
|
|
30
31
|
TabButtonsSlots,
|
|
31
32
|
TabButtonValue,
|
|
@@ -147,15 +148,19 @@ const indicatorRect = ref<{
|
|
|
147
148
|
// slides in on mount.
|
|
148
149
|
const indicatorAnimated = ref(false)
|
|
149
150
|
|
|
151
|
+
// The rendered tab itself — the `<button>`, `<a href>` or `<RouterLink>` that
|
|
152
|
+
// takes focus and that the indicator measures. Not the track around it, and
|
|
153
|
+
// not the `Pill` inside it.
|
|
154
|
+
const TAB_BUTTON = '[data-slot="tab-button"]'
|
|
155
|
+
const ACTIVE_TAB_BUTTON = `${TAB_BUTTON}[data-state="active"]`
|
|
156
|
+
|
|
150
157
|
function measureIndicator() {
|
|
151
158
|
const track = trackRef.value
|
|
152
159
|
if (!track || !hasIndicator.value) {
|
|
153
160
|
indicatorRect.value = null
|
|
154
161
|
return
|
|
155
162
|
}
|
|
156
|
-
const checked = track.querySelector<HTMLElement>(
|
|
157
|
-
'[data-slot="tab-button"][data-state="active"]',
|
|
158
|
-
)
|
|
163
|
+
const checked = track.querySelector<HTMLElement>(ACTIVE_TAB_BUTTON)
|
|
159
164
|
if (!checked) {
|
|
160
165
|
indicatorRect.value = null
|
|
161
166
|
return
|
|
@@ -306,6 +311,24 @@ function tabElementProps(button: (typeof resolvedButtons.value)[number]) {
|
|
|
306
311
|
}
|
|
307
312
|
return { type: 'button' as const, disabled: button.disabled }
|
|
308
313
|
}
|
|
314
|
+
|
|
315
|
+
// INP-Q5: the group is one tabstop, so focus goes to the selected option, or
|
|
316
|
+
// to the first enabled one when nothing is selected — where a Tab press lands.
|
|
317
|
+
// A disabled option is skipped whichever form it renders as: `tabElement`
|
|
318
|
+
// gives a disabled `route`/`href` option a real disabled `<button>`, which
|
|
319
|
+
// cannot take focus at all. With nothing left to focus the call does nothing.
|
|
320
|
+
defineExpose<TabButtonsExposed>({
|
|
321
|
+
focus: (options?: FocusOptions) => {
|
|
322
|
+
const track = trackRef.value
|
|
323
|
+
if (!track) return
|
|
324
|
+
const target =
|
|
325
|
+
track.querySelector<HTMLElement>(
|
|
326
|
+
`${ACTIVE_TAB_BUTTON}:not([data-disabled])`,
|
|
327
|
+
) ??
|
|
328
|
+
track.querySelector<HTMLElement>(`${TAB_BUTTON}:not([data-disabled])`)
|
|
329
|
+
target?.focus(options)
|
|
330
|
+
},
|
|
331
|
+
})
|
|
309
332
|
</script>
|
|
310
333
|
|
|
311
334
|
<template>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { InputExposed } from '../../composables/inputTypes'
|
|
1
2
|
import type { RouteDestination } from '../shared/route'
|
|
2
3
|
import type {
|
|
3
4
|
TabIcon,
|
|
@@ -48,6 +49,15 @@ export interface TabButtonsEmits {
|
|
|
48
49
|
'update:modelValue': [value: TabButtonValue]
|
|
49
50
|
}
|
|
50
51
|
|
|
52
|
+
/**
|
|
53
|
+
* What a `<TabButtons>` template ref hands back. `focus()` moves focus to the
|
|
54
|
+
* selected option, or to the first enabled one when nothing is selected —
|
|
55
|
+
* the same element a `Tab` press reaches, since the group is one tabstop.
|
|
56
|
+
* A group with no options, or with every option disabled, has nothing to
|
|
57
|
+
* focus and the call does nothing.
|
|
58
|
+
*/
|
|
59
|
+
export interface TabButtonsExposed extends InputExposed {}
|
|
60
|
+
|
|
51
61
|
export interface TabButtonsSlots {
|
|
52
62
|
/** Slot before the tab button label. */
|
|
53
63
|
prefix?: (props: {
|
|
@@ -2,12 +2,15 @@
|
|
|
2
2
|
import {
|
|
3
3
|
computed,
|
|
4
4
|
getCurrentInstance,
|
|
5
|
+
inject,
|
|
6
|
+
onUnmounted,
|
|
5
7
|
provide,
|
|
6
8
|
ref,
|
|
7
9
|
shallowRef,
|
|
8
10
|
watch,
|
|
9
11
|
} from 'vue'
|
|
10
12
|
import { TabsRoot } from 'reka-ui'
|
|
13
|
+
import { routerKey } from 'vue-router'
|
|
11
14
|
import TabList from './TabList.vue'
|
|
12
15
|
import TabTrigger from './TabTrigger.vue'
|
|
13
16
|
import TabPanel from './TabPanel.vue'
|
|
@@ -212,6 +215,38 @@ function triggerFor(value: TabValue) {
|
|
|
212
215
|
return triggers.value.find((t) => t.value() === value)
|
|
213
216
|
}
|
|
214
217
|
|
|
218
|
+
// The override lasts until the route leaves the page the user clicked from.
|
|
219
|
+
// Two things end it.
|
|
220
|
+
//
|
|
221
|
+
// A navigation that lands on a different path. That covers `/inbox` →
|
|
222
|
+
// `/sent`, and `/inbox` → `/inbox/42` too, where the same tab stays matched
|
|
223
|
+
// but the URL is a page the clicked tab does not stand for.
|
|
224
|
+
//
|
|
225
|
+
// A navigation that did not land — aborted by a guard, cancelled by a newer
|
|
226
|
+
// one, or a duplicate of the current URL — leaves `failure` set and changed
|
|
227
|
+
// nothing the user can see, so it must not throw away their click.
|
|
228
|
+
//
|
|
229
|
+
// `inject` rather than `useRouter`: Tabs is used with no router at all far
|
|
230
|
+
// more often than with one, and `useRouter` warns when the injection is
|
|
231
|
+
// missing.
|
|
232
|
+
const router = inject(routerKey, null)
|
|
233
|
+
if (router) {
|
|
234
|
+
const stopRouteReset = router.afterEach((to, from, failure) => {
|
|
235
|
+
if (failure) return
|
|
236
|
+
if (to.path !== from.path) routeOverride.value = null
|
|
237
|
+
})
|
|
238
|
+
onUnmounted(stopRouteReset)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// And a change in which tab the URL matches, whatever moved it. That includes
|
|
242
|
+
// a change with no navigation behind it, which the hook above cannot see — a
|
|
243
|
+
// trigger's `route` prop changing, or a routed trigger mounting that matches
|
|
244
|
+
// the current URL. Both have always ended the click.
|
|
245
|
+
//
|
|
246
|
+
// What neither rule catches keeps the click: a query-only or hash-only write
|
|
247
|
+
// that leaves the same tab matched. A panel that keeps its own state in the
|
|
248
|
+
// URL — a page number, a filter — writes exactly that, and clearing there
|
|
249
|
+
// would throw the user out of the panel they are standing in.
|
|
215
250
|
watch(routeSelected, () => {
|
|
216
251
|
routeOverride.value = null
|
|
217
252
|
})
|
|
@@ -97,7 +97,7 @@
|
|
|
97
97
|
</template>
|
|
98
98
|
|
|
99
99
|
<script setup lang="ts">
|
|
100
|
-
import { computed, nextTick, ref, watch } from 'vue'
|
|
100
|
+
import { computed, nextTick, onMounted, ref, watch } from 'vue'
|
|
101
101
|
import {
|
|
102
102
|
PopoverAnchor,
|
|
103
103
|
PopoverContent,
|
|
@@ -195,7 +195,9 @@ function onInteractOutside(event: Event) {
|
|
|
195
195
|
}
|
|
196
196
|
const uid = Math.random().toString(36).slice(2, 9)
|
|
197
197
|
|
|
198
|
-
|
|
198
|
+
// Seeded from the prop, so a parent that mounts the picker with `open` already
|
|
199
|
+
// true gets an open panel. The watch below only sees later changes.
|
|
200
|
+
const isOpen = ref(props.open === true)
|
|
199
201
|
|
|
200
202
|
// Canonical 24-hour value (`HH:mm` or `HH:mm:ss`) — the source of truth.
|
|
201
203
|
const canonicalValue = ref<string>(
|
|
@@ -522,16 +524,28 @@ function scrollOnOpen() {
|
|
|
522
524
|
})
|
|
523
525
|
}
|
|
524
526
|
|
|
527
|
+
function onOpened() {
|
|
528
|
+
highlightIndex.value = -1
|
|
529
|
+
scrollOnOpen()
|
|
530
|
+
}
|
|
531
|
+
|
|
525
532
|
watch(isOpen, (open) => {
|
|
526
533
|
emit('update:open', open)
|
|
527
534
|
if (open) {
|
|
528
|
-
|
|
529
|
-
scrollOnOpen()
|
|
535
|
+
onOpened()
|
|
530
536
|
} else {
|
|
531
537
|
isTyping.value = false
|
|
532
538
|
}
|
|
533
539
|
})
|
|
534
540
|
|
|
541
|
+
// A picker mounted with `open` already true never crosses the watch above, so
|
|
542
|
+
// the open-time work — the highlight seed and the scroll to the current value —
|
|
543
|
+
// runs here instead. No `update:open`: the parent is the one that asked for an
|
|
544
|
+
// open panel.
|
|
545
|
+
onMounted(() => {
|
|
546
|
+
if (isOpen.value) onOpened()
|
|
547
|
+
})
|
|
548
|
+
|
|
535
549
|
watch(
|
|
536
550
|
() => props.open,
|
|
537
551
|
(val) => {
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
</template>
|
|
62
62
|
|
|
63
63
|
<script setup lang="ts">
|
|
64
|
-
import { computed, nextTick, ref, watch } from 'vue'
|
|
64
|
+
import { computed, nextTick, onMounted, ref, watch } from 'vue'
|
|
65
65
|
import Popover from '../../Popover/Popover.vue'
|
|
66
66
|
import { TextInput } from '../../TextInput'
|
|
67
67
|
import { useReactiveSlots } from '../../../composables/useReactiveSlots'
|
|
@@ -92,9 +92,12 @@ const emit = defineEmits<{
|
|
|
92
92
|
/** Signal that the parent should move keyboard focus into the popover
|
|
93
93
|
* content (e.g. into a calendar grid). Fired:
|
|
94
94
|
* - When the user presses ↓ on the trigger.
|
|
95
|
-
* - When the popover
|
|
96
|
-
* into, so focus should jump straight into the
|
|
97
|
-
*
|
|
95
|
+
* - When a gesture opens the popover and the trigger is a custom one (no
|
|
96
|
+
* `TextInput` to type into, so focus should jump straight into the
|
|
97
|
+
* content).
|
|
98
|
+
* The default trigger keeps focus on the `TextInput` for typing. A shell
|
|
99
|
+
* that is already open on its first render never fires this: nobody asked
|
|
100
|
+
* for focus to move. */
|
|
98
101
|
(e: 'requestFocus'): void
|
|
99
102
|
}>()
|
|
100
103
|
|
|
@@ -174,19 +177,27 @@ const triggerSlotProps = computed<PickerShellTriggerSlotProps>(() => ({
|
|
|
174
177
|
|
|
175
178
|
const hasCustomTrigger = computed(() => !!slots.trigger)
|
|
176
179
|
|
|
180
|
+
// `moveFocus` is false on the mount path. Every other route into `onOpened`
|
|
181
|
+
// follows a gesture, and a panel the user just opened should take focus; a
|
|
182
|
+
// panel that is simply part of the first render follows no gesture, so moving
|
|
183
|
+
// focus would take it from wherever the page put it.
|
|
184
|
+
function onOpened(moveFocus = true) {
|
|
185
|
+
emit('open')
|
|
186
|
+
nextTick(() => {
|
|
187
|
+
panelId.value = popoverRef.value?.contentEl?.id || undefined
|
|
188
|
+
})
|
|
189
|
+
// Custom triggers (e.g. a button) have no typing context — once the
|
|
190
|
+
// popover is open the user wants to interact with the content. Signal
|
|
191
|
+
// the parent to move focus there. The default `TextInput` trigger
|
|
192
|
+
// keeps its focus so the user can type, and only the explicit ↓
|
|
193
|
+
// handler emits `requestFocus`.
|
|
194
|
+
if (moveFocus && hasCustomTrigger.value) emit('requestFocus')
|
|
195
|
+
}
|
|
196
|
+
|
|
177
197
|
watch(open, (val, prev) => {
|
|
178
198
|
if (val === prev) return
|
|
179
199
|
if (val) {
|
|
180
|
-
|
|
181
|
-
nextTick(() => {
|
|
182
|
-
panelId.value = popoverRef.value?.contentEl?.id || undefined
|
|
183
|
-
})
|
|
184
|
-
// Custom triggers (e.g. a button) have no typing context — once the
|
|
185
|
-
// popover is open the user wants to interact with the content. Signal
|
|
186
|
-
// the parent to move focus there. The default `TextInput` trigger
|
|
187
|
-
// keeps its focus so the user can type, and only the explicit ↓
|
|
188
|
-
// handler emits `requestFocus`.
|
|
189
|
-
if (hasCustomTrigger.value) emit('requestFocus')
|
|
200
|
+
onOpened()
|
|
190
201
|
} else {
|
|
191
202
|
// Restore focus to the trigger input if the popover content had focus
|
|
192
203
|
// (Esc, date selection in auto-close mode). Click-outside leaves focus
|
|
@@ -202,6 +213,16 @@ watch(open, (val, prev) => {
|
|
|
202
213
|
}
|
|
203
214
|
})
|
|
204
215
|
|
|
216
|
+
// A shell mounted with `open` already true never crosses the watch above, so
|
|
217
|
+
// the open-time work — the panel id behind `aria-controls`, the parent's draft
|
|
218
|
+
// initialization — runs here instead. No `update:open`: the parent is the one
|
|
219
|
+
// that asked for an open panel. Focus stays put: the default trigger does not
|
|
220
|
+
// take focus at mount either, because `:auto-focus="false"` cancels reka's
|
|
221
|
+
// mount autofocus, and a page has no way to know the panel was coming.
|
|
222
|
+
onMounted(() => {
|
|
223
|
+
if (open.value) onOpened(false)
|
|
224
|
+
})
|
|
225
|
+
|
|
205
226
|
defineExpose<PickerShellExposed>({
|
|
206
227
|
open: () => setOpen(true),
|
|
207
228
|
close,
|
|
@@ -129,8 +129,13 @@ if (import.meta.env.DEV) {
|
|
|
129
129
|
)
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
+
// Membership is asked once per rendered row and once per universe entry on
|
|
133
|
+
// every select-all recompute, so scanning the `selection` array each time costs
|
|
134
|
+
// rows x selection. The Set is rebuilt only when `selection` itself changes.
|
|
135
|
+
const selectionSet = computed(() => new Set(selection.value))
|
|
136
|
+
|
|
132
137
|
function isSelected(value: string) {
|
|
133
|
-
return
|
|
138
|
+
return selectionSet.value.has(value)
|
|
134
139
|
}
|
|
135
140
|
|
|
136
141
|
function toggleSelection(value: string) {
|
|
@@ -157,9 +162,11 @@ function setAllValues(values: string[]) {
|
|
|
157
162
|
const selectAllState = computed<'none' | 'some' | 'all'>(() => {
|
|
158
163
|
const universe = allValues.value
|
|
159
164
|
if (!universe.length) return 'none'
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
)
|
|
165
|
+
const selected = selectionSet.value
|
|
166
|
+
let selectedCount = 0
|
|
167
|
+
for (const value of universe) {
|
|
168
|
+
if (selected.has(value)) selectedCount++
|
|
169
|
+
}
|
|
163
170
|
if (selectedCount === 0) return 'none'
|
|
164
171
|
return selectedCount === universe.length ? 'all' : 'some'
|
|
165
172
|
})
|
|
@@ -90,13 +90,19 @@ const { rows, wrapperProps, anchor } = useVirtualRows(
|
|
|
90
90
|
// every row's value — even the virtualized ones that aren't mounted. Uses the
|
|
91
91
|
// same `getItemValue` as the render `:key` and scoped `value` slot prop, so
|
|
92
92
|
// row identity has one source.
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
93
|
+
//
|
|
94
|
+
// Deriving it through a computed keeps the two in step: the watcher then tracks
|
|
95
|
+
// what identity is actually made of — the array's entries, the active `rowKey`,
|
|
96
|
+
// and the item fields that key reads — so a push, a splice, a swapped entry, a
|
|
97
|
+
// renamed id or a different `rowKey` all move the universe. Watching
|
|
98
|
+
// `props.items` alone only sees the array swapped for another one, and an item
|
|
99
|
+
// field nothing reads for identity stays untracked: this is not a deep watch.
|
|
100
|
+
const itemValues = computed(() =>
|
|
101
|
+
props.items.map((item, i) => getItemValue(item, i)),
|
|
99
102
|
)
|
|
103
|
+
watch(itemValues, (values) => context?.setAllValues(values), {
|
|
104
|
+
immediate: true,
|
|
105
|
+
})
|
|
100
106
|
onBeforeUnmount(() => context?.setAllValues([]))
|
|
101
107
|
|
|
102
108
|
function getItemValue(item: T, index: number) {
|