@crisp-ui-kit/crisp 0.26.0 → 0.27.0
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/multiselect/multiselect.js +3 -2
- package/dist/cjs/components/radiogroup/radiogroup.js +2 -2
- package/dist/cjs/components/select/select.js +2 -2
- package/dist/cjs/components/switch/switch.js +9 -9
- package/dist/cjs/product/command/command.js +26 -0
- package/dist/esm/components/multiselect/multiselect.js +3 -2
- package/dist/esm/components/radiogroup/radiogroup.js +2 -2
- package/dist/esm/components/select/select.js +2 -2
- package/dist/esm/components/switch/switch.js +10 -10
- package/dist/esm/product/command/command.js +26 -0
- package/dist/types/components/multiselect/multiselect.d.ts +8 -1
- package/dist/types/components/radiogroup/radiogroup.d.ts +6 -0
- package/dist/types/components/select/select.d.ts +8 -1
- package/dist/types/components/switch/switch.d.ts +18 -0
- package/package.json +1 -1
|
@@ -65,7 +65,7 @@ function Chip({ opt }) {
|
|
|
65
65
|
* popover with a search field over rows of "[chip] … [checkbox]" that toggle on
|
|
66
66
|
* click (the menu stays open). Keyboard: ↑/↓ move, Enter toggles, Esc closes.
|
|
67
67
|
*/
|
|
68
|
-
function MultiSelect({ options, value: valueProp, defaultValue, onValueChange, placeholder = "Select…", searchable = true, searchPlaceholder = "Search…", emptyLabel = "No results", disabled, className, "aria-label": ariaLabel, ...rest }) {
|
|
68
|
+
function MultiSelect({ options, value: valueProp, defaultValue, onValueChange, name, placeholder = "Select…", searchable = true, searchPlaceholder = "Search…", emptyLabel = "No results", disabled, className, "aria-label": ariaLabel, ...rest }) {
|
|
69
69
|
// Virtual focus: DOM focus stays on the trigger (or the search field) and the
|
|
70
70
|
// active option is named by `aria-activedescendant`. `data-highlighted` is a
|
|
71
71
|
// styling hook only -- a screen reader cannot see it.
|
|
@@ -154,7 +154,8 @@ function MultiSelect({ options, value: valueProp, defaultValue, onValueChange, p
|
|
|
154
154
|
document.addEventListener("pointerdown", onDown);
|
|
155
155
|
return () => document.removeEventListener("pointerdown", onDown);
|
|
156
156
|
}, [open]);
|
|
157
|
-
return ((0, jsx_runtime_1.jsxs)("div", { ref: rootRef, className: (0, multiselect_recipe_1.multiselectRecipe)({ className }), ...rest, children: [
|
|
157
|
+
return ((0, jsx_runtime_1.jsxs)("div", { ref: rootRef, className: (0, multiselect_recipe_1.multiselectRecipe)({ className }), ...rest, children: [name &&
|
|
158
|
+
value.map((v) => (0, jsx_runtime_1.jsx)("input", { type: "hidden", name: name, value: v }, v)), (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) => {
|
|
158
159
|
if (open && !searchable)
|
|
159
160
|
onNavKeys(e);
|
|
160
161
|
else if (!open && (e.key === "ArrowDown" || e.key === "Enter")) {
|
|
@@ -57,7 +57,7 @@ function RadioGroup({ value, defaultValue, onValueChange, variant = "default", n
|
|
|
57
57
|
onValueChange?.(v);
|
|
58
58
|
}, [value, onValueChange]);
|
|
59
59
|
const { onKeyDown, groupRef } = (0, radiogroup_keyboard_1.useRadioGroupKeyboard)({ select });
|
|
60
|
-
return ((0, jsx_runtime_1.jsx)(RadioContext.Provider, { value: { value: selected, select, name: groupName, variant }, children: (0, jsx_runtime_1.
|
|
60
|
+
return ((0, jsx_runtime_1.jsx)(RadioContext.Provider, { value: { value: selected, select, name: groupName, variant }, children: (0, jsx_runtime_1.jsxs)("div", { ref: groupRef, role: "radiogroup", "aria-label": ariaLabel, "data-variant": variant, className: (0, radiogroup_recipe_1.radiogroupRecipe)({ className }), ...rest, onKeyDown: onKeyDown, children: [children, name && selected && ((0, jsx_runtime_1.jsx)("input", { type: "hidden", name: name, value: selected }))] }) }));
|
|
61
61
|
}
|
|
62
62
|
RadioGroup.displayName = "RadioGroup";
|
|
63
63
|
function Radio({ value, label, children, disabled }) {
|
|
@@ -65,7 +65,7 @@ function Radio({ value, label, children, disabled }) {
|
|
|
65
65
|
if (!ctx)
|
|
66
66
|
throw new Error("Radio must be used within a RadioGroup");
|
|
67
67
|
const checked = ctx.value === value;
|
|
68
|
-
return ((0, jsx_runtime_1.jsx)("button", { type: "button", role: "radio", "aria-checked": checked,
|
|
68
|
+
return ((0, jsx_runtime_1.jsx)("button", { type: "button", role: "radio", "aria-checked": checked, disabled: disabled, "data-value": value, "data-checked": checked ? "" : undefined,
|
|
69
69
|
// roving tabindex: the selected (or first, when none) radio is tabbable
|
|
70
70
|
tabIndex: checked ? 0 : -1, className: "radio", onClick: () => ctx.select(value), children: ctx.variant === "card" ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("span", { className: "radio-preview", children: children }), (0, jsx_runtime_1.jsx)("span", { className: "radio-cardfoot", children: (0, jsx_runtime_1.jsx)("span", { className: "radio-label", children: label }) })] })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("span", { className: "radio-dot" }), (0, jsx_runtime_1.jsx)("span", { className: "radio-label", children: label ?? children })] })) }));
|
|
71
71
|
}
|
|
@@ -52,7 +52,7 @@ function optionText(o) {
|
|
|
52
52
|
* "No results" empty state). Full keyboard model — ↑/↓ move, Enter/Space pick,
|
|
53
53
|
* Esc close, Home/End jump, opens upward when tight. Controlled or uncontrolled.
|
|
54
54
|
*/
|
|
55
|
-
function Select({ options, value, defaultValue, onValueChange, placeholder = "Select…", variant = "default", width = "full", searchable, searchPlaceholder = "Search…", emptyLabel = "No results", disabled, className, "aria-label": ariaLabel, id, ...rest }) {
|
|
55
|
+
function Select({ options, value, defaultValue, onValueChange, name, placeholder = "Select…", variant = "default", width = "full", searchable, searchPlaceholder = "Search…", emptyLabel = "No results", disabled, className, "aria-label": ariaLabel, id, ...rest }) {
|
|
56
56
|
// Virtual focus: DOM focus stays on the trigger (or the search field), and the
|
|
57
57
|
// active option is named by `aria-activedescendant`. That is the only thing a
|
|
58
58
|
// screen reader has to go on -- `data-highlighted` is invisible to it -- so the
|
|
@@ -150,7 +150,7 @@ function Select({ options, value, defaultValue, onValueChange, placeholder = "Se
|
|
|
150
150
|
document.addEventListener("pointerdown", onDown);
|
|
151
151
|
return () => document.removeEventListener("pointerdown", onDown);
|
|
152
152
|
}, [open]);
|
|
153
|
-
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) => {
|
|
153
|
+
return ((0, jsx_runtime_1.jsxs)("div", { ref: rootRef, className: (0, select_recipe_1.selectRecipe)({ variant, width, className }), ...rest, children: [name && selected && (0, jsx_runtime_1.jsx)("input", { type: "hidden", name: name, value: selected }), (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) => {
|
|
154
154
|
setQuery(e.target.value);
|
|
155
155
|
setHighlight(0);
|
|
156
156
|
}, onKeyDown: onNavKeys })] })), filtered.length === 0 ? ((0, jsx_runtime_1.jsx)("div", { className: "select-empty", children: emptyLabel })) : (filtered.map((opt, i) => {
|
|
@@ -43,16 +43,16 @@ const switch_recipe_1 = require("./switch.recipe");
|
|
|
43
43
|
* label association and keyboard (Space/Enter) come for free. Controlled via
|
|
44
44
|
* `checked`/`onCheckedChange`, or uncontrolled from `defaultChecked`.
|
|
45
45
|
*/
|
|
46
|
-
exports.Switch = React.forwardRef(({ checked, defaultChecked = false, onCheckedChange, disabled, className, onClick, ...props }, ref) => {
|
|
46
|
+
exports.Switch = React.forwardRef(({ checked, defaultChecked = false, onCheckedChange, disabled, className, onClick, name, value = "on", ...props }, ref) => {
|
|
47
47
|
const [internal, setInternal] = React.useState(defaultChecked);
|
|
48
48
|
const on = checked ?? internal;
|
|
49
|
-
return ((0, jsx_runtime_1.jsx)("button", { ref: ref, type: "button", role: "switch", "aria-checked": on, disabled: disabled, "data-checked": on ? "" : undefined, className: (0, switch_recipe_1.switchRecipe)({ className }), onClick: (e) => {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
49
|
+
return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("button", { ref: ref, type: "button", role: "switch", "aria-checked": on, disabled: disabled, "data-checked": on ? "" : undefined, className: (0, switch_recipe_1.switchRecipe)({ className }), onClick: (e) => {
|
|
50
|
+
onClick?.(e);
|
|
51
|
+
if (e.defaultPrevented)
|
|
52
|
+
return;
|
|
53
|
+
if (checked === undefined)
|
|
54
|
+
setInternal((v) => !v);
|
|
55
|
+
onCheckedChange?.(!on);
|
|
56
|
+
}, ...props, children: (0, jsx_runtime_1.jsx)("span", { className: "switch-thumb" }) }), name && on && (0, jsx_runtime_1.jsx)("input", { type: "hidden", name: name, value: value })] }));
|
|
57
57
|
});
|
|
58
58
|
exports.Switch.displayName = "Switch";
|
|
@@ -159,6 +159,31 @@ function Command({ open, onOpenChange, items = exports.defaultCommandItems, plac
|
|
|
159
159
|
if (queryProp === undefined)
|
|
160
160
|
setInternalQuery(next);
|
|
161
161
|
}, [onQueryChange, queryProp]);
|
|
162
|
+
/**
|
|
163
|
+
* Where the user was when the palette opened.
|
|
164
|
+
*
|
|
165
|
+
* The dialog machine restores focus on close, and for Dialog and Drawer that
|
|
166
|
+
* lands on the trigger: focus-trap remembers whatever was focused when the
|
|
167
|
+
* trap activated. The palette is different — the combobox focuses the search
|
|
168
|
+
* input as part of the same commit, so the element the trap remembers is the
|
|
169
|
+
* input, which is then removed with the palette, and focus falls to
|
|
170
|
+
* `document.body`. The palette is the one surface opened from anywhere with
|
|
171
|
+
* a shortcut, so losing the caller's place is worst exactly here (#241).
|
|
172
|
+
*
|
|
173
|
+
* Recorded by watching focus WHILE CLOSED rather than reading
|
|
174
|
+
* `document.activeElement` when `open` flips: by the time an effect of ours
|
|
175
|
+
* runs, the input may already hold focus.
|
|
176
|
+
*/
|
|
177
|
+
const openerRef = React.useRef(null);
|
|
178
|
+
React.useEffect(() => {
|
|
179
|
+
if (open)
|
|
180
|
+
return;
|
|
181
|
+
const onFocusIn = (e) => {
|
|
182
|
+
openerRef.current = e.target;
|
|
183
|
+
};
|
|
184
|
+
document.addEventListener("focusin", onFocusIn);
|
|
185
|
+
return () => document.removeEventListener("focusin", onFocusIn);
|
|
186
|
+
}, [open]);
|
|
162
187
|
const dialogService = (0, react_1.useMachine)(dialog.machine, {
|
|
163
188
|
id: dialogId,
|
|
164
189
|
open,
|
|
@@ -166,6 +191,7 @@ function Command({ open, onOpenChange, items = exports.defaultCommandItems, plac
|
|
|
166
191
|
onOpenChange(details.open);
|
|
167
192
|
},
|
|
168
193
|
initialFocusEl: () => inputRef.current,
|
|
194
|
+
finalFocusEl: () => openerRef.current,
|
|
169
195
|
"aria-label": ariaLabel,
|
|
170
196
|
});
|
|
171
197
|
const dlg = dialog.connect(dialogService, react_1.normalizeProps);
|
|
@@ -29,7 +29,7 @@ function Chip({ opt }) {
|
|
|
29
29
|
* popover with a search field over rows of "[chip] … [checkbox]" that toggle on
|
|
30
30
|
* click (the menu stays open). Keyboard: ↑/↓ move, Enter toggles, Esc closes.
|
|
31
31
|
*/
|
|
32
|
-
export function MultiSelect({ options, value: valueProp, defaultValue, onValueChange, placeholder = "Select…", searchable = true, searchPlaceholder = "Search…", emptyLabel = "No results", disabled, className, "aria-label": ariaLabel, ...rest }) {
|
|
32
|
+
export function MultiSelect({ options, value: valueProp, defaultValue, onValueChange, name, placeholder = "Select…", searchable = true, searchPlaceholder = "Search…", emptyLabel = "No results", disabled, className, "aria-label": ariaLabel, ...rest }) {
|
|
33
33
|
// Virtual focus: DOM focus stays on the trigger (or the search field) and the
|
|
34
34
|
// active option is named by `aria-activedescendant`. `data-highlighted` is a
|
|
35
35
|
// styling hook only -- a screen reader cannot see it.
|
|
@@ -118,7 +118,8 @@ export function MultiSelect({ options, value: valueProp, defaultValue, onValueCh
|
|
|
118
118
|
document.addEventListener("pointerdown", onDown);
|
|
119
119
|
return () => document.removeEventListener("pointerdown", onDown);
|
|
120
120
|
}, [open]);
|
|
121
|
-
return (_jsxs("div", { ref: rootRef, className: multiselectRecipe({ className }), ...rest, children: [
|
|
121
|
+
return (_jsxs("div", { ref: rootRef, className: multiselectRecipe({ className }), ...rest, children: [name &&
|
|
122
|
+
value.map((v) => _jsx("input", { type: "hidden", name: name, value: v }, v)), _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) => {
|
|
122
123
|
if (open && !searchable)
|
|
123
124
|
onNavKeys(e);
|
|
124
125
|
else if (!open && (e.key === "ArrowDown" || e.key === "Enter")) {
|
|
@@ -20,7 +20,7 @@ export function RadioGroup({ value, defaultValue, onValueChange, variant = "defa
|
|
|
20
20
|
onValueChange?.(v);
|
|
21
21
|
}, [value, onValueChange]);
|
|
22
22
|
const { onKeyDown, groupRef } = useRadioGroupKeyboard({ select });
|
|
23
|
-
return (_jsx(RadioContext.Provider, { value: { value: selected, select, name: groupName, variant }, children:
|
|
23
|
+
return (_jsx(RadioContext.Provider, { value: { value: selected, select, name: groupName, variant }, children: _jsxs("div", { ref: groupRef, role: "radiogroup", "aria-label": ariaLabel, "data-variant": variant, className: radiogroupRecipe({ className }), ...rest, onKeyDown: onKeyDown, children: [children, name && selected && (_jsx("input", { type: "hidden", name: name, value: selected }))] }) }));
|
|
24
24
|
}
|
|
25
25
|
RadioGroup.displayName = "RadioGroup";
|
|
26
26
|
export function Radio({ value, label, children, disabled }) {
|
|
@@ -28,7 +28,7 @@ export function Radio({ value, label, children, disabled }) {
|
|
|
28
28
|
if (!ctx)
|
|
29
29
|
throw new Error("Radio must be used within a RadioGroup");
|
|
30
30
|
const checked = ctx.value === value;
|
|
31
|
-
return (_jsx("button", { type: "button", role: "radio", "aria-checked": checked,
|
|
31
|
+
return (_jsx("button", { type: "button", role: "radio", "aria-checked": checked, disabled: disabled, "data-value": value, "data-checked": checked ? "" : undefined,
|
|
32
32
|
// roving tabindex: the selected (or first, when none) radio is tabbable
|
|
33
33
|
tabIndex: checked ? 0 : -1, className: "radio", onClick: () => ctx.select(value), children: ctx.variant === "card" ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "radio-preview", children: children }), _jsx("span", { className: "radio-cardfoot", children: _jsx("span", { className: "radio-label", children: label }) })] })) : (_jsxs(_Fragment, { children: [_jsx("span", { className: "radio-dot" }), _jsx("span", { className: "radio-label", children: label ?? children })] })) }));
|
|
34
34
|
}
|
|
@@ -16,7 +16,7 @@ function optionText(o) {
|
|
|
16
16
|
* "No results" empty state). Full keyboard model — ↑/↓ move, Enter/Space pick,
|
|
17
17
|
* Esc close, Home/End jump, opens upward when tight. Controlled or uncontrolled.
|
|
18
18
|
*/
|
|
19
|
-
export function Select({ options, value, defaultValue, onValueChange, placeholder = "Select…", variant = "default", width = "full", searchable, searchPlaceholder = "Search…", emptyLabel = "No results", disabled, className, "aria-label": ariaLabel, id, ...rest }) {
|
|
19
|
+
export function Select({ options, value, defaultValue, onValueChange, name, placeholder = "Select…", variant = "default", width = "full", searchable, searchPlaceholder = "Search…", emptyLabel = "No results", disabled, className, "aria-label": ariaLabel, id, ...rest }) {
|
|
20
20
|
// Virtual focus: DOM focus stays on the trigger (or the search field), and the
|
|
21
21
|
// active option is named by `aria-activedescendant`. That is the only thing a
|
|
22
22
|
// screen reader has to go on -- `data-highlighted` is invisible to it -- so the
|
|
@@ -114,7 +114,7 @@ export function Select({ options, value, defaultValue, onValueChange, placeholde
|
|
|
114
114
|
document.addEventListener("pointerdown", onDown);
|
|
115
115
|
return () => document.removeEventListener("pointerdown", onDown);
|
|
116
116
|
}, [open]);
|
|
117
|
-
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) => {
|
|
117
|
+
return (_jsxs("div", { ref: rootRef, className: selectRecipe({ variant, width, className }), ...rest, children: [name && selected && _jsx("input", { type: "hidden", name: name, value: selected }), _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) => {
|
|
118
118
|
setQuery(e.target.value);
|
|
119
119
|
setHighlight(0);
|
|
120
120
|
}, onKeyDown: onNavKeys })] })), filtered.length === 0 ? (_jsx("div", { className: "select-empty", children: emptyLabel })) : (filtered.map((opt, i) => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
import { switchRecipe } from "./switch.recipe.js";
|
|
4
4
|
/**
|
|
@@ -7,16 +7,16 @@ import { switchRecipe } from "./switch.recipe.js";
|
|
|
7
7
|
* label association and keyboard (Space/Enter) come for free. Controlled via
|
|
8
8
|
* `checked`/`onCheckedChange`, or uncontrolled from `defaultChecked`.
|
|
9
9
|
*/
|
|
10
|
-
export const Switch = React.forwardRef(({ checked, defaultChecked = false, onCheckedChange, disabled, className, onClick, ...props }, ref) => {
|
|
10
|
+
export const Switch = React.forwardRef(({ checked, defaultChecked = false, onCheckedChange, disabled, className, onClick, name, value = "on", ...props }, ref) => {
|
|
11
11
|
const [internal, setInternal] = React.useState(defaultChecked);
|
|
12
12
|
const on = checked ?? internal;
|
|
13
|
-
return (_jsx("button", { ref: ref, type: "button", role: "switch", "aria-checked": on, disabled: disabled, "data-checked": on ? "" : undefined, className: switchRecipe({ className }), onClick: (e) => {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
13
|
+
return (_jsxs(_Fragment, { children: [_jsx("button", { ref: ref, type: "button", role: "switch", "aria-checked": on, disabled: disabled, "data-checked": on ? "" : undefined, className: switchRecipe({ className }), onClick: (e) => {
|
|
14
|
+
onClick?.(e);
|
|
15
|
+
if (e.defaultPrevented)
|
|
16
|
+
return;
|
|
17
|
+
if (checked === undefined)
|
|
18
|
+
setInternal((v) => !v);
|
|
19
|
+
onCheckedChange?.(!on);
|
|
20
|
+
}, ...props, children: _jsx("span", { className: "switch-thumb" }) }), name && on && _jsx("input", { type: "hidden", name: name, value: value })] }));
|
|
21
21
|
});
|
|
22
22
|
Switch.displayName = "Switch";
|
|
@@ -122,6 +122,31 @@ export function Command({ open, onOpenChange, items = defaultCommandItems, place
|
|
|
122
122
|
if (queryProp === undefined)
|
|
123
123
|
setInternalQuery(next);
|
|
124
124
|
}, [onQueryChange, queryProp]);
|
|
125
|
+
/**
|
|
126
|
+
* Where the user was when the palette opened.
|
|
127
|
+
*
|
|
128
|
+
* The dialog machine restores focus on close, and for Dialog and Drawer that
|
|
129
|
+
* lands on the trigger: focus-trap remembers whatever was focused when the
|
|
130
|
+
* trap activated. The palette is different — the combobox focuses the search
|
|
131
|
+
* input as part of the same commit, so the element the trap remembers is the
|
|
132
|
+
* input, which is then removed with the palette, and focus falls to
|
|
133
|
+
* `document.body`. The palette is the one surface opened from anywhere with
|
|
134
|
+
* a shortcut, so losing the caller's place is worst exactly here (#241).
|
|
135
|
+
*
|
|
136
|
+
* Recorded by watching focus WHILE CLOSED rather than reading
|
|
137
|
+
* `document.activeElement` when `open` flips: by the time an effect of ours
|
|
138
|
+
* runs, the input may already hold focus.
|
|
139
|
+
*/
|
|
140
|
+
const openerRef = React.useRef(null);
|
|
141
|
+
React.useEffect(() => {
|
|
142
|
+
if (open)
|
|
143
|
+
return;
|
|
144
|
+
const onFocusIn = (e) => {
|
|
145
|
+
openerRef.current = e.target;
|
|
146
|
+
};
|
|
147
|
+
document.addEventListener("focusin", onFocusIn);
|
|
148
|
+
return () => document.removeEventListener("focusin", onFocusIn);
|
|
149
|
+
}, [open]);
|
|
125
150
|
const dialogService = useMachine(dialog.machine, {
|
|
126
151
|
id: dialogId,
|
|
127
152
|
open,
|
|
@@ -129,6 +154,7 @@ export function Command({ open, onOpenChange, items = defaultCommandItems, place
|
|
|
129
154
|
onOpenChange(details.open);
|
|
130
155
|
},
|
|
131
156
|
initialFocusEl: () => inputRef.current,
|
|
157
|
+
finalFocusEl: () => openerRef.current,
|
|
132
158
|
"aria-label": ariaLabel,
|
|
133
159
|
});
|
|
134
160
|
const dlg = dialog.connect(dialogService, normalizeProps);
|
|
@@ -11,6 +11,13 @@ export type MultiSelectProps = Omit<React.ComponentPropsWithoutRef<"div">, "valu
|
|
|
11
11
|
value?: string[];
|
|
12
12
|
/** Initially selected values, when uncontrolled. */
|
|
13
13
|
defaultValue?: string[];
|
|
14
|
+
/**
|
|
15
|
+
* Submits with the form under this name. crisp's MultiSelect is a
|
|
16
|
+
* a button and a listbox, not a native control, so the value reaches a `<form>` only through a
|
|
17
|
+
* hidden input — without one, `name` type-checks, renders, and contributes
|
|
18
|
+
* nothing to FormData (#243).
|
|
19
|
+
*/
|
|
20
|
+
name?: string;
|
|
14
21
|
/** Fires with the next value on every toggle, controlled or not. */
|
|
15
22
|
onValueChange?: (value: string[]) => void;
|
|
16
23
|
placeholder?: string;
|
|
@@ -29,7 +36,7 @@ export type MultiSelectProps = Omit<React.ComponentPropsWithoutRef<"div">, "valu
|
|
|
29
36
|
* popover with a search field over rows of "[chip] … [checkbox]" that toggle on
|
|
30
37
|
* click (the menu stays open). Keyboard: ↑/↓ move, Enter toggles, Esc closes.
|
|
31
38
|
*/
|
|
32
|
-
export declare function MultiSelect({ options, value: valueProp, defaultValue, onValueChange, placeholder, searchable, searchPlaceholder, emptyLabel, disabled, className, "aria-label": ariaLabel, ...rest }: MultiSelectProps): React.JSX.Element;
|
|
39
|
+
export declare function MultiSelect({ options, value: valueProp, defaultValue, onValueChange, name, placeholder, searchable, searchPlaceholder, emptyLabel, disabled, className, "aria-label": ariaLabel, ...rest }: MultiSelectProps): React.JSX.Element;
|
|
33
40
|
export declare namespace MultiSelect {
|
|
34
41
|
var displayName: string;
|
|
35
42
|
}
|
|
@@ -6,6 +6,12 @@ export type RadioGroupProps = Omit<React.ComponentPropsWithoutRef<"div">, "value
|
|
|
6
6
|
onValueChange?: (value: string) => void;
|
|
7
7
|
/** `default` = rows (dot + label); `card` = bordered preview cards. */
|
|
8
8
|
variant?: "default" | "card";
|
|
9
|
+
/**
|
|
10
|
+
* Submits with the form under this name. It used to land on each radio
|
|
11
|
+
* `<button>`, where a name means nothing unless the button submits the form:
|
|
12
|
+
* the prop type-checked, rendered, and contributed nothing to FormData
|
|
13
|
+
* (#243). The value now rides on a hidden input.
|
|
14
|
+
*/
|
|
9
15
|
name?: string;
|
|
10
16
|
className?: string;
|
|
11
17
|
"aria-label"?: string;
|
|
@@ -14,6 +14,13 @@ export type SelectProps = Omit<React.ComponentPropsWithoutRef<"div">, "value" |
|
|
|
14
14
|
value?: string;
|
|
15
15
|
defaultValue?: string;
|
|
16
16
|
onValueChange?: (value: string) => void;
|
|
17
|
+
/**
|
|
18
|
+
* Submits with the form under this name. crisp's Select is a
|
|
19
|
+
* a button and a listbox, not a native control, so the value reaches a `<form>` only through a
|
|
20
|
+
* hidden input — without one, `name` type-checks, renders, and contributes
|
|
21
|
+
* nothing to FormData (#243).
|
|
22
|
+
*/
|
|
23
|
+
name?: string;
|
|
17
24
|
placeholder?: string;
|
|
18
25
|
/** `ghost` is borderless until hover — for inline selects (e.g. sort rows). */
|
|
19
26
|
variant?: "default" | "ghost";
|
|
@@ -36,7 +43,7 @@ export type SelectProps = Omit<React.ComponentPropsWithoutRef<"div">, "value" |
|
|
|
36
43
|
* "No results" empty state). Full keyboard model — ↑/↓ move, Enter/Space pick,
|
|
37
44
|
* Esc close, Home/End jump, opens upward when tight. Controlled or uncontrolled.
|
|
38
45
|
*/
|
|
39
|
-
export declare function Select({ options, value, defaultValue, onValueChange, placeholder, variant, width, searchable, searchPlaceholder, emptyLabel, disabled, className, "aria-label": ariaLabel, id, ...rest }: SelectProps): React.JSX.Element;
|
|
46
|
+
export declare function Select({ options, value, defaultValue, onValueChange, name, placeholder, variant, width, searchable, searchPlaceholder, emptyLabel, disabled, className, "aria-label": ariaLabel, id, ...rest }: SelectProps): React.JSX.Element;
|
|
40
47
|
export declare namespace Select {
|
|
41
48
|
var displayName: string;
|
|
42
49
|
}
|
|
@@ -4,6 +4,15 @@ export type SwitchProps = Omit<React.ComponentPropsWithoutRef<"button">, "onChan
|
|
|
4
4
|
checked?: boolean;
|
|
5
5
|
defaultChecked?: boolean;
|
|
6
6
|
onCheckedChange?: (checked: boolean) => void;
|
|
7
|
+
/**
|
|
8
|
+
* Submits with the form under this name. crisp's Switch is a
|
|
9
|
+
* `role="switch"` button`, not a native control, so the value reaches a `<form>` only
|
|
10
|
+
* through a hidden input — without one, `name` type-checks, renders, and
|
|
11
|
+
* contributes nothing to FormData (#243).
|
|
12
|
+
*/
|
|
13
|
+
name?: string;
|
|
14
|
+
/** The value submitted when it is on (default "on", as a checkbox does). */
|
|
15
|
+
value?: string;
|
|
7
16
|
};
|
|
8
17
|
/**
|
|
9
18
|
* An on/off toggle — the settings switch, measured from Attio. Renders a
|
|
@@ -16,4 +25,13 @@ export declare const Switch: React.ForwardRefExoticComponent<Omit<Omit<React.Det
|
|
|
16
25
|
checked?: boolean;
|
|
17
26
|
defaultChecked?: boolean;
|
|
18
27
|
onCheckedChange?: (checked: boolean) => void;
|
|
28
|
+
/**
|
|
29
|
+
* Submits with the form under this name. crisp's Switch is a
|
|
30
|
+
* `role="switch"` button`, not a native control, so the value reaches a `<form>` only
|
|
31
|
+
* through a hidden input — without one, `name` type-checks, renders, and
|
|
32
|
+
* contributes nothing to FormData (#243).
|
|
33
|
+
*/
|
|
34
|
+
name?: string;
|
|
35
|
+
/** The value submitted when it is on (default "on", as a checkbox does). */
|
|
36
|
+
value?: string;
|
|
19
37
|
} & React.RefAttributes<HTMLButtonElement>>;
|