@crisp-ui-kit/crisp 0.30.1 → 0.31.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/components/multiselect/multiselect.js +2 -2
- package/dist/cjs/components/radiogroup/radiogroup-keyboard.js +6 -1
- 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/components/tabs/tabs-keyboard.js +9 -2
- package/dist/cjs/core/form-value.js +29 -0
- package/dist/cjs/core/index.js +3 -1
- package/dist/cjs/product/board/board.js +7 -5
- package/dist/cjs/product/recordpage/recordpage.js +9 -4
- package/dist/cjs/product/recordstable/recordstable.js +9 -4
- package/dist/esm/components/multiselect/multiselect.js +3 -3
- package/dist/esm/components/radiogroup/radiogroup-keyboard.js +6 -1
- 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/components/tabs/tabs-keyboard.js +9 -2
- package/dist/esm/core/form-value.js +26 -0
- package/dist/esm/core/index.js +1 -0
- package/dist/esm/product/board/board.js +7 -5
- package/dist/esm/product/recordpage/recordpage.js +9 -4
- package/dist/esm/product/recordstable/recordstable.js +9 -4
- 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")) {
|
|
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.useRadioGroupKeyboard = useRadioGroupKeyboard;
|
|
37
37
|
const React = __importStar(require("react"));
|
|
38
|
+
const core_1 = require("../../core");
|
|
38
39
|
/**
|
|
39
40
|
* The WAI-ARIA radio group keyboard model: ←/→/↑/↓ move focus to the next or
|
|
40
41
|
* previous enabled radio, wrapping, and check it on arrival. Home/End jump to
|
|
@@ -113,7 +114,11 @@ function useRadioGroupKeyboard({ select, groupRef: externalRef, }) {
|
|
|
113
114
|
else if (e.key === "End")
|
|
114
115
|
next = radios[radios.length - 1];
|
|
115
116
|
else {
|
|
116
|
-
|
|
117
|
+
// Down always means forward; the horizontal pair follows the direction
|
|
118
|
+
// the radios are drawn in, which for Segmented's pill row is the whole
|
|
119
|
+
// point (#255).
|
|
120
|
+
const rtl = (0, core_1.isRtlElement)(e.currentTarget);
|
|
121
|
+
const fwd = e.key === "ArrowDown" || e.key === (rtl ? "ArrowLeft" : "ArrowRight");
|
|
117
122
|
// focus outside the group: enter at the near end rather than stepping
|
|
118
123
|
// off an index of -1, which would land two from the back going backwards
|
|
119
124
|
if (idx < 0)
|
|
@@ -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";
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.useTablistKeyboard = useTablistKeyboard;
|
|
4
|
+
const core_1 = require("../../core");
|
|
4
5
|
/**
|
|
5
6
|
* The WAI-ARIA tablist keyboard model: ←/→ to move (wrapping), Home/End to
|
|
6
7
|
* jump, disabled tabs skipped, focus following selection.
|
|
@@ -20,9 +21,15 @@ function useTablistKeyboard({ tabs, active, select, listRef, }) {
|
|
|
20
21
|
return;
|
|
21
22
|
const idx = enabled.findIndex((t) => t.value === active);
|
|
22
23
|
let next = -1;
|
|
23
|
-
|
|
24
|
+
// The arrows follow the strip as it is drawn: under RTL the next tab is to
|
|
25
|
+
// the LEFT, so the keys swap. Everything else about the model is unchanged
|
|
26
|
+
// (#255).
|
|
27
|
+
const rtl = (0, core_1.isRtlElement)(e.currentTarget);
|
|
28
|
+
const forwardKey = rtl ? "ArrowLeft" : "ArrowRight";
|
|
29
|
+
const backKey = rtl ? "ArrowRight" : "ArrowLeft";
|
|
30
|
+
if (e.key === forwardKey)
|
|
24
31
|
next = (idx + 1) % enabled.length;
|
|
25
|
-
else if (e.key ===
|
|
32
|
+
else if (e.key === backKey)
|
|
26
33
|
next = (idx - 1 + enabled.length) % enabled.length;
|
|
27
34
|
else if (e.key === "Home")
|
|
28
35
|
next = 0;
|
|
@@ -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");
|
|
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.Board = Board;
|
|
37
37
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
38
38
|
const React = __importStar(require("react"));
|
|
39
|
+
const core_1 = require("../../core");
|
|
39
40
|
const board_recipe_1 = require("./board.recipe");
|
|
40
41
|
/**
|
|
41
42
|
* A kanban board with native drag-and-drop between columns. Cards are
|
|
@@ -108,11 +109,12 @@ function Board({ columns, cards, onCardsChange, renderCard, onNewCard, newCardLa
|
|
|
108
109
|
}, onKeyDown: (e) => {
|
|
109
110
|
if (!(e.ctrlKey || e.metaKey))
|
|
110
111
|
return;
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
112
|
+
// the columns are a flex row, so under RTL the next one
|
|
113
|
+
// is to the LEFT and the keys swap (#255)
|
|
114
|
+
const rtl = (0, core_1.isRtlElement)(e.currentTarget);
|
|
115
|
+
const forwardKey = rtl ? "ArrowLeft" : "ArrowRight";
|
|
116
|
+
const backKey = rtl ? "ArrowRight" : "ArrowLeft";
|
|
117
|
+
const delta = e.key === backKey ? -1 : e.key === forwardKey ? 1 : 0;
|
|
116
118
|
// only swallow the key when the move actually happens, so
|
|
117
119
|
// Ctrl+Right at the last column still reaches the browser
|
|
118
120
|
if (delta && moveByKey(card, delta))
|
|
@@ -414,10 +414,15 @@ function RecordPage({ record, columns, collection = "Records", labels, onBack, o
|
|
|
414
414
|
if (!next)
|
|
415
415
|
setFieldQuery("");
|
|
416
416
|
}, children: showAllValues ? t.viewLess : t.viewAllValues }))] })) : ((0, jsx_runtime_1.jsxs)("div", { className: "recordpage-noresults", children: ["No fields match \u201C", fieldQuery, "\u201D."] })))] }), (0, jsx_runtime_1.jsxs)("div", { className: "recordpage-section", children: [(0, jsx_runtime_1.jsx)(SectionHead, { title: t.tabs.lists, action: (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "recordpage-addbtn", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 14, strokeWidth: 2 }), t.addToList] }) }), (0, jsx_runtime_1.jsx)("div", { className: "recordpage-emptystate", children: t.noLists })] })] }), (0, jsx_runtime_1.jsx)("div", { className: "recordpage-resizer", "data-active": resizing ? "" : undefined, role: "separator", "aria-orientation": "vertical", "aria-label": t.resizeDetails, "aria-valuenow": Math.round(detailsW), "aria-valuemin": 300, "aria-valuemax": 560, tabIndex: 0, onPointerDown: onResizeDown, onPointerMove: onResizeMove, onPointerUp: onResizeUp, onKeyDown: (e) => {
|
|
417
|
-
if (e.key
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
417
|
+
if (e.key !== "ArrowLeft" && e.key !== "ArrowRight")
|
|
418
|
+
return;
|
|
419
|
+
// The pointer drag learned its direction in #212; this did not.
|
|
420
|
+
// The details column is the flex row's first child, so it sits on
|
|
421
|
+
// the RIGHT under RTL and ArrowLeft has to grow it — the same
|
|
422
|
+
// half-conversion the drag had, one handler away (#255).
|
|
423
|
+
const rtl = (0, core_1.isRtlElement)(e.currentTarget);
|
|
424
|
+
const grow = e.key === (rtl ? "ArrowLeft" : "ArrowRight");
|
|
425
|
+
setDetailsW((w) => grow ? Math.min(560, w + 16) : Math.max(300, w - 16));
|
|
421
426
|
} }), (0, jsx_runtime_1.jsx)("main", { className: "recordpage-main", children: (0, jsx_runtime_1.jsx)(tabs_1.Tabs, { tabs: tabs, defaultValue: "overview", "aria-label": `${name} record`, children: panel }) })] })] }));
|
|
422
427
|
}
|
|
423
428
|
RecordPage.displayName = "RecordPage";
|
|
@@ -640,10 +640,15 @@ function RecordsTable({ labels, columns: columnsProp = exports.defaultColumns, r
|
|
|
640
640
|
nr = Math.max(0, r - 1);
|
|
641
641
|
else if (e.key === "ArrowDown")
|
|
642
642
|
nr = Math.min(visibleRows.length - 1, r + 1);
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
643
|
+
// The arrows follow what the user SEES. Under RTL the first column renders
|
|
644
|
+
// rightmost, so ArrowLeft moves to the next column and ArrowRight to the
|
|
645
|
+
// previous — the grid's own half-conversion, of the same kind the resize
|
|
646
|
+
// grips had (#255).
|
|
647
|
+
else if (e.key === "ArrowLeft" || e.key === "ArrowRight") {
|
|
648
|
+
const rtl = (0, core_1.isRtlElement)(e.currentTarget);
|
|
649
|
+
const forward = e.key === (rtl ? "ArrowLeft" : "ArrowRight");
|
|
650
|
+
nc = forward ? Math.min(cols.length - 1, c + 1) : Math.max(0, c - 1);
|
|
651
|
+
}
|
|
647
652
|
else if (e.key === "Escape") {
|
|
648
653
|
setSelCell(null);
|
|
649
654
|
return;
|
|
@@ -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,4 +1,5 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
+
import { isRtlElement } from "../../core/index.js";
|
|
2
3
|
/**
|
|
3
4
|
* The WAI-ARIA radio group keyboard model: ←/→/↑/↓ move focus to the next or
|
|
4
5
|
* previous enabled radio, wrapping, and check it on arrival. Home/End jump to
|
|
@@ -77,7 +78,11 @@ export function useRadioGroupKeyboard({ select, groupRef: externalRef, }) {
|
|
|
77
78
|
else if (e.key === "End")
|
|
78
79
|
next = radios[radios.length - 1];
|
|
79
80
|
else {
|
|
80
|
-
|
|
81
|
+
// Down always means forward; the horizontal pair follows the direction
|
|
82
|
+
// the radios are drawn in, which for Segmented's pill row is the whole
|
|
83
|
+
// point (#255).
|
|
84
|
+
const rtl = isRtlElement(e.currentTarget);
|
|
85
|
+
const fwd = e.key === "ArrowDown" || e.key === (rtl ? "ArrowLeft" : "ArrowRight");
|
|
81
86
|
// focus outside the group: enter at the near end rather than stepping
|
|
82
87
|
// off an index of -1, which would land two from the back going backwards
|
|
83
88
|
if (idx < 0)
|
|
@@ -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";
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isRtlElement } from "../../core/index.js";
|
|
1
2
|
/**
|
|
2
3
|
* The WAI-ARIA tablist keyboard model: ←/→ to move (wrapping), Home/End to
|
|
3
4
|
* jump, disabled tabs skipped, focus following selection.
|
|
@@ -17,9 +18,15 @@ export function useTablistKeyboard({ tabs, active, select, listRef, }) {
|
|
|
17
18
|
return;
|
|
18
19
|
const idx = enabled.findIndex((t) => t.value === active);
|
|
19
20
|
let next = -1;
|
|
20
|
-
|
|
21
|
+
// The arrows follow the strip as it is drawn: under RTL the next tab is to
|
|
22
|
+
// the LEFT, so the keys swap. Everything else about the model is unchanged
|
|
23
|
+
// (#255).
|
|
24
|
+
const rtl = isRtlElement(e.currentTarget);
|
|
25
|
+
const forwardKey = rtl ? "ArrowLeft" : "ArrowRight";
|
|
26
|
+
const backKey = rtl ? "ArrowRight" : "ArrowLeft";
|
|
27
|
+
if (e.key === forwardKey)
|
|
21
28
|
next = (idx + 1) % enabled.length;
|
|
22
|
-
else if (e.key ===
|
|
29
|
+
else if (e.key === backKey)
|
|
23
30
|
next = (idx - 1 + enabled.length) % enabled.length;
|
|
24
31
|
else if (e.key === "Home")
|
|
25
32
|
next = 0;
|
|
@@ -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";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import * as React from "react";
|
|
3
|
+
import { isRtlElement } from "../../core/index.js";
|
|
3
4
|
import { boardRecipe } from "./board.recipe.js";
|
|
4
5
|
/**
|
|
5
6
|
* A kanban board with native drag-and-drop between columns. Cards are
|
|
@@ -72,11 +73,12 @@ export function Board({ columns, cards, onCardsChange, renderCard, onNewCard, ne
|
|
|
72
73
|
}, onKeyDown: (e) => {
|
|
73
74
|
if (!(e.ctrlKey || e.metaKey))
|
|
74
75
|
return;
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
76
|
+
// the columns are a flex row, so under RTL the next one
|
|
77
|
+
// is to the LEFT and the keys swap (#255)
|
|
78
|
+
const rtl = isRtlElement(e.currentTarget);
|
|
79
|
+
const forwardKey = rtl ? "ArrowLeft" : "ArrowRight";
|
|
80
|
+
const backKey = rtl ? "ArrowRight" : "ArrowLeft";
|
|
81
|
+
const delta = e.key === backKey ? -1 : e.key === forwardKey ? 1 : 0;
|
|
80
82
|
// only swallow the key when the move actually happens, so
|
|
81
83
|
// Ctrl+Right at the last column still reaches the browser
|
|
82
84
|
if (delta && moveByKey(card, delta))
|
|
@@ -377,10 +377,15 @@ export function RecordPage({ record, columns, collection = "Records", labels, on
|
|
|
377
377
|
if (!next)
|
|
378
378
|
setFieldQuery("");
|
|
379
379
|
}, children: showAllValues ? t.viewLess : t.viewAllValues }))] })) : (_jsxs("div", { className: "recordpage-noresults", children: ["No fields match \u201C", fieldQuery, "\u201D."] })))] }), _jsxs("div", { className: "recordpage-section", children: [_jsx(SectionHead, { title: t.tabs.lists, action: _jsxs("button", { type: "button", className: "recordpage-addbtn", children: [_jsx(Plus, { size: 14, strokeWidth: 2 }), t.addToList] }) }), _jsx("div", { className: "recordpage-emptystate", children: t.noLists })] })] }), _jsx("div", { className: "recordpage-resizer", "data-active": resizing ? "" : undefined, role: "separator", "aria-orientation": "vertical", "aria-label": t.resizeDetails, "aria-valuenow": Math.round(detailsW), "aria-valuemin": 300, "aria-valuemax": 560, tabIndex: 0, onPointerDown: onResizeDown, onPointerMove: onResizeMove, onPointerUp: onResizeUp, onKeyDown: (e) => {
|
|
380
|
-
if (e.key
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
380
|
+
if (e.key !== "ArrowLeft" && e.key !== "ArrowRight")
|
|
381
|
+
return;
|
|
382
|
+
// The pointer drag learned its direction in #212; this did not.
|
|
383
|
+
// The details column is the flex row's first child, so it sits on
|
|
384
|
+
// the RIGHT under RTL and ArrowLeft has to grow it — the same
|
|
385
|
+
// half-conversion the drag had, one handler away (#255).
|
|
386
|
+
const rtl = isRtlElement(e.currentTarget);
|
|
387
|
+
const grow = e.key === (rtl ? "ArrowLeft" : "ArrowRight");
|
|
388
|
+
setDetailsW((w) => grow ? Math.min(560, w + 16) : Math.max(300, w - 16));
|
|
384
389
|
} }), _jsx("main", { className: "recordpage-main", children: _jsx(Tabs, { tabs: tabs, defaultValue: "overview", "aria-label": `${name} record`, children: panel }) })] })] }));
|
|
385
390
|
}
|
|
386
391
|
RecordPage.displayName = "RecordPage";
|
|
@@ -600,10 +600,15 @@ export function RecordsTable({ labels, columns: columnsProp = defaultColumns, ro
|
|
|
600
600
|
nr = Math.max(0, r - 1);
|
|
601
601
|
else if (e.key === "ArrowDown")
|
|
602
602
|
nr = Math.min(visibleRows.length - 1, r + 1);
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
603
|
+
// The arrows follow what the user SEES. Under RTL the first column renders
|
|
604
|
+
// rightmost, so ArrowLeft moves to the next column and ArrowRight to the
|
|
605
|
+
// previous — the grid's own half-conversion, of the same kind the resize
|
|
606
|
+
// grips had (#255).
|
|
607
|
+
else if (e.key === "ArrowLeft" || e.key === "ArrowRight") {
|
|
608
|
+
const rtl = isRtlElement(e.currentTarget);
|
|
609
|
+
const forward = e.key === (rtl ? "ArrowLeft" : "ArrowRight");
|
|
610
|
+
nc = forward ? Math.min(cols.length - 1, c + 1) : Math.max(0, c - 1);
|
|
611
|
+
}
|
|
607
612
|
else if (e.key === "Escape") {
|
|
608
613
|
setSelCell(null);
|
|
609
614
|
return;
|
|
@@ -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";
|