reka-ui 2.9.8 → 2.9.9
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/dist/Autocomplete/AutocompleteInput.cjs +12 -16
- package/dist/Autocomplete/AutocompleteInput.cjs.map +1 -1
- package/dist/Autocomplete/AutocompleteInput.js +13 -17
- package/dist/Autocomplete/AutocompleteInput.js.map +1 -1
- package/dist/ColorField/ColorFieldInput.cjs +10 -2
- package/dist/ColorField/ColorFieldInput.cjs.map +1 -1
- package/dist/ColorField/ColorFieldInput.js +10 -2
- package/dist/ColorField/ColorFieldInput.js.map +1 -1
- package/dist/Combobox/ComboboxInput.cjs +27 -9
- package/dist/Combobox/ComboboxInput.cjs.map +1 -1
- package/dist/Combobox/ComboboxInput.js +28 -10
- package/dist/Combobox/ComboboxInput.js.map +1 -1
- package/dist/DateField/DateFieldInput.cjs +4 -1
- package/dist/DateField/DateFieldInput.cjs.map +1 -1
- package/dist/DateField/DateFieldInput.js +4 -1
- package/dist/DateField/DateFieldInput.js.map +1 -1
- package/dist/DateField/DateFieldRoot.cjs +1 -0
- package/dist/DateField/DateFieldRoot.cjs.map +1 -1
- package/dist/DateField/DateFieldRoot.js +1 -0
- package/dist/DateField/DateFieldRoot.js.map +1 -1
- package/dist/DatePicker/DatePickerRoot.cjs +15 -1
- package/dist/DatePicker/DatePickerRoot.cjs.map +1 -1
- package/dist/DatePicker/DatePickerRoot.js +15 -1
- package/dist/DatePicker/DatePickerRoot.js.map +1 -1
- package/dist/DateRangeField/DateRangeFieldInput.cjs +4 -1
- package/dist/DateRangeField/DateRangeFieldInput.cjs.map +1 -1
- package/dist/DateRangeField/DateRangeFieldInput.js +4 -1
- package/dist/DateRangeField/DateRangeFieldInput.js.map +1 -1
- package/dist/DateRangeField/DateRangeFieldRoot.cjs +1 -0
- package/dist/DateRangeField/DateRangeFieldRoot.cjs.map +1 -1
- package/dist/DateRangeField/DateRangeFieldRoot.js +1 -0
- package/dist/DateRangeField/DateRangeFieldRoot.js.map +1 -1
- package/dist/Dialog/DialogOverlayImpl.cjs +5 -1
- package/dist/Dialog/DialogOverlayImpl.cjs.map +1 -1
- package/dist/Dialog/DialogOverlayImpl.js +5 -1
- package/dist/Dialog/DialogOverlayImpl.js.map +1 -1
- package/dist/DropdownMenu/DropdownMenuFilter.cjs +19 -2
- package/dist/DropdownMenu/DropdownMenuFilter.cjs.map +1 -1
- package/dist/DropdownMenu/DropdownMenuFilter.js +19 -2
- package/dist/DropdownMenu/DropdownMenuFilter.js.map +1 -1
- package/dist/Listbox/ListboxFilter.cjs +29 -10
- package/dist/Listbox/ListboxFilter.cjs.map +1 -1
- package/dist/Listbox/ListboxFilter.js +30 -11
- package/dist/Listbox/ListboxFilter.js.map +1 -1
- package/dist/Listbox/ListboxRoot.cjs +12 -6
- package/dist/Listbox/ListboxRoot.cjs.map +1 -1
- package/dist/Listbox/ListboxRoot.js +12 -6
- package/dist/Listbox/ListboxRoot.js.map +1 -1
- package/dist/NavigationMenu/NavigationMenuRoot.cjs +16 -3
- package/dist/NavigationMenu/NavigationMenuRoot.cjs.map +1 -1
- package/dist/NavigationMenu/NavigationMenuRoot.js +16 -3
- package/dist/NavigationMenu/NavigationMenuRoot.js.map +1 -1
- package/dist/NumberField/NumberFieldInput.cjs +46 -13
- package/dist/NumberField/NumberFieldInput.cjs.map +1 -1
- package/dist/NumberField/NumberFieldInput.js +47 -14
- package/dist/NumberField/NumberFieldInput.js.map +1 -1
- package/dist/PinInput/PinInputInput.cjs +37 -2
- package/dist/PinInput/PinInputInput.cjs.map +1 -1
- package/dist/PinInput/PinInputInput.js +37 -2
- package/dist/PinInput/PinInputInput.js.map +1 -1
- package/dist/ScrollArea/ScrollAreaScrollbarX.cjs +3 -0
- package/dist/ScrollArea/ScrollAreaScrollbarX.cjs.map +1 -1
- package/dist/ScrollArea/ScrollAreaScrollbarX.js +4 -1
- package/dist/ScrollArea/ScrollAreaScrollbarX.js.map +1 -1
- package/dist/ScrollArea/ScrollAreaScrollbarY.cjs +3 -0
- package/dist/ScrollArea/ScrollAreaScrollbarY.cjs.map +1 -1
- package/dist/ScrollArea/ScrollAreaScrollbarY.js +4 -1
- package/dist/ScrollArea/ScrollAreaScrollbarY.js.map +1 -1
- package/dist/TagsInput/TagsInputInput.cjs +12 -13
- package/dist/TagsInput/TagsInputInput.cjs.map +1 -1
- package/dist/TagsInput/TagsInputInput.js +13 -14
- package/dist/TagsInput/TagsInputInput.js.map +1 -1
- package/dist/TagsInput/TagsInputRoot.cjs +1 -0
- package/dist/TagsInput/TagsInputRoot.cjs.map +1 -1
- package/dist/TagsInput/TagsInputRoot.js +1 -0
- package/dist/TagsInput/TagsInputRoot.js.map +1 -1
- package/dist/TimeField/TimeFieldInput.cjs +4 -1
- package/dist/TimeField/TimeFieldInput.cjs.map +1 -1
- package/dist/TimeField/TimeFieldInput.js +4 -1
- package/dist/TimeField/TimeFieldInput.js.map +1 -1
- package/dist/TimeField/TimeFieldRoot.cjs +1 -0
- package/dist/TimeField/TimeFieldRoot.cjs.map +1 -1
- package/dist/TimeField/TimeFieldRoot.js +1 -0
- package/dist/TimeField/TimeFieldRoot.js.map +1 -1
- package/dist/TimeRangeField/TimeRangeFieldInput.cjs +4 -1
- package/dist/TimeRangeField/TimeRangeFieldInput.cjs.map +1 -1
- package/dist/TimeRangeField/TimeRangeFieldInput.js +4 -1
- package/dist/TimeRangeField/TimeRangeFieldInput.js.map +1 -1
- package/dist/TimeRangeField/TimeRangeFieldRoot.cjs +1 -0
- package/dist/TimeRangeField/TimeRangeFieldRoot.cjs.map +1 -1
- package/dist/TimeRangeField/TimeRangeFieldRoot.js +1 -0
- package/dist/TimeRangeField/TimeRangeFieldRoot.js.map +1 -1
- package/dist/constant.d.cts.map +1 -1
- package/dist/date/useDateField.cjs +38 -0
- package/dist/date/useDateField.cjs.map +1 -1
- package/dist/date/useDateField.js +38 -0
- package/dist/date/useDateField.js.map +1 -1
- package/dist/index.cjs +1 -0
- package/dist/index.js +1 -0
- package/dist/index3.d.cts +24 -16
- package/dist/index3.d.cts.map +1 -1
- package/dist/index3.d.ts +13 -5
- package/dist/index3.d.ts.map +1 -1
- package/dist/index4.d.cts +657 -657
- package/dist/index4.d.cts.map +1 -1
- package/dist/index4.d.ts +668 -668
- package/dist/index4.d.ts.map +1 -1
- package/dist/internal.cjs +1 -0
- package/dist/internal.d.cts +2 -2
- package/dist/internal.d.cts.map +1 -1
- package/dist/internal.d.ts +2 -2
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +1 -0
- package/dist/shared/useComposing.cjs +30 -0
- package/dist/shared/useComposing.cjs.map +1 -0
- package/dist/shared/useComposing.js +24 -0
- package/dist/shared/useComposing.js.map +1 -0
- package/dist/shared.cjs +2 -0
- package/dist/shared.d.cts +2 -2
- package/dist/shared.d.ts +2 -2
- package/dist/shared.js +2 -1
- package/package.json +4 -4
- package/src/Autocomplete/AutocompleteInput.vue +13 -17
- package/src/ColorField/ColorFieldInput.vue +11 -0
- package/src/Combobox/ComboboxInput.vue +37 -7
- package/src/DateField/DateFieldInput.vue +6 -0
- package/src/DateField/DateFieldRoot.vue +3 -0
- package/src/DatePicker/DatePickerRoot.vue +18 -2
- package/src/DateRangeField/DateRangeFieldInput.vue +6 -0
- package/src/DateRangeField/DateRangeFieldRoot.vue +3 -0
- package/src/Dialog/DialogOverlayImpl.vue +1 -1
- package/src/DropdownMenu/DropdownMenuFilter.vue +20 -1
- package/src/Listbox/ListboxFilter.vue +39 -8
- package/src/Listbox/ListboxRoot.vue +17 -4
- package/src/NavigationMenu/NavigationMenuRoot.vue +19 -3
- package/src/NumberField/NumberFieldInput.vue +45 -8
- package/src/PinInput/PinInputInput.vue +44 -1
- package/src/ScrollArea/ScrollAreaScrollbarX.vue +6 -1
- package/src/ScrollArea/ScrollAreaScrollbarY.vue +6 -1
- package/src/TagsInput/TagsInputInput.vue +16 -14
- package/src/TagsInput/TagsInputRoot.vue +3 -0
- package/src/TimeField/TimeFieldInput.vue +6 -0
- package/src/TimeField/TimeFieldRoot.vue +3 -0
- package/src/TimeRangeField/TimeRangeFieldInput.vue +6 -0
- package/src/TimeRangeField/TimeRangeFieldRoot.vue +3 -0
- package/src/shared/date/useDateField.ts +75 -1
- package/src/shared/index.ts +1 -0
- package/src/shared/useComposing.ts +18 -0
|
@@ -324,7 +324,7 @@ function handleMultipleReplace(event: KeyboardEvent, targetEl: HTMLElement) {
|
|
|
324
324
|
}
|
|
325
325
|
}
|
|
326
326
|
|
|
327
|
-
async function highlightSelected(event?: Event) {
|
|
327
|
+
async function highlightSelected(event?: Event, scroll = true) {
|
|
328
328
|
// highlightSelected is called inside a watch with immediate set to true.
|
|
329
329
|
// This results in code execution during SSR.
|
|
330
330
|
// Ensure this code only runs in a browser environment, since it performs
|
|
@@ -339,18 +339,31 @@ async function highlightSelected(event?: Event) {
|
|
|
339
339
|
else {
|
|
340
340
|
const collection = getCollectionItem()
|
|
341
341
|
const item = collection.find(i => i.dataset.state === 'checked')
|
|
342
|
+
// On the initial (mount) highlight we only set the roving-tabindex target.
|
|
343
|
+
// Focusing/scrolling here would scroll the page to a Listbox the user never
|
|
344
|
+
// interacted with (e.g. one below the fold). Later highlights scroll as before.
|
|
345
|
+
const focus = scroll ? undefined : false
|
|
342
346
|
if (item)
|
|
343
|
-
changeHighlight(item)
|
|
347
|
+
changeHighlight(item, scroll, focus)
|
|
344
348
|
else if (collection.length)
|
|
345
|
-
changeHighlight(collection[0])
|
|
349
|
+
changeHighlight(collection[0], scroll, focus)
|
|
346
350
|
}
|
|
347
351
|
}
|
|
348
352
|
|
|
353
|
+
// `false` until the initial (mount) modelValue highlight has been queued.
|
|
354
|
+
// Flipped synchronously in the watcher so the "is this the mount highlight?"
|
|
355
|
+
// decision never depends on nextTick ordering, which differs between a client
|
|
356
|
+
// mount and SSR hydration. The intent travels with the call as an argument
|
|
357
|
+
// rather than via a shared flag released on a later tick.
|
|
358
|
+
let hasHighlightedOnMount = false
|
|
359
|
+
|
|
349
360
|
// watch for only programmatic changes
|
|
350
361
|
watch(modelValue, () => {
|
|
351
362
|
if (!isUserAction.value) {
|
|
363
|
+
const scroll = hasHighlightedOnMount
|
|
364
|
+
hasHighlightedOnMount = true
|
|
352
365
|
nextTick(() => {
|
|
353
|
-
highlightSelected()
|
|
366
|
+
highlightSelected(undefined, scroll)
|
|
354
367
|
})
|
|
355
368
|
}
|
|
356
369
|
}, { immediate: true, deep: true })
|
|
@@ -140,6 +140,7 @@ const { delayDuration, skipDelayDuration, dir: propDir, disableClickTrigger, dis
|
|
|
140
140
|
const dir = useDirection(propDir)
|
|
141
141
|
|
|
142
142
|
const isDelaySkipped = refAutoReset(false, skipDelayDuration)
|
|
143
|
+
const skipNextClose = ref(false)
|
|
143
144
|
const computedDelay = computed(() => {
|
|
144
145
|
const isOpen = modelValue.value !== ''
|
|
145
146
|
if (isOpen || isDelaySkipped.value)
|
|
@@ -150,8 +151,14 @@ const computedDelay = computed(() => {
|
|
|
150
151
|
const debouncedFn = useDebounceFn((val?: string) => {
|
|
151
152
|
// passing `undefined` meant to reset the debounce timer
|
|
152
153
|
if (typeof val === 'string') {
|
|
154
|
+
if (val === '' && skipNextClose.value) {
|
|
155
|
+
skipNextClose.value = false
|
|
156
|
+
return
|
|
157
|
+
}
|
|
153
158
|
previousValue.value = modelValue.value
|
|
154
159
|
modelValue.value = val
|
|
160
|
+
if (val === '')
|
|
161
|
+
isDelaySkipped.value = true
|
|
155
162
|
}
|
|
156
163
|
}, computedDelay)
|
|
157
164
|
|
|
@@ -188,18 +195,27 @@ provideNavigationMenuContext({
|
|
|
188
195
|
viewport.value = val
|
|
189
196
|
},
|
|
190
197
|
onTriggerEnter: (val) => {
|
|
191
|
-
|
|
198
|
+
if (modelValue.value !== '') {
|
|
199
|
+
skipNextClose.value = true
|
|
200
|
+
previousValue.value = modelValue.value
|
|
201
|
+
modelValue.value = val
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
debouncedFn(val)
|
|
205
|
+
}
|
|
192
206
|
},
|
|
193
207
|
onTriggerLeave: () => {
|
|
194
|
-
|
|
208
|
+
skipNextClose.value = false
|
|
195
209
|
debouncedFn('')
|
|
196
210
|
},
|
|
197
211
|
onContentEnter: () => {
|
|
198
212
|
debouncedFn()
|
|
199
213
|
},
|
|
200
214
|
onContentLeave: () => {
|
|
201
|
-
if (!props.disablePointerLeaveClose)
|
|
215
|
+
if (!props.disablePointerLeaveClose) {
|
|
216
|
+
skipNextClose.value = false
|
|
202
217
|
debouncedFn('')
|
|
218
|
+
}
|
|
203
219
|
},
|
|
204
220
|
onItemSelect: (val) => {
|
|
205
221
|
// When selecting item we trigger update immediately
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { PrimitiveProps } from '@/Primitive'
|
|
3
3
|
import { onMounted, ref, watch } from 'vue'
|
|
4
|
-
import { getActiveElement } from '@/shared'
|
|
4
|
+
import { getActiveElement, useComposing, useKbd } from '@/shared'
|
|
5
5
|
import { injectNumberFieldRootContext } from './NumberFieldRoot.vue'
|
|
6
6
|
|
|
7
7
|
export interface NumberFieldInputProps extends PrimitiveProps {
|
|
@@ -17,6 +17,46 @@ const props = withDefaults(defineProps<NumberFieldInputProps>(), {
|
|
|
17
17
|
|
|
18
18
|
const { primitiveElement, currentElement } = usePrimitiveElement()
|
|
19
19
|
const rootContext = injectNumberFieldRootContext()
|
|
20
|
+
const kbd = useKbd()
|
|
21
|
+
const { isComposing, handleCompositionStart, handleCompositionEnd } = useComposing()
|
|
22
|
+
|
|
23
|
+
function handleKeydown(event: KeyboardEvent) {
|
|
24
|
+
// Don't step/apply mid-composition, keys are used for IME candidate navigation and commit.
|
|
25
|
+
// `isComposing` stays true until the tick after `compositionend`, so the commit keydown
|
|
26
|
+
// (which can report `event.isComposing === false`) is still skipped.
|
|
27
|
+
if (isComposing.value || event.isComposing)
|
|
28
|
+
return
|
|
29
|
+
|
|
30
|
+
switch (event.key) {
|
|
31
|
+
case kbd.ARROW_UP:
|
|
32
|
+
event.preventDefault()
|
|
33
|
+
rootContext.handleIncrease()
|
|
34
|
+
break
|
|
35
|
+
case kbd.ARROW_DOWN:
|
|
36
|
+
event.preventDefault()
|
|
37
|
+
rootContext.handleDecrease()
|
|
38
|
+
break
|
|
39
|
+
case kbd.PAGE_UP:
|
|
40
|
+
event.preventDefault()
|
|
41
|
+
rootContext.handleIncrease(10)
|
|
42
|
+
break
|
|
43
|
+
case kbd.PAGE_DOWN:
|
|
44
|
+
event.preventDefault()
|
|
45
|
+
rootContext.handleDecrease(10)
|
|
46
|
+
break
|
|
47
|
+
case kbd.HOME:
|
|
48
|
+
event.preventDefault()
|
|
49
|
+
rootContext.handleMinMaxValue('min')
|
|
50
|
+
break
|
|
51
|
+
case kbd.END:
|
|
52
|
+
event.preventDefault()
|
|
53
|
+
rootContext.handleMinMaxValue('max')
|
|
54
|
+
break
|
|
55
|
+
case kbd.ENTER:
|
|
56
|
+
rootContext.applyInputValue((event.target as HTMLInputElement)?.value)
|
|
57
|
+
break
|
|
58
|
+
}
|
|
59
|
+
}
|
|
20
60
|
|
|
21
61
|
function handleWheelEvent(event: WheelEvent) {
|
|
22
62
|
if (rootContext.disableWheelChange.value)
|
|
@@ -77,14 +117,10 @@ function handleChange() {
|
|
|
77
117
|
:aria-valuenow="rootContext.modelValue.value"
|
|
78
118
|
:aria-valuemin="rootContext.min.value"
|
|
79
119
|
:aria-valuemax="rootContext.max.value"
|
|
80
|
-
@keydown
|
|
81
|
-
@keydown.down.prevent="rootContext.handleDecrease()"
|
|
82
|
-
@keydown.page-up.prevent="rootContext.handleIncrease(10)"
|
|
83
|
-
@keydown.page-down.prevent="rootContext.handleDecrease(10)"
|
|
84
|
-
@keydown.home.prevent="rootContext.handleMinMaxValue('min')"
|
|
85
|
-
@keydown.end.prevent="rootContext.handleMinMaxValue('max')"
|
|
120
|
+
@keydown="handleKeydown"
|
|
86
121
|
@wheel="handleWheelEvent"
|
|
87
122
|
@beforeinput="(event: InputEvent) => {
|
|
123
|
+
if (event.isComposing) return
|
|
88
124
|
const target = event.target as HTMLInputElement
|
|
89
125
|
let nextValue
|
|
90
126
|
= target.value.slice(0, target.selectionStart ?? undefined)
|
|
@@ -100,8 +136,9 @@ function handleChange() {
|
|
|
100
136
|
inputValue = target.value
|
|
101
137
|
}"
|
|
102
138
|
@change="handleChange"
|
|
103
|
-
@keydown.enter="rootContext.applyInputValue($event.target?.value)"
|
|
104
139
|
@blur="rootContext.applyInputValue($event.target?.value)"
|
|
140
|
+
@compositionstart="handleCompositionStart"
|
|
141
|
+
@compositionend="handleCompositionEnd"
|
|
105
142
|
>
|
|
106
143
|
<slot />
|
|
107
144
|
</Primitive>
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import type { PinInputContextValue } from './PinInputRoot.vue'
|
|
3
3
|
import type { PrimitiveProps } from '@/Primitive'
|
|
4
4
|
import { Primitive, usePrimitiveElement } from '@/Primitive'
|
|
5
|
-
import { getActiveElement, useArrowNavigation } from '@/shared'
|
|
5
|
+
import { getActiveElement, useArrowNavigation, useComposing } from '@/shared'
|
|
6
6
|
import { injectPinInputRootContext } from './PinInputRoot.vue'
|
|
7
7
|
|
|
8
8
|
export interface PinInputInputProps extends PrimitiveProps {
|
|
@@ -32,7 +32,45 @@ const NUMBER_REG = /^\d*$/
|
|
|
32
32
|
const NON_NUMBER_REG = /\D/g
|
|
33
33
|
|
|
34
34
|
const { primitiveElement, currentElement } = usePrimitiveElement()
|
|
35
|
+
|
|
36
|
+
const { isComposing, handleCompositionStart, handleCompositionEnd } = useComposing((event) => {
|
|
37
|
+
const target = event.target as HTMLInputElement
|
|
38
|
+
const value = event.data || target.value
|
|
39
|
+
|
|
40
|
+
if (context.isNumericMode.value) {
|
|
41
|
+
const filtered = value.replace(NON_NUMBER_REG, '')
|
|
42
|
+
if (!filtered) {
|
|
43
|
+
target.value = ''
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
if (filtered.length > 1) {
|
|
47
|
+
handleMultipleCharacter(filtered)
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
target.value = filtered
|
|
51
|
+
updateModelValueAt(props.index, filtered)
|
|
52
|
+
const nextEl = inputElements.value[props.index + 1]
|
|
53
|
+
if (nextEl)
|
|
54
|
+
nextEl.focus()
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (value.length > 1) {
|
|
59
|
+
handleMultipleCharacter(value)
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
target.value = value
|
|
64
|
+
updateModelValueAt(props.index, value)
|
|
65
|
+
|
|
66
|
+
const nextEl = inputElements.value[props.index + 1]
|
|
67
|
+
if (nextEl)
|
|
68
|
+
nextEl.focus()
|
|
69
|
+
})
|
|
70
|
+
|
|
35
71
|
function handleInput(event: InputEvent) {
|
|
72
|
+
if (isComposing.value || event.isComposing)
|
|
73
|
+
return
|
|
36
74
|
const target = event.target as HTMLInputElement
|
|
37
75
|
|
|
38
76
|
if ((event.data?.length ?? 0) > 1) {
|
|
@@ -69,6 +107,9 @@ function updatePlaceholder() {
|
|
|
69
107
|
}
|
|
70
108
|
|
|
71
109
|
function handleKeydown(event: KeyboardEvent) {
|
|
110
|
+
// Don't move between inputs mid-composition, arrow keys are used for IME candidate navigation
|
|
111
|
+
if (isComposing.value || event.isComposing)
|
|
112
|
+
return
|
|
72
113
|
useArrowNavigation(event, getActiveElement() as HTMLElement, undefined, {
|
|
73
114
|
itemsArray: inputElements.value,
|
|
74
115
|
focus: true,
|
|
@@ -223,6 +264,8 @@ onUnmounted(() => {
|
|
|
223
264
|
@focus="handleFocus"
|
|
224
265
|
@blur="handleBlur"
|
|
225
266
|
@paste="handlePaste"
|
|
267
|
+
@compositionstart="handleCompositionStart"
|
|
268
|
+
@compositionend="handleCompositionEnd"
|
|
226
269
|
>
|
|
227
270
|
<slot />
|
|
228
271
|
</Primitive>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { computed, onMounted } from 'vue'
|
|
2
|
+
import { computed, onMounted, onUnmounted } from 'vue'
|
|
3
3
|
import { useForwardExpose } from '@/shared'
|
|
4
4
|
import { injectScrollAreaRootContext } from './ScrollAreaRoot.vue'
|
|
5
5
|
import ScrollAreaScrollbarImpl from './ScrollAreaScrollbarImpl.vue'
|
|
@@ -15,6 +15,11 @@ onMounted(() => {
|
|
|
15
15
|
if (scrollbarElement.value)
|
|
16
16
|
rootContext.onScrollbarXChange(scrollbarElement.value)
|
|
17
17
|
})
|
|
18
|
+
// Clear the registration on unmount so consumers (e.g. ScrollAreaCorner) don't
|
|
19
|
+
// hold a stale reference once the scrollbar is removed (e.g. between hover cycles).
|
|
20
|
+
onUnmounted(() => {
|
|
21
|
+
rootContext.onScrollbarXChange(null)
|
|
22
|
+
})
|
|
18
23
|
const sizes = computed(() => scrollbarVisibleContext.sizes.value)
|
|
19
24
|
</script>
|
|
20
25
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { computed, onMounted } from 'vue'
|
|
2
|
+
import { computed, onMounted, onUnmounted } from 'vue'
|
|
3
3
|
import { useForwardExpose } from '@/shared'
|
|
4
4
|
import { injectScrollAreaRootContext } from './ScrollAreaRoot.vue'
|
|
5
5
|
import ScrollAreaScrollbarImpl from './ScrollAreaScrollbarImpl.vue'
|
|
@@ -15,6 +15,11 @@ onMounted(() => {
|
|
|
15
15
|
if (scrollbarElement.value)
|
|
16
16
|
rootContext.onScrollbarYChange(scrollbarElement.value)
|
|
17
17
|
})
|
|
18
|
+
// Clear the registration on unmount so consumers (e.g. ScrollAreaCorner) don't
|
|
19
|
+
// hold a stale reference once the scrollbar is removed (e.g. between hover cycles).
|
|
20
|
+
onUnmounted(() => {
|
|
21
|
+
rootContext.onScrollbarYChange(null)
|
|
22
|
+
})
|
|
18
23
|
|
|
19
24
|
const sizes = computed(() => scrollbarVisibleContext.sizes.value)
|
|
20
25
|
</script>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { PrimitiveProps } from '@/Primitive'
|
|
3
|
-
import { useForwardExpose } from '@/shared'
|
|
3
|
+
import { useComposing, useForwardExpose } from '@/shared'
|
|
4
4
|
|
|
5
5
|
export interface TagsInputInputProps extends PrimitiveProps {
|
|
6
6
|
/** The placeholder character to use for empty tags input. */
|
|
@@ -13,7 +13,7 @@ export interface TagsInputInputProps extends PrimitiveProps {
|
|
|
13
13
|
</script>
|
|
14
14
|
|
|
15
15
|
<script setup lang="ts">
|
|
16
|
-
import { nextTick, onMounted
|
|
16
|
+
import { nextTick, onMounted } from 'vue'
|
|
17
17
|
import { Primitive } from '@/Primitive'
|
|
18
18
|
import { injectTagsInputRootContext } from './TagsInputRoot.vue'
|
|
19
19
|
|
|
@@ -55,15 +55,7 @@ function handleTab(event: Event) {
|
|
|
55
55
|
handleCustomKeydown(event)
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
const isComposing =
|
|
59
|
-
function onCompositionStart() {
|
|
60
|
-
isComposing.value = true
|
|
61
|
-
}
|
|
62
|
-
function onCompositionEnd() {
|
|
63
|
-
nextTick(() => {
|
|
64
|
-
isComposing.value = false
|
|
65
|
-
})
|
|
66
|
-
}
|
|
58
|
+
const { isComposing, handleCompositionStart, handleCompositionEnd } = useComposing()
|
|
67
59
|
async function handleCustomKeydown(event: Event) {
|
|
68
60
|
if (isComposing.value)
|
|
69
61
|
return
|
|
@@ -85,6 +77,8 @@ async function handleCustomKeydown(event: Event) {
|
|
|
85
77
|
}
|
|
86
78
|
|
|
87
79
|
function handleInput(event: InputEvent) {
|
|
80
|
+
if (isComposing.value)
|
|
81
|
+
return
|
|
88
82
|
context.isInvalidInput.value = false
|
|
89
83
|
if (event.data === null)
|
|
90
84
|
return
|
|
@@ -106,6 +100,14 @@ function handleInput(event: InputEvent) {
|
|
|
106
100
|
}
|
|
107
101
|
}
|
|
108
102
|
|
|
103
|
+
function handleInputKeydown(event: KeyboardEvent) {
|
|
104
|
+
// `isComposing` stays true until the tick after `compositionend`, so arrow/backspace
|
|
105
|
+
// tag navigation is skipped even when the commit keydown reports `event.isComposing === false`.
|
|
106
|
+
if (isComposing.value)
|
|
107
|
+
return
|
|
108
|
+
context.onInputKeydown(event)
|
|
109
|
+
}
|
|
110
|
+
|
|
109
111
|
function handlePaste(event: ClipboardEvent) {
|
|
110
112
|
if (context.addOnPaste.value) {
|
|
111
113
|
event.preventDefault()
|
|
@@ -160,9 +162,9 @@ onMounted(() => {
|
|
|
160
162
|
@keydown.enter="handleCustomKeydown"
|
|
161
163
|
@keydown.tab="handleTab"
|
|
162
164
|
@blur="handleBlur"
|
|
163
|
-
@keydown="
|
|
164
|
-
@compositionstart="
|
|
165
|
-
@compositionend="
|
|
165
|
+
@keydown="handleInputKeydown"
|
|
166
|
+
@compositionstart="handleCompositionStart"
|
|
167
|
+
@compositionend="handleCompositionEnd"
|
|
166
168
|
@paste="handlePaste"
|
|
167
169
|
>
|
|
168
170
|
<slot />
|
|
@@ -156,6 +156,9 @@ provideTagsInputRootContext({
|
|
|
156
156
|
},
|
|
157
157
|
onRemoveValue: handleRemoveTag,
|
|
158
158
|
onInputKeydown: (event) => {
|
|
159
|
+
// Don't navigate/select tags mid-composition, keys are used for IME candidate navigation
|
|
160
|
+
if (event.isComposing)
|
|
161
|
+
return
|
|
159
162
|
const target = event.target as HTMLInputElement
|
|
160
163
|
const collection = getItems().map(i => i.ref).filter(i => i.dataset.disabled !== '')
|
|
161
164
|
if (!collection.length)
|
|
@@ -23,6 +23,9 @@ const lastKeyZero = ref(false)
|
|
|
23
23
|
const {
|
|
24
24
|
handleSegmentClick,
|
|
25
25
|
handleSegmentKeydown,
|
|
26
|
+
handleSegmentBeforeInput,
|
|
27
|
+
handleSegmentCompositionStart,
|
|
28
|
+
handleSegmentCompositionEnd,
|
|
26
29
|
handleSegmentFocusOut,
|
|
27
30
|
attributes,
|
|
28
31
|
} = useDateField({
|
|
@@ -61,6 +64,9 @@ const isInvalid = computed(() => rootContext.isInvalid.value)
|
|
|
61
64
|
v-on="part !== 'literal' ? {
|
|
62
65
|
mousedown: handleSegmentClick,
|
|
63
66
|
keydown: handleSegmentKeydown,
|
|
67
|
+
beforeinput: handleSegmentBeforeInput,
|
|
68
|
+
compositionstart: handleSegmentCompositionStart,
|
|
69
|
+
compositionend: handleSegmentCompositionEnd,
|
|
64
70
|
focusout: () => {
|
|
65
71
|
hasLeftFocus = true
|
|
66
72
|
handleSegmentFocusOut()
|
|
@@ -297,6 +297,9 @@ const prevFocusableSegment = computed(() => {
|
|
|
297
297
|
const kbd = useKbd()
|
|
298
298
|
|
|
299
299
|
function handleKeydown(e: KeyboardEvent) {
|
|
300
|
+
// Don't navigate between segments mid-composition, arrow keys are used for IME candidate navigation
|
|
301
|
+
if (e.isComposing)
|
|
302
|
+
return
|
|
300
303
|
if (!isSegmentNavigationKey(e.key))
|
|
301
304
|
return
|
|
302
305
|
if (e.key === kbd.ARROW_LEFT)
|
|
@@ -26,6 +26,9 @@ const lastKeyZero = ref(false)
|
|
|
26
26
|
const {
|
|
27
27
|
handleSegmentClick,
|
|
28
28
|
handleSegmentKeydown,
|
|
29
|
+
handleSegmentBeforeInput,
|
|
30
|
+
handleSegmentCompositionStart,
|
|
31
|
+
handleSegmentCompositionEnd,
|
|
29
32
|
attributes,
|
|
30
33
|
} = useDateField({
|
|
31
34
|
hasLeftFocus,
|
|
@@ -63,6 +66,9 @@ const isInvalid = computed(() => rootContext.isInvalid.value)
|
|
|
63
66
|
v-on="part !== 'literal' ? {
|
|
64
67
|
mousedown: handleSegmentClick,
|
|
65
68
|
keydown: handleSegmentKeydown,
|
|
69
|
+
beforeinput: handleSegmentBeforeInput,
|
|
70
|
+
compositionstart: handleSegmentCompositionStart,
|
|
71
|
+
compositionend: handleSegmentCompositionEnd,
|
|
66
72
|
focusout: () => { hasLeftFocus = true },
|
|
67
73
|
focusin: (e: FocusEvent) => {
|
|
68
74
|
rootContext.setFocusedElement(e.target as HTMLElement)
|
|
@@ -402,6 +402,9 @@ const prevFocusableSegment = computed(() => {
|
|
|
402
402
|
const kbd = useKbd()
|
|
403
403
|
|
|
404
404
|
function handleKeydown(e: KeyboardEvent) {
|
|
405
|
+
// Don't navigate between segments mid-composition, arrow keys are used for IME candidate navigation
|
|
406
|
+
if (e.isComposing)
|
|
407
|
+
return
|
|
405
408
|
if (!isSegmentNavigationKey(e.key))
|
|
406
409
|
return
|
|
407
410
|
if (e.key === kbd.ARROW_LEFT)
|
|
@@ -7,9 +7,11 @@ import {
|
|
|
7
7
|
} from '@internationalized/date'
|
|
8
8
|
import { computed } from 'vue'
|
|
9
9
|
import { getDaysInMonth, toDate } from '@/date'
|
|
10
|
-
import { snapValueToStep, useKbd } from '@/shared'
|
|
10
|
+
import { getActiveElement, snapValueToStep, useKbd } from '@/shared'
|
|
11
11
|
import { isAcceptableSegmentKey, isNumberString, isSegmentNavigationKey } from './segment'
|
|
12
12
|
|
|
13
|
+
const DIGIT_REG = /^\d$/
|
|
14
|
+
|
|
13
15
|
type MinuteSecondIncrementProps = {
|
|
14
16
|
e: KeyboardEvent
|
|
15
17
|
part: keyof TimeFields
|
|
@@ -877,6 +879,13 @@ export function useDateField(props: UseDateFieldProps) {
|
|
|
877
879
|
}
|
|
878
880
|
|
|
879
881
|
function handleSegmentKeydown(e: KeyboardEvent) {
|
|
882
|
+
// A genuine composition keydown sets `isComposing` or reports `key === 'Process'`.
|
|
883
|
+
// Don't use `keyCode === 229` alone: a CJK IME (e.g. Pinyin) keeps that flag set
|
|
884
|
+
// while passing a directly-typed key through, so a real digit (`key === '1'`)
|
|
885
|
+
// would be dropped and leak into the contenteditable as raw text.
|
|
886
|
+
if (e.isComposing || e.key === 'Process')
|
|
887
|
+
return
|
|
888
|
+
|
|
880
889
|
const disabled = props.disabled.value
|
|
881
890
|
const readonly = props.readonly.value
|
|
882
891
|
if (disabled || readonly)
|
|
@@ -935,9 +944,74 @@ export function useDateField(props: UseDateFieldProps) {
|
|
|
935
944
|
}
|
|
936
945
|
}
|
|
937
946
|
|
|
947
|
+
// Snapshot of the segment's child nodes (and their text) before the IME mutates
|
|
948
|
+
// the contenteditable. Vue renders the value into one text node but may keep
|
|
949
|
+
// empty sibling text nodes around it, so we must preserve EVERY node it owns and
|
|
950
|
+
// restore them verbatim on `compositionend`. Recreating nodes (textContent) would
|
|
951
|
+
// detach Vue's binding and freeze the segment; recreating the element isn't an
|
|
952
|
+
// option either, since the root captures segment elements once via
|
|
953
|
+
// `getSegmentElements` and a fresh node would break navigation.
|
|
954
|
+
let preCompositionNodes: { node: ChildNode, value: string | null }[] | null = null
|
|
955
|
+
|
|
956
|
+
function handleSegmentBeforeInput(e: InputEvent) {
|
|
957
|
+
// The segment's text is driven programmatically (keydown -> segmentValues ->
|
|
958
|
+
// Vue render), so the contenteditable must never be edited directly. Safari
|
|
959
|
+
// dispatches `beforeinput`/`input` BEFORE `keydown` while an IME is active,
|
|
960
|
+
// so a passed-through key (e.g. a digit typed while Pinyin is selected) would
|
|
961
|
+
// leak into the DOM as raw text before `handleSegmentKeydown` can prevent it.
|
|
962
|
+
// Composition input keeps `isComposing` true, so let it through and reconcile
|
|
963
|
+
// on `compositionend`.
|
|
964
|
+
if (!e.isComposing)
|
|
965
|
+
e.preventDefault()
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
function handleSegmentCompositionStart(e: CompositionEvent) {
|
|
969
|
+
const el = e.target as HTMLElement
|
|
970
|
+
preCompositionNodes = Array.from(el.childNodes, node => ({ node, value: node.nodeValue }))
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
function handleSegmentCompositionEnd(e: CompositionEvent) {
|
|
974
|
+
const el = e.target as HTMLElement
|
|
975
|
+
const original = preCompositionNodes
|
|
976
|
+
preCompositionNodes = null
|
|
977
|
+
|
|
978
|
+
// Restore Vue's original nodes (with their text) and drop anything the IME
|
|
979
|
+
// inserted, keeping Vue's bindings intact so later renders still patch the DOM.
|
|
980
|
+
// When no composition was tracked (e.g. synthetic events in tests) leave it be.
|
|
981
|
+
if (original) {
|
|
982
|
+
for (const { node, value } of original)
|
|
983
|
+
node.nodeValue = value
|
|
984
|
+
el.replaceChildren(...original.map(o => o.node))
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
const data = e.data
|
|
988
|
+
if (!data)
|
|
989
|
+
return
|
|
990
|
+
|
|
991
|
+
for (const char of data) {
|
|
992
|
+
if (!DIGIT_REG.test(char))
|
|
993
|
+
continue
|
|
994
|
+
|
|
995
|
+
// Dispatch to the focused segment so subsequent digits follow focus
|
|
996
|
+
// after `focusNext()` advances to the next segment (e.g. "34" → 3 in day, 4 in month).
|
|
997
|
+
const target = getActiveElement()
|
|
998
|
+
if (!(target instanceof HTMLElement))
|
|
999
|
+
break
|
|
1000
|
+
|
|
1001
|
+
target.dispatchEvent(new KeyboardEvent('keydown', {
|
|
1002
|
+
key: char,
|
|
1003
|
+
bubbles: true,
|
|
1004
|
+
cancelable: true,
|
|
1005
|
+
}))
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
|
|
938
1009
|
return {
|
|
939
1010
|
handleSegmentClick,
|
|
940
1011
|
handleSegmentKeydown,
|
|
1012
|
+
handleSegmentBeforeInput,
|
|
1013
|
+
handleSegmentCompositionStart,
|
|
1014
|
+
handleSegmentCompositionEnd,
|
|
941
1015
|
handleSegmentFocusOut,
|
|
942
1016
|
attributes,
|
|
943
1017
|
}
|
package/src/shared/index.ts
CHANGED
|
@@ -14,6 +14,7 @@ export { renderSlotFragments } from './renderSlotFragments'
|
|
|
14
14
|
export { trapFocus } from './trap-focus'
|
|
15
15
|
export { useArrowNavigation } from './useArrowNavigation'
|
|
16
16
|
export { useBodyScrollLock } from './useBodyScrollLock'
|
|
17
|
+
export { useComposing } from './useComposing'
|
|
17
18
|
export { type Formatter, useDateFormatter } from './useDateFormatter'
|
|
18
19
|
export { useDirection } from './useDirection'
|
|
19
20
|
export { useEmitAsProps } from './useEmitAsProps'
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { nextTick, ref } from 'vue'
|
|
2
|
+
|
|
3
|
+
export function useComposing(onEnd?: (event: CompositionEvent) => void) {
|
|
4
|
+
const isComposing = ref(false)
|
|
5
|
+
|
|
6
|
+
function handleCompositionStart() {
|
|
7
|
+
isComposing.value = true
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function handleCompositionEnd(event: CompositionEvent) {
|
|
11
|
+
nextTick(() => {
|
|
12
|
+
isComposing.value = false
|
|
13
|
+
onEnd?.(event)
|
|
14
|
+
})
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return { isComposing, handleCompositionStart, handleCompositionEnd }
|
|
18
|
+
}
|