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.
- package/AGENTS.md +150 -0
- package/README.md +5 -1
- package/dist/components/autocomplete/autocomplete-clear-button.js +1 -1
- package/dist/components/autocomplete/autocomplete-root.js +3 -0
- package/dist/components/checkbox/checkbox-root.js +6 -1
- package/dist/components/combo-box/combo-box-root.js +1 -0
- package/dist/components/date-picker/date-picker-root.js +1 -0
- package/dist/components/date-range-picker/date-range-picker-root.js +1 -0
- package/dist/components/form/form-root.js +7 -1
- package/dist/components/radio/radio-root.js +4 -1
- package/dist/components/scroll-shadow/scroll-shadow-root.js +2 -10
- package/dist/components/scroll-shadow/use-scroll-shadow.d.ts +10 -0
- package/dist/components/scroll-shadow/use-scroll-shadow.js +34 -2
- package/dist/components/search-field/search-field-group.js +21 -1
- package/dist/components/select/index.d.ts +3 -2
- package/dist/components/select/index.js +2 -1
- package/dist/components/select/select-clear-button.d.ts +21 -0
- package/dist/components/select/select-clear-button.js +66 -0
- package/dist/components/select/select-root.d.ts +2 -1
- package/dist/components/select/select-root.js +16 -1
- package/dist/components/select/select.context.d.ts +11 -0
- package/dist/components/select/select.types.d.ts +3 -0
- package/dist/components/switch/switch-root.js +6 -1
- package/dist/components/table/table-row.js +3 -2
- package/dist/components/table/use-grid-keyboard.js +2 -3
- package/dist/components/table/use-table-collection.d.ts +0 -11
- package/dist/components/table/use-table-collection.js +1 -17
- package/dist/components/tabs/tabs-root.js +5 -1
- package/dist/components/tabs/tabs.types.d.ts +6 -0
- package/dist/components/toast/index.d.ts +1 -1
- package/dist/components/toast/index.js +3 -3
- package/dist/components/toast/toast-default-content.js +26 -4
- package/dist/components/toast/toast-indicator.js +8 -2
- package/dist/components/toast/toast-provider.js +53 -5
- package/dist/components/toast/toast-queue.d.ts +60 -5
- package/dist/components/toast/toast-queue.js +85 -27
- package/dist/components/toast/toast-root.js +67 -14
- package/dist/components/toast/toast.constants.d.ts +17 -1
- package/dist/components/toast/toast.constants.js +18 -2
- package/dist/components/toast/toast.context.d.ts +6 -0
- package/dist/components/toast/toast.types.d.ts +19 -0
- package/dist/composables/use-checkbox-group-state.js +2 -0
- package/dist/composables/use-color-field-state.js +1 -0
- package/dist/composables/use-combo-box-state.d.ts +2 -0
- package/dist/composables/use-combo-box-state.js +3 -1
- package/dist/composables/use-date-field-state.js +1 -0
- package/dist/composables/use-date-picker-state.d.ts +2 -0
- package/dist/composables/use-date-picker-state.js +1 -0
- package/dist/composables/use-date-range-picker-state.d.ts +2 -0
- package/dist/composables/use-date-range-picker-state.js +2 -0
- package/dist/composables/use-description.js +1 -0
- package/dist/composables/use-form-validation-state.d.ts +48 -5
- package/dist/composables/use-form-validation-state.js +76 -7
- package/dist/composables/use-list-keyboard.d.ts +1 -1
- package/dist/composables/use-list-keyboard.js +4 -5
- package/dist/composables/use-measured-height.d.ts +18 -10
- package/dist/composables/use-measured-height.js +53 -15
- package/dist/composables/use-number-field-state.js +1 -0
- package/dist/composables/use-overlay-position.js +7 -4
- package/dist/composables/use-radio-group-state.js +2 -0
- package/dist/composables/use-select-state.d.ts +10 -0
- package/dist/composables/use-select-state.js +7 -0
- package/dist/composables/use-select.d.ts +7 -0
- package/dist/composables/use-select.js +6 -0
- package/dist/composables/use-text-field.js +1 -0
- package/dist/composables/use-toast-region.d.ts +18 -4
- package/dist/composables/use-toast-region.js +128 -6
- package/dist/composables/use-toast.d.ts +1 -1
- package/dist/composables/use-toast.js +4 -3
- package/dist/composables/use-toolbar.js +3 -1
- package/dist/composables/use-typeahead.js +3 -0
- package/dist/index.js +6 -5
- package/dist/ropav.min.css +1 -1
- package/dist/utils/focus.d.ts +13 -0
- package/dist/utils/focus.js +18 -1
- package/dist/version.js +1 -1
- 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
|
-
· [
|
|
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.
|
|
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({
|
|
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({
|
|
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
|
-
|
|
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
|
|
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" */
|