ropav 0.12.0 → 0.12.2

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 (61) hide show
  1. package/AGENTS.md +150 -0
  2. package/README.md +5 -1
  3. package/dist/components/breadcrumbs/breadcrumbs-item.js +2 -0
  4. package/dist/components/calendar-year-picker/calendar-year-picker-grid.js +1 -0
  5. package/dist/components/checkbox/checkbox-root.js +2 -0
  6. package/dist/components/combo-box/combo-box-root.js +1 -0
  7. package/dist/components/date-input-group/date-input-group-hidden-input.js +2 -1
  8. package/dist/components/date-picker/date-picker-root.js +1 -0
  9. package/dist/components/date-range-picker/date-range-picker-root.js +1 -0
  10. package/dist/components/form/form-root.js +7 -1
  11. package/dist/components/radio-group/radio-group-root.js +2 -1
  12. package/dist/components/search-field/search-field-group.js +21 -1
  13. package/dist/components/select/select-root.js +1 -0
  14. package/dist/components/splitter/splitter.state.js +6 -1
  15. package/dist/components/switch/switch-root.js +2 -0
  16. package/dist/components/table/table-body.js +1 -1
  17. package/dist/components/table/table-cell.js +3 -1
  18. package/dist/components/table/table-column-resizer.js +2 -1
  19. package/dist/components/table/table-load-more.js +1 -1
  20. package/dist/components/table/use-grid-selection-announcement.js +3 -1
  21. package/dist/components/table/use-table-collection.js +3 -0
  22. package/dist/components/tabs/tabs-list-container.js +2 -1
  23. package/dist/composables/use-calendar-grid.js +2 -1
  24. package/dist/composables/use-checkbox-group-state.js +2 -0
  25. package/dist/composables/use-collection.js +12 -0
  26. package/dist/composables/use-color-field-state.js +1 -0
  27. package/dist/composables/use-color-field.js +31 -29
  28. package/dist/composables/use-combo-box-state.d.ts +2 -0
  29. package/dist/composables/use-combo-box-state.js +5 -2
  30. package/dist/composables/use-combo-box.js +2 -1
  31. package/dist/composables/use-date-field-state.js +1 -0
  32. package/dist/composables/use-date-picker-state.d.ts +2 -0
  33. package/dist/composables/use-date-picker-state.js +1 -0
  34. package/dist/composables/use-date-range-picker-state.d.ts +2 -0
  35. package/dist/composables/use-date-range-picker-state.js +2 -0
  36. package/dist/composables/use-description.js +1 -0
  37. package/dist/composables/use-disclosure-panel.js +2 -1
  38. package/dist/composables/use-drag.js +9 -6
  39. package/dist/composables/use-drop.js +2 -1
  40. package/dist/composables/use-droppable-collection.js +2 -1
  41. package/dist/composables/use-form-validation-state.d.ts +48 -5
  42. package/dist/composables/use-form-validation-state.js +76 -7
  43. package/dist/composables/use-input-otp.js +5 -2
  44. package/dist/composables/use-move.js +2 -1
  45. package/dist/composables/use-number-field-state.js +1 -0
  46. package/dist/composables/use-radio-group-state.js +2 -0
  47. package/dist/composables/use-select-state.d.ts +2 -0
  48. package/dist/composables/use-select-state.js +2 -0
  49. package/dist/composables/use-single-select-list-state.js +1 -0
  50. package/dist/composables/use-text-field.js +1 -0
  51. package/dist/composables/use-time-field-state.js +2 -1
  52. package/dist/composables/use-toolbar.js +3 -1
  53. package/dist/composables/use-virtualizer.js +1 -0
  54. package/dist/index.js +2 -2
  55. package/dist/ropav.min.css +1 -1
  56. package/dist/utils/create-context.js +2 -1
  57. package/dist/utils/platform.js +2 -1
  58. package/dist/utils/position.js +4 -2
  59. package/dist/version.js +1 -1
  60. package/package.json +4 -7
  61. package/dist/styles-compat-0.10.css +0 -3
package/AGENTS.md ADDED
@@ -0,0 +1,150 @@
1
+ # Writing Ropav code
2
+
3
+ Ropav components are compound: a family root provides context, and named parts arrange inside it.
4
+ The arrangement is the API, and it is not guessable from the names. This file is the short list of
5
+ rules that go wrong most often. For a worked arrangement and the props of every family, fetch
6
+ <https://ropav.netlify.app/llms-full.txt> — one file, the whole package.
7
+
8
+ ## Importing
9
+
10
+ ```ts
11
+ import { Button, Select, SelectPopover } from "ropav";
12
+ ```
13
+
14
+ The published package also carries one subpath per family — `import { Button } from "ropav/button"` —
15
+ so a bundler drops what is never imported. The stylesheet is a separate import:
16
+
17
+ ```ts
18
+ import "ropav/styles/bundled.css";
19
+ ```
20
+
21
+ Every component takes `class`, `aria-label`, `aria-labelledby` and `aria-describedby`. Props are
22
+ camelCase in the types and kebab-case in a template: `itemTextValue` is written `item-text-value`.
23
+
24
+ ## The rules
25
+
26
+ **1. A part only works inside its root.** Contexts are strict, so a part rendered outside its
27
+ family throws during setup rather than rendering something degraded. There is no standalone
28
+ `SelectTrigger`.
29
+
30
+ **2. Collections arrive as data, not as children.** `Select` takes a required `items` array — it
31
+ does not read its options by walking children, because a closed select has rendered none.
32
+ `item-text-value` is the string typeahead matches; `item-key` is the identity.
33
+
34
+ ```vue
35
+ <Select :item-text-value="(city) => city.name" :items="cities" placeholder="Pick a city">
36
+ <Label>Destination</Label>
37
+ <SelectTrigger>
38
+ <SelectValue />
39
+ <SelectIndicator />
40
+ </SelectTrigger>
41
+ <SelectPopover>
42
+ <ListBox>
43
+ <ListBoxItem v-for="city in cities" :id="city.id" :key="city.id" :text-value="city.name">
44
+ {{ city.name }}
45
+ <ListBoxItemIndicator />
46
+ </ListBoxItem>
47
+ </ListBox>
48
+ </SelectPopover>
49
+ </Select>
50
+ ```
51
+
52
+ **3. Overlays take a positional trigger.** `Modal`, `Popover`, `Dropdown`, `Drawer` and
53
+ `AlertDialog` make their *first child* the trigger. There is no trigger slot.
54
+
55
+ ```vue
56
+ <Modal>
57
+ <Button>Open</Button>
58
+ <ModalBackdrop>
59
+ <ModalContainer>
60
+ <ModalDialog>
61
+ <ModalHeader>
62
+ <ModalHeading>Delete workspace</ModalHeading>
63
+ </ModalHeader>
64
+ <ModalBody>This cannot be undone.</ModalBody>
65
+ </ModalDialog>
66
+ </ModalContainer>
67
+ </ModalBackdrop>
68
+ </Modal>
69
+ ```
70
+
71
+ **4. The part holding the state is not always the root.** `Table` takes `class` and `variant` and
72
+ nothing else; `TableContent` owns `aria-label`, `selection-mode`, `v-model:selected-keys` and
73
+ `v-model:sort-descriptor`. Putting them on `Table` compiles and does nothing.
74
+
75
+ ```vue
76
+ <Table>
77
+ <TableScrollContainer>
78
+ <TableContent aria-label="Members" selection-mode="multiple">
79
+ <TableHeader>
80
+ <TableColumn id="name" is-row-header>Member</TableColumn>
81
+ </TableHeader>
82
+ <TableBody>
83
+ <TableRow id="1">
84
+ <TableCell>Ada Lovelace</TableCell>
85
+ </TableRow>
86
+ </TableBody>
87
+ </TableContent>
88
+ </TableScrollContainer>
89
+ </Table>
90
+ ```
91
+
92
+ **5. An indicator belongs to its item.** `TabsIndicator` goes inside each `TabsTab`, not beside the
93
+ list. Same for `ListBoxItemIndicator` inside `ListBoxItem` and `MenuItemIndicator` inside
94
+ `MenuItem`.
95
+
96
+ ```vue
97
+ <Tabs>
98
+ <TabsListContainer>
99
+ <TabsList aria-label="Views">
100
+ <TabsTab id="all">
101
+ All
102
+ <TabsIndicator />
103
+ </TabsTab>
104
+ </TabsList>
105
+ </TabsListContainer>
106
+ <TabsPanel id="all">Everything.</TabsPanel>
107
+ </Tabs>
108
+ ```
109
+
110
+ **6. Menu parts are shared.** There is no `DropdownItem` export — compose `MenuItem` inside
111
+ `DropdownMenu`. The stories alias the name for brevity, so markup copied from one will not compile.
112
+
113
+ ```vue
114
+ <Dropdown>
115
+ <Button>Actions</Button>
116
+ <DropdownPopover>
117
+ <DropdownMenu>
118
+ <MenuItem id="rename" text-value="Rename">
119
+ <Label>Rename</Label>
120
+ </MenuItem>
121
+ </DropdownMenu>
122
+ </DropdownPopover>
123
+ </Dropdown>
124
+ ```
125
+
126
+ **7. `Menu` is the open list; `Dropdown` is that menu in a popover.** A bare `Menu` needs its own
127
+ `Surface` to have a background and a border. A `Dropdown` brings one.
128
+
129
+ **8. Every collection item needs an `id`**, and a `text-value` whenever its content is not plain
130
+ text. `id` is what a selection or an action event reports; `text-value` is what typeahead matches.
131
+
132
+ ## Restyling
133
+
134
+ Every component takes a `class`, and every colour, radius and curve is a custom property, so a
135
+ change is a CSS change rather than a fork. State is published on data attributes — `data-hovered`,
136
+ `data-pressed`, `data-selected`, `data-focus-visible` — which is how a single state is retuned
137
+ without a wrapper.
138
+
139
+ ```css
140
+ .thing {
141
+ background-color: var(--rp-accent);
142
+ color: var(--rp-accent-foreground);
143
+ }
144
+ ```
145
+
146
+ ## Where to look next
147
+
148
+ - <https://ropav.netlify.app/llms-full.txt> — every family's arrangement and props, in one file.
149
+ - <https://ropav.netlify.app/llms.txt> — the index, when you want a specific page.
150
+ - <https://ropav.netlify.app/patterns/> — whole screen fragments to copy.
package/README.md CHANGED
@@ -5,7 +5,11 @@ A Vue 3 component library built on [Vue Vapor](https://github.com/vuejs/core-vap
5
5
  [`ropav`](https://www.npmjs.com/package/ropav).
6
6
 
7
7
  **[Documentation](https://ropav.netlify.app)** · [Components](https://ropav.netlify.app/components/)
8
- · [Theming](https://ropav.netlify.app/theming/) · [Storybook](https://ropav-storybook.netlify.app)
8
+ · [Patterns](https://ropav.netlify.app/patterns/) · [Theming](https://ropav.netlify.app/theming/)
9
+ · [Storybook](https://ropav-storybook.netlify.app)
10
+
11
+ Writing this with a coding agent: point it at [`AGENTS.md`](./AGENTS.md), which ships in the
12
+ package, or at [llms-full.txt](https://ropav.netlify.app/llms-full.txt) for every component at once.
9
13
 
10
14
  > Ropav is a port of [HeroUI v3](https://github.com/heroui-inc/heroui) to Vue Vapor: the style layer is
11
15
  > vendored from `@heroui/styles@3.2.4`, the behaviour layer is re-implemented from React Aria.
@@ -54,6 +54,8 @@ var breadcrumbs_item_vue_vue_type_script_setup_true_vapor_true_lang_default = /*
54
54
  immediate: true
55
55
  });
56
56
  const isCurrent = computed(() => {
57
+ context.collection.size.value;
58
+ context.orderVersion.value;
57
59
  return context.collection.getLastKey() === itemKey.value;
58
60
  });
59
61
  const isDisabled = computed(() => Boolean(props.isDisabled || context.isDisabled.value || isCurrent.value));
@@ -55,6 +55,7 @@ var calendar_year_picker_grid_vue_vue_type_script_setup_true_vapor_true_lang_def
55
55
  const grid = element.value;
56
56
  const calendar = calendarElement.value;
57
57
  if (!grid || !calendar) return;
58
+ state.focusedDate.value;
58
59
  const dayGrid = calendar.querySelector(`[data-slot='${calendarGridSlot}']`);
59
60
  if (!dayGrid) return;
60
61
  grid.style.top = `${dayGrid.offsetTop}px`;
@@ -81,7 +81,9 @@ var checkbox_root_vue_vue_type_script_setup_true_vapor_true_lang_default = /*@__
81
81
  const styles = computed(() => checkboxVariants({ variant: resolvedVariant.value }));
82
82
  const ownValidation = useFormValidationState({
83
83
  isInvalid: () => props.isInvalid,
84
+ isRequired: resolvedIsRequired,
84
85
  name: () => props.name,
86
+ requiredControl: "checkbox",
85
87
  validate: () => props.validate,
86
88
  validationBehavior: () => props.validationBehavior,
87
89
  value: () => isSelected.value
@@ -115,6 +115,7 @@ var combo_box_root_vue_vue_type_script_setup_true_vapor_true_lang_default = /*@_
115
115
  inputValue: () => props.inputValue,
116
116
  isInvalid: () => props.isInvalid,
117
117
  isReadOnly: () => props.isReadOnly,
118
+ isRequired: () => props.isRequired,
118
119
  itemDisabled: props.itemDisabled,
119
120
  itemKey: props.itemKey,
120
121
  items: () => props.items,
@@ -60,7 +60,8 @@ var date_input_group_hidden_input_vue_vue_type_script_setup_true_vapor_true_lang
60
60
  const text = event.target.value;
61
61
  if (!text) return;
62
62
  const granularity = state.granularity.value;
63
- const times = TIME_SEGMENTS.slice(0, granularity === "day" ? 0 : GRANULARITY_DEPTH[granularity]);
63
+ const depth = granularity === "day" ? 0 : GRANULARITY_DEPTH[granularity];
64
+ const times = TIME_SEGMENTS.slice(0, depth);
64
65
  try {
65
66
  const parsed = granularity === "day" ? parseDate(text) : parseDateTime(text);
66
67
  for (const segment of DATE_SEGMENTS) if (segment in parsed) state.setSegment(segment, parsed[segment]);
@@ -99,6 +99,7 @@ var date_picker_root_vue_vue_type_script_setup_true_vapor_true_lang_default = /*
99
99
  isDateUnavailable: props.isDateUnavailable,
100
100
  isInvalid: () => props.isInvalid,
101
101
  isOpen: () => props.isOpen,
102
+ isRequired: () => props.isRequired,
102
103
  maxValue: () => props.maxValue,
103
104
  minValue: () => props.minValue,
104
105
  name: () => props.name,
@@ -102,6 +102,7 @@ var date_range_picker_root_vue_vue_type_script_setup_true_vapor_true_lang_defaul
102
102
  isDateUnavailable: props.isDateUnavailable,
103
103
  isInvalid: () => props.isInvalid,
104
104
  isOpen: () => props.isOpen,
105
+ isRequired: () => props.isRequired,
105
106
  maxValue: () => props.maxValue,
106
107
  minValue: () => props.minValue,
107
108
  onChange: (value) => {
@@ -1,5 +1,5 @@
1
1
  import { provideFormContext } from "../../composables/use-form-validation-state.js";
2
- import { computed, createSlot, defineVaporComponent, renderEffect, setClass, setInsertionState, setProp, template } from "vue";
2
+ import { computed, createSlot, defineVaporComponent, on, renderEffect, setClass, setInsertionState, setProp, shallowRef, template } from "vue";
3
3
  //#region src/components/form/form-root.vue?vue&type=script&setup=true&vapor=true&lang.ts
4
4
  var t0 = template("<form>", 1);
5
5
  var form_root_vue_vue_type_script_setup_true_vapor_true_lang_default = /*@__PURE__*/ defineVaporComponent({
@@ -11,7 +11,12 @@ var form_root_vue_vue_type_script_setup_true_vapor_true_lang_default = /*@__PURE
11
11
  },
12
12
  setup(__props) {
13
13
  const props = __props;
14
+ const submitCount = shallowRef(0);
15
+ const onSubmit = () => {
16
+ submitCount.value += 1;
17
+ };
14
18
  provideFormContext({
19
+ submitCount,
15
20
  validationBehavior: computed(() => props.validationBehavior ?? "native"),
16
21
  validationErrors: computed(() => props.validationErrors ?? {})
17
22
  });
@@ -23,6 +28,7 @@ var form_root_vue_vue_type_script_setup_true_vapor_true_lang_default = /*@__PURE
23
28
  });
24
29
  setInsertionState(n1);
25
30
  createSlot();
31
+ on(n1, "submit", onSubmit);
26
32
  return n1;
27
33
  }
28
34
  });
@@ -101,7 +101,8 @@ var radio_group_root_vue_vue_type_script_setup_true_vapor_true_lang_default = /*
101
101
  const onKeydown = (event) => {
102
102
  const isHorizontalFlip = locale.value.direction === "rtl" && resolvedOrientation.value !== "vertical";
103
103
  const isNext = event.key === "ArrowDown" || event.key === (isHorizontalFlip ? "ArrowLeft" : "ArrowRight");
104
- if (!isNext && !(event.key === "ArrowUp" || event.key === (isHorizontalFlip ? "ArrowRight" : "ArrowLeft"))) return;
104
+ const isPrevious = event.key === "ArrowUp" || event.key === (isHorizontalFlip ? "ArrowRight" : "ArrowLeft");
105
+ if (!isNext && !isPrevious) return;
105
106
  if (state.isDisabled.value || state.isReadOnly.value) return;
106
107
  const radios = focusableRadios();
107
108
  if (radios.length === 0) return;
@@ -1,8 +1,9 @@
1
1
  import { dataAttr } from "../../utils/assertion.js";
2
2
  import { useFocusWithin, useInteractionStates } from "../../composables/use-interaction-states.js";
3
+ import { isNestedControl } from "../../utils/focus.js";
3
4
  import { useTextFieldControlContext } from "../../composables/use-text-field.js";
4
5
  import { useSearchFieldContext } from "./search-field.context.js";
5
- import { computed, createSlot, defineVaporComponent, on, renderEffect, setAttr, setClass, setInsertionState, template, unref } from "vue";
6
+ import { computed, createSlot, defineVaporComponent, on, renderEffect, setAttr, setClass, setInsertionState, setTemplateRefBinding, shallowRef, template, unref } from "vue";
6
7
  //#region src/components/search-field/search-field-group.vue?vue&type=script&setup=true&vapor=true&lang.ts
7
8
  var t0 = template("<div data-slot=search-field-group role=group>", 1);
8
9
  var search_field_group_vue_vue_type_script_setup_true_vapor_true_lang_default = /*@__PURE__*/ defineVaporComponent({
@@ -12,11 +13,28 @@ var search_field_group_vue_vue_type_script_setup_true_vapor_true_lang_default =
12
13
  const props = __props;
13
14
  const { slots } = useSearchFieldContext();
14
15
  const control = useTextFieldControlContext();
16
+ const element = shallowRef(null);
17
+ const setElement = (next) => {
18
+ element.value = next instanceof HTMLDivElement ? next : null;
19
+ };
15
20
  const styles = computed(() => slots.value.group({ class: props.class }));
16
21
  const isDisabled = computed(() => control?.isDisabled.value ?? false);
17
22
  const isInvalid = computed(() => control?.isInvalid.value ?? false);
18
23
  const interaction = useInteractionStates({ isDisabled });
19
24
  const focusWithin = useFocusWithin();
25
+ /**
26
+ * The clear button keeps its place in the layout while the field is empty — the stylesheet only
27
+ * fades it out and takes it out of hit-testing — so a click aimed at it lands on the group, and
28
+ * the caret would otherwise stay wherever it was. The search icon is unhittable for the same
29
+ * reason, and so is the padding either side of the control.
30
+ *
31
+ * A click that did reach a control belongs to that control: the browser has already put the caret
32
+ * in the input, and the clear button takes focus back on the way down by itself.
33
+ */
34
+ const onClick = (event) => {
35
+ if (isNestedControl(event.target)) return;
36
+ element.value?.querySelector("input")?.focus();
37
+ };
20
38
  const n1 = t0();
21
39
  renderEffect(() => {
22
40
  const _dataAttr = unref(dataAttr);
@@ -36,10 +54,12 @@ var search_field_group_vue_vue_type_script_setup_true_vapor_true_lang_default =
36
54
  isHovered: () => unref(interaction).isHovered.value,
37
55
  isInvalid: () => isInvalid.value
38
56
  });
57
+ on(n1, "click", onClick);
39
58
  on(n1, "focusin", (e) => unref(focusWithin).onFocusin(e));
40
59
  on(n1, "focusout", (e) => unref(focusWithin).onFocusout(e));
41
60
  on(n1, "pointerenter", (e) => unref(interaction).onPointerenter(e));
42
61
  on(n1, "pointerleave", (e) => unref(interaction).onPointerleave(e));
62
+ setTemplateRefBinding(n1, () => setElement);
43
63
  return n1;
44
64
  }
45
65
  });
@@ -88,6 +88,7 @@ var select_root_vue_vue_type_script_setup_true_vapor_true_lang_default = /*@__PU
88
88
  disabledKeys: () => props.disabledKeys,
89
89
  isInvalid: () => props.isInvalid,
90
90
  isOpen: () => props.isOpen,
91
+ isRequired: () => props.isRequired,
91
92
  itemDisabled: props.itemDisabled,
92
93
  itemKey: props.itemKey,
93
94
  items: () => props.items,
@@ -25,9 +25,11 @@ var registry = () => {
25
25
  const version = shallowRef(0);
26
26
  return {
27
27
  get: (key) => {
28
+ version.value;
28
29
  return items.get(key);
29
30
  },
30
31
  keys: computed(() => {
32
+ version.value;
31
33
  return sortByDocumentOrder([...items.entries()], ([, meta]) => meta.element()).map(([key]) => key);
32
34
  }),
33
35
  register: (key, meta) => {
@@ -83,7 +85,8 @@ var useSplitterState = (options) => {
83
85
  const indexOf = (key) => panels.keys.value.indexOf(key);
84
86
  const getPanelSize = (key) => layout.value[indexOf(key)] ?? 0;
85
87
  const boundsOf = (index) => {
86
- const panel = panels.get(panels.keys.value[index]);
88
+ const key = panels.keys.value[index];
89
+ const panel = panels.get(key);
87
90
  const available = availableSize.value;
88
91
  const min = getMinSize(panel.minSize(), available);
89
92
  const shut = panel.isCollapsible() ? getMinSize(panel.collapsedSize() ?? 0, available) : min;
@@ -206,6 +209,8 @@ var useSplitterState = (options) => {
206
209
  };
207
210
  };
208
211
  const neighbourCache = computed(() => {
212
+ panels.keys.value;
213
+ handles.keys.value;
209
214
  return /* @__PURE__ */ new Map();
210
215
  });
211
216
  const neighboursOf = (handleKey) => {
@@ -50,7 +50,9 @@ var switch_root_vue_vue_type_script_setup_true_vapor_true_lang_default = /*@__PU
50
50
  const styles = computed(() => switchVariants({ size: props.size }));
51
51
  const validation = useFormValidationState({
52
52
  isInvalid: () => props.isInvalid,
53
+ isRequired: () => props.isRequired,
53
54
  name: () => props.name,
55
+ requiredControl: "checkbox",
54
56
  validate: () => props.validate,
55
57
  validationBehavior: () => props.validationBehavior,
56
58
  value: () => state.value
@@ -107,7 +107,7 @@ var table_body_vue_vue_type_script_setup_true_vapor_true_lang_default = /*@__PUR
107
107
  return createDynamicComponent(() => unref(virtualizer) ? "div" : "td", {
108
108
  "aria-colspan": () => unref(virtualizer) ? columnCount.value : void 0,
109
109
  colspan: () => unref(virtualizer) ? void 0 : columnCount.value,
110
- role: "rowheader",
110
+ role: "gridcell",
111
111
  style: () => emptyStateStyle.value
112
112
  }, extend(() => {
113
113
  return createSlot("empty", null, null, 4);
@@ -58,7 +58,8 @@ var table_cell_vue_vue_type_script_setup_true_vapor_true_lang_default = /*@__PUR
58
58
  const virtualizer = useTableVirtualizerContext();
59
59
  const layoutInfo = computed(() => {
60
60
  if (!virtualizer || columnKey.value == null) return null;
61
- return virtualizer.getLayoutInfo(virtualizer.collection.value.cellKey(rowKey.value, columnKey.value));
61
+ const collection = virtualizer.collection.value;
62
+ return virtualizer.getLayoutInfo(collection.cellKey(rowKey.value, columnKey.value));
62
63
  });
63
64
  const parentLayoutInfo = computed(() => virtualizer ? virtualizer.getLayoutInfo(rowKey.value) : null);
64
65
  const placement = virtualizer ? useVirtualizerItem({
@@ -69,6 +70,7 @@ var table_cell_vue_vue_type_script_setup_true_vapor_true_lang_default = /*@__PUR
69
70
  const n1 = createDynamicComponent(() => unref(virtualizer) ? "div" : "td", {
70
71
  id: () => cellId.value,
71
72
  "aria-colindex": () => unref(virtualizer) && index.value >= 0 ? index.value + 1 : void 0,
73
+ "aria-label": () => isRowHeader.value ? props.textValue : void 0,
72
74
  class: () => ["rp-table__cell", props.class],
73
75
  "data-collection": () => unref(collectionId),
74
76
  "data-column-index": () => index.value < 0 ? void 0 : index.value,
@@ -103,7 +103,8 @@ var table_column_resizer_vue_vue_type_script_setup_true_vapor_true_lang_default
103
103
  * arrow press does.
104
104
  */
105
105
  const onChange = (event) => {
106
- resize(parseFloat(event.target.value) > columnWidth.value ? columnWidth.value + KEYBOARD_STEP : columnWidth.value - KEYBOARD_STEP);
106
+ const next = parseFloat(event.target.value);
107
+ resize(next > columnWidth.value ? columnWidth.value + KEYBOARD_STEP : columnWidth.value - KEYBOARD_STEP);
107
108
  };
108
109
  const n1 = t0();
109
110
  const n0 = child(n1);
@@ -100,7 +100,7 @@ var table_load_more_vue_vue_type_script_setup_true_vapor_true_lang_default = /*@
100
100
  return createDynamicComponent(() => unref(virtualizer) ? "div" : "td", {
101
101
  "aria-colspan": () => unref(virtualizer) ? columnCount.value : void 0,
102
102
  colspan: () => unref(virtualizer) ? void 0 : columnCount.value,
103
- role: "rowheader",
103
+ role: "gridcell",
104
104
  style: () => indicatorStyle.value
105
105
  }, extend(() => {
106
106
  return createSlot("default", null, null, 4);
@@ -41,7 +41,9 @@ var useGridSelectionAnnouncement = (options) => {
41
41
  else if (added.size === 1 && removed.size === 0) single([...added][0], "selected");
42
42
  else if (removed.size === 1 && added.size === 0) single([...removed][0], "not selected");
43
43
  if (selection.selectionMode.value === "multiple") {
44
- if (messages.length === 0 || current === "all" || (current === "all" ? 0 : current.size) > 1 || last === "all" || (last === "all" ? 0 : last.size) > 1) messages.push(current === "all" ? "All items selected." : countMessage(selection.selectedKeys.value.size));
44
+ const size = current === "all" ? 0 : current.size;
45
+ const lastSize = last === "all" ? 0 : last.size;
46
+ if (messages.length === 0 || current === "all" || size > 1 || last === "all" || lastSize > 1) messages.push(current === "all" ? "All items selected." : countMessage(selection.selectedKeys.value.size));
45
47
  }
46
48
  if (messages.length > 0) announce(messages.join(" "));
47
49
  });
@@ -19,10 +19,12 @@ var createTableRegistry = () => {
19
19
  const items = /* @__PURE__ */ new Map();
20
20
  const version = shallowRef(0);
21
21
  const orderedKeys = computed(() => {
22
+ version.value;
22
23
  return [...items.entries()].filter(([, meta]) => meta.element()?.isConnected).sort(([, a], [, b]) => a.element().compareDocumentPosition(b.element()) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1).map(([key]) => key);
23
24
  });
24
25
  return {
25
26
  getItem: (key) => {
27
+ version.value;
26
28
  return items.get(key);
27
29
  },
28
30
  indexOf: (key) => orderedKeys.value.indexOf(key),
@@ -39,6 +41,7 @@ var createTableRegistry = () => {
39
41
  };
40
42
  },
41
43
  size: computed(() => {
44
+ version.value;
42
45
  return orderedKeys.value.length;
43
46
  })
44
47
  };
@@ -26,7 +26,8 @@ var tabs_list_container_vue_vue_type_script_setup_true_vapor_true_lang_default =
26
26
  if (!element) return;
27
27
  const vertical = isVertical.value;
28
28
  const size = vertical ? element.clientHeight : element.clientWidth;
29
- const maxScroll = Math.max(0, (vertical ? element.scrollHeight : element.scrollWidth) - size);
29
+ const scrollSize = vertical ? element.scrollHeight : element.scrollWidth;
30
+ const maxScroll = Math.max(0, scrollSize - size);
30
31
  const isRtl = !vertical && getComputedStyle(element).direction === "rtl";
31
32
  const delta = direction * size * SCROLL_FRACTION * (isRtl ? -1 : 1);
32
33
  const current = vertical ? element.scrollTop : element.scrollLeft;
@@ -92,7 +92,8 @@ var useCalendarGrid = (options, state, shared) => {
92
92
  const isDayView = Boolean(duration.days && duration.days < 7);
93
93
  const firstDayOfWeek = toValue(options.firstDayOfWeek);
94
94
  const weekStart = isDayView ? startDate.value : startOfWeek(today(state.timeZone.value), locale.value.locale, firstDayOfWeek);
95
- return Array.from({ length: isDayView ? duration.days : 7 }, (_, index) => dayFormatter.value.format(weekStart.add({ days: index }).toDate(state.timeZone.value)));
95
+ const days = isDayView ? duration.days : 7;
96
+ return Array.from({ length: days }, (_, index) => dayFormatter.value.format(weekStart.add({ days: index }).toDate(state.timeZone.value)));
96
97
  });
97
98
  return {
98
99
  attrs: computed(() => ({
@@ -39,7 +39,9 @@ var useCheckboxGroupState = (options = {}) => {
39
39
  const isItemRequired = computed(() => isRequired.value && value.value.length === 0);
40
40
  const validation = useFormValidationState({
41
41
  isInvalid: () => toValue(options.isInvalid),
42
+ isRequired,
42
43
  name: () => toValue(options.name),
44
+ requiredControl: "checkbox",
43
45
  validate: () => toValue(options.validate),
44
46
  validationBehavior: () => toValue(options.validationBehavior),
45
47
  value: () => value.value
@@ -123,13 +123,24 @@ var useCollection = (options = {}) => {
123
123
  if (index === -1) return null;
124
124
  return keys[index + step] ?? null;
125
125
  };
126
+ /**
127
+ * Read the registration counter, so a lookup made inside a `computed` re-runs once the item
128
+ * it asked about registers. `getItem` and `getElement` are read during render — an item's
129
+ * own disabled state and tabindex come from them — unlike `orderedKeys()`, which is only ever
130
+ * read on an interaction and so deliberately stays outside reactivity.
131
+ */
132
+ const track = () => {
133
+ version.value;
134
+ };
126
135
  return {
127
136
  getElement: (key) => {
137
+ track();
128
138
  return items.get(key)?.element() ?? null;
129
139
  },
130
140
  getFirstKey: () => keyAt(0),
131
141
  getIndex: (key) => order().indexOf(key),
132
142
  getItem: (key) => {
143
+ track();
133
144
  return items.get(key) ?? metaFromSource(key);
134
145
  },
135
146
  getKeyAfter: (key) => neighbour(key, 1),
@@ -138,6 +149,7 @@ var useCollection = (options = {}) => {
138
149
  orderedKeys,
139
150
  register,
140
151
  size: computed(() => {
152
+ track();
141
153
  return source()?.itemCount ?? items.size;
142
154
  }),
143
155
  withOrder
@@ -68,6 +68,7 @@ var useColorFieldState = (options = {}) => {
68
68
  const inputValue = shallowRef(hasInitialValue && colorValue.value ? colorValue.value.toString("hex") : "");
69
69
  const validation = useFormValidationState({
70
70
  isInvalid: () => toValue(options.isInvalid),
71
+ isRequired: () => toValue(options.isRequired),
71
72
  name: () => toValue(options.name),
72
73
  validate: () => toValue(options.validate),
73
74
  validationBehavior: () => toValue(options.validationBehavior),
@@ -108,38 +108,40 @@ var useColorField = (options = {}) => {
108
108
  else if (deltaY < 0) state.decrement();
109
109
  }
110
110
  });
111
+ const attrs = computed(() => {
112
+ const all = {
113
+ ...field.attrs.value,
114
+ "aria-disabled": isDisabled.value || void 0,
115
+ "aria-readonly": isReadOnly.value || void 0
116
+ };
117
+ for (const key of Object.keys(all)) if (all[key] === void 0) delete all[key];
118
+ return all;
119
+ });
120
+ const handlers = {
121
+ onBlur: (event) => {
122
+ isFocused.value = false;
123
+ spin.onBlur();
124
+ state.commit();
125
+ reassert();
126
+ field.handlers.onBlur(event);
127
+ },
128
+ onFocus: (event) => {
129
+ isFocused.value = true;
130
+ spin.onFocus();
131
+ field.handlers.onFocus(event);
132
+ },
133
+ onInput: field.handlers.onInput,
134
+ onKeydown: (event) => {
135
+ spin.onKeydown(event);
136
+ field.handlers.onKeydown(event);
137
+ },
138
+ onKeyup: field.handlers.onKeyup
139
+ };
111
140
  return {
112
- attrs: computed(() => {
113
- const all = {
114
- ...field.attrs.value,
115
- "aria-disabled": isDisabled.value || void 0,
116
- "aria-readonly": isReadOnly.value || void 0
117
- };
118
- for (const key of Object.keys(all)) if (all[key] === void 0) delete all[key];
119
- return all;
120
- }),
141
+ attrs,
121
142
  element,
122
143
  fieldIds: field.fieldIds,
123
- handlers: {
124
- onBlur: (event) => {
125
- isFocused.value = false;
126
- spin.onBlur();
127
- state.commit();
128
- reassert();
129
- field.handlers.onBlur(event);
130
- },
131
- onFocus: (event) => {
132
- isFocused.value = true;
133
- spin.onFocus();
134
- field.handlers.onFocus(event);
135
- },
136
- onInput: field.handlers.onInput,
137
- onKeydown: (event) => {
138
- spin.onKeydown(event);
139
- field.handlers.onKeydown(event);
140
- },
141
- onKeyup: field.handlers.onKeyup
142
- },
144
+ handlers,
143
145
  isDisabled,
144
146
  isInvalid: field.isInvalid,
145
147
  isReadOnly,
@@ -78,6 +78,8 @@ export interface UseComboBoxStateOptions<T> {
78
78
  disabledBehavior?: MaybeRefOrGetter<DisabledBehavior | undefined>;
79
79
  isReadOnly?: MaybeRefOrGetter<boolean | undefined>;
80
80
  isInvalid?: MaybeRefOrGetter<boolean | undefined>;
81
+ /** Whether the field has to hold a value. Enforced by the validation state under `"aria"`. */
82
+ isRequired?: MaybeRefOrGetter<boolean | undefined>;
81
83
  validate?: MaybeRefOrGetter<ValidationFunction<ComboBoxValidationValue> | undefined>;
82
84
  validationBehavior?: MaybeRefOrGetter<ValidationBehavior | undefined>;
83
85
  name?: MaybeRefOrGetter<string | undefined>;
@@ -2,7 +2,7 @@ import { useControllableState } from "./use-controllable-state.js";
2
2
  import { useMenuTriggerState } from "./use-overlay-trigger-state.js";
3
3
  import { useCollection } from "./use-collection.js";
4
4
  import { createListCollection } from "../utils/virtualizer-collection.js";
5
- import { useFormValidationState } from "./use-form-validation-state.js";
5
+ import { isValueMissing, useFormValidationState } from "./use-form-validation-state.js";
6
6
  import { useSelectionManager } from "./use-selection-manager.js";
7
7
  import { defaultItemTextValue } from "./use-select-state.js";
8
8
  import { useFilter } from "./use-filter.js";
@@ -128,7 +128,8 @@ var useComboBoxState = (options) => {
128
128
  const given = toValue(options.defaultInputValue);
129
129
  if (given != null) return given;
130
130
  const value = defaultValue.value;
131
- return selectionMode.value === "single" ? textOf(Array.isArray(value) ? value[0] ?? null : value) : "";
131
+ const key = Array.isArray(value) ? value[0] ?? null : value;
132
+ return selectionMode.value === "single" ? textOf(key) : "";
132
133
  });
133
134
  const input = useControllableState({
134
135
  defaultValue: defaultInputValue.value,
@@ -137,7 +138,9 @@ var useComboBoxState = (options) => {
137
138
  });
138
139
  const inputValue = computed(() => input.state.value);
139
140
  const validation = useFormValidationState({
141
+ isEmpty: (current) => !current || isValueMissing(current.value),
140
142
  isInvalid: options.isInvalid,
143
+ isRequired: options.isRequired,
141
144
  name: options.name,
142
145
  validate: options.validate,
143
146
  validationBehavior: options.validationBehavior,