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.
Files changed (148) hide show
  1. package/dist/Autocomplete/AutocompleteInput.cjs +12 -16
  2. package/dist/Autocomplete/AutocompleteInput.cjs.map +1 -1
  3. package/dist/Autocomplete/AutocompleteInput.js +13 -17
  4. package/dist/Autocomplete/AutocompleteInput.js.map +1 -1
  5. package/dist/ColorField/ColorFieldInput.cjs +10 -2
  6. package/dist/ColorField/ColorFieldInput.cjs.map +1 -1
  7. package/dist/ColorField/ColorFieldInput.js +10 -2
  8. package/dist/ColorField/ColorFieldInput.js.map +1 -1
  9. package/dist/Combobox/ComboboxInput.cjs +27 -9
  10. package/dist/Combobox/ComboboxInput.cjs.map +1 -1
  11. package/dist/Combobox/ComboboxInput.js +28 -10
  12. package/dist/Combobox/ComboboxInput.js.map +1 -1
  13. package/dist/DateField/DateFieldInput.cjs +4 -1
  14. package/dist/DateField/DateFieldInput.cjs.map +1 -1
  15. package/dist/DateField/DateFieldInput.js +4 -1
  16. package/dist/DateField/DateFieldInput.js.map +1 -1
  17. package/dist/DateField/DateFieldRoot.cjs +1 -0
  18. package/dist/DateField/DateFieldRoot.cjs.map +1 -1
  19. package/dist/DateField/DateFieldRoot.js +1 -0
  20. package/dist/DateField/DateFieldRoot.js.map +1 -1
  21. package/dist/DatePicker/DatePickerRoot.cjs +15 -1
  22. package/dist/DatePicker/DatePickerRoot.cjs.map +1 -1
  23. package/dist/DatePicker/DatePickerRoot.js +15 -1
  24. package/dist/DatePicker/DatePickerRoot.js.map +1 -1
  25. package/dist/DateRangeField/DateRangeFieldInput.cjs +4 -1
  26. package/dist/DateRangeField/DateRangeFieldInput.cjs.map +1 -1
  27. package/dist/DateRangeField/DateRangeFieldInput.js +4 -1
  28. package/dist/DateRangeField/DateRangeFieldInput.js.map +1 -1
  29. package/dist/DateRangeField/DateRangeFieldRoot.cjs +1 -0
  30. package/dist/DateRangeField/DateRangeFieldRoot.cjs.map +1 -1
  31. package/dist/DateRangeField/DateRangeFieldRoot.js +1 -0
  32. package/dist/DateRangeField/DateRangeFieldRoot.js.map +1 -1
  33. package/dist/Dialog/DialogOverlayImpl.cjs +5 -1
  34. package/dist/Dialog/DialogOverlayImpl.cjs.map +1 -1
  35. package/dist/Dialog/DialogOverlayImpl.js +5 -1
  36. package/dist/Dialog/DialogOverlayImpl.js.map +1 -1
  37. package/dist/DropdownMenu/DropdownMenuFilter.cjs +19 -2
  38. package/dist/DropdownMenu/DropdownMenuFilter.cjs.map +1 -1
  39. package/dist/DropdownMenu/DropdownMenuFilter.js +19 -2
  40. package/dist/DropdownMenu/DropdownMenuFilter.js.map +1 -1
  41. package/dist/Listbox/ListboxFilter.cjs +29 -10
  42. package/dist/Listbox/ListboxFilter.cjs.map +1 -1
  43. package/dist/Listbox/ListboxFilter.js +30 -11
  44. package/dist/Listbox/ListboxFilter.js.map +1 -1
  45. package/dist/Listbox/ListboxRoot.cjs +12 -6
  46. package/dist/Listbox/ListboxRoot.cjs.map +1 -1
  47. package/dist/Listbox/ListboxRoot.js +12 -6
  48. package/dist/Listbox/ListboxRoot.js.map +1 -1
  49. package/dist/NavigationMenu/NavigationMenuRoot.cjs +16 -3
  50. package/dist/NavigationMenu/NavigationMenuRoot.cjs.map +1 -1
  51. package/dist/NavigationMenu/NavigationMenuRoot.js +16 -3
  52. package/dist/NavigationMenu/NavigationMenuRoot.js.map +1 -1
  53. package/dist/NumberField/NumberFieldInput.cjs +46 -13
  54. package/dist/NumberField/NumberFieldInput.cjs.map +1 -1
  55. package/dist/NumberField/NumberFieldInput.js +47 -14
  56. package/dist/NumberField/NumberFieldInput.js.map +1 -1
  57. package/dist/PinInput/PinInputInput.cjs +37 -2
  58. package/dist/PinInput/PinInputInput.cjs.map +1 -1
  59. package/dist/PinInput/PinInputInput.js +37 -2
  60. package/dist/PinInput/PinInputInput.js.map +1 -1
  61. package/dist/ScrollArea/ScrollAreaScrollbarX.cjs +3 -0
  62. package/dist/ScrollArea/ScrollAreaScrollbarX.cjs.map +1 -1
  63. package/dist/ScrollArea/ScrollAreaScrollbarX.js +4 -1
  64. package/dist/ScrollArea/ScrollAreaScrollbarX.js.map +1 -1
  65. package/dist/ScrollArea/ScrollAreaScrollbarY.cjs +3 -0
  66. package/dist/ScrollArea/ScrollAreaScrollbarY.cjs.map +1 -1
  67. package/dist/ScrollArea/ScrollAreaScrollbarY.js +4 -1
  68. package/dist/ScrollArea/ScrollAreaScrollbarY.js.map +1 -1
  69. package/dist/TagsInput/TagsInputInput.cjs +12 -13
  70. package/dist/TagsInput/TagsInputInput.cjs.map +1 -1
  71. package/dist/TagsInput/TagsInputInput.js +13 -14
  72. package/dist/TagsInput/TagsInputInput.js.map +1 -1
  73. package/dist/TagsInput/TagsInputRoot.cjs +1 -0
  74. package/dist/TagsInput/TagsInputRoot.cjs.map +1 -1
  75. package/dist/TagsInput/TagsInputRoot.js +1 -0
  76. package/dist/TagsInput/TagsInputRoot.js.map +1 -1
  77. package/dist/TimeField/TimeFieldInput.cjs +4 -1
  78. package/dist/TimeField/TimeFieldInput.cjs.map +1 -1
  79. package/dist/TimeField/TimeFieldInput.js +4 -1
  80. package/dist/TimeField/TimeFieldInput.js.map +1 -1
  81. package/dist/TimeField/TimeFieldRoot.cjs +1 -0
  82. package/dist/TimeField/TimeFieldRoot.cjs.map +1 -1
  83. package/dist/TimeField/TimeFieldRoot.js +1 -0
  84. package/dist/TimeField/TimeFieldRoot.js.map +1 -1
  85. package/dist/TimeRangeField/TimeRangeFieldInput.cjs +4 -1
  86. package/dist/TimeRangeField/TimeRangeFieldInput.cjs.map +1 -1
  87. package/dist/TimeRangeField/TimeRangeFieldInput.js +4 -1
  88. package/dist/TimeRangeField/TimeRangeFieldInput.js.map +1 -1
  89. package/dist/TimeRangeField/TimeRangeFieldRoot.cjs +1 -0
  90. package/dist/TimeRangeField/TimeRangeFieldRoot.cjs.map +1 -1
  91. package/dist/TimeRangeField/TimeRangeFieldRoot.js +1 -0
  92. package/dist/TimeRangeField/TimeRangeFieldRoot.js.map +1 -1
  93. package/dist/constant.d.cts.map +1 -1
  94. package/dist/date/useDateField.cjs +38 -0
  95. package/dist/date/useDateField.cjs.map +1 -1
  96. package/dist/date/useDateField.js +38 -0
  97. package/dist/date/useDateField.js.map +1 -1
  98. package/dist/index.cjs +1 -0
  99. package/dist/index.js +1 -0
  100. package/dist/index3.d.cts +24 -16
  101. package/dist/index3.d.cts.map +1 -1
  102. package/dist/index3.d.ts +13 -5
  103. package/dist/index3.d.ts.map +1 -1
  104. package/dist/index4.d.cts +657 -657
  105. package/dist/index4.d.cts.map +1 -1
  106. package/dist/index4.d.ts +668 -668
  107. package/dist/index4.d.ts.map +1 -1
  108. package/dist/internal.cjs +1 -0
  109. package/dist/internal.d.cts +2 -2
  110. package/dist/internal.d.cts.map +1 -1
  111. package/dist/internal.d.ts +2 -2
  112. package/dist/internal.d.ts.map +1 -1
  113. package/dist/internal.js +1 -0
  114. package/dist/shared/useComposing.cjs +30 -0
  115. package/dist/shared/useComposing.cjs.map +1 -0
  116. package/dist/shared/useComposing.js +24 -0
  117. package/dist/shared/useComposing.js.map +1 -0
  118. package/dist/shared.cjs +2 -0
  119. package/dist/shared.d.cts +2 -2
  120. package/dist/shared.d.ts +2 -2
  121. package/dist/shared.js +2 -1
  122. package/package.json +4 -4
  123. package/src/Autocomplete/AutocompleteInput.vue +13 -17
  124. package/src/ColorField/ColorFieldInput.vue +11 -0
  125. package/src/Combobox/ComboboxInput.vue +37 -7
  126. package/src/DateField/DateFieldInput.vue +6 -0
  127. package/src/DateField/DateFieldRoot.vue +3 -0
  128. package/src/DatePicker/DatePickerRoot.vue +18 -2
  129. package/src/DateRangeField/DateRangeFieldInput.vue +6 -0
  130. package/src/DateRangeField/DateRangeFieldRoot.vue +3 -0
  131. package/src/Dialog/DialogOverlayImpl.vue +1 -1
  132. package/src/DropdownMenu/DropdownMenuFilter.vue +20 -1
  133. package/src/Listbox/ListboxFilter.vue +39 -8
  134. package/src/Listbox/ListboxRoot.vue +17 -4
  135. package/src/NavigationMenu/NavigationMenuRoot.vue +19 -3
  136. package/src/NumberField/NumberFieldInput.vue +45 -8
  137. package/src/PinInput/PinInputInput.vue +44 -1
  138. package/src/ScrollArea/ScrollAreaScrollbarX.vue +6 -1
  139. package/src/ScrollArea/ScrollAreaScrollbarY.vue +6 -1
  140. package/src/TagsInput/TagsInputInput.vue +16 -14
  141. package/src/TagsInput/TagsInputRoot.vue +3 -0
  142. package/src/TimeField/TimeFieldInput.vue +6 -0
  143. package/src/TimeField/TimeFieldRoot.vue +3 -0
  144. package/src/TimeRangeField/TimeRangeFieldInput.vue +6 -0
  145. package/src/TimeRangeField/TimeRangeFieldRoot.vue +3 -0
  146. package/src/shared/date/useDateField.ts +75 -1
  147. package/src/shared/index.ts +1 -0
  148. 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
- debouncedFn(val)
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
- isDelaySkipped.value = true
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.up.prevent="rootContext.handleIncrease()"
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, ref } from 'vue'
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 = ref(false)
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="context.onInputKeydown"
164
- @compositionstart="onCompositionStart"
165
- @compositionend="onCompositionEnd"
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
  }
@@ -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
+ }