@crisp-ui-kit/crisp 0.7.0 → 0.7.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.
- package/dist/cjs/components/datepicker/datepicker.recipe.js +13 -3
- package/dist/cjs/components/multiselect/multiselect.js +12 -3
- package/dist/cjs/components/select/select.js +13 -2
- package/dist/datepicker.css +13 -3
- package/dist/esm/components/datepicker/datepicker.recipe.js +13 -3
- package/dist/esm/components/multiselect/multiselect.js +12 -3
- package/dist/esm/components/select/select.js +13 -2
- package/dist/styles.css +13 -3
- package/package.json +1 -1
|
@@ -81,16 +81,26 @@ const config = (0, core_1.defineRecipe)({
|
|
|
81
81
|
}
|
|
82
82
|
& .datepicker-empty-cell { min-height: ${tokens_1.contract.size.calendarCell}; }
|
|
83
83
|
|
|
84
|
-
/*
|
|
84
|
+
/* The bridge joining days inside a completed range.
|
|
85
|
+
The endpoints do NOT carry data-in-range -- only the interior days do --
|
|
86
|
+
so these are three independent rules, not a base plus two modifiers. A
|
|
87
|
+
modifier that sets only inset-inline-start never generates a pseudo
|
|
88
|
+
element on an endpoint, which leaves a half-cell of white between the
|
|
89
|
+
endpoint's pill and the run of grey beside it. Each rule declares its
|
|
90
|
+
own content. */
|
|
85
91
|
& .datepicker-cell[data-range-complete][data-in-range]::before {
|
|
86
92
|
content: ""; position: absolute; z-index: -1; inset-inline: 0;
|
|
87
93
|
top: ${pillInset}; height: ${tokens_1.contract.size.calendarPill}; background: ${tokens_1.contract.bgNeutralWeak};
|
|
88
94
|
}
|
|
89
95
|
& .datepicker-cell[data-range-complete][data-range-start]:not([data-range-end])::before {
|
|
90
|
-
|
|
96
|
+
content: ""; position: absolute; z-index: -1;
|
|
97
|
+
inset-inline-start: 50%; inset-inline-end: 0;
|
|
98
|
+
top: ${pillInset}; height: ${tokens_1.contract.size.calendarPill}; background: ${tokens_1.contract.bgNeutralWeak};
|
|
91
99
|
}
|
|
92
100
|
& .datepicker-cell[data-range-complete][data-range-end]:not([data-range-start])::before {
|
|
93
|
-
|
|
101
|
+
content: ""; position: absolute; z-index: -1;
|
|
102
|
+
inset-inline-start: 0; inset-inline-end: 50%;
|
|
103
|
+
top: ${pillInset}; height: ${tokens_1.contract.size.calendarPill}; background: ${tokens_1.contract.bgNeutralWeak};
|
|
94
104
|
}
|
|
95
105
|
|
|
96
106
|
& .datepicker-date {
|
|
@@ -64,6 +64,12 @@ function Chip({ opt }) {
|
|
|
64
64
|
* click (the menu stays open). Keyboard: ↑/↓ move, Enter toggles, Esc closes.
|
|
65
65
|
*/
|
|
66
66
|
function MultiSelect({ options, value: valueProp, defaultValue, onValueChange, placeholder = "Select…", searchable = true, searchPlaceholder = "Search…", disabled, className, "aria-label": ariaLabel, ...rest }) {
|
|
67
|
+
// Virtual focus: DOM focus stays on the trigger (or the search field) and the
|
|
68
|
+
// active option is named by `aria-activedescendant`. `data-highlighted` is a
|
|
69
|
+
// styling hook only -- a screen reader cannot see it.
|
|
70
|
+
const autoId = React.useId();
|
|
71
|
+
const listboxId = `${autoId}-listbox`;
|
|
72
|
+
const optionId = (i) => `${autoId}-opt-${i}`;
|
|
67
73
|
const [open, setOpen] = React.useState(false);
|
|
68
74
|
const [up, setUp] = React.useState(false);
|
|
69
75
|
const [pos, setPos] = React.useState({});
|
|
@@ -105,6 +111,9 @@ function MultiSelect({ options, value: valueProp, defaultValue, onValueChange, p
|
|
|
105
111
|
setOpen(false);
|
|
106
112
|
triggerRef.current?.focus({ preventScroll: true });
|
|
107
113
|
};
|
|
114
|
+
const activeId = open && highlight >= 0 && highlight < filtered.length
|
|
115
|
+
? optionId(highlight)
|
|
116
|
+
: undefined;
|
|
108
117
|
const toggle = (v) => {
|
|
109
118
|
const next = selectedSet.has(v)
|
|
110
119
|
? value.filter((x) => x !== v)
|
|
@@ -143,19 +152,19 @@ function MultiSelect({ options, value: valueProp, defaultValue, onValueChange, p
|
|
|
143
152
|
document.addEventListener("pointerdown", onDown);
|
|
144
153
|
return () => document.removeEventListener("pointerdown", onDown);
|
|
145
154
|
}, [open]);
|
|
146
|
-
return ((0, jsx_runtime_1.jsxs)("div", { ref: rootRef, className: (0, multiselect_recipe_1.multiselectRecipe)({ className }), ...rest, children: [(0, jsx_runtime_1.jsxs)("button", { ref: triggerRef, type: "button", "aria-haspopup": "listbox", "aria-expanded": open, "aria-label": ariaLabel, disabled: disabled, "data-open": open ? "" : undefined, className: "multiselect-trigger", onClick: () => (open ? setOpen(false) : openMenu()), onKeyDown: (e) => {
|
|
155
|
+
return ((0, jsx_runtime_1.jsxs)("div", { ref: rootRef, className: (0, multiselect_recipe_1.multiselectRecipe)({ className }), ...rest, children: [(0, jsx_runtime_1.jsxs)("button", { ref: triggerRef, type: "button", role: "combobox", "aria-haspopup": "listbox", "aria-expanded": open, "aria-controls": open ? listboxId : undefined, "aria-activedescendant": searchable ? undefined : activeId, "aria-label": ariaLabel, disabled: disabled, "data-open": open ? "" : undefined, className: "multiselect-trigger", onClick: () => (open ? setOpen(false) : openMenu()), onKeyDown: (e) => {
|
|
147
156
|
if (open && !searchable)
|
|
148
157
|
onNavKeys(e);
|
|
149
158
|
else if (!open && (e.key === "ArrowDown" || e.key === "Enter")) {
|
|
150
159
|
e.preventDefault();
|
|
151
160
|
openMenu();
|
|
152
161
|
}
|
|
153
|
-
}, children: [(0, jsx_runtime_1.jsx)("span", { className: "multiselect-chips", children: selectedOptions.length === 0 ? ((0, jsx_runtime_1.jsx)("span", { className: "multiselect-placeholder", children: placeholder })) : (selectedOptions.map((o) => (0, jsx_runtime_1.jsx)(Chip, { opt: o }, o.value))) }), (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronDown, { className: "multiselect-chevron", size: 16, strokeWidth: 1.75 })] }), open && ((0, jsx_runtime_1.jsxs)("div", { className: "multiselect-listbox", role: "listbox", "aria-multiselectable": "true", "aria-label": ariaLabel, "data-up": up ? "" : undefined, style: pos, children: [searchable && ((0, jsx_runtime_1.jsxs)("div", { className: "multiselect-search", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Search, { size: 15, strokeWidth: 1.75 }), (0, jsx_runtime_1.jsx)("input", { ref: searchRef, type: "text", placeholder: searchPlaceholder, "aria-label": searchPlaceholder, value: query, onChange: (e) => {
|
|
162
|
+
}, children: [(0, jsx_runtime_1.jsx)("span", { className: "multiselect-chips", children: selectedOptions.length === 0 ? ((0, jsx_runtime_1.jsx)("span", { className: "multiselect-placeholder", children: placeholder })) : (selectedOptions.map((o) => (0, jsx_runtime_1.jsx)(Chip, { opt: o }, o.value))) }), (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronDown, { className: "multiselect-chevron", size: 16, strokeWidth: 1.75 })] }), open && ((0, jsx_runtime_1.jsxs)("div", { id: listboxId, className: "multiselect-listbox", role: "listbox", "aria-multiselectable": "true", "aria-label": ariaLabel, "data-up": up ? "" : undefined, style: pos, children: [searchable && ((0, jsx_runtime_1.jsxs)("div", { className: "multiselect-search", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Search, { size: 15, strokeWidth: 1.75 }), (0, jsx_runtime_1.jsx)("input", { ref: searchRef, type: "text", placeholder: searchPlaceholder, "aria-label": searchPlaceholder, "aria-controls": listboxId, "aria-activedescendant": activeId, value: query, onChange: (e) => {
|
|
154
163
|
setQuery(e.target.value);
|
|
155
164
|
setHighlight(0);
|
|
156
165
|
}, onKeyDown: onNavKeys })] })), filtered.length === 0 ? ((0, jsx_runtime_1.jsx)("div", { className: "multiselect-empty", children: "No results" })) : (filtered.map((opt, i) => {
|
|
157
166
|
const sel = selectedSet.has(opt.value);
|
|
158
|
-
return ((0, jsx_runtime_1.jsxs)("button", { type: "button", role: "option", "aria-selected": sel, "data-highlighted": i === highlight ? "" : undefined, className: "multiselect-option", onClick: () => toggle(opt.value), onMouseEnter: () => setHighlight(i), children: [(0, jsx_runtime_1.jsx)("span", { className: "multiselect-option-chip", children: (0, jsx_runtime_1.jsx)(Chip, { opt: opt }) }), (0, jsx_runtime_1.jsx)("span", { className: "multiselect-check", children: sel && (0, jsx_runtime_1.jsx)(lucide_react_1.Check, { size: 12, strokeWidth: 3 }) })] }, opt.value));
|
|
167
|
+
return ((0, jsx_runtime_1.jsxs)("button", { type: "button", id: optionId(i), role: "option", "aria-selected": sel, tabIndex: -1, "data-highlighted": i === highlight ? "" : undefined, className: "multiselect-option", onClick: () => toggle(opt.value), onMouseEnter: () => setHighlight(i), children: [(0, jsx_runtime_1.jsx)("span", { className: "multiselect-option-chip", children: (0, jsx_runtime_1.jsx)(Chip, { opt: opt }) }), (0, jsx_runtime_1.jsx)("span", { className: "multiselect-check", children: sel && (0, jsx_runtime_1.jsx)(lucide_react_1.Check, { size: 12, strokeWidth: 3 }) })] }, opt.value));
|
|
159
168
|
}))] }))] }));
|
|
160
169
|
}
|
|
161
170
|
MultiSelect.displayName = "MultiSelect";
|
|
@@ -51,6 +51,14 @@ function optionText(o) {
|
|
|
51
51
|
* Esc close, Home/End jump, opens upward when tight. Controlled or uncontrolled.
|
|
52
52
|
*/
|
|
53
53
|
function Select({ options, value, defaultValue, onValueChange, placeholder = "Select…", variant = "default", width = "full", searchable, searchPlaceholder = "Search…", disabled, className, "aria-label": ariaLabel, id, ...rest }) {
|
|
54
|
+
// Virtual focus: DOM focus stays on the trigger (or the search field), and the
|
|
55
|
+
// active option is named by `aria-activedescendant`. That is the only thing a
|
|
56
|
+
// screen reader has to go on -- `data-highlighted` is invisible to it -- so the
|
|
57
|
+
// ids below have to resolve to the element the highlight is actually drawn on.
|
|
58
|
+
const autoId = React.useId();
|
|
59
|
+
const baseId = id ?? autoId;
|
|
60
|
+
const listboxId = `${baseId}-listbox`;
|
|
61
|
+
const optionId = (i) => `${baseId}-opt-${i}`;
|
|
54
62
|
const [internal, setInternal] = React.useState(defaultValue);
|
|
55
63
|
const selected = value ?? internal;
|
|
56
64
|
const [open, setOpen] = React.useState(false);
|
|
@@ -70,6 +78,9 @@ function Select({ options, value, defaultValue, onValueChange, placeholder = "Se
|
|
|
70
78
|
const enabledIndexes = filtered
|
|
71
79
|
.map((o, i) => (o.disabled ? -1 : i))
|
|
72
80
|
.filter((i) => i >= 0);
|
|
81
|
+
const activeId = open && highlight >= 0 && highlight < filtered.length
|
|
82
|
+
? optionId(highlight)
|
|
83
|
+
: undefined;
|
|
73
84
|
const openMenu = () => {
|
|
74
85
|
if (disabled)
|
|
75
86
|
return;
|
|
@@ -169,12 +180,12 @@ function Select({ options, value, defaultValue, onValueChange, placeholder = "Se
|
|
|
169
180
|
document.addEventListener("pointerdown", onDown);
|
|
170
181
|
return () => document.removeEventListener("pointerdown", onDown);
|
|
171
182
|
}, [open]);
|
|
172
|
-
return ((0, jsx_runtime_1.jsxs)("div", { ref: rootRef, className: (0, select_recipe_1.selectRecipe)({ variant, width, className }), ...rest, children: [(0, jsx_runtime_1.jsxs)("button", { ref: triggerRef, type: "button", id: id, role: "combobox", "aria-haspopup": "listbox", "aria-expanded": open, "aria-label": ariaLabel, disabled: disabled, "data-open": open ? "" : undefined, className: "select-trigger", onClick: () => (open ? setOpen(false) : openMenu()), onKeyDown: onTriggerKeyDown, children: [(0, jsx_runtime_1.jsx)("span", { className: "select-value", children: selectedOption ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [selectedOption.color && ((0, jsx_runtime_1.jsx)("span", { className: "select-option-dot", style: { background: selectedOption.color } })), selectedOption.label] })) : ((0, jsx_runtime_1.jsx)("span", { className: "select-placeholder", children: placeholder })) }), (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronDown, { className: "select-chevron", size: 16, strokeWidth: 1.75 })] }), open && ((0, jsx_runtime_1.jsxs)("div", { className: "select-listbox", role: "listbox", "aria-label": ariaLabel, "data-up": up ? "" : undefined, style: pos, children: [searchable && ((0, jsx_runtime_1.jsxs)("div", { className: "select-search", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Search, { size: 15, strokeWidth: 1.75 }), (0, jsx_runtime_1.jsx)("input", { ref: searchRef, type: "text", placeholder: searchPlaceholder, "aria-label": searchPlaceholder, value: query, onChange: (e) => {
|
|
183
|
+
return ((0, jsx_runtime_1.jsxs)("div", { ref: rootRef, className: (0, select_recipe_1.selectRecipe)({ variant, width, className }), ...rest, children: [(0, jsx_runtime_1.jsxs)("button", { ref: triggerRef, type: "button", id: id, role: "combobox", "aria-haspopup": "listbox", "aria-expanded": open, "aria-controls": open ? listboxId : undefined, "aria-activedescendant": searchable ? undefined : activeId, "aria-label": ariaLabel, disabled: disabled, "data-open": open ? "" : undefined, className: "select-trigger", onClick: () => (open ? setOpen(false) : openMenu()), onKeyDown: onTriggerKeyDown, children: [(0, jsx_runtime_1.jsx)("span", { className: "select-value", children: selectedOption ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [selectedOption.color && ((0, jsx_runtime_1.jsx)("span", { className: "select-option-dot", style: { background: selectedOption.color } })), selectedOption.label] })) : ((0, jsx_runtime_1.jsx)("span", { className: "select-placeholder", children: placeholder })) }), (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronDown, { className: "select-chevron", size: 16, strokeWidth: 1.75 })] }), open && ((0, jsx_runtime_1.jsxs)("div", { id: listboxId, className: "select-listbox", role: "listbox", "aria-label": ariaLabel, "data-up": up ? "" : undefined, style: pos, children: [searchable && ((0, jsx_runtime_1.jsxs)("div", { className: "select-search", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Search, { size: 15, strokeWidth: 1.75 }), (0, jsx_runtime_1.jsx)("input", { ref: searchRef, type: "text", placeholder: searchPlaceholder, "aria-label": searchPlaceholder, "aria-controls": listboxId, "aria-activedescendant": activeId, value: query, onChange: (e) => {
|
|
173
184
|
setQuery(e.target.value);
|
|
174
185
|
setHighlight(0);
|
|
175
186
|
}, onKeyDown: onNavKeys })] })), filtered.length === 0 ? ((0, jsx_runtime_1.jsx)("div", { className: "select-empty", children: "No results" })) : (filtered.map((opt, i) => {
|
|
176
187
|
const isSel = opt.value === selected;
|
|
177
|
-
return ((0, jsx_runtime_1.jsxs)("button", { type: "button", role: "option", "aria-selected": isSel, disabled: opt.disabled, "data-disabled": opt.disabled ? "" : undefined, "data-highlighted": i === highlight ? "" : undefined, className: "select-option", onClick: () => pick(opt), onMouseEnter: () => setHighlight(i), children: [opt.color && ((0, jsx_runtime_1.jsx)("span", { className: "select-option-dot", style: { background: opt.color } })), (0, jsx_runtime_1.jsx)("span", { className: "select-option-label", children: opt.label }), isSel && ((0, jsx_runtime_1.jsx)(lucide_react_1.Check, { className: "select-option-check", size: 15, strokeWidth: 2 }))] }, opt.value));
|
|
188
|
+
return ((0, jsx_runtime_1.jsxs)("button", { type: "button", id: optionId(i), role: "option", "aria-selected": isSel, tabIndex: -1, disabled: opt.disabled, "data-disabled": opt.disabled ? "" : undefined, "data-highlighted": i === highlight ? "" : undefined, className: "select-option", onClick: () => pick(opt), onMouseEnter: () => setHighlight(i), children: [opt.color && ((0, jsx_runtime_1.jsx)("span", { className: "select-option-dot", style: { background: opt.color } })), (0, jsx_runtime_1.jsx)("span", { className: "select-option-label", children: opt.label }), isSel && ((0, jsx_runtime_1.jsx)(lucide_react_1.Check, { className: "select-option-check", size: 15, strokeWidth: 2 }))] }, opt.value));
|
|
178
189
|
}))] }))] }));
|
|
179
190
|
}
|
|
180
191
|
Select.displayName = "Select";
|
package/dist/datepicker.css
CHANGED
|
@@ -62,16 +62,26 @@
|
|
|
62
62
|
}
|
|
63
63
|
& .datepicker-empty-cell { min-height: var(--size-calendarCell); }
|
|
64
64
|
|
|
65
|
-
/*
|
|
65
|
+
/* The bridge joining days inside a completed range.
|
|
66
|
+
The endpoints do NOT carry data-in-range -- only the interior days do --
|
|
67
|
+
so these are three independent rules, not a base plus two modifiers. A
|
|
68
|
+
modifier that sets only inset-inline-start never generates a pseudo
|
|
69
|
+
element on an endpoint, which leaves a half-cell of white between the
|
|
70
|
+
endpoint's pill and the run of grey beside it. Each rule declares its
|
|
71
|
+
own content. */
|
|
66
72
|
& .datepicker-cell[data-range-complete][data-in-range]::before {
|
|
67
73
|
content: ""; position: absolute; z-index: -1; inset-inline: 0;
|
|
68
74
|
top: calc((var(--size-calendarCell) - var(--size-calendarPill)) / 2); height: var(--size-calendarPill); background: var(--bg-neutral-weak);
|
|
69
75
|
}
|
|
70
76
|
& .datepicker-cell[data-range-complete][data-range-start]:not([data-range-end])::before {
|
|
71
|
-
|
|
77
|
+
content: ""; position: absolute; z-index: -1;
|
|
78
|
+
inset-inline-start: 50%; inset-inline-end: 0;
|
|
79
|
+
top: calc((var(--size-calendarCell) - var(--size-calendarPill)) / 2); height: var(--size-calendarPill); background: var(--bg-neutral-weak);
|
|
72
80
|
}
|
|
73
81
|
& .datepicker-cell[data-range-complete][data-range-end]:not([data-range-start])::before {
|
|
74
|
-
|
|
82
|
+
content: ""; position: absolute; z-index: -1;
|
|
83
|
+
inset-inline-start: 0; inset-inline-end: 50%;
|
|
84
|
+
top: calc((var(--size-calendarCell) - var(--size-calendarPill)) / 2); height: var(--size-calendarPill); background: var(--bg-neutral-weak);
|
|
75
85
|
}
|
|
76
86
|
|
|
77
87
|
& .datepicker-date {
|
|
@@ -78,16 +78,26 @@ const config = defineRecipe({
|
|
|
78
78
|
}
|
|
79
79
|
& .datepicker-empty-cell { min-height: ${c.size.calendarCell}; }
|
|
80
80
|
|
|
81
|
-
/*
|
|
81
|
+
/* The bridge joining days inside a completed range.
|
|
82
|
+
The endpoints do NOT carry data-in-range -- only the interior days do --
|
|
83
|
+
so these are three independent rules, not a base plus two modifiers. A
|
|
84
|
+
modifier that sets only inset-inline-start never generates a pseudo
|
|
85
|
+
element on an endpoint, which leaves a half-cell of white between the
|
|
86
|
+
endpoint's pill and the run of grey beside it. Each rule declares its
|
|
87
|
+
own content. */
|
|
82
88
|
& .datepicker-cell[data-range-complete][data-in-range]::before {
|
|
83
89
|
content: ""; position: absolute; z-index: -1; inset-inline: 0;
|
|
84
90
|
top: ${pillInset}; height: ${c.size.calendarPill}; background: ${c.bgNeutralWeak};
|
|
85
91
|
}
|
|
86
92
|
& .datepicker-cell[data-range-complete][data-range-start]:not([data-range-end])::before {
|
|
87
|
-
|
|
93
|
+
content: ""; position: absolute; z-index: -1;
|
|
94
|
+
inset-inline-start: 50%; inset-inline-end: 0;
|
|
95
|
+
top: ${pillInset}; height: ${c.size.calendarPill}; background: ${c.bgNeutralWeak};
|
|
88
96
|
}
|
|
89
97
|
& .datepicker-cell[data-range-complete][data-range-end]:not([data-range-start])::before {
|
|
90
|
-
|
|
98
|
+
content: ""; position: absolute; z-index: -1;
|
|
99
|
+
inset-inline-start: 0; inset-inline-end: 50%;
|
|
100
|
+
top: ${pillInset}; height: ${c.size.calendarPill}; background: ${c.bgNeutralWeak};
|
|
91
101
|
}
|
|
92
102
|
|
|
93
103
|
& .datepicker-date {
|
|
@@ -28,6 +28,12 @@ function Chip({ opt }) {
|
|
|
28
28
|
* click (the menu stays open). Keyboard: ↑/↓ move, Enter toggles, Esc closes.
|
|
29
29
|
*/
|
|
30
30
|
export function MultiSelect({ options, value: valueProp, defaultValue, onValueChange, placeholder = "Select…", searchable = true, searchPlaceholder = "Search…", disabled, className, "aria-label": ariaLabel, ...rest }) {
|
|
31
|
+
// Virtual focus: DOM focus stays on the trigger (or the search field) and the
|
|
32
|
+
// active option is named by `aria-activedescendant`. `data-highlighted` is a
|
|
33
|
+
// styling hook only -- a screen reader cannot see it.
|
|
34
|
+
const autoId = React.useId();
|
|
35
|
+
const listboxId = `${autoId}-listbox`;
|
|
36
|
+
const optionId = (i) => `${autoId}-opt-${i}`;
|
|
31
37
|
const [open, setOpen] = React.useState(false);
|
|
32
38
|
const [up, setUp] = React.useState(false);
|
|
33
39
|
const [pos, setPos] = React.useState({});
|
|
@@ -69,6 +75,9 @@ export function MultiSelect({ options, value: valueProp, defaultValue, onValueCh
|
|
|
69
75
|
setOpen(false);
|
|
70
76
|
triggerRef.current?.focus({ preventScroll: true });
|
|
71
77
|
};
|
|
78
|
+
const activeId = open && highlight >= 0 && highlight < filtered.length
|
|
79
|
+
? optionId(highlight)
|
|
80
|
+
: undefined;
|
|
72
81
|
const toggle = (v) => {
|
|
73
82
|
const next = selectedSet.has(v)
|
|
74
83
|
? value.filter((x) => x !== v)
|
|
@@ -107,19 +116,19 @@ export function MultiSelect({ options, value: valueProp, defaultValue, onValueCh
|
|
|
107
116
|
document.addEventListener("pointerdown", onDown);
|
|
108
117
|
return () => document.removeEventListener("pointerdown", onDown);
|
|
109
118
|
}, [open]);
|
|
110
|
-
return (_jsxs("div", { ref: rootRef, className: multiselectRecipe({ className }), ...rest, children: [_jsxs("button", { ref: triggerRef, type: "button", "aria-haspopup": "listbox", "aria-expanded": open, "aria-label": ariaLabel, disabled: disabled, "data-open": open ? "" : undefined, className: "multiselect-trigger", onClick: () => (open ? setOpen(false) : openMenu()), onKeyDown: (e) => {
|
|
119
|
+
return (_jsxs("div", { ref: rootRef, className: multiselectRecipe({ className }), ...rest, children: [_jsxs("button", { ref: triggerRef, type: "button", role: "combobox", "aria-haspopup": "listbox", "aria-expanded": open, "aria-controls": open ? listboxId : undefined, "aria-activedescendant": searchable ? undefined : activeId, "aria-label": ariaLabel, disabled: disabled, "data-open": open ? "" : undefined, className: "multiselect-trigger", onClick: () => (open ? setOpen(false) : openMenu()), onKeyDown: (e) => {
|
|
111
120
|
if (open && !searchable)
|
|
112
121
|
onNavKeys(e);
|
|
113
122
|
else if (!open && (e.key === "ArrowDown" || e.key === "Enter")) {
|
|
114
123
|
e.preventDefault();
|
|
115
124
|
openMenu();
|
|
116
125
|
}
|
|
117
|
-
}, children: [_jsx("span", { className: "multiselect-chips", children: selectedOptions.length === 0 ? (_jsx("span", { className: "multiselect-placeholder", children: placeholder })) : (selectedOptions.map((o) => _jsx(Chip, { opt: o }, o.value))) }), _jsx(ChevronDown, { className: "multiselect-chevron", size: 16, strokeWidth: 1.75 })] }), open && (_jsxs("div", { className: "multiselect-listbox", role: "listbox", "aria-multiselectable": "true", "aria-label": ariaLabel, "data-up": up ? "" : undefined, style: pos, children: [searchable && (_jsxs("div", { className: "multiselect-search", children: [_jsx(Search, { size: 15, strokeWidth: 1.75 }), _jsx("input", { ref: searchRef, type: "text", placeholder: searchPlaceholder, "aria-label": searchPlaceholder, value: query, onChange: (e) => {
|
|
126
|
+
}, children: [_jsx("span", { className: "multiselect-chips", children: selectedOptions.length === 0 ? (_jsx("span", { className: "multiselect-placeholder", children: placeholder })) : (selectedOptions.map((o) => _jsx(Chip, { opt: o }, o.value))) }), _jsx(ChevronDown, { className: "multiselect-chevron", size: 16, strokeWidth: 1.75 })] }), open && (_jsxs("div", { id: listboxId, className: "multiselect-listbox", role: "listbox", "aria-multiselectable": "true", "aria-label": ariaLabel, "data-up": up ? "" : undefined, style: pos, children: [searchable && (_jsxs("div", { className: "multiselect-search", children: [_jsx(Search, { size: 15, strokeWidth: 1.75 }), _jsx("input", { ref: searchRef, type: "text", placeholder: searchPlaceholder, "aria-label": searchPlaceholder, "aria-controls": listboxId, "aria-activedescendant": activeId, value: query, onChange: (e) => {
|
|
118
127
|
setQuery(e.target.value);
|
|
119
128
|
setHighlight(0);
|
|
120
129
|
}, onKeyDown: onNavKeys })] })), filtered.length === 0 ? (_jsx("div", { className: "multiselect-empty", children: "No results" })) : (filtered.map((opt, i) => {
|
|
121
130
|
const sel = selectedSet.has(opt.value);
|
|
122
|
-
return (_jsxs("button", { type: "button", role: "option", "aria-selected": sel, "data-highlighted": i === highlight ? "" : undefined, className: "multiselect-option", onClick: () => toggle(opt.value), onMouseEnter: () => setHighlight(i), children: [_jsx("span", { className: "multiselect-option-chip", children: _jsx(Chip, { opt: opt }) }), _jsx("span", { className: "multiselect-check", children: sel && _jsx(Check, { size: 12, strokeWidth: 3 }) })] }, opt.value));
|
|
131
|
+
return (_jsxs("button", { type: "button", id: optionId(i), role: "option", "aria-selected": sel, tabIndex: -1, "data-highlighted": i === highlight ? "" : undefined, className: "multiselect-option", onClick: () => toggle(opt.value), onMouseEnter: () => setHighlight(i), children: [_jsx("span", { className: "multiselect-option-chip", children: _jsx(Chip, { opt: opt }) }), _jsx("span", { className: "multiselect-check", children: sel && _jsx(Check, { size: 12, strokeWidth: 3 }) })] }, opt.value));
|
|
123
132
|
}))] }))] }));
|
|
124
133
|
}
|
|
125
134
|
MultiSelect.displayName = "MultiSelect";
|
|
@@ -15,6 +15,14 @@ function optionText(o) {
|
|
|
15
15
|
* Esc close, Home/End jump, opens upward when tight. Controlled or uncontrolled.
|
|
16
16
|
*/
|
|
17
17
|
export function Select({ options, value, defaultValue, onValueChange, placeholder = "Select…", variant = "default", width = "full", searchable, searchPlaceholder = "Search…", disabled, className, "aria-label": ariaLabel, id, ...rest }) {
|
|
18
|
+
// Virtual focus: DOM focus stays on the trigger (or the search field), and the
|
|
19
|
+
// active option is named by `aria-activedescendant`. That is the only thing a
|
|
20
|
+
// screen reader has to go on -- `data-highlighted` is invisible to it -- so the
|
|
21
|
+
// ids below have to resolve to the element the highlight is actually drawn on.
|
|
22
|
+
const autoId = React.useId();
|
|
23
|
+
const baseId = id ?? autoId;
|
|
24
|
+
const listboxId = `${baseId}-listbox`;
|
|
25
|
+
const optionId = (i) => `${baseId}-opt-${i}`;
|
|
18
26
|
const [internal, setInternal] = React.useState(defaultValue);
|
|
19
27
|
const selected = value ?? internal;
|
|
20
28
|
const [open, setOpen] = React.useState(false);
|
|
@@ -34,6 +42,9 @@ export function Select({ options, value, defaultValue, onValueChange, placeholde
|
|
|
34
42
|
const enabledIndexes = filtered
|
|
35
43
|
.map((o, i) => (o.disabled ? -1 : i))
|
|
36
44
|
.filter((i) => i >= 0);
|
|
45
|
+
const activeId = open && highlight >= 0 && highlight < filtered.length
|
|
46
|
+
? optionId(highlight)
|
|
47
|
+
: undefined;
|
|
37
48
|
const openMenu = () => {
|
|
38
49
|
if (disabled)
|
|
39
50
|
return;
|
|
@@ -133,12 +144,12 @@ export function Select({ options, value, defaultValue, onValueChange, placeholde
|
|
|
133
144
|
document.addEventListener("pointerdown", onDown);
|
|
134
145
|
return () => document.removeEventListener("pointerdown", onDown);
|
|
135
146
|
}, [open]);
|
|
136
|
-
return (_jsxs("div", { ref: rootRef, className: selectRecipe({ variant, width, className }), ...rest, children: [_jsxs("button", { ref: triggerRef, type: "button", id: id, role: "combobox", "aria-haspopup": "listbox", "aria-expanded": open, "aria-label": ariaLabel, disabled: disabled, "data-open": open ? "" : undefined, className: "select-trigger", onClick: () => (open ? setOpen(false) : openMenu()), onKeyDown: onTriggerKeyDown, children: [_jsx("span", { className: "select-value", children: selectedOption ? (_jsxs(_Fragment, { children: [selectedOption.color && (_jsx("span", { className: "select-option-dot", style: { background: selectedOption.color } })), selectedOption.label] })) : (_jsx("span", { className: "select-placeholder", children: placeholder })) }), _jsx(ChevronDown, { className: "select-chevron", size: 16, strokeWidth: 1.75 })] }), open && (_jsxs("div", { className: "select-listbox", role: "listbox", "aria-label": ariaLabel, "data-up": up ? "" : undefined, style: pos, children: [searchable && (_jsxs("div", { className: "select-search", children: [_jsx(Search, { size: 15, strokeWidth: 1.75 }), _jsx("input", { ref: searchRef, type: "text", placeholder: searchPlaceholder, "aria-label": searchPlaceholder, value: query, onChange: (e) => {
|
|
147
|
+
return (_jsxs("div", { ref: rootRef, className: selectRecipe({ variant, width, className }), ...rest, children: [_jsxs("button", { ref: triggerRef, type: "button", id: id, role: "combobox", "aria-haspopup": "listbox", "aria-expanded": open, "aria-controls": open ? listboxId : undefined, "aria-activedescendant": searchable ? undefined : activeId, "aria-label": ariaLabel, disabled: disabled, "data-open": open ? "" : undefined, className: "select-trigger", onClick: () => (open ? setOpen(false) : openMenu()), onKeyDown: onTriggerKeyDown, children: [_jsx("span", { className: "select-value", children: selectedOption ? (_jsxs(_Fragment, { children: [selectedOption.color && (_jsx("span", { className: "select-option-dot", style: { background: selectedOption.color } })), selectedOption.label] })) : (_jsx("span", { className: "select-placeholder", children: placeholder })) }), _jsx(ChevronDown, { className: "select-chevron", size: 16, strokeWidth: 1.75 })] }), open && (_jsxs("div", { id: listboxId, className: "select-listbox", role: "listbox", "aria-label": ariaLabel, "data-up": up ? "" : undefined, style: pos, children: [searchable && (_jsxs("div", { className: "select-search", children: [_jsx(Search, { size: 15, strokeWidth: 1.75 }), _jsx("input", { ref: searchRef, type: "text", placeholder: searchPlaceholder, "aria-label": searchPlaceholder, "aria-controls": listboxId, "aria-activedescendant": activeId, value: query, onChange: (e) => {
|
|
137
148
|
setQuery(e.target.value);
|
|
138
149
|
setHighlight(0);
|
|
139
150
|
}, onKeyDown: onNavKeys })] })), filtered.length === 0 ? (_jsx("div", { className: "select-empty", children: "No results" })) : (filtered.map((opt, i) => {
|
|
140
151
|
const isSel = opt.value === selected;
|
|
141
|
-
return (_jsxs("button", { type: "button", role: "option", "aria-selected": isSel, disabled: opt.disabled, "data-disabled": opt.disabled ? "" : undefined, "data-highlighted": i === highlight ? "" : undefined, className: "select-option", onClick: () => pick(opt), onMouseEnter: () => setHighlight(i), children: [opt.color && (_jsx("span", { className: "select-option-dot", style: { background: opt.color } })), _jsx("span", { className: "select-option-label", children: opt.label }), isSel && (_jsx(Check, { className: "select-option-check", size: 15, strokeWidth: 2 }))] }, opt.value));
|
|
152
|
+
return (_jsxs("button", { type: "button", id: optionId(i), role: "option", "aria-selected": isSel, tabIndex: -1, disabled: opt.disabled, "data-disabled": opt.disabled ? "" : undefined, "data-highlighted": i === highlight ? "" : undefined, className: "select-option", onClick: () => pick(opt), onMouseEnter: () => setHighlight(i), children: [opt.color && (_jsx("span", { className: "select-option-dot", style: { background: opt.color } })), _jsx("span", { className: "select-option-label", children: opt.label }), isSel && (_jsx(Check, { className: "select-option-check", size: 15, strokeWidth: 2 }))] }, opt.value));
|
|
142
153
|
}))] }))] }));
|
|
143
154
|
}
|
|
144
155
|
Select.displayName = "Select";
|
package/dist/styles.css
CHANGED
|
@@ -632,16 +632,26 @@
|
|
|
632
632
|
}
|
|
633
633
|
& .datepicker-empty-cell { min-height: var(--size-calendarCell); }
|
|
634
634
|
|
|
635
|
-
/*
|
|
635
|
+
/* The bridge joining days inside a completed range.
|
|
636
|
+
The endpoints do NOT carry data-in-range -- only the interior days do --
|
|
637
|
+
so these are three independent rules, not a base plus two modifiers. A
|
|
638
|
+
modifier that sets only inset-inline-start never generates a pseudo
|
|
639
|
+
element on an endpoint, which leaves a half-cell of white between the
|
|
640
|
+
endpoint's pill and the run of grey beside it. Each rule declares its
|
|
641
|
+
own content. */
|
|
636
642
|
& .datepicker-cell[data-range-complete][data-in-range]::before {
|
|
637
643
|
content: ""; position: absolute; z-index: -1; inset-inline: 0;
|
|
638
644
|
top: calc((var(--size-calendarCell) - var(--size-calendarPill)) / 2); height: var(--size-calendarPill); background: var(--bg-neutral-weak);
|
|
639
645
|
}
|
|
640
646
|
& .datepicker-cell[data-range-complete][data-range-start]:not([data-range-end])::before {
|
|
641
|
-
|
|
647
|
+
content: ""; position: absolute; z-index: -1;
|
|
648
|
+
inset-inline-start: 50%; inset-inline-end: 0;
|
|
649
|
+
top: calc((var(--size-calendarCell) - var(--size-calendarPill)) / 2); height: var(--size-calendarPill); background: var(--bg-neutral-weak);
|
|
642
650
|
}
|
|
643
651
|
& .datepicker-cell[data-range-complete][data-range-end]:not([data-range-start])::before {
|
|
644
|
-
|
|
652
|
+
content: ""; position: absolute; z-index: -1;
|
|
653
|
+
inset-inline-start: 0; inset-inline-end: 50%;
|
|
654
|
+
top: calc((var(--size-calendarCell) - var(--size-calendarPill)) / 2); height: var(--size-calendarPill); background: var(--bg-neutral-weak);
|
|
645
655
|
}
|
|
646
656
|
|
|
647
657
|
& .datepicker-date {
|