@crisp-ui-kit/crisp 0.30.1 → 0.31.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 +2 -2
- package/dist/cjs/components/radiogroup/radiogroup.js +2 -2
- package/dist/cjs/components/select/select.js +5 -2
- package/dist/cjs/components/switch/switch.js +2 -2
- package/dist/cjs/core/form-value.js +29 -0
- package/dist/cjs/core/index.js +3 -1
- package/dist/esm/components/multiselect/multiselect.js +3 -3
- package/dist/esm/components/radiogroup/radiogroup.js +3 -3
- package/dist/esm/components/select/select.js +6 -3
- package/dist/esm/components/switch/switch.js +3 -3
- package/dist/esm/core/form-value.js +26 -0
- package/dist/esm/core/index.js +1 -0
- package/dist/types/components/multiselect/multiselect.d.ts +11 -1
- package/dist/types/components/radiogroup/radiogroup.d.ts +11 -1
- package/dist/types/components/select/select.d.ts +11 -1
- package/dist/types/components/switch/switch.d.ts +20 -0
- package/dist/types/core/form-value.d.ts +18 -0
- package/dist/types/core/index.d.ts +1 -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, name,
|
|
68
|
+
function MultiSelect({ options, value: valueProp, defaultValue, onValueChange, name, required,
|
|
69
69
|
// The id belongs on the trigger, not the wrapper: `<label for>` only names a
|
|
70
70
|
// LABELABLE element, so an id left on the root div meant a FormField label
|
|
71
71
|
// above this control named nothing (#247). Select already did this.
|
|
@@ -166,7 +166,7 @@ id, placeholder = "Select…", searchable = true, searchPlaceholder = "Search…
|
|
|
166
166
|
return () => document.removeEventListener("pointerdown", onDown);
|
|
167
167
|
}, [open]);
|
|
168
168
|
return ((0, jsx_runtime_1.jsxs)("div", { ref: rootRef, className: (0, multiselect_recipe_1.multiselectRecipe)({ className }), ...rest, children: [name &&
|
|
169
|
-
value.map((v) => (0, jsx_runtime_1.jsx)("input", {
|
|
169
|
+
(value.length > 0 ? value : required ? [""] : []).map((v) => ((0, jsx_runtime_1.jsx)("input", { className: "crisp-formvalue", style: core_1.formValueStyle, name: name, value: v, required: required, tabIndex: -1, "aria-hidden": "true", onChange: () => { } }, v))), (0, jsx_runtime_1.jsxs)("button", { ref: triggerRef, id: id, 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) => {
|
|
170
170
|
if (open && !searchable)
|
|
171
171
|
onNavKeys(e);
|
|
172
172
|
else if (!open && (e.key === "ArrowDown" || e.key === "Enter")) {
|
|
@@ -47,7 +47,7 @@ const RadioContext = React.createContext(null);
|
|
|
47
47
|
* preview cards whose border turns brand when picked (the Theme picker). Arrow
|
|
48
48
|
* keys move between options (roving focus). Controlled or uncontrolled.
|
|
49
49
|
*/
|
|
50
|
-
function RadioGroup({ value, defaultValue, onValueChange, variant = "default", name, className, "aria-label": ariaLabel, children, ...rest }) {
|
|
50
|
+
function RadioGroup({ value, defaultValue, onValueChange, variant = "default", name, required, className, "aria-label": ariaLabel, children, ...rest }) {
|
|
51
51
|
const [internal, setInternal] = React.useState(defaultValue);
|
|
52
52
|
const selected = value ?? internal;
|
|
53
53
|
const generatedName = React.useId();
|
|
@@ -68,7 +68,7 @@ function RadioGroup({ value, defaultValue, onValueChange, variant = "default", n
|
|
|
68
68
|
if (value === undefined)
|
|
69
69
|
setInternal(defaultValue);
|
|
70
70
|
});
|
|
71
|
-
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", {
|
|
71
|
+
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 || required) && ((0, jsx_runtime_1.jsx)("input", { className: "crisp-formvalue", style: core_1.formValueStyle, name: name, value: selected ?? "", required: required, tabIndex: -1, "aria-hidden": "true", onChange: () => { } }))] }) }));
|
|
72
72
|
}
|
|
73
73
|
RadioGroup.displayName = "RadioGroup";
|
|
74
74
|
function Radio({ value, label, children, disabled }) {
|
|
@@ -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, name, 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, required, 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
|
|
@@ -156,7 +156,10 @@ function Select({ options, value, defaultValue, onValueChange, name, placeholder
|
|
|
156
156
|
document.addEventListener("pointerdown", onDown);
|
|
157
157
|
return () => document.removeEventListener("pointerdown", onDown);
|
|
158
158
|
}, [open]);
|
|
159
|
-
return ((0, jsx_runtime_1.jsxs)("div", { ref: rootRef, className: (0, select_recipe_1.selectRecipe)({ variant, width, className }), ...rest, children: [name &&
|
|
159
|
+
return ((0, jsx_runtime_1.jsxs)("div", { ref: rootRef, className: (0, select_recipe_1.selectRecipe)({ variant, width, className }), ...rest, children: [name && (selected || required) && ((0, jsx_runtime_1.jsx)("input", { className: "crisp-formvalue", style: core_1.formValueStyle, name: name, value: selected ?? "", required: required, tabIndex: -1, "aria-hidden": "true",
|
|
160
|
+
// controlled by the listbox above; `readOnly` would bar it from
|
|
161
|
+
// validation, which is the only reason this input is not hidden
|
|
162
|
+
onChange: () => { } })), (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) => {
|
|
160
163
|
setQuery(e.target.value);
|
|
161
164
|
setHighlight(0);
|
|
162
165
|
}, onKeyDown: onNavKeys })] })), filtered.length === 0 ? ((0, jsx_runtime_1.jsx)("div", { className: "select-empty", children: emptyLabel })) : (filtered.map((opt, i) => {
|
|
@@ -44,7 +44,7 @@ const switch_recipe_1 = require("./switch.recipe");
|
|
|
44
44
|
* label association and keyboard (Space/Enter) come for free. Controlled via
|
|
45
45
|
* `checked`/`onCheckedChange`, or uncontrolled from `defaultChecked`.
|
|
46
46
|
*/
|
|
47
|
-
exports.Switch = React.forwardRef(({ checked, defaultChecked = false, onCheckedChange, disabled, className, onClick, name, value = "on", ...props }, ref) => {
|
|
47
|
+
exports.Switch = React.forwardRef(({ checked, defaultChecked = false, onCheckedChange, disabled, className, onClick, name, value = "on", required, ...props }, ref) => {
|
|
48
48
|
const [internal, setInternal] = React.useState(defaultChecked);
|
|
49
49
|
const on = checked ?? internal;
|
|
50
50
|
// `form.reset()` restores native controls; a role="switch" button holds its
|
|
@@ -63,6 +63,6 @@ exports.Switch = React.forwardRef(({ checked, defaultChecked = false, onCheckedC
|
|
|
63
63
|
if (checked === undefined)
|
|
64
64
|
setInternal((v) => !v);
|
|
65
65
|
onCheckedChange?.(!on);
|
|
66
|
-
}, ...props, children: (0, jsx_runtime_1.jsx)("span", { className: "switch-thumb" }) }), name && on && (0, jsx_runtime_1.jsx)("input", { type: "
|
|
66
|
+
}, ...props, children: (0, jsx_runtime_1.jsx)("span", { className: "switch-thumb" }) }), name && (on || required) && ((0, jsx_runtime_1.jsx)("input", { className: "crisp-formvalue", style: core_1.formValueStyle, type: "checkbox", name: name, value: value, checked: on, required: required, tabIndex: -1, "aria-hidden": "true", onChange: () => { } }))] }));
|
|
67
67
|
});
|
|
68
68
|
exports.Switch.displayName = "Switch";
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.formValueStyle = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* The style for the input that carries a non-native control's value into the
|
|
6
|
+
* form.
|
|
7
|
+
*
|
|
8
|
+
* It cannot be `type="hidden"`: hidden inputs are *barred from constraint
|
|
9
|
+
* validation*, so `required` on one is ignored and the form submits with the
|
|
10
|
+
* field empty (#253). A laid-out but invisible input validates normally — the
|
|
11
|
+
* browser blocks the submit, focuses it and shows its own message beside the
|
|
12
|
+
* control.
|
|
13
|
+
*
|
|
14
|
+
* `opacity: 0` rather than `display: none` or `visibility: hidden`, both of
|
|
15
|
+
* which bar it again. Inline rather than a recipe rule because Switch renders
|
|
16
|
+
* its input as a SIBLING of the button that is its root: a nested `& .class`
|
|
17
|
+
* rule never matches there, and the first version of this shipped a visible
|
|
18
|
+
* 13px checkbox next to every switch — caught in a browser, not by a test.
|
|
19
|
+
*/
|
|
20
|
+
exports.formValueStyle = {
|
|
21
|
+
position: "absolute",
|
|
22
|
+
width: 1,
|
|
23
|
+
height: 1,
|
|
24
|
+
opacity: 0,
|
|
25
|
+
margin: 0,
|
|
26
|
+
padding: 0,
|
|
27
|
+
border: 0,
|
|
28
|
+
pointerEvents: "none",
|
|
29
|
+
};
|
package/dist/cjs/core/index.js
CHANGED
|
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.usePrefersReducedMotion = exports.useLabelAssociation = exports.useFormReset = exports.useFocusAfterRemoval = exports.resolveRecipe = exports.Primitive = exports.placeFloating = exports.isRtlElement = exports.withDescribedBy = exports.defineRecipe = exports.cx = void 0;
|
|
17
|
+
exports.usePrefersReducedMotion = exports.useLabelAssociation = exports.useFormReset = exports.useFocusAfterRemoval = exports.resolveRecipe = exports.Primitive = exports.placeFloating = exports.formValueStyle = exports.isRtlElement = exports.withDescribedBy = exports.defineRecipe = exports.cx = void 0;
|
|
18
18
|
var cx_1 = require("./cx");
|
|
19
19
|
Object.defineProperty(exports, "cx", { enumerable: true, get: function () { return cx_1.cx; } });
|
|
20
20
|
var define_1 = require("./define");
|
|
@@ -23,6 +23,8 @@ var dialog_description_1 = require("./dialog-description");
|
|
|
23
23
|
Object.defineProperty(exports, "withDescribedBy", { enumerable: true, get: function () { return dialog_description_1.withDescribedBy; } });
|
|
24
24
|
var direction_1 = require("./direction");
|
|
25
25
|
Object.defineProperty(exports, "isRtlElement", { enumerable: true, get: function () { return direction_1.isRtlElement; } });
|
|
26
|
+
var form_value_1 = require("./form-value");
|
|
27
|
+
Object.defineProperty(exports, "formValueStyle", { enumerable: true, get: function () { return form_value_1.formValueStyle; } });
|
|
26
28
|
var place_floating_1 = require("./place-floating");
|
|
27
29
|
Object.defineProperty(exports, "placeFloating", { enumerable: true, get: function () { return place_floating_1.placeFloating; } });
|
|
28
30
|
var primitive_1 = require("./primitive");
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Check, ChevronDown, Search } from "lucide-react";
|
|
3
3
|
import * as React from "react";
|
|
4
|
-
import { placeFloating, useFormReset } from "../../core/index.js";
|
|
4
|
+
import { formValueStyle, placeFloating, useFormReset } from "../../core/index.js";
|
|
5
5
|
import { useListboxKeyboard } from "../select/index.js";
|
|
6
6
|
import { multiselectRecipe } from "./multiselect.recipe.js";
|
|
7
7
|
const TONES = [
|
|
@@ -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, name,
|
|
32
|
+
export function MultiSelect({ options, value: valueProp, defaultValue, onValueChange, name, required,
|
|
33
33
|
// The id belongs on the trigger, not the wrapper: `<label for>` only names a
|
|
34
34
|
// LABELABLE element, so an id left on the root div meant a FormField label
|
|
35
35
|
// above this control named nothing (#247). Select already did this.
|
|
@@ -130,7 +130,7 @@ id, placeholder = "Select…", searchable = true, searchPlaceholder = "Search…
|
|
|
130
130
|
return () => document.removeEventListener("pointerdown", onDown);
|
|
131
131
|
}, [open]);
|
|
132
132
|
return (_jsxs("div", { ref: rootRef, className: multiselectRecipe({ className }), ...rest, children: [name &&
|
|
133
|
-
value.map((v) => _jsx("input", {
|
|
133
|
+
(value.length > 0 ? value : required ? [""] : []).map((v) => (_jsx("input", { className: "crisp-formvalue", style: formValueStyle, name: name, value: v, required: required, tabIndex: -1, "aria-hidden": "true", onChange: () => { } }, v))), _jsxs("button", { ref: triggerRef, id: id, 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) => {
|
|
134
134
|
if (open && !searchable)
|
|
135
135
|
onNavKeys(e);
|
|
136
136
|
else if (!open && (e.key === "ArrowDown" || e.key === "Enter")) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import * as React from "react";
|
|
3
|
-
import { useFormReset, useLabelAssociation } from "../../core/index.js";
|
|
3
|
+
import { formValueStyle, useFormReset, useLabelAssociation } from "../../core/index.js";
|
|
4
4
|
import { radiogroupRecipe } from "./radiogroup.recipe.js";
|
|
5
5
|
import { useRadioGroupKeyboard } from "./radiogroup-keyboard.js";
|
|
6
6
|
const RadioContext = React.createContext(null);
|
|
@@ -10,7 +10,7 @@ const RadioContext = React.createContext(null);
|
|
|
10
10
|
* preview cards whose border turns brand when picked (the Theme picker). Arrow
|
|
11
11
|
* keys move between options (roving focus). Controlled or uncontrolled.
|
|
12
12
|
*/
|
|
13
|
-
export function RadioGroup({ value, defaultValue, onValueChange, variant = "default", name, className, "aria-label": ariaLabel, children, ...rest }) {
|
|
13
|
+
export function RadioGroup({ value, defaultValue, onValueChange, variant = "default", name, required, className, "aria-label": ariaLabel, children, ...rest }) {
|
|
14
14
|
const [internal, setInternal] = React.useState(defaultValue);
|
|
15
15
|
const selected = value ?? internal;
|
|
16
16
|
const generatedName = React.useId();
|
|
@@ -31,7 +31,7 @@ export function RadioGroup({ value, defaultValue, onValueChange, variant = "defa
|
|
|
31
31
|
if (value === undefined)
|
|
32
32
|
setInternal(defaultValue);
|
|
33
33
|
});
|
|
34
|
-
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", {
|
|
34
|
+
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 || required) && (_jsx("input", { className: "crisp-formvalue", style: formValueStyle, name: name, value: selected ?? "", required: required, tabIndex: -1, "aria-hidden": "true", onChange: () => { } }))] }) }));
|
|
35
35
|
}
|
|
36
36
|
RadioGroup.displayName = "RadioGroup";
|
|
37
37
|
export function Radio({ value, label, children, disabled }) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Check, ChevronDown, Search } from "lucide-react";
|
|
3
3
|
import * as React from "react";
|
|
4
|
-
import { placeFloating, useFormReset } from "../../core/index.js";
|
|
4
|
+
import { formValueStyle, placeFloating, useFormReset } from "../../core/index.js";
|
|
5
5
|
import { useListboxKeyboard } from "./listbox-keyboard.js";
|
|
6
6
|
import { selectRecipe } from "./select.recipe.js";
|
|
7
7
|
function optionText(o) {
|
|
@@ -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, name, 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, required, 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
|
|
@@ -120,7 +120,10 @@ export function Select({ options, value, defaultValue, onValueChange, name, plac
|
|
|
120
120
|
document.addEventListener("pointerdown", onDown);
|
|
121
121
|
return () => document.removeEventListener("pointerdown", onDown);
|
|
122
122
|
}, [open]);
|
|
123
|
-
return (_jsxs("div", { ref: rootRef, className: selectRecipe({ variant, width, className }), ...rest, children: [name &&
|
|
123
|
+
return (_jsxs("div", { ref: rootRef, className: selectRecipe({ variant, width, className }), ...rest, children: [name && (selected || required) && (_jsx("input", { className: "crisp-formvalue", style: formValueStyle, name: name, value: selected ?? "", required: required, tabIndex: -1, "aria-hidden": "true",
|
|
124
|
+
// controlled by the listbox above; `readOnly` would bar it from
|
|
125
|
+
// validation, which is the only reason this input is not hidden
|
|
126
|
+
onChange: () => { } })), _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) => {
|
|
124
127
|
setQuery(e.target.value);
|
|
125
128
|
setHighlight(0);
|
|
126
129
|
}, onKeyDown: onNavKeys })] })), filtered.length === 0 ? (_jsx("div", { className: "select-empty", children: emptyLabel })) : (filtered.map((opt, i) => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import * as React from "react";
|
|
3
|
-
import { useFormReset } from "../../core/index.js";
|
|
3
|
+
import { formValueStyle, useFormReset } from "../../core/index.js";
|
|
4
4
|
import { switchRecipe } from "./switch.recipe.js";
|
|
5
5
|
/**
|
|
6
6
|
* An on/off toggle — the settings switch, measured from Attio. Renders a
|
|
@@ -8,7 +8,7 @@ import { switchRecipe } from "./switch.recipe.js";
|
|
|
8
8
|
* label association and keyboard (Space/Enter) come for free. Controlled via
|
|
9
9
|
* `checked`/`onCheckedChange`, or uncontrolled from `defaultChecked`.
|
|
10
10
|
*/
|
|
11
|
-
export const Switch = React.forwardRef(({ checked, defaultChecked = false, onCheckedChange, disabled, className, onClick, name, value = "on", ...props }, ref) => {
|
|
11
|
+
export const Switch = React.forwardRef(({ checked, defaultChecked = false, onCheckedChange, disabled, className, onClick, name, value = "on", required, ...props }, ref) => {
|
|
12
12
|
const [internal, setInternal] = React.useState(defaultChecked);
|
|
13
13
|
const on = checked ?? internal;
|
|
14
14
|
// `form.reset()` restores native controls; a role="switch" button holds its
|
|
@@ -27,6 +27,6 @@ export const Switch = React.forwardRef(({ checked, defaultChecked = false, onChe
|
|
|
27
27
|
if (checked === undefined)
|
|
28
28
|
setInternal((v) => !v);
|
|
29
29
|
onCheckedChange?.(!on);
|
|
30
|
-
}, ...props, children: _jsx("span", { className: "switch-thumb" }) }), name && on && _jsx("input", { type: "
|
|
30
|
+
}, ...props, children: _jsx("span", { className: "switch-thumb" }) }), name && (on || required) && (_jsx("input", { className: "crisp-formvalue", style: formValueStyle, type: "checkbox", name: name, value: value, checked: on, required: required, tabIndex: -1, "aria-hidden": "true", onChange: () => { } }))] }));
|
|
31
31
|
});
|
|
32
32
|
Switch.displayName = "Switch";
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The style for the input that carries a non-native control's value into the
|
|
3
|
+
* form.
|
|
4
|
+
*
|
|
5
|
+
* It cannot be `type="hidden"`: hidden inputs are *barred from constraint
|
|
6
|
+
* validation*, so `required` on one is ignored and the form submits with the
|
|
7
|
+
* field empty (#253). A laid-out but invisible input validates normally — the
|
|
8
|
+
* browser blocks the submit, focuses it and shows its own message beside the
|
|
9
|
+
* control.
|
|
10
|
+
*
|
|
11
|
+
* `opacity: 0` rather than `display: none` or `visibility: hidden`, both of
|
|
12
|
+
* which bar it again. Inline rather than a recipe rule because Switch renders
|
|
13
|
+
* its input as a SIBLING of the button that is its root: a nested `& .class`
|
|
14
|
+
* rule never matches there, and the first version of this shipped a visible
|
|
15
|
+
* 13px checkbox next to every switch — caught in a browser, not by a test.
|
|
16
|
+
*/
|
|
17
|
+
export const formValueStyle = {
|
|
18
|
+
position: "absolute",
|
|
19
|
+
width: 1,
|
|
20
|
+
height: 1,
|
|
21
|
+
opacity: 0,
|
|
22
|
+
margin: 0,
|
|
23
|
+
padding: 0,
|
|
24
|
+
border: 0,
|
|
25
|
+
pointerEvents: "none",
|
|
26
|
+
};
|
package/dist/esm/core/index.js
CHANGED
|
@@ -2,6 +2,7 @@ export { cx } from "./cx.js";
|
|
|
2
2
|
export { defineRecipe } from "./define.js";
|
|
3
3
|
export { withDescribedBy } from "./dialog-description.js";
|
|
4
4
|
export { isRtlElement } from "./direction.js";
|
|
5
|
+
export { formValueStyle } from "./form-value.js";
|
|
5
6
|
export { placeFloating, } from "./place-floating.js";
|
|
6
7
|
export { Primitive } from "./primitive.js";
|
|
7
8
|
export { resolveRecipe } from "./resolve.js";
|
|
@@ -18,6 +18,16 @@ export type MultiSelectProps = Omit<React.ComponentPropsWithoutRef<"div">, "valu
|
|
|
18
18
|
* nothing to FormData (#243).
|
|
19
19
|
*/
|
|
20
20
|
name?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Blocks the form until the field is answered, using the browser's own
|
|
23
|
+
* validation: the submit is stopped, the control is focused and the
|
|
24
|
+
* browser's message is shown.
|
|
25
|
+
*
|
|
26
|
+
* Needs `name`, because the value rides on a real input — a `type="hidden"`
|
|
27
|
+
* one is barred from constraint validation and ignores `required`
|
|
28
|
+
* entirely (#253).
|
|
29
|
+
*/
|
|
30
|
+
required?: boolean;
|
|
21
31
|
/** Fires with the next value on every toggle, controlled or not. */
|
|
22
32
|
onValueChange?: (value: string[]) => void;
|
|
23
33
|
placeholder?: string;
|
|
@@ -36,7 +46,7 @@ export type MultiSelectProps = Omit<React.ComponentPropsWithoutRef<"div">, "valu
|
|
|
36
46
|
* popover with a search field over rows of "[chip] … [checkbox]" that toggle on
|
|
37
47
|
* click (the menu stays open). Keyboard: ↑/↓ move, Enter toggles, Esc closes.
|
|
38
48
|
*/
|
|
39
|
-
export declare function MultiSelect({ options, value: valueProp, defaultValue, onValueChange, name, id, placeholder, searchable, searchPlaceholder, emptyLabel, disabled, className, "aria-label": ariaLabel, ...rest }: MultiSelectProps): React.JSX.Element;
|
|
49
|
+
export declare function MultiSelect({ options, value: valueProp, defaultValue, onValueChange, name, required, id, placeholder, searchable, searchPlaceholder, emptyLabel, disabled, className, "aria-label": ariaLabel, ...rest }: MultiSelectProps): React.JSX.Element;
|
|
40
50
|
export declare namespace MultiSelect {
|
|
41
51
|
var displayName: string;
|
|
42
52
|
}
|
|
@@ -13,6 +13,16 @@ export type RadioGroupProps = Omit<React.ComponentPropsWithoutRef<"div">, "value
|
|
|
13
13
|
* (#243). The value now rides on a hidden input.
|
|
14
14
|
*/
|
|
15
15
|
name?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Blocks the form until the field is answered, using the browser's own
|
|
18
|
+
* validation: the submit is stopped, the control is focused and the
|
|
19
|
+
* browser's message is shown.
|
|
20
|
+
*
|
|
21
|
+
* Needs `name`, because the value rides on a real input — a `type="hidden"`
|
|
22
|
+
* one is barred from constraint validation and ignores `required`
|
|
23
|
+
* entirely (#253).
|
|
24
|
+
*/
|
|
25
|
+
required?: boolean;
|
|
16
26
|
className?: string;
|
|
17
27
|
"aria-label"?: string;
|
|
18
28
|
children: React.ReactNode;
|
|
@@ -23,7 +33,7 @@ export type RadioGroupProps = Omit<React.ComponentPropsWithoutRef<"div">, "value
|
|
|
23
33
|
* preview cards whose border turns brand when picked (the Theme picker). Arrow
|
|
24
34
|
* keys move between options (roving focus). Controlled or uncontrolled.
|
|
25
35
|
*/
|
|
26
|
-
export declare function RadioGroup({ value, defaultValue, onValueChange, variant, name, className, "aria-label": ariaLabel, children, ...rest }: RadioGroupProps): React.JSX.Element;
|
|
36
|
+
export declare function RadioGroup({ value, defaultValue, onValueChange, variant, name, required, className, "aria-label": ariaLabel, children, ...rest }: RadioGroupProps): React.JSX.Element;
|
|
27
37
|
export declare namespace RadioGroup {
|
|
28
38
|
var displayName: string;
|
|
29
39
|
}
|
|
@@ -21,6 +21,16 @@ export type SelectProps = Omit<React.ComponentPropsWithoutRef<"div">, "value" |
|
|
|
21
21
|
* nothing to FormData (#243).
|
|
22
22
|
*/
|
|
23
23
|
name?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Blocks the form until the field is answered, using the browser's own
|
|
26
|
+
* validation: the submit is stopped, the control is focused and the
|
|
27
|
+
* browser's message is shown.
|
|
28
|
+
*
|
|
29
|
+
* Needs `name`, because the value rides on a real input — a `type="hidden"`
|
|
30
|
+
* one is barred from constraint validation and ignores `required`
|
|
31
|
+
* entirely (#253).
|
|
32
|
+
*/
|
|
33
|
+
required?: boolean;
|
|
24
34
|
placeholder?: string;
|
|
25
35
|
/** `ghost` is borderless until hover — for inline selects (e.g. sort rows). */
|
|
26
36
|
variant?: "default" | "ghost";
|
|
@@ -43,7 +53,7 @@ export type SelectProps = Omit<React.ComponentPropsWithoutRef<"div">, "value" |
|
|
|
43
53
|
* "No results" empty state). Full keyboard model — ↑/↓ move, Enter/Space pick,
|
|
44
54
|
* Esc close, Home/End jump, opens upward when tight. Controlled or uncontrolled.
|
|
45
55
|
*/
|
|
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;
|
|
56
|
+
export declare function Select({ options, value, defaultValue, onValueChange, name, required, placeholder, variant, width, searchable, searchPlaceholder, emptyLabel, disabled, className, "aria-label": ariaLabel, id, ...rest }: SelectProps): React.JSX.Element;
|
|
47
57
|
export declare namespace Select {
|
|
48
58
|
var displayName: string;
|
|
49
59
|
}
|
|
@@ -13,6 +13,16 @@ export type SwitchProps = Omit<React.ComponentPropsWithoutRef<"button">, "onChan
|
|
|
13
13
|
name?: string;
|
|
14
14
|
/** The value submitted when it is on (default "on", as a checkbox does). */
|
|
15
15
|
value?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Blocks the form until the field is answered, using the browser's own
|
|
18
|
+
* validation: the submit is stopped, the control is focused and the
|
|
19
|
+
* browser's message is shown.
|
|
20
|
+
*
|
|
21
|
+
* Needs `name`, because the value rides on a real input — a `type="hidden"`
|
|
22
|
+
* one is barred from constraint validation and ignores `required`
|
|
23
|
+
* entirely (#253).
|
|
24
|
+
*/
|
|
25
|
+
required?: boolean;
|
|
16
26
|
};
|
|
17
27
|
/**
|
|
18
28
|
* An on/off toggle — the settings switch, measured from Attio. Renders a
|
|
@@ -34,4 +44,14 @@ export declare const Switch: React.ForwardRefExoticComponent<Omit<Omit<React.Det
|
|
|
34
44
|
name?: string;
|
|
35
45
|
/** The value submitted when it is on (default "on", as a checkbox does). */
|
|
36
46
|
value?: string;
|
|
47
|
+
/**
|
|
48
|
+
* Blocks the form until the field is answered, using the browser's own
|
|
49
|
+
* validation: the submit is stopped, the control is focused and the
|
|
50
|
+
* browser's message is shown.
|
|
51
|
+
*
|
|
52
|
+
* Needs `name`, because the value rides on a real input — a `type="hidden"`
|
|
53
|
+
* one is barred from constraint validation and ignores `required`
|
|
54
|
+
* entirely (#253).
|
|
55
|
+
*/
|
|
56
|
+
required?: boolean;
|
|
37
57
|
} & React.RefAttributes<HTMLButtonElement>>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type * as React from "react";
|
|
2
|
+
/**
|
|
3
|
+
* The style for the input that carries a non-native control's value into the
|
|
4
|
+
* form.
|
|
5
|
+
*
|
|
6
|
+
* It cannot be `type="hidden"`: hidden inputs are *barred from constraint
|
|
7
|
+
* validation*, so `required` on one is ignored and the form submits with the
|
|
8
|
+
* field empty (#253). A laid-out but invisible input validates normally — the
|
|
9
|
+
* browser blocks the submit, focuses it and shows its own message beside the
|
|
10
|
+
* control.
|
|
11
|
+
*
|
|
12
|
+
* `opacity: 0` rather than `display: none` or `visibility: hidden`, both of
|
|
13
|
+
* which bar it again. Inline rather than a recipe rule because Switch renders
|
|
14
|
+
* its input as a SIBLING of the button that is its root: a nested `& .class`
|
|
15
|
+
* rule never matches there, and the first version of this shipped a visible
|
|
16
|
+
* 13px checkbox next to every switch — caught in a browser, not by a test.
|
|
17
|
+
*/
|
|
18
|
+
export declare const formValueStyle: React.CSSProperties;
|
|
@@ -2,6 +2,7 @@ export { cx } from "./cx";
|
|
|
2
2
|
export { defineRecipe } from "./define";
|
|
3
3
|
export { withDescribedBy } from "./dialog-description";
|
|
4
4
|
export { isRtlElement } from "./direction";
|
|
5
|
+
export { formValueStyle } from "./form-value";
|
|
5
6
|
export { type FloatingAlign, type PlaceFloatingOptions, placeFloating, } from "./place-floating";
|
|
6
7
|
export { Primitive } from "./primitive";
|
|
7
8
|
export { resolveRecipe } from "./resolve";
|