reka-ui 2.9.9 → 2.9.10

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 (44) hide show
  1. package/dist/Combobox/ComboboxContentImpl.cjs +8 -2
  2. package/dist/Combobox/ComboboxContentImpl.cjs.map +1 -1
  3. package/dist/Combobox/ComboboxContentImpl.js +8 -2
  4. package/dist/Combobox/ComboboxContentImpl.js.map +1 -1
  5. package/dist/Dialog/DialogContent.cjs +2 -1
  6. package/dist/Dialog/DialogContent.cjs.map +1 -1
  7. package/dist/Dialog/DialogContent.js +2 -1
  8. package/dist/Dialog/DialogContent.js.map +1 -1
  9. package/dist/Dialog/DialogContentModal.cjs +4 -3
  10. package/dist/Dialog/DialogContentModal.cjs.map +1 -1
  11. package/dist/Dialog/DialogContentModal.js +4 -3
  12. package/dist/Dialog/DialogContentModal.js.map +1 -1
  13. package/dist/DismissableLayer/DismissableLayer.cjs +10 -9
  14. package/dist/DismissableLayer/DismissableLayer.cjs.map +1 -1
  15. package/dist/DismissableLayer/DismissableLayer.js +11 -10
  16. package/dist/DismissableLayer/DismissableLayer.js.map +1 -1
  17. package/dist/Listbox/ListboxRoot.cjs +4 -1
  18. package/dist/Listbox/ListboxRoot.cjs.map +1 -1
  19. package/dist/Listbox/ListboxRoot.js +4 -1
  20. package/dist/Listbox/ListboxRoot.js.map +1 -1
  21. package/dist/Listbox/ListboxVirtualizer.cjs +7 -3
  22. package/dist/Listbox/ListboxVirtualizer.cjs.map +1 -1
  23. package/dist/Listbox/ListboxVirtualizer.js +7 -3
  24. package/dist/Listbox/ListboxVirtualizer.js.map +1 -1
  25. package/dist/index3.d.cts +19 -19
  26. package/dist/index3.d.ts +3 -3
  27. package/dist/index4.d.cts +631 -628
  28. package/dist/index4.d.cts.map +1 -1
  29. package/dist/index4.d.ts +675 -672
  30. package/dist/index4.d.ts.map +1 -1
  31. package/dist/internal.d.ts +2 -2
  32. package/dist/internal.d.ts.map +1 -1
  33. package/dist/shared/useId.cjs +7 -8
  34. package/dist/shared/useId.cjs.map +1 -1
  35. package/dist/shared/useId.js +7 -8
  36. package/dist/shared/useId.js.map +1 -1
  37. package/package.json +6 -5
  38. package/src/Combobox/ComboboxContentImpl.vue +17 -4
  39. package/src/Dialog/DialogContent.vue +7 -1
  40. package/src/Dialog/DialogContentModal.vue +4 -2
  41. package/src/DismissableLayer/DismissableLayer.vue +39 -21
  42. package/src/Listbox/ListboxRoot.vue +7 -4
  43. package/src/Listbox/ListboxVirtualizer.vue +19 -3
  44. package/src/shared/useId.ts +14 -6
@@ -1,7 +1,7 @@
1
1
  import "./index2.js";
2
2
  import "./index3.js";
3
3
  import { MenuArrowProps, MenuCheckboxItemEmits, MenuCheckboxItemProps, MenuContentEmits, MenuContentProps, MenuEmits, MenuGroupProps, MenuItemEmits, MenuItemIndicatorProps, MenuItemProps, MenuLabelProps, MenuPortalProps, MenuProps, MenuRadioGroupEmits, MenuRadioGroupProps, MenuRadioItemEmits, MenuRadioItemProps, MenuSeparatorProps, MenuSubContentEmits, MenuSubContentProps, MenuSubEmits, MenuSubProps, MenuSubTriggerProps, PopperAnchorProps, _default$277 as _default$13, _default$278 as _default$8, _default$279 as _default, _default$280 as _default$6, _default$281 as _default$10, _default$282 as _default$3, _default$283 as _default$14, _default$284 as _default$12, _default$285 as _default$2, _default$286 as _default$1, _default$287 as _default$11, _default$288 as _default$7, _default$290 as _default$9, _default$291 as _default$4, _default$292 as _default$5, injectMenuContext, injectMenuRootContext } from "./index4.js";
4
- import * as vue372 from "vue";
4
+ import * as vue1641 from "vue";
5
5
 
6
6
  //#region src/Menu/MenuAnchor.vue.d.ts
7
7
  interface MenuAnchorProps extends PopperAnchorProps {}
@@ -9,7 +9,7 @@ declare var __VLS_8: {};
9
9
  type __VLS_Slots = {} & {
10
10
  default?: (props: typeof __VLS_8) => any;
11
11
  };
12
- declare const __VLS_base: vue372.DefineComponent<MenuAnchorProps, {}, {}, {}, {}, vue372.ComponentOptionsMixin, vue372.ComponentOptionsMixin, {}, string, vue372.PublicProps, Readonly<MenuAnchorProps> & Readonly<{}>, {}, {}, {}, {}, string, vue372.ComponentProvideOptions, false, {}, any>;
12
+ declare const __VLS_base: vue1641.DefineComponent<MenuAnchorProps, {}, {}, {}, {}, vue1641.ComponentOptionsMixin, vue1641.ComponentOptionsMixin, {}, string, vue1641.PublicProps, Readonly<MenuAnchorProps> & Readonly<{}>, {}, {}, {}, {}, string, vue1641.ComponentProvideOptions, false, {}, any>;
13
13
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
14
14
  declare const _default$15: typeof __VLS_export;
15
15
  type __VLS_WithSlots<T, S> = T & {
@@ -1 +1 @@
1
- {"version":3,"file":"internal.d.ts","names":[],"sources":["../src/Menu/MenuAnchor.vue"],"sourcesContent":[],"mappings":";;;;;;UAoBU,eAAA,SAAwB;YAsC9B;KACC,WAAA;2BACwB;AA3CoB,CAAA;AAGE,cA2C7C,UALgB,EAKN,MAAA,CAAA,eALM,CAKN,eALM,EAAA,CAAA,CAAA,EAAA,CAAA,CAAA,EAAA,CAAA,CAAA,EAAA,CAAA,CAAA,EAKN,MAAA,CAAA,qBAAA,EAAA,MAAA,CAAA,qBAAA,EALM,CAAA,CAAA,EAAA,MAAA,EAKN,MAAA,CAAA,WAAA,EAAA,QALM,CAKN,eALM,CAAA,GAKN,QALM,CAAA,CAAA,CAAA,CAAA,EAAA,CAAA,CAAA,EAAA,CAAA,CAAA,EAAA,CAAA,CAAA,EAAA,CAAA,CAAA,EAAA,MAAA,EAKN,MAAA,CAAA,uBAAA,EALM,KAAA,EAAA,CAAA,CAAA,EAAA,GAAA,CAAA;AAAA,cAQhB,YAPU,EAOW,eANS,CAAA,OAMc,UANd,EAM0B,WAN1B,CAAA;AAAA,cAMM,WADxC,EAAA,OAE0B,YAF1B;KAGG,eALW,CAAA,CAAA,EAAA,CAAA,CAAA,GAKa,CALb,GAAA;EAAA,MAAA,EAAA;IAAA,MAAA,EAON,CAPM;EAAA,CAAA;CAAA"}
1
+ {"version":3,"file":"internal.d.ts","names":[],"sources":["../src/Menu/MenuAnchor.vue"],"sourcesContent":[],"mappings":";;;;;;UAoBU,eAAA,SAAwB;YAsC9B;KACC,WAAA;2BACwB;AA3CoB,CAAA;AAGE,cA2C7C,UALgB,EAKN,OAAA,CAAA,eALM,CAKN,eALM,EAAA,CAAA,CAAA,EAAA,CAAA,CAAA,EAAA,CAAA,CAAA,EAAA,CAAA,CAAA,EAKN,OAAA,CAAA,qBAAA,EAAA,OAAA,CAAA,qBAAA,EALM,CAAA,CAAA,EAAA,MAAA,EAKN,OAAA,CAAA,WAAA,EAAA,QALM,CAKN,eALM,CAAA,GAKN,QALM,CAAA,CAAA,CAAA,CAAA,EAAA,CAAA,CAAA,EAAA,CAAA,CAAA,EAAA,CAAA,CAAA,EAAA,CAAA,CAAA,EAAA,MAAA,EAKN,OAAA,CAAA,uBAAA,EALM,KAAA,EAAA,CAAA,CAAA,EAAA,GAAA,CAAA;AAAA,cAQhB,YAPU,EAOW,eANS,CAAA,OAMc,UANd,EAM0B,WAN1B,CAAA;AAAA,cAMM,WADxC,EAAA,OAE0B,YAF1B;KAGG,eALW,CAAA,CAAA,EAAA,CAAA,CAAA,GAKa,CALb,GAAA;EAAA,MAAA,EAAA;IAAA,MAAA,EAON,CAPM;EAAA,CAAA;CAAA"}
@@ -5,21 +5,20 @@ const vue = require_rolldown_runtime.__toESM(require("vue"));
5
5
  //#region src/shared/useId.ts
6
6
  let count = 0;
7
7
  /**
8
- * The `useId` function generates a unique identifier using a provided deterministic ID or a default
9
- * one prefixed with "reka-", or the provided one via `useId` props from `<ConfigProvider>`.
8
+ * The `useId` function generates a unique identifier using a provided deterministic ID,
9
+ * a configured `<ConfigProvider>` ID source, Vue's native `useId`, or a fallback counter.
10
10
  * @param {string | null | undefined} [deterministicId] - The `useId` function you provided takes an
11
11
  * optional parameter `deterministicId`, which can be a string, null, or undefined. If
12
12
  * `deterministicId` is provided, the function will return it. Otherwise, it will generate an id using
13
- * the `useId` function obtained
13
+ * the configured ID source.
14
14
  */
15
15
  function useId(deterministicId, prefix = "reka") {
16
16
  if (deterministicId) return deterministicId;
17
17
  let id;
18
- if ("useId" in vue) id = vue.useId?.();
19
- else {
20
- const configProviderContext = require_ConfigProvider_ConfigProvider.injectConfigProviderContext({ useId: void 0 });
21
- id = configProviderContext.useId?.() ?? `${++count}`;
22
- }
18
+ const configProviderContext = require_ConfigProvider_ConfigProvider.injectConfigProviderContext({ useId: void 0 });
19
+ if (configProviderContext.useId) id = configProviderContext.useId();
20
+ else if ("useId" in vue) id = vue.useId?.();
21
+ else id = `${++count}`;
23
22
  return prefix ? `${prefix}-${id}` : id;
24
23
  }
25
24
 
@@ -1 +1 @@
1
- {"version":3,"file":"useId.cjs","names":["deterministicId?: string | null | undefined","id: string"],"sources":["../../src/shared/useId.ts"],"sourcesContent":[],"mappings":";;;;;AAKA,IAAI,QAAQ;;;;;;;;;AASZ,SAAgB,MAAMA,iBAA6C,SAAS,QAAQ;AAClF,KAAI,gBACF,QAAO;CAET,IAAIC;AACJ,KAAI,WAAW,IACb,MAAK,IAAI,SAAS;MAEf;EACH,MAAM,wBAAwB,kEAA4B,EAAE,cAAkB,EAAC;AAC/E,OAAK,sBAAsB,SAAS,IAAI,GAAG,EAAE,OAAO;CACrD;AAED,QAAO,SAAS,GAAG,OAAO,CAAC,EAAE,IAAI,GAAG;AACrC"}
1
+ {"version":3,"file":"useId.cjs","names":["deterministicId?: string | null | undefined","id: string"],"sources":["../../src/shared/useId.ts"],"sourcesContent":[],"mappings":";;;;;AAKA,IAAI,QAAQ;;;;;;;;;AASZ,SAAgB,MAAMA,iBAA6C,SAAS,QAAQ;AAClF,KAAI,gBACF,QAAO;CAET,IAAIC;CACJ,MAAM,wBAAwB,kEAA4B,EAAE,cAAkB,EAAC;AAM/E,KAAI,sBAAsB,MACxB,MAAK,sBAAsB,OAAO;UAE3B,WAAW,IAClB,MAAK,IAAI,SAAS;KAGlB,MAAK,GAAG,EAAE,OAAO;AAGnB,QAAO,SAAS,GAAG,OAAO,CAAC,EAAE,IAAI,GAAG;AACrC"}
@@ -4,21 +4,20 @@ import * as vue from "vue";
4
4
  //#region src/shared/useId.ts
5
5
  let count = 0;
6
6
  /**
7
- * The `useId` function generates a unique identifier using a provided deterministic ID or a default
8
- * one prefixed with "reka-", or the provided one via `useId` props from `<ConfigProvider>`.
7
+ * The `useId` function generates a unique identifier using a provided deterministic ID,
8
+ * a configured `<ConfigProvider>` ID source, Vue's native `useId`, or a fallback counter.
9
9
  * @param {string | null | undefined} [deterministicId] - The `useId` function you provided takes an
10
10
  * optional parameter `deterministicId`, which can be a string, null, or undefined. If
11
11
  * `deterministicId` is provided, the function will return it. Otherwise, it will generate an id using
12
- * the `useId` function obtained
12
+ * the configured ID source.
13
13
  */
14
14
  function useId(deterministicId, prefix = "reka") {
15
15
  if (deterministicId) return deterministicId;
16
16
  let id;
17
- if ("useId" in vue) id = vue.useId?.();
18
- else {
19
- const configProviderContext = injectConfigProviderContext({ useId: void 0 });
20
- id = configProviderContext.useId?.() ?? `${++count}`;
21
- }
17
+ const configProviderContext = injectConfigProviderContext({ useId: void 0 });
18
+ if (configProviderContext.useId) id = configProviderContext.useId();
19
+ else if ("useId" in vue) id = vue.useId?.();
20
+ else id = `${++count}`;
22
21
  return prefix ? `${prefix}-${id}` : id;
23
22
  }
24
23
 
@@ -1 +1 @@
1
- {"version":3,"file":"useId.js","names":["deterministicId?: string | null | undefined","id: string"],"sources":["../../src/shared/useId.ts"],"sourcesContent":[],"mappings":";;;;AAKA,IAAI,QAAQ;;;;;;;;;AASZ,SAAgB,MAAMA,iBAA6C,SAAS,QAAQ;AAClF,KAAI,gBACF,QAAO;CAET,IAAIC;AACJ,KAAI,WAAW,IACb,MAAK,IAAI,SAAS;MAEf;EACH,MAAM,wBAAwB,4BAA4B,EAAE,cAAkB,EAAC;AAC/E,OAAK,sBAAsB,SAAS,IAAI,GAAG,EAAE,OAAO;CACrD;AAED,QAAO,SAAS,GAAG,OAAO,CAAC,EAAE,IAAI,GAAG;AACrC"}
1
+ {"version":3,"file":"useId.js","names":["deterministicId?: string | null | undefined","id: string"],"sources":["../../src/shared/useId.ts"],"sourcesContent":[],"mappings":";;;;AAKA,IAAI,QAAQ;;;;;;;;;AASZ,SAAgB,MAAMA,iBAA6C,SAAS,QAAQ;AAClF,KAAI,gBACF,QAAO;CAET,IAAIC;CACJ,MAAM,wBAAwB,4BAA4B,EAAE,cAAkB,EAAC;AAM/E,KAAI,sBAAsB,MACxB,MAAK,sBAAsB,OAAO;UAE3B,WAAW,IAClB,MAAK,IAAI,SAAS;KAGlB,MAAK,GAAG,EAAE,OAAO;AAGnB,QAAO,SAAS,GAAG,OAAO,CAAC,EAAE,IAAI,GAAG;AACrC"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "reka-ui",
3
3
  "type": "module",
4
- "version": "2.9.9",
4
+ "version": "2.9.10",
5
5
  "description": "Vue port for Radix UI Primitives.",
6
6
  "author": "UnoVue Contributors (https://github.com/unovue)",
7
7
  "license": "MIT",
@@ -112,14 +112,15 @@
112
112
  "@types/jsdom": "^28.0.0",
113
113
  "@types/node": "^24.0.13",
114
114
  "@vitejs/plugin-vue": "^6.0.7",
115
- "@vitest/coverage-istanbul": "^3.2.4",
116
- "@vue/test-utils": "^2.4.10",
115
+ "@vitest/coverage-istanbul": "^3.2.6",
116
+ "@vue/server-renderer": "^3.5.17",
117
+ "@vue/test-utils": "^2.4.11",
117
118
  "@vue/tsconfig": "^0.7.0",
118
119
  "jsdom": "^26.1.0",
119
120
  "size-limit": "^12.0.1",
120
121
  "tsdown": "^0.12.9",
121
- "vite": "^8.0.14",
122
- "vitest": "^4.1.0",
122
+ "vite": "^8.0.16",
123
+ "vitest": "^4.1.8",
123
124
  "vitest-axe": "0.1.0",
124
125
  "vitest-canvas-mock": "^0.3.3",
125
126
  "vue": "3.5.17",
@@ -97,6 +97,19 @@ onUnmounted(() => {
97
97
  rootContext.triggerElement.value?.focus()
98
98
  }
99
99
  })
100
+
101
+ function isEventTargetWithinCombobox(target: EventTarget | null) {
102
+ if (rootContext.parentElement.value?.contains(target as Node))
103
+ return true
104
+
105
+ // A `<label>` associated (via `for`) with an element inside the combobox forwards its
106
+ // click/focus to that control, so interacting with it should not dismiss the content.
107
+ // Without this, clicking such a label while open dismisses on `pointerdown` and the
108
+ // forwarded click/focus immediately re-opens it.
109
+ const label = target instanceof Element ? target.closest('label') : null
110
+ const control = label?.control
111
+ return !!control && !!rootContext.parentElement.value?.contains(control)
112
+ }
100
113
  </script>
101
114
 
102
115
  <template>
@@ -111,15 +124,15 @@ onUnmounted(() => {
111
124
  :disable-outside-pointer-events="disableOutsidePointerEvents"
112
125
  @dismiss="rootContext.onOpenChange(false)"
113
126
  @focus-outside="(ev) => {
114
- // if clicking inside the combobox, prevent dismiss
115
- if (rootContext.parentElement.value?.contains(ev.target as Node)) ev.preventDefault()
127
+ // if focusing inside the combobox (or a label tied to it), prevent dismiss
128
+ if (isEventTargetWithinCombobox(ev.target)) ev.preventDefault()
116
129
  emits('focusOutside', ev)
117
130
  }"
118
131
  @interact-outside="emits('interactOutside', $event)"
119
132
  @escape-key-down="emits('escapeKeyDown', $event)"
120
133
  @pointer-down-outside="(ev) => {
121
- // if clicking inside the combobox, prevent dismiss
122
- if (rootContext.parentElement.value?.contains(ev.target as Node)) ev.preventDefault()
134
+ // if clicking inside the combobox (or a label tied to it), prevent dismiss
135
+ if (isEventTargetWithinCombobox(ev.target)) ev.preventDefault()
123
136
  emits('pointerDownOutside', ev)
124
137
  }"
125
138
  >
@@ -22,7 +22,13 @@ import DialogContentModal from './DialogContentModal.vue'
22
22
  import DialogContentNonModal from './DialogContentNonModal.vue'
23
23
  import { injectDialogRootContext } from './DialogRoot.vue'
24
24
 
25
- const props = defineProps<DialogContentProps>()
25
+ const props = withDefaults(defineProps<DialogContentProps>(), {
26
+ // Keep `undefined` (instead of Vue's boolean coercion to `false`) so the
27
+ // modal/non-modal child can apply its own default. This lets a modal
28
+ // `DialogContent` stay locked by default while still honoring an explicit
29
+ // `:disable-outside-pointer-events="false"` (#2677).
30
+ disableOutsidePointerEvents: undefined,
31
+ })
26
32
  const emits = defineEmits<DialogContentEmits>()
27
33
 
28
34
  const rootContext = injectDialogRootContext()
@@ -4,7 +4,9 @@ import { useEmitAsProps, useForwardExpose, useHideOthers } from '@/shared'
4
4
  import DialogContentImpl from './DialogContentImpl.vue'
5
5
  import { injectDialogRootContext } from './DialogRoot.vue'
6
6
 
7
- const props = defineProps<DialogContentImplProps>()
7
+ const props = withDefaults(defineProps<DialogContentImplProps>(), {
8
+ disableOutsidePointerEvents: true,
9
+ })
8
10
  const emits = defineEmits<DialogContentImplEmits>()
9
11
 
10
12
  const rootContext = injectDialogRootContext()
@@ -20,7 +22,7 @@ useHideOthers(currentElement)
20
22
  v-bind="{ ...props, ...emitsAsProps }"
21
23
  :ref="forwardRef"
22
24
  :trap-focus="rootContext.open.value"
23
- :disable-outside-pointer-events="true"
25
+ :disable-outside-pointer-events="props.disableOutsidePointerEvents"
24
26
  @close-auto-focus="
25
27
  (event) => {
26
28
  if (!event.defaultPrevented) {
@@ -9,6 +9,7 @@ import {
9
9
  computed,
10
10
  nextTick,
11
11
  reactive,
12
+ watch,
12
13
  watchEffect,
13
14
  } from 'vue'
14
15
  import { isNullish, useForwardExpose } from '@/shared'
@@ -138,28 +139,45 @@ onKeyStroke('Escape', (event) => {
138
139
  emits('dismiss')
139
140
  })
140
141
 
141
- watchEffect((cleanupFn) => {
142
- if (!layerElement.value)
143
- return
144
- if (props.disableOutsidePointerEvents) {
145
- if (context.layersWithOutsidePointerEventsDisabled.size === 0) {
146
- context.originalBodyPointerEvents = ownerDocument.value.body.style.pointerEvents
147
- ownerDocument.value.body.style.pointerEvents = 'none'
148
- }
149
- context.layersWithOutsidePointerEventsDisabled.add(layerElement.value)
150
- }
151
- layers.value.add(layerElement.value)
152
-
153
- cleanupFn(() => {
154
- if (
155
- props.disableOutsidePointerEvents
156
- && context.layersWithOutsidePointerEventsDisabled.size === 1
157
- && !isNullish(context.originalBodyPointerEvents)
158
- ) {
159
- ownerDocument.value.body.style.pointerEvents = context.originalBodyPointerEvents
142
+ // Use `watch` with explicit sources (instead of `watchEffect`) so this effect
143
+ // only re-runs when `layerElement` or `disableOutsidePointerEvents` change.
144
+ // Reading `context.layersWithOutsidePointerEventsDisabled.size` inside the
145
+ // callback must NOT make it reactive: otherwise adding/removing any other
146
+ // layer would re-run this effect and its cleanup could prematurely restore the
147
+ // body's `pointer-events` while an ancestor layer is still open (#2674).
148
+ watch(
149
+ [layerElement, () => props.disableOutsidePointerEvents],
150
+ ([element, disableOutsidePointerEvents], _, onCleanup) => {
151
+ if (!element)
152
+ return
153
+ if (disableOutsidePointerEvents) {
154
+ if (context.layersWithOutsidePointerEventsDisabled.size === 0) {
155
+ context.originalBodyPointerEvents = ownerDocument.value.body.style.pointerEvents
156
+ ownerDocument.value.body.style.pointerEvents = 'none'
157
+ }
158
+ context.layersWithOutsidePointerEventsDisabled.add(element)
159
+
160
+ // Remove this layer from the set on cleanup (re-run via prop toggle, or
161
+ // unmount) and restore the body's `pointer-events` only once the last
162
+ // disabling layer is gone. Removing here — rather than relying solely on
163
+ // the unmount-only effect below — keeps the set accurate when
164
+ // `disableOutsidePointerEvents` toggles `true -> false` while still
165
+ // mounted (e.g. a modal Menu closing). Checking `size === 0` *after*
166
+ // deletion makes the restore independent of cleanup ordering (#2674).
167
+ onCleanup(() => {
168
+ context.layersWithOutsidePointerEventsDisabled.delete(element)
169
+ if (
170
+ context.layersWithOutsidePointerEventsDisabled.size === 0
171
+ && !isNullish(context.originalBodyPointerEvents)
172
+ ) {
173
+ ownerDocument.value.body.style.pointerEvents = context.originalBodyPointerEvents
174
+ }
175
+ })
160
176
  }
161
- })
162
- })
177
+ layers.value.add(element)
178
+ },
179
+ { immediate: true },
180
+ )
163
181
 
164
182
  watchEffect((cleanupFn) => {
165
183
  cleanupFn(() => {
@@ -16,7 +16,7 @@ type ListboxRootContext<T> = {
16
16
  highlightOnHover: Ref<boolean>
17
17
  highlightedElement: Ref<HTMLElement | null>
18
18
  isVirtual: Ref<boolean>
19
- virtualFocusHook: EventHook<Event | null | undefined>
19
+ virtualFocusHook: EventHook<{ event?: Event, scroll: boolean }>
20
20
  virtualKeydownHook: EventHook<KeyboardEvent>
21
21
  virtualHighlightHook: EventHook<any>
22
22
  by?: string | ((a: T, b: T) => boolean)
@@ -150,7 +150,7 @@ const highlightedElement = ref<HTMLElement | null>(null)
150
150
  const previousElement = ref<HTMLElement | null>(null)
151
151
  const isVirtual = ref(false)
152
152
  const isComposing = ref(false)
153
- const virtualFocusHook = createEventHook<Event | null | undefined>()
153
+ const virtualFocusHook = createEventHook<{ event?: Event, scroll: boolean }>()
154
154
  const virtualKeydownHook = createEventHook<KeyboardEvent>()
155
155
  const virtualHighlightHook = createEventHook<T>()
156
156
 
@@ -333,8 +333,11 @@ async function highlightSelected(event?: Event, scroll = true) {
333
333
  return
334
334
  await nextTick()
335
335
  if (isVirtual.value) {
336
- // Trigger on nextTick for Virtualizer to be mounted
337
- virtualFocusHook.trigger(event)
336
+ // Trigger on nextTick for Virtualizer to be mounted.
337
+ // `scroll` is `false` on the initial mount highlight, so the virtualizer sets
338
+ // its roving-tabindex target without focusing/scrolling — otherwise a
339
+ // virtualized Listbox below the fold would pull the page to it on load.
340
+ virtualFocusHook.trigger({ event, scroll })
338
341
  }
339
342
  else {
340
343
  const collection = getCollectionItem()
@@ -104,7 +104,11 @@ const virtualizedItems = computed(() => virtualizer.value.getVirtualItems().map(
104
104
  }
105
105
  }))
106
106
 
107
- rootContext.virtualFocusHook.on((event) => {
107
+ rootContext.virtualFocusHook.on(({ event, scroll }) => {
108
+ // `scroll` is `false` only for the initial mount highlight. There we set the
109
+ // roving-tabindex target without focusing or scrolling, so a virtualized
110
+ // Listbox below the fold doesn't pull the page to it on load. User-driven
111
+ // highlights (keyboard, typeahead, select) keep scrolling as before.
108
112
  const index = props.options.findIndex((option) => {
109
113
  if (Array.isArray(rootContext.modelValue.value))
110
114
  return compare(option, rootContext.modelValue.value[0], rootContext.by)
@@ -114,19 +118,31 @@ rootContext.virtualFocusHook.on((event) => {
114
118
  if (index !== -1) {
115
119
  event?.preventDefault()
116
120
 
121
+ // Bringing the checked item into the (internal) scroll viewport is safe — it
122
+ // only scrolls the listbox container, never the page.
117
123
  virtualizer.value.scrollToIndex(index, { align: 'start' })
118
124
  requestAnimationFrame(() => {
119
125
  const item = queryCheckedElement(parentEl.value)
120
126
  if (item) {
121
- rootContext.changeHighlight(item)
127
+ rootContext.changeHighlight(item, scroll, scroll ? undefined : false)
122
128
  if (event)
123
129
  item?.focus()
124
130
  }
125
131
  })
126
132
  }
127
- else {
133
+ else if (scroll) {
128
134
  rootContext.highlightFirstItem()
129
135
  }
136
+ else {
137
+ // Mount highlight with no checked item: highlight the first enabled item only,
138
+ // mirroring the non-virtual path. `highlightFirstItem` is reserved for
139
+ // user-driven PageUp/Home navigation, which focuses and scrolls.
140
+ requestAnimationFrame(() => {
141
+ const item = getItems().find(i => i.ref.dataset.disabled !== '')?.ref
142
+ if (item)
143
+ rootContext.changeHighlight(item, false, false)
144
+ })
145
+ }
130
146
  })
131
147
 
132
148
  rootContext.virtualHighlightHook.on((value) => {
@@ -5,24 +5,32 @@ import { injectConfigProviderContext } from '@/ConfigProvider/ConfigProvider.vue
5
5
 
6
6
  let count = 0
7
7
  /**
8
- * The `useId` function generates a unique identifier using a provided deterministic ID or a default
9
- * one prefixed with "reka-", or the provided one via `useId` props from `<ConfigProvider>`.
8
+ * The `useId` function generates a unique identifier using a provided deterministic ID,
9
+ * a configured `<ConfigProvider>` ID source, Vue's native `useId`, or a fallback counter.
10
10
  * @param {string | null | undefined} [deterministicId] - The `useId` function you provided takes an
11
11
  * optional parameter `deterministicId`, which can be a string, null, or undefined. If
12
12
  * `deterministicId` is provided, the function will return it. Otherwise, it will generate an id using
13
- * the `useId` function obtained
13
+ * the configured ID source.
14
14
  */
15
15
  export function useId(deterministicId?: string | null | undefined, prefix = 'reka') {
16
16
  if (deterministicId)
17
17
  return deterministicId
18
18
 
19
19
  let id: string
20
- if ('useId' in vue) {
20
+ const configProviderContext = injectConfigProviderContext({ useId: undefined })
21
+
22
+ // Keep the app-provided ID source authoritative. Frameworks such as Nuxt can
23
+ // prerender with a different Vue app ID prefix than the hydrating client, so
24
+ // falling through to Vue's native useId would bypass the stable source that
25
+ // ConfigProvider was explicitly given.
26
+ if (configProviderContext.useId) {
27
+ id = configProviderContext.useId()
28
+ }
29
+ else if ('useId' in vue) {
21
30
  id = vue.useId?.()
22
31
  }
23
32
  else {
24
- const configProviderContext = injectConfigProviderContext({ useId: undefined })
25
- id = configProviderContext.useId?.() ?? `${++count}`
33
+ id = `${++count}`
26
34
  }
27
35
 
28
36
  return prefix ? `${prefix}-${id}` : id