ropav 0.11.1 → 0.12.1

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 (77) hide show
  1. package/AGENTS.md +150 -0
  2. package/README.md +5 -1
  3. package/dist/components/autocomplete/autocomplete-clear-button.js +1 -1
  4. package/dist/components/autocomplete/autocomplete-root.js +3 -0
  5. package/dist/components/checkbox/checkbox-root.js +6 -1
  6. package/dist/components/combo-box/combo-box-root.js +1 -0
  7. package/dist/components/date-picker/date-picker-root.js +1 -0
  8. package/dist/components/date-range-picker/date-range-picker-root.js +1 -0
  9. package/dist/components/form/form-root.js +7 -1
  10. package/dist/components/radio/radio-root.js +4 -1
  11. package/dist/components/scroll-shadow/scroll-shadow-root.js +2 -10
  12. package/dist/components/scroll-shadow/use-scroll-shadow.d.ts +10 -0
  13. package/dist/components/scroll-shadow/use-scroll-shadow.js +34 -2
  14. package/dist/components/search-field/search-field-group.js +21 -1
  15. package/dist/components/select/index.d.ts +3 -2
  16. package/dist/components/select/index.js +2 -1
  17. package/dist/components/select/select-clear-button.d.ts +21 -0
  18. package/dist/components/select/select-clear-button.js +66 -0
  19. package/dist/components/select/select-root.d.ts +2 -1
  20. package/dist/components/select/select-root.js +16 -1
  21. package/dist/components/select/select.context.d.ts +11 -0
  22. package/dist/components/select/select.types.d.ts +3 -0
  23. package/dist/components/switch/switch-root.js +6 -1
  24. package/dist/components/table/table-row.js +3 -2
  25. package/dist/components/table/use-grid-keyboard.js +2 -3
  26. package/dist/components/table/use-table-collection.d.ts +0 -11
  27. package/dist/components/table/use-table-collection.js +1 -17
  28. package/dist/components/tabs/tabs-root.js +5 -1
  29. package/dist/components/tabs/tabs.types.d.ts +6 -0
  30. package/dist/components/toast/index.d.ts +1 -1
  31. package/dist/components/toast/index.js +3 -3
  32. package/dist/components/toast/toast-default-content.js +26 -4
  33. package/dist/components/toast/toast-indicator.js +8 -2
  34. package/dist/components/toast/toast-provider.js +53 -5
  35. package/dist/components/toast/toast-queue.d.ts +60 -5
  36. package/dist/components/toast/toast-queue.js +85 -27
  37. package/dist/components/toast/toast-root.js +67 -14
  38. package/dist/components/toast/toast.constants.d.ts +17 -1
  39. package/dist/components/toast/toast.constants.js +18 -2
  40. package/dist/components/toast/toast.context.d.ts +6 -0
  41. package/dist/components/toast/toast.types.d.ts +19 -0
  42. package/dist/composables/use-checkbox-group-state.js +2 -0
  43. package/dist/composables/use-color-field-state.js +1 -0
  44. package/dist/composables/use-combo-box-state.d.ts +2 -0
  45. package/dist/composables/use-combo-box-state.js +3 -1
  46. package/dist/composables/use-date-field-state.js +1 -0
  47. package/dist/composables/use-date-picker-state.d.ts +2 -0
  48. package/dist/composables/use-date-picker-state.js +1 -0
  49. package/dist/composables/use-date-range-picker-state.d.ts +2 -0
  50. package/dist/composables/use-date-range-picker-state.js +2 -0
  51. package/dist/composables/use-description.js +1 -0
  52. package/dist/composables/use-form-validation-state.d.ts +48 -5
  53. package/dist/composables/use-form-validation-state.js +76 -7
  54. package/dist/composables/use-list-keyboard.d.ts +1 -1
  55. package/dist/composables/use-list-keyboard.js +4 -5
  56. package/dist/composables/use-measured-height.d.ts +18 -10
  57. package/dist/composables/use-measured-height.js +53 -15
  58. package/dist/composables/use-number-field-state.js +1 -0
  59. package/dist/composables/use-overlay-position.js +7 -4
  60. package/dist/composables/use-radio-group-state.js +2 -0
  61. package/dist/composables/use-select-state.d.ts +10 -0
  62. package/dist/composables/use-select-state.js +7 -0
  63. package/dist/composables/use-select.d.ts +7 -0
  64. package/dist/composables/use-select.js +6 -0
  65. package/dist/composables/use-text-field.js +1 -0
  66. package/dist/composables/use-toast-region.d.ts +18 -4
  67. package/dist/composables/use-toast-region.js +128 -6
  68. package/dist/composables/use-toast.d.ts +1 -1
  69. package/dist/composables/use-toast.js +4 -3
  70. package/dist/composables/use-toolbar.js +3 -1
  71. package/dist/composables/use-typeahead.js +3 -0
  72. package/dist/index.js +6 -5
  73. package/dist/ropav.min.css +1 -1
  74. package/dist/utils/focus.d.ts +13 -0
  75. package/dist/utils/focus.js +18 -1
  76. package/dist/version.js +1 -1
  77. package/package.json +5 -4
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.
@@ -23,7 +23,7 @@ var autocomplete_clear_button_vue_vue_type_script_setup_true_vapor_true_lang_def
23
23
  };
24
24
  /** Empties the selection, and only the selection — the text that was typed is left alone. */
25
25
  const onClick = () => {
26
- state.selection.clearSelection();
26
+ state.clearValue();
27
27
  onClear();
28
28
  };
29
29
  const n1 = t0();
@@ -178,7 +178,10 @@ var autocomplete_root_vue_vue_type_script_setup_true_vapor_true_lang_default = /
178
178
  * that part is what {@link provideAutocompleteContext} above carries.
179
179
  */
180
180
  provideSelectContext({
181
+ isDisabled,
182
+ onClear: () => emit("clear"),
181
183
  placeholder,
184
+ registerClearButton: () => () => {},
182
185
  select,
183
186
  selectedItems: state.selectedItems,
184
187
  selectedText,
@@ -81,14 +81,19 @@ 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
88
90
  });
89
91
  const validation = group ? group.state.itemValidation : ownValidation;
90
92
  provideFieldErrorContext(group ? null : { validation: validation.displayValidation });
91
- const { context: fieldIds, describedBy } = useFieldIds({ slots: ["description", "errorMessage"] });
93
+ const { context: fieldIds, describedBy } = useFieldIds({
94
+ labelElementType: "span",
95
+ slots: ["description", "errorMessage"]
96
+ });
92
97
  provideFieldIdsContext(fieldIds);
93
98
  const resolvedDescribedBy = computed(() => {
94
99
  const ids = [
@@ -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,
@@ -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
  });
@@ -23,7 +23,10 @@ var radio_root_vue_vue_type_script_setup_true_vapor_true_lang_default = /*@__PUR
23
23
  const styles = computed(() => radioVariants());
24
24
  const isSelected = computed(() => state.selectedValue.value === props.value);
25
25
  const isDisabled = computed(() => Boolean(props.isDisabled) || state.isDisabled.value);
26
- const { context: fieldIds, describedBy } = useFieldIds({ slots: ["description"] });
26
+ const { context: fieldIds, describedBy } = useFieldIds({
27
+ labelElementType: "span",
28
+ slots: ["description"]
29
+ });
27
30
  provideFieldIdsContext(fieldIds);
28
31
  const resolvedDescribedBy = computed(() => {
29
32
  const ids = [
@@ -1,5 +1,5 @@
1
1
  import { focusableIn } from "../../utils/focus.js";
2
- import { useScrollShadow } from "./use-scroll-shadow.js";
2
+ import { clearScrollShadowVisibility, useScrollShadow } from "./use-scroll-shadow.js";
3
3
  import { computed, createSlot, defineVaporComponent, renderEffect, setAttr, setClass, setInsertionState, setProp, setStaticTemplateRef, setStyle, shallowRef, template, watch } from "vue";
4
4
  import { scrollShadowVariants } from "@ropav/styles";
5
5
  //#region src/components/scroll-shadow/scroll-shadow-root.vue?vue&type=script&setup=true&vapor=true&lang.ts
@@ -43,21 +43,13 @@ var scroll_shadow_root_vue_vue_type_script_setup_true_vapor_true_lang_default =
43
43
  variant: props.variant
44
44
  }).base({ class: props.class }));
45
45
  const style = computed(() => [{ "--scroll-shadow-size": `${props.size}px` }, props.style]);
46
- const clearVisibility = (current) => {
47
- delete current.dataset["topScroll"];
48
- delete current.dataset["bottomScroll"];
49
- delete current.dataset["topBottomScroll"];
50
- delete current.dataset["leftScroll"];
51
- delete current.dataset["rightScroll"];
52
- delete current.dataset["leftRightScroll"];
53
- };
54
46
  watch([
55
47
  element,
56
48
  () => props.visibility,
57
49
  () => props.orientation
58
50
  ], ([current, visibility, orientation]) => {
59
51
  if (!current || visibility === "auto") return;
60
- clearVisibility(current);
52
+ clearScrollShadowVisibility(current);
61
53
  if (visibility === "both") current.dataset[orientation === "vertical" ? "topBottomScroll" : "leftRightScroll"] = "true";
62
54
  else if (visibility !== "none") current.dataset[`${visibility}Scroll`] = "true";
63
55
  }, {
@@ -12,6 +12,16 @@ export interface UseScrollShadowReturn {
12
12
  /** Re-measure immediately. The DOM write is batched into the next animation frame. */
13
13
  checkOverflow: () => void;
14
14
  }
15
+ /**
16
+ * Take every edge attribute off, so the stylesheet's mask stops applying.
17
+ *
18
+ * The attributes are the whole state, and the one place the mask is keyed on, so whoever stops
19
+ * writing them has to take the last set off too or the fade stays painted at whatever it was.
20
+ *
21
+ * Reports whether anything was actually removed, so a caller telling the world the fade is gone
22
+ * only says it when there was one.
23
+ */
24
+ export declare const clearScrollShadowVisibility: (element: HTMLElement) => boolean;
15
25
  /**
16
26
  * Detect the scrollable edges around an element and publish them as `data-*` attributes.
17
27
  *
@@ -1,5 +1,32 @@
1
1
  import { computed, onScopeDispose, toValue, watch } from "vue";
2
2
  //#region src/components/scroll-shadow/use-scroll-shadow.ts
3
+ /** Every attribute an edge is published as, which together are the whole state. */
4
+ var EDGE_ATTRIBUTES = [
5
+ "topScroll",
6
+ "bottomScroll",
7
+ "topBottomScroll",
8
+ "leftScroll",
9
+ "rightScroll",
10
+ "leftRightScroll"
11
+ ];
12
+ /**
13
+ * Take every edge attribute off, so the stylesheet's mask stops applying.
14
+ *
15
+ * The attributes are the whole state, and the one place the mask is keyed on, so whoever stops
16
+ * writing them has to take the last set off too or the fade stays painted at whatever it was.
17
+ *
18
+ * Reports whether anything was actually removed, so a caller telling the world the fade is gone
19
+ * only says it when there was one.
20
+ */
21
+ var clearScrollShadowVisibility = (element) => {
22
+ let cleared = false;
23
+ for (const attribute of EDGE_ATTRIBUTES) {
24
+ if (element.dataset[attribute] === void 0) continue;
25
+ delete element.dataset[attribute];
26
+ cleared = true;
27
+ }
28
+ return cleared;
29
+ };
3
30
  /**
4
31
  * Detect the scrollable edges around an element and publish them as `data-*` attributes.
5
32
  *
@@ -73,7 +100,12 @@ var useScrollShadow = (options) => {
73
100
  ], ([current, enabled, mode], _previous, onCleanup) => {
74
101
  previous = null;
75
102
  cancelPendingFrame();
76
- if (!current || !enabled || mode !== "auto") return;
103
+ if (!current) return;
104
+ if (mode !== "auto") return;
105
+ if (!enabled) {
106
+ if (clearScrollShadowVisibility(current)) options.onVisibilityChange?.("none");
107
+ return;
108
+ }
77
109
  checkOverflow();
78
110
  current.addEventListener("scroll", checkOverflow, { passive: true });
79
111
  const observer = typeof ResizeObserver === "undefined" ? void 0 : new ResizeObserver(checkOverflow);
@@ -120,4 +152,4 @@ var useScrollShadow = (options) => {
120
152
  return { checkOverflow };
121
153
  };
122
154
  //#endregion
123
- export { useScrollShadow };
155
+ export { clearScrollShadowVisibility, useScrollShadow };
@@ -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
  });
@@ -1,10 +1,11 @@
1
+ import SelectClearButton from "./select-clear-button.js";
1
2
  import SelectIndicator from "./select-indicator.js";
2
3
  import SelectPopover from "./select-popover.js";
3
4
  import SelectRoot from "./select-root.js";
4
5
  import SelectTrigger from "./select-trigger.js";
5
6
  import SelectValue from "./select-value.js";
6
- export { SelectIndicator, SelectPopover, SelectRoot as Select, SelectTrigger, SelectValue };
7
- export type { SelectIndicatorProps, SelectPopoverProps, SelectRootProps as SelectProps, SelectRootSlotProps as SelectSlotProps, SelectTriggerProps, SelectValueProps, SelectValueSlotProps, } from "./select.types.js";
7
+ export { SelectClearButton, SelectIndicator, SelectPopover, SelectRoot as Select, SelectTrigger, SelectValue, };
8
+ export type { SelectClearButtonProps, SelectIndicatorProps, SelectPopoverProps, SelectRootProps as SelectProps, SelectRootSlotProps as SelectSlotProps, SelectTriggerProps, SelectValueProps, SelectValueSlotProps, } from "./select.types.js";
8
9
  export { useSelectContext } from "./select.context.js";
9
10
  export type { SelectContext } from "./select.context.js";
10
11
  export type { UseSelectReturn } from "../../composables/use-select.js";
@@ -1,8 +1,9 @@
1
1
  import { useSelectContext } from "./select.context.js";
2
+ import select_clear_button_default from "./select-clear-button.js";
2
3
  import select_indicator_default from "./select-indicator.js";
3
4
  import select_popover_default from "./select-popover.js";
4
5
  import select_root_default from "./select-root.js";
5
6
  import select_trigger_default from "./select-trigger.js";
6
7
  import select_value_default from "./select-value.js";
7
8
  import { selectVariants } from "@ropav/styles";
8
- export { select_root_default as Select, select_indicator_default as SelectIndicator, select_popover_default as SelectPopover, select_trigger_default as SelectTrigger, select_value_default as SelectValue, selectVariants, useSelectContext };
9
+ export { select_root_default as Select, select_clear_button_default as SelectClearButton, select_indicator_default as SelectIndicator, select_popover_default as SelectPopover, select_trigger_default as SelectTrigger, select_value_default as SelectValue, selectVariants, useSelectContext };
@@ -0,0 +1,21 @@
1
+ import type { SelectClearButtonProps } from "./select.types.js";
2
+ declare const __VLS_export: (__VLS_props: NonNullable<Awaited<typeof __VLS_setup>>['props'], __VLS_ctx?: __VLS_PrettifyLocal<Pick<NonNullable<Awaited<typeof __VLS_setup>>, 'attrs' | 'emit' | 'slots'>>, __VLS_exposed?: NonNullable<Awaited<typeof __VLS_setup>>['expose'], __VLS_setup?: Promise<{
3
+ props: import('vue').PublicProps & __VLS_PrettifyLocal<SelectClearButtonProps> & (typeof globalThis extends {
4
+ __VLS_PROPS_FALLBACK: infer P;
5
+ } ? P : {});
6
+ expose: (exposed: {}) => void;
7
+ attrs: any;
8
+ slots: {
9
+ default?: (props: {}) => any;
10
+ };
11
+ emit: {};
12
+ }>) => import('vue').VNode & {
13
+ __ctx?: NonNullable<Awaited<typeof __VLS_setup>>;
14
+ };
15
+ declare const _default: typeof __VLS_export;
16
+ export default _default;
17
+ type __VLS_PrettifyLocal<T> = (T extends any ? {
18
+ [K in keyof T]: T[K];
19
+ } : {
20
+ [K in keyof T as K]: T[K];
21
+ }) & {};
@@ -0,0 +1,66 @@
1
+ import { dataAttr } from "../../utils/assertion.js";
2
+ import icon_close_default from "../icons/icon-close.js";
3
+ import { useSelectContext } from "./select.context.js";
4
+ import { computed, createComponent, createSlot, defineVaporComponent, on, onMounted, onUnmounted, renderEffect, setAttr, setClass, setInsertionState, template, unref } from "vue";
5
+ //#region src/components/select/select-clear-button.vue?vue&type=script&setup=true&vapor=true&lang.ts
6
+ var t0 = template("<span aria-hidden=true data-slot=select-clear-button>", 1);
7
+ var select_clear_button_vue_vue_type_script_setup_true_vapor_true_lang_default = /*@__PURE__*/ defineVaporComponent({
8
+ __name: "select-clear-button",
9
+ props: { class: {} },
10
+ setup(__props) {
11
+ const props = __props;
12
+ const { isDisabled, onClear, registerClearButton, select, selectedItems, slots, state } = useSelectContext();
13
+ const styles = computed(() => slots.value.clearButton({ class: props.class }));
14
+ /**
15
+ * Whether there is nothing to clear, which is what the stylesheet fades this out on.
16
+ *
17
+ * It stays in the DOM either way: appearing and disappearing would reflow the trigger every time
18
+ * a choice was made or undone.
19
+ *
20
+ * Read from the chosen items rather than the selected keys, so this agrees with what the trigger
21
+ * shows: a value naming an option the collection does not hold renders as the placeholder, and a
22
+ * button offering to clear what the user cannot see is a button offering to clear nothing.
23
+ */
24
+ const isEmpty = computed(() => selectedItems.value.length === 0);
25
+ let unregister;
26
+ onMounted(() => {
27
+ unregister = registerClearButton();
28
+ });
29
+ onUnmounted(() => {
30
+ unregister?.();
31
+ unregister = void 0;
32
+ });
33
+ /**
34
+ * Keeps the press off the trigger, so clearing does not also open the popover.
35
+ *
36
+ * Guarded the same way the click is: with nothing to clear the stylesheet takes this out of the
37
+ * hit test, and a press swallowed here rather than there would leave a strip of the trigger that
38
+ * neither clears nor opens.
39
+ */
40
+ const onPointerdown = (event) => {
41
+ if (isDisabled.value || isEmpty.value) return;
42
+ event.stopPropagation();
43
+ };
44
+ const onClick = (event) => {
45
+ event.stopPropagation();
46
+ if (isDisabled.value || isEmpty.value) return;
47
+ state.clearValue();
48
+ onClear();
49
+ select.triggerElement.value?.focus();
50
+ };
51
+ const n3 = t0();
52
+ renderEffect(() => {
53
+ setClass(n3, styles.value);
54
+ setAttr(n3, "data-empty", unref(dataAttr)(isEmpty.value));
55
+ });
56
+ setInsertionState(n3);
57
+ createSlot("default", null, () => {
58
+ return createComponent(unref(icon_close_default), { "data-slot": "select-clear-button-icon" });
59
+ });
60
+ on(n3, "click", onClick);
61
+ on(n3, "pointerdown", onPointerdown);
62
+ return n3;
63
+ }
64
+ });
65
+ //#endregion
66
+ export { select_clear_button_vue_vue_type_script_setup_true_vapor_true_lang_default as default };
@@ -3,6 +3,7 @@ import type { SelectRootProps, SelectRootSlotProps } from "./select.types.js";
3
3
  declare const __VLS_export: <T>(__VLS_props: NonNullable<Awaited<typeof __VLS_setup>>['props'], __VLS_ctx?: __VLS_PrettifyLocal<Pick<NonNullable<Awaited<typeof __VLS_setup>>, 'attrs' | 'emit' | 'slots'>>, __VLS_exposed?: NonNullable<Awaited<typeof __VLS_setup>>['expose'], __VLS_setup?: Promise<{
4
4
  props: import('vue').PublicProps & __VLS_PrettifyLocal<SelectRootProps<T> & {
5
5
  onChange?: ((value: SelectedValue) => any) | undefined;
6
+ onClear?: (() => any) | undefined;
6
7
  onFocusChange?: ((isFocused: boolean) => any) | undefined;
7
8
  onOpenChange?: ((isOpen: boolean) => any) | undefined;
8
9
  "onUpdate:isOpen"?: ((isOpen: boolean) => any) | undefined;
@@ -15,7 +16,7 @@ declare const __VLS_export: <T>(__VLS_props: NonNullable<Awaited<typeof __VLS_se
15
16
  slots: {
16
17
  default?: (props: SelectRootSlotProps) => unknown;
17
18
  };
18
- emit: ((evt: "change", value: SelectedValue) => void) & ((evt: "focusChange", isFocused: boolean) => void) & ((evt: "openChange", isOpen: boolean) => void) & ((evt: "update:isOpen", isOpen: boolean) => void) & ((evt: "update:value", value: SelectedValue) => void);
19
+ emit: ((evt: "change", value: SelectedValue) => void) & ((evt: "clear") => void) & ((evt: "focusChange", isFocused: boolean) => void) & ((evt: "openChange", isOpen: boolean) => void) & ((evt: "update:isOpen", isOpen: boolean) => void) & ((evt: "update:value", value: SelectedValue) => void);
19
20
  }>) => import('vue').VNode & {
20
21
  __ctx?: NonNullable<Awaited<typeof __VLS_setup>>;
21
22
  };
@@ -74,7 +74,8 @@ var select_root_vue_vue_type_script_setup_true_vapor_true_lang_default = /*@__PU
74
74
  "update:value",
75
75
  "openChange",
76
76
  "update:isOpen",
77
- "focusChange"
77
+ "focusChange",
78
+ "clear"
78
79
  ],
79
80
  setup(__props, { emit: __emit }) {
80
81
  const props = __props;
@@ -87,6 +88,7 @@ var select_root_vue_vue_type_script_setup_true_vapor_true_lang_default = /*@__PU
87
88
  disabledKeys: () => props.disabledKeys,
88
89
  isInvalid: () => props.isInvalid,
89
90
  isOpen: () => props.isOpen,
91
+ isRequired: () => props.isRequired,
90
92
  itemDisabled: props.itemDisabled,
91
93
  itemKey: props.itemKey,
92
94
  items: () => props.items,
@@ -106,12 +108,22 @@ var select_root_vue_vue_type_script_setup_true_vapor_true_lang_default = /*@__PU
106
108
  validationBehavior: () => props.validationBehavior,
107
109
  value: () => props.value
108
110
  });
111
+ let clearButtons = 0;
112
+ const registerClearButton = () => {
113
+ clearButtons += 1;
114
+ return () => {
115
+ clearButtons -= 1;
116
+ };
117
+ };
118
+ const onClear = () => emit("clear");
109
119
  const select = useSelect({
110
120
  ariaDescribedby: () => props.ariaDescribedby,
111
121
  ariaLabel: () => props.ariaLabel,
112
122
  ariaLabelledby: () => props.ariaLabelledby,
123
+ hasClearButton: () => clearButtons > 0,
113
124
  isDisabled: () => props.isDisabled,
114
125
  isRequired: () => props.isRequired,
126
+ onClear,
115
127
  onFocusChange: (isFocused) => emit("focusChange", isFocused)
116
128
  }, state);
117
129
  const strings = useLocalizedStringFormatter(selectStrings);
@@ -136,7 +148,10 @@ var select_root_vue_vue_type_script_setup_true_vapor_true_lang_default = /*@__PU
136
148
  provideFieldIdsContext(select.fieldIds.context);
137
149
  provideFieldErrorContext({ validation: state.displayValidation });
138
150
  provideSelectContext({
151
+ isDisabled: computed(() => Boolean(props.isDisabled)),
152
+ onClear,
139
153
  placeholder,
154
+ registerClearButton,
140
155
  select,
141
156
  selectedItems: state.selectedItems,
142
157
  selectedText,
@@ -12,6 +12,17 @@ export interface SelectContext {
12
12
  selectedText: ComputedRef<string>;
13
13
  /** What the trigger shows when nothing is chosen, already resolved to the localized default. */
14
14
  placeholder: ComputedRef<string>;
15
+ /** Whether the whole select is disabled, which the clear button has to answer to as well. */
16
+ isDisabled: ComputedRef<boolean>;
17
+ /** Called after the selection is emptied, whichever way it was emptied. */
18
+ onClear: () => void;
19
+ /**
20
+ * Registers a mounted clear button and returns the call that unregisters it.
21
+ *
22
+ * Counted rather than a flag, so unmounting one cannot switch the trigger's clear shortcut off
23
+ * while another is still mounted.
24
+ */
25
+ registerClearButton: () => () => void;
15
26
  }
16
27
  /**
17
28
  * Strict: every part of a select reads the state the root holds, so one rendered outside a root
@@ -90,6 +90,9 @@ export interface SelectValueSlotProps<T = unknown> {
90
90
  export interface SelectIndicatorProps {
91
91
  class?: string;
92
92
  }
93
+ export interface SelectClearButtonProps {
94
+ class?: string;
95
+ }
93
96
  export interface SelectPopoverProps {
94
97
  class?: string;
95
98
  /** @default "bottom" */