reka-ui 2.9.7 → 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 (178) 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 +6 -1
  34. package/dist/Dialog/DialogOverlayImpl.cjs.map +1 -1
  35. package/dist/Dialog/DialogOverlayImpl.js +7 -2
  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/FocusScope/FocusScope.cjs +2 -0
  42. package/dist/FocusScope/FocusScope.cjs.map +1 -1
  43. package/dist/FocusScope/FocusScope.js +2 -0
  44. package/dist/FocusScope/FocusScope.js.map +1 -1
  45. package/dist/Listbox/ListboxFilter.cjs +29 -10
  46. package/dist/Listbox/ListboxFilter.cjs.map +1 -1
  47. package/dist/Listbox/ListboxFilter.js +30 -11
  48. package/dist/Listbox/ListboxFilter.js.map +1 -1
  49. package/dist/Listbox/ListboxItem.cjs +7 -2
  50. package/dist/Listbox/ListboxItem.cjs.map +1 -1
  51. package/dist/Listbox/ListboxItem.js +7 -2
  52. package/dist/Listbox/ListboxItem.js.map +1 -1
  53. package/dist/Listbox/ListboxRoot.cjs +12 -6
  54. package/dist/Listbox/ListboxRoot.cjs.map +1 -1
  55. package/dist/Listbox/ListboxRoot.js +12 -6
  56. package/dist/Listbox/ListboxRoot.js.map +1 -1
  57. package/dist/Menu/MenuItemImpl.cjs +1 -1
  58. package/dist/Menu/MenuItemImpl.cjs.map +1 -1
  59. package/dist/Menu/MenuItemImpl.js +1 -1
  60. package/dist/Menu/MenuItemImpl.js.map +1 -1
  61. package/dist/NavigationMenu/NavigationMenuContentImpl.cjs +1 -0
  62. package/dist/NavigationMenu/NavigationMenuContentImpl.cjs.map +1 -1
  63. package/dist/NavigationMenu/NavigationMenuContentImpl.js +1 -0
  64. package/dist/NavigationMenu/NavigationMenuContentImpl.js.map +1 -1
  65. package/dist/NavigationMenu/NavigationMenuRoot.cjs +16 -3
  66. package/dist/NavigationMenu/NavigationMenuRoot.cjs.map +1 -1
  67. package/dist/NavigationMenu/NavigationMenuRoot.js +16 -3
  68. package/dist/NavigationMenu/NavigationMenuRoot.js.map +1 -1
  69. package/dist/NumberField/NumberFieldInput.cjs +46 -13
  70. package/dist/NumberField/NumberFieldInput.cjs.map +1 -1
  71. package/dist/NumberField/NumberFieldInput.js +47 -14
  72. package/dist/NumberField/NumberFieldInput.js.map +1 -1
  73. package/dist/PinInput/PinInputInput.cjs +37 -2
  74. package/dist/PinInput/PinInputInput.cjs.map +1 -1
  75. package/dist/PinInput/PinInputInput.js +37 -2
  76. package/dist/PinInput/PinInputInput.js.map +1 -1
  77. package/dist/ScrollArea/ScrollAreaScrollbarX.cjs +3 -0
  78. package/dist/ScrollArea/ScrollAreaScrollbarX.cjs.map +1 -1
  79. package/dist/ScrollArea/ScrollAreaScrollbarX.js +4 -1
  80. package/dist/ScrollArea/ScrollAreaScrollbarX.js.map +1 -1
  81. package/dist/ScrollArea/ScrollAreaScrollbarY.cjs +3 -0
  82. package/dist/ScrollArea/ScrollAreaScrollbarY.cjs.map +1 -1
  83. package/dist/ScrollArea/ScrollAreaScrollbarY.js +4 -1
  84. package/dist/ScrollArea/ScrollAreaScrollbarY.js.map +1 -1
  85. package/dist/Select/SelectContent.cjs +15 -2
  86. package/dist/Select/SelectContent.cjs.map +1 -1
  87. package/dist/Select/SelectContent.js +16 -3
  88. package/dist/Select/SelectContent.js.map +1 -1
  89. package/dist/TagsInput/TagsInputInput.cjs +12 -13
  90. package/dist/TagsInput/TagsInputInput.cjs.map +1 -1
  91. package/dist/TagsInput/TagsInputInput.js +13 -14
  92. package/dist/TagsInput/TagsInputInput.js.map +1 -1
  93. package/dist/TagsInput/TagsInputRoot.cjs +1 -0
  94. package/dist/TagsInput/TagsInputRoot.cjs.map +1 -1
  95. package/dist/TagsInput/TagsInputRoot.js +1 -0
  96. package/dist/TagsInput/TagsInputRoot.js.map +1 -1
  97. package/dist/TimeField/TimeFieldInput.cjs +4 -1
  98. package/dist/TimeField/TimeFieldInput.cjs.map +1 -1
  99. package/dist/TimeField/TimeFieldInput.js +4 -1
  100. package/dist/TimeField/TimeFieldInput.js.map +1 -1
  101. package/dist/TimeField/TimeFieldRoot.cjs +1 -0
  102. package/dist/TimeField/TimeFieldRoot.cjs.map +1 -1
  103. package/dist/TimeField/TimeFieldRoot.js +1 -0
  104. package/dist/TimeField/TimeFieldRoot.js.map +1 -1
  105. package/dist/TimeRangeField/TimeRangeFieldInput.cjs +4 -1
  106. package/dist/TimeRangeField/TimeRangeFieldInput.cjs.map +1 -1
  107. package/dist/TimeRangeField/TimeRangeFieldInput.js +4 -1
  108. package/dist/TimeRangeField/TimeRangeFieldInput.js.map +1 -1
  109. package/dist/TimeRangeField/TimeRangeFieldRoot.cjs +1 -0
  110. package/dist/TimeRangeField/TimeRangeFieldRoot.cjs.map +1 -1
  111. package/dist/TimeRangeField/TimeRangeFieldRoot.js +1 -0
  112. package/dist/TimeRangeField/TimeRangeFieldRoot.js.map +1 -1
  113. package/dist/date/useDateField.cjs +38 -0
  114. package/dist/date/useDateField.cjs.map +1 -1
  115. package/dist/date/useDateField.js +38 -0
  116. package/dist/date/useDateField.js.map +1 -1
  117. package/dist/index.cjs +1 -0
  118. package/dist/index.js +1 -0
  119. package/dist/index2.d.ts.map +1 -1
  120. package/dist/index3.d.cts +24 -16
  121. package/dist/index3.d.cts.map +1 -1
  122. package/dist/index3.d.ts +13 -5
  123. package/dist/index3.d.ts.map +1 -1
  124. package/dist/index4.d.cts +655 -655
  125. package/dist/index4.d.cts.map +1 -1
  126. package/dist/index4.d.ts +684 -684
  127. package/dist/index4.d.ts.map +1 -1
  128. package/dist/internal.cjs +1 -0
  129. package/dist/internal.d.cts +2 -2
  130. package/dist/internal.d.cts.map +1 -1
  131. package/dist/internal.d.ts +2 -2
  132. package/dist/internal.d.ts.map +1 -1
  133. package/dist/internal.js +1 -0
  134. package/dist/shared/useComposing.cjs +30 -0
  135. package/dist/shared/useComposing.cjs.map +1 -0
  136. package/dist/shared/useComposing.js +24 -0
  137. package/dist/shared/useComposing.js.map +1 -0
  138. package/dist/shared.cjs +2 -0
  139. package/dist/shared.d.cts +2 -2
  140. package/dist/shared.d.ts +2 -2
  141. package/dist/shared.js +2 -1
  142. package/package.json +4 -4
  143. package/src/Autocomplete/AutocompleteInput.vue +13 -17
  144. package/src/Calendar/CalendarRoot.vue +1 -1
  145. package/src/ColorField/ColorFieldInput.vue +11 -0
  146. package/src/Combobox/ComboboxInput.vue +37 -7
  147. package/src/DateField/DateFieldInput.vue +6 -0
  148. package/src/DateField/DateFieldRoot.vue +3 -0
  149. package/src/DatePicker/DatePickerRoot.vue +18 -2
  150. package/src/DateRangeField/DateRangeFieldInput.vue +6 -0
  151. package/src/DateRangeField/DateRangeFieldRoot.vue +4 -1
  152. package/src/Dialog/DialogOverlayImpl.vue +1 -0
  153. package/src/DropdownMenu/DropdownMenuFilter.vue +20 -1
  154. package/src/FocusScope/FocusScope.vue +5 -0
  155. package/src/Listbox/ListboxFilter.vue +39 -8
  156. package/src/Listbox/ListboxItem.vue +2 -2
  157. package/src/Listbox/ListboxRoot.vue +17 -4
  158. package/src/Menu/MenuItemImpl.vue +1 -1
  159. package/src/MonthPicker/MonthPickerRoot.vue +1 -1
  160. package/src/NavigationMenu/NavigationMenuContentImpl.vue +3 -0
  161. package/src/NavigationMenu/NavigationMenuRoot.vue +19 -3
  162. package/src/NavigationMenu/__test__/NavigationMenuUnmountOnHideFalse.vue +45 -0
  163. package/src/NumberField/NumberFieldInput.vue +45 -8
  164. package/src/PinInput/PinInputInput.vue +44 -1
  165. package/src/ScrollArea/ScrollAreaScrollbarX.vue +6 -1
  166. package/src/ScrollArea/ScrollAreaScrollbarY.vue +6 -1
  167. package/src/Select/SelectContent.vue +19 -3
  168. package/src/Select/__test__/SelectUnmountCleanup.vue +43 -0
  169. package/src/TagsInput/TagsInputInput.vue +16 -14
  170. package/src/TagsInput/TagsInputRoot.vue +3 -0
  171. package/src/TimeField/TimeFieldInput.vue +6 -0
  172. package/src/TimeField/TimeFieldRoot.vue +3 -0
  173. package/src/TimeRangeField/TimeRangeFieldInput.vue +6 -0
  174. package/src/TimeRangeField/TimeRangeFieldRoot.vue +3 -0
  175. package/src/YearPicker/YearPickerRoot.vue +1 -1
  176. package/src/shared/date/useDateField.ts +75 -1
  177. package/src/shared/index.ts +1 -0
  178. package/src/shared/useComposing.ts +18 -0
@@ -3,6 +3,7 @@ import type { PrimitiveProps } from '..'
3
3
  import { useVModel } from '@vueuse/core'
4
4
  import { computed, onMounted, onUnmounted, ref, watchSyncEffect } from 'vue'
5
5
  import { usePrimitiveElement } from '@/Primitive'
6
+ import { useComposing } from '@/shared'
6
7
  import { Primitive } from '..'
7
8
  import { injectListboxRootContext } from './ListboxRoot.vue'
8
9
 
@@ -59,6 +60,39 @@ onMounted(() => {
59
60
  onUnmounted(() => {
60
61
  rootContext.focusable.value = true
61
62
  })
63
+
64
+ const { isComposing, handleCompositionStart, handleCompositionEnd } = useComposing((event) => {
65
+ modelValue.value = (event.target as HTMLInputElement).value
66
+ rootContext.onCompositionEnd()
67
+ rootContext.highlightFirstItem()
68
+ })
69
+
70
+ function onCompositionStart() {
71
+ rootContext.onCompositionStart()
72
+ handleCompositionStart()
73
+ }
74
+
75
+ function handleInput(event: InputEvent) {
76
+ if (isComposing.value)
77
+ return
78
+ modelValue.value = (event.target as HTMLInputElement).value
79
+ rootContext.highlightFirstItem()
80
+ }
81
+
82
+ function handleKeydownNavigation(event: KeyboardEvent) {
83
+ // Don't navigate mid-composition, arrow keys are used for IME candidate navigation
84
+ if (isComposing.value)
85
+ return
86
+ event.preventDefault()
87
+ rootContext.onKeydownNavigation(event)
88
+ }
89
+
90
+ function handleKeydownEnter(event: KeyboardEvent) {
91
+ // Don't select mid-composition, Enter commits the IME candidate
92
+ if (isComposing.value)
93
+ return
94
+ rootContext.onKeydownEnter(event)
95
+ }
62
96
  </script>
63
97
 
64
98
  <template>
@@ -72,14 +106,11 @@ onUnmounted(() => {
72
106
  :aria-disabled="disabled ?? undefined"
73
107
  :aria-activedescendant="activedescendant"
74
108
  type="text"
75
- @keydown.down.up.home.end.prevent="rootContext.onKeydownNavigation"
76
- @keydown.enter="rootContext.onKeydownEnter"
77
- @input="(event: InputEvent) => {
78
- modelValue = (event.target as HTMLInputElement).value
79
- rootContext.highlightFirstItem()
80
- }"
81
- @compositionstart="rootContext.onCompositionStart"
82
- @compositionend="rootContext.onCompositionEnd"
109
+ @keydown.down.up.home.end="handleKeydownNavigation"
110
+ @keydown.enter="handleKeydownEnter"
111
+ @input="handleInput"
112
+ @compositionstart="onCompositionStart"
113
+ @compositionend="handleCompositionEnd"
83
114
  >
84
115
  <slot :model-value="modelValue" />
85
116
  </Primitive>
@@ -44,7 +44,7 @@ const { CollectionItem } = useCollection()
44
44
  const { forwardRef, currentElement } = useForwardExpose()
45
45
  const rootContext = injectListboxRootContext()
46
46
 
47
- const isHighlighted = computed(() => currentElement.value === rootContext.highlightedElement.value)
47
+ const isHighlighted = computed(() => currentElement.value != null && currentElement.value === rootContext.highlightedElement.value)
48
48
  const isSelected = computed(() => valueComparator(rootContext.modelValue.value, props.value, rootContext.by))
49
49
 
50
50
  const disabled = computed(() => rootContext.disabled.value || props.disabled)
@@ -76,7 +76,7 @@ provideListboxItemContext({
76
76
  :id="id"
77
77
  v-bind="$attrs"
78
78
  :ref="forwardRef"
79
- v-memo="[isHighlighted, isSelected]"
79
+ v-memo="[isHighlighted, isSelected, disabled, rootContext.focusable.value]"
80
80
  role="option"
81
81
  :tabindex="rootContext.focusable.value ? isHighlighted ? '0' : '-1' : -1"
82
82
  :aria-selected="isSelected"
@@ -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 })
@@ -33,7 +33,7 @@ const { forwardRef, currentElement } = useForwardExpose()
33
33
  const { CollectionItem } = useCollection()
34
34
 
35
35
  const isFocused = ref(false)
36
- const isHighlighted = computed(() => isFocused.value || (contentContext.highlightedElement.value === currentElement.value))
36
+ const isHighlighted = computed(() => isFocused.value || (currentElement.value != null && contentContext.highlightedElement.value === currentElement.value))
37
37
 
38
38
  async function handlePointerMove(event: PointerEvent) {
39
39
  if (event.defaultPrevented || !isMouseEvent(event))
@@ -74,7 +74,7 @@ export interface MonthPickerRootProps extends PrimitiveProps {
74
74
  /** A function that returns the previous page of the month picker. Receives the current placeholder as an argument. */
75
75
  prevPage?: (placeholder: DateValue) => DateValue
76
76
  /** The controlled selected month value of the month picker. Can be bound as `v-model`. */
77
- modelValue?: DateValue | DateValue[] | undefined
77
+ modelValue?: DateValue | DateValue[] | null
78
78
  /** Whether multiple months can be selected */
79
79
  multiple?: boolean
80
80
  }
@@ -180,6 +180,9 @@ function handleKeydown(ev: KeyboardEvent) {
180
180
  }
181
181
 
182
182
  function handleDismiss() {
183
+ if (menuContext.modelValue.value !== itemContext.value)
184
+ return
185
+
183
186
  const rootContentDismissEvent = new Event(EVENT_ROOT_CONTENT_DISMISS, {
184
187
  bubbles: true,
185
188
  cancelable: 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
@@ -0,0 +1,45 @@
1
+ <script setup lang="ts">
2
+ import { ref } from 'vue'
3
+ import {
4
+ NavigationMenuContent,
5
+ NavigationMenuItem,
6
+ NavigationMenuList,
7
+ NavigationMenuRoot,
8
+ NavigationMenuTrigger,
9
+ } from '..'
10
+
11
+ const modelValue = ref('')
12
+ </script>
13
+
14
+ <template>
15
+ <NavigationMenuRoot
16
+ v-model="modelValue"
17
+ :unmount-on-hide="false"
18
+ :disable-hover-trigger="true"
19
+ >
20
+ <span data-testid="model-value">{{ modelValue }}</span>
21
+ <NavigationMenuList>
22
+ <NavigationMenuItem value="one">
23
+ <NavigationMenuTrigger data-testid="trigger-one">
24
+ One
25
+ </NavigationMenuTrigger>
26
+ <NavigationMenuContent data-testid="content-one">
27
+ <button data-testid="inside-one">
28
+ Inside one
29
+ </button>
30
+ </NavigationMenuContent>
31
+ </NavigationMenuItem>
32
+
33
+ <NavigationMenuItem value="two">
34
+ <NavigationMenuTrigger data-testid="trigger-two">
35
+ Two
36
+ </NavigationMenuTrigger>
37
+ <NavigationMenuContent data-testid="content-two">
38
+ <button data-testid="inside-two">
39
+ Inside two
40
+ </button>
41
+ </NavigationMenuContent>
42
+ </NavigationMenuItem>
43
+ </NavigationMenuList>
44
+ </NavigationMenuRoot>
45
+ </template>
@@ -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>
@@ -3,7 +3,7 @@ import type {
3
3
  SelectContentImplEmits,
4
4
  SelectContentImplProps,
5
5
  } from './SelectContentImpl.vue'
6
- import { computed, onMounted, ref, watch } from 'vue'
6
+ import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
7
7
 
8
8
  export type SelectContentEmits = SelectContentImplEmits
9
9
 
@@ -44,15 +44,31 @@ const presenceRef = ref<InstanceType<typeof Presence>>()
44
44
  const present = computed(() => props.forceMount || rootContext.open.value)
45
45
  const renderPresence = ref(present.value)
46
46
 
47
- watch(present, () => {
47
+ let renderPresenceTimeout: ReturnType<typeof setTimeout> | undefined
48
+
49
+ function clearRenderPresenceTimeout() {
50
+ if (renderPresenceTimeout) {
51
+ clearTimeout(renderPresenceTimeout)
52
+ renderPresenceTimeout = undefined
53
+ }
54
+ }
55
+
56
+ watch(present, (_value, _oldValue, onCleanup) => {
48
57
  // Toggle render presence after a delay (nextTick is not enough)
49
58
  // to allow children to re-render with the latest state.
50
59
  // Otherwise, they would remain in the old state during the transition,
51
60
  // which would prevent the animation that depend on state (e.g., data-[state=closed])
52
61
  // from being applied accurately.
53
62
  // @see https://github.com/unovue/reka-ui/issues/1865
54
- setTimeout(() => renderPresence.value = present.value)
63
+ clearRenderPresenceTimeout()
64
+ renderPresenceTimeout = setTimeout(() => {
65
+ renderPresence.value = present.value
66
+ renderPresenceTimeout = undefined
67
+ })
68
+ onCleanup(clearRenderPresenceTimeout)
55
69
  })
70
+
71
+ onUnmounted(clearRenderPresenceTimeout)
56
72
  </script>
57
73
 
58
74
  <template>
@@ -0,0 +1,43 @@
1
+ <script setup lang="ts">
2
+ import { ref } from 'vue'
3
+ import {
4
+ SelectContent,
5
+ SelectItem,
6
+ SelectItemText,
7
+ SelectPortal,
8
+ SelectRoot,
9
+ SelectTrigger,
10
+ SelectValue,
11
+ SelectViewport,
12
+ } from '..'
13
+
14
+ const open = ref(true)
15
+ const mounted = ref(true)
16
+ </script>
17
+
18
+ <template>
19
+ <button @click="open = false">
20
+ Close
21
+ </button>
22
+ <button @click="mounted = false">
23
+ Unmount
24
+ </button>
25
+
26
+ <SelectRoot
27
+ v-if="mounted"
28
+ v-model:open="open"
29
+ >
30
+ <SelectTrigger aria-label="Fruit">
31
+ <SelectValue placeholder="Please select a fruit" />
32
+ </SelectTrigger>
33
+ <SelectPortal>
34
+ <SelectContent position="popper">
35
+ <SelectViewport>
36
+ <SelectItem value="apple">
37
+ <SelectItemText>Apple</SelectItemText>
38
+ </SelectItem>
39
+ </SelectViewport>
40
+ </SelectContent>
41
+ </SelectPortal>
42
+ </SelectRoot>
43
+ </template>
@@ -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)