@crisp-ui-kit/crisp 0.28.0 → 0.29.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 +6 -2
- package/dist/cjs/components/quantitypicker/quantitypicker.js +39 -1
- package/dist/cjs/components/radiogroup/radiogroup.js +3 -0
- package/dist/cjs/components/segmented/segmented.js +4 -0
- package/dist/cjs/core/index.js +3 -1
- package/dist/cjs/core/use-label-association.js +77 -0
- package/dist/esm/components/multiselect/multiselect.js +6 -2
- package/dist/esm/components/quantitypicker/quantitypicker.js +6 -1
- package/dist/esm/components/radiogroup/radiogroup.js +4 -1
- package/dist/esm/components/segmented/segmented.js +4 -0
- package/dist/esm/core/index.js +1 -0
- package/dist/esm/core/use-label-association.js +41 -0
- package/dist/types/components/multiselect/multiselect.d.ts +1 -1
- package/dist/types/components/quantitypicker/quantitypicker.d.ts +2 -1
- package/dist/types/core/index.d.ts +1 -0
- package/dist/types/core/use-label-association.d.ts +21 -0
- package/package.json +1 -1
|
@@ -65,7 +65,11 @@ 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,
|
|
69
|
+
// The id belongs on the trigger, not the wrapper: `<label for>` only names a
|
|
70
|
+
// LABELABLE element, so an id left on the root div meant a FormField label
|
|
71
|
+
// above this control named nothing (#247). Select already did this.
|
|
72
|
+
id, placeholder = "Select…", searchable = true, searchPlaceholder = "Search…", emptyLabel = "No results", disabled, className, "aria-label": ariaLabel, ...rest }) {
|
|
69
73
|
// Virtual focus: DOM focus stays on the trigger (or the search field) and the
|
|
70
74
|
// active option is named by `aria-activedescendant`. `data-highlighted` is a
|
|
71
75
|
// styling hook only -- a screen reader cannot see it.
|
|
@@ -162,7 +166,7 @@ function MultiSelect({ options, value: valueProp, defaultValue, onValueChange, n
|
|
|
162
166
|
return () => document.removeEventListener("pointerdown", onDown);
|
|
163
167
|
}, [open]);
|
|
164
168
|
return ((0, jsx_runtime_1.jsxs)("div", { ref: rootRef, className: (0, multiselect_recipe_1.multiselectRecipe)({ className }), ...rest, children: [name &&
|
|
165
|
-
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) => {
|
|
169
|
+
value.map((v) => (0, jsx_runtime_1.jsx)("input", { type: "hidden", name: name, value: v }, 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) => {
|
|
166
170
|
if (open && !searchable)
|
|
167
171
|
onNavKeys(e);
|
|
168
172
|
else if (!open && (e.key === "ArrowDown" || e.key === "Enter")) {
|
|
@@ -1,10 +1,45 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
"use client";
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
+
var ownKeys = function(o) {
|
|
21
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
+
var ar = [];
|
|
23
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
+
return ar;
|
|
25
|
+
};
|
|
26
|
+
return ownKeys(o);
|
|
27
|
+
};
|
|
28
|
+
return function (mod) {
|
|
29
|
+
if (mod && mod.__esModule) return mod;
|
|
30
|
+
var result = {};
|
|
31
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
+
__setModuleDefault(result, mod);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
})();
|
|
3
36
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
37
|
exports.QuantityPicker = QuantityPicker;
|
|
5
38
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
6
39
|
const react_quantity_picker_1 = require("@seed-design/react-quantity-picker");
|
|
7
40
|
const lucide_react_1 = require("lucide-react");
|
|
41
|
+
const React = __importStar(require("react"));
|
|
42
|
+
const core_1 = require("../../core");
|
|
8
43
|
const spinner_1 = require("../spinner");
|
|
9
44
|
const quantitypicker_recipe_1 = require("./quantitypicker.recipe");
|
|
10
45
|
/** seed's icon box per size, read from their stylesheet. */
|
|
@@ -57,6 +92,9 @@ function IncrementIcon({ size }) {
|
|
|
57
92
|
* reverses its visible children rather than relying on CSS.
|
|
58
93
|
*/
|
|
59
94
|
function QuantityPicker({ size = "medium", fill = false, className, name, decrementLabel = "Decrease quantity", incrementLabel = "Increase quantity", ...props }) {
|
|
60
|
-
|
|
95
|
+
// A `<label for>` cannot name a div, and this root is a role="group" (#247).
|
|
96
|
+
const rootRef = React.useRef(null);
|
|
97
|
+
(0, core_1.useLabelAssociation)(rootRef);
|
|
98
|
+
return ((0, jsx_runtime_1.jsxs)(react_quantity_picker_1.QuantityPickerRoot, { ref: rootRef, ...props, className: (0, quantitypicker_recipe_1.quantitypickerRecipe)({ size, fill, className }), children: [(0, jsx_runtime_1.jsx)(react_quantity_picker_1.QuantityPickerDecrementButton, { className: "quantitypicker-button", "aria-label": decrementLabel, children: (0, jsx_runtime_1.jsx)(DecrementIcon, { size: size }) }), (0, jsx_runtime_1.jsx)("span", { className: "quantitypicker-divider" }), (0, jsx_runtime_1.jsx)(Value, {}), (0, jsx_runtime_1.jsx)("span", { className: "quantitypicker-divider" }), (0, jsx_runtime_1.jsx)(react_quantity_picker_1.QuantityPickerIncrementButton, { className: "quantitypicker-button", "aria-label": incrementLabel, children: (0, jsx_runtime_1.jsx)(IncrementIcon, { size: size }) }), name ? (0, jsx_runtime_1.jsx)(react_quantity_picker_1.QuantityPickerHiddenInput, { name: name }) : null] }));
|
|
61
99
|
}
|
|
62
100
|
QuantityPicker.displayName = "QuantityPicker";
|
|
@@ -61,6 +61,9 @@ function RadioGroup({ value, defaultValue, onValueChange, variant = "default", n
|
|
|
61
61
|
// `form.reset()` restores native controls; a group of role="radio" buttons
|
|
62
62
|
// holds its answer in React, so it has to listen (#245). A controlled group
|
|
63
63
|
// is left alone — the value is the consumer's.
|
|
64
|
+
// A `<label for>` cannot name a div, so a FormField above this group named
|
|
65
|
+
// nothing until now (#247).
|
|
66
|
+
(0, core_1.useLabelAssociation)(groupRef);
|
|
64
67
|
(0, core_1.useFormReset)(groupRef, () => {
|
|
65
68
|
if (value === undefined)
|
|
66
69
|
setInternal(defaultValue);
|
|
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.Segmented = Segmented;
|
|
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 radiogroup_1 = require("../radiogroup");
|
|
40
41
|
const segmented_recipe_1 = require("./segmented.recipe");
|
|
41
42
|
/**
|
|
@@ -69,6 +70,9 @@ function Segmented({ options, value, defaultValue, onValueChange, "aria-label":
|
|
|
69
70
|
// replaces closed over its own index, so it selected a neighbour without
|
|
70
71
|
// moving focus and every press after the first recomputed the same step.
|
|
71
72
|
const { onKeyDown } = (0, radiogroup_1.useRadioGroupKeyboard)({ select, groupRef: rootRef });
|
|
73
|
+
// A `<label for>` cannot name a div, and this root is a role="radiogroup"
|
|
74
|
+
// (#247).
|
|
75
|
+
(0, core_1.useLabelAssociation)(rootRef);
|
|
72
76
|
return ((0, jsx_runtime_1.jsxs)("div", { ref: rootRef, className: (0, segmented_recipe_1.segmentedRecipe)({ className }), ...rest, role: "radiogroup", "aria-label": ariaLabel, onKeyDown: onKeyDown, children: [thumb && ((0, jsx_runtime_1.jsx)("span", { className: "segmented-thumb", "aria-hidden": "true", style: {
|
|
73
77
|
width: thumb.width,
|
|
74
78
|
transform: `translateX(${thumb.left}px)`,
|
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.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.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");
|
|
@@ -34,5 +34,7 @@ var use_focus_after_removal_1 = require("./use-focus-after-removal");
|
|
|
34
34
|
Object.defineProperty(exports, "useFocusAfterRemoval", { enumerable: true, get: function () { return use_focus_after_removal_1.useFocusAfterRemoval; } });
|
|
35
35
|
var use_form_reset_1 = require("./use-form-reset");
|
|
36
36
|
Object.defineProperty(exports, "useFormReset", { enumerable: true, get: function () { return use_form_reset_1.useFormReset; } });
|
|
37
|
+
var use_label_association_1 = require("./use-label-association");
|
|
38
|
+
Object.defineProperty(exports, "useLabelAssociation", { enumerable: true, get: function () { return use_label_association_1.useLabelAssociation; } });
|
|
37
39
|
var use_prefers_reduced_motion_1 = require("./use-prefers-reduced-motion");
|
|
38
40
|
Object.defineProperty(exports, "usePrefersReducedMotion", { enumerable: true, get: function () { return use_prefers_reduced_motion_1.usePrefersReducedMotion; } });
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.useLabelAssociation = useLabelAssociation;
|
|
37
|
+
const React = __importStar(require("react"));
|
|
38
|
+
/**
|
|
39
|
+
* Let a `<label for=…>` name a group.
|
|
40
|
+
*
|
|
41
|
+
* `for` only associates with a LABELABLE element — button, input, select,
|
|
42
|
+
* textarea, meter, output, progress. A `role="radiogroup"` or `role="group"`
|
|
43
|
+
* is a `<div>`, so a FormField label sitting above one names nothing at all:
|
|
44
|
+
* the markup looks right, the label is clickable, and a screen reader
|
|
45
|
+
* announces "group" with no name (#247).
|
|
46
|
+
*
|
|
47
|
+
* ARIA's answer for a group is `aria-labelledby`, which is exactly what this
|
|
48
|
+
* repairs: when the element carries neither `aria-label` nor
|
|
49
|
+
* `aria-labelledby`, and a label points at its id, that label becomes its
|
|
50
|
+
* name. Done from the DOM rather than through a prop because the label is
|
|
51
|
+
* rendered by whoever wrote the markup — crisp's FormField, or a hand-written
|
|
52
|
+
* `<label>` — and neither can reach into the group's props.
|
|
53
|
+
*
|
|
54
|
+
* The label is given an id if it has none, derived from the control's own id
|
|
55
|
+
* so two groups on a page cannot collide.
|
|
56
|
+
*/
|
|
57
|
+
function useLabelAssociation(ref) {
|
|
58
|
+
React.useEffect(() => {
|
|
59
|
+
const el = ref.current;
|
|
60
|
+
if (!el?.id)
|
|
61
|
+
return;
|
|
62
|
+
if (el.getAttribute("aria-label") || el.getAttribute("aria-labelledby"))
|
|
63
|
+
return;
|
|
64
|
+
const label = el.ownerDocument.querySelector(`label[for="${CSS.escape(el.id)}"]`);
|
|
65
|
+
if (!label)
|
|
66
|
+
return;
|
|
67
|
+
if (!label.id)
|
|
68
|
+
label.id = `${el.id}-label`;
|
|
69
|
+
el.setAttribute("aria-labelledby", label.id);
|
|
70
|
+
return () => {
|
|
71
|
+
// only take back what this put there: a consumer who set their own
|
|
72
|
+
// aria-labelledby in the meantime keeps it
|
|
73
|
+
if (el.getAttribute("aria-labelledby") === label.id)
|
|
74
|
+
el.removeAttribute("aria-labelledby");
|
|
75
|
+
};
|
|
76
|
+
});
|
|
77
|
+
}
|
|
@@ -29,7 +29,11 @@ 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,
|
|
33
|
+
// The id belongs on the trigger, not the wrapper: `<label for>` only names a
|
|
34
|
+
// LABELABLE element, so an id left on the root div meant a FormField label
|
|
35
|
+
// above this control named nothing (#247). Select already did this.
|
|
36
|
+
id, placeholder = "Select…", searchable = true, searchPlaceholder = "Search…", emptyLabel = "No results", disabled, className, "aria-label": ariaLabel, ...rest }) {
|
|
33
37
|
// Virtual focus: DOM focus stays on the trigger (or the search field) and the
|
|
34
38
|
// active option is named by `aria-activedescendant`. `data-highlighted` is a
|
|
35
39
|
// styling hook only -- a screen reader cannot see it.
|
|
@@ -126,7 +130,7 @@ export function MultiSelect({ options, value: valueProp, defaultValue, onValueCh
|
|
|
126
130
|
return () => document.removeEventListener("pointerdown", onDown);
|
|
127
131
|
}, [open]);
|
|
128
132
|
return (_jsxs("div", { ref: rootRef, className: multiselectRecipe({ className }), ...rest, children: [name &&
|
|
129
|
-
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) => {
|
|
133
|
+
value.map((v) => _jsx("input", { type: "hidden", name: name, value: v }, 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) => {
|
|
130
134
|
if (open && !searchable)
|
|
131
135
|
onNavKeys(e);
|
|
132
136
|
else if (!open && (e.key === "ArrowDown" || e.key === "Enter")) {
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { QuantityPickerDecrementButton, QuantityPickerHiddenInput, QuantityPickerIncrementButton, QuantityPickerRoot, useQuantityPickerContext, } from "@seed-design/react-quantity-picker";
|
|
4
4
|
import { Minus, Plus, Trash2 } from "lucide-react";
|
|
5
|
+
import * as React from "react";
|
|
6
|
+
import { useLabelAssociation } from "../../core/index.js";
|
|
5
7
|
import { Spinner } from "../spinner/index.js";
|
|
6
8
|
import { quantitypickerRecipe } from "./quantitypicker.recipe.js";
|
|
7
9
|
/** seed's icon box per size, read from their stylesheet. */
|
|
@@ -54,6 +56,9 @@ function IncrementIcon({ size }) {
|
|
|
54
56
|
* reverses its visible children rather than relying on CSS.
|
|
55
57
|
*/
|
|
56
58
|
export function QuantityPicker({ size = "medium", fill = false, className, name, decrementLabel = "Decrease quantity", incrementLabel = "Increase quantity", ...props }) {
|
|
57
|
-
|
|
59
|
+
// A `<label for>` cannot name a div, and this root is a role="group" (#247).
|
|
60
|
+
const rootRef = React.useRef(null);
|
|
61
|
+
useLabelAssociation(rootRef);
|
|
62
|
+
return (_jsxs(QuantityPickerRoot, { ref: rootRef, ...props, className: quantitypickerRecipe({ size, fill, className }), children: [_jsx(QuantityPickerDecrementButton, { className: "quantitypicker-button", "aria-label": decrementLabel, children: _jsx(DecrementIcon, { size: size }) }), _jsx("span", { className: "quantitypicker-divider" }), _jsx(Value, {}), _jsx("span", { className: "quantitypicker-divider" }), _jsx(QuantityPickerIncrementButton, { className: "quantitypicker-button", "aria-label": incrementLabel, children: _jsx(IncrementIcon, { size: size }) }), name ? _jsx(QuantityPickerHiddenInput, { name: name }) : null] }));
|
|
58
63
|
}
|
|
59
64
|
QuantityPicker.displayName = "QuantityPicker";
|
|
@@ -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 { 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);
|
|
@@ -24,6 +24,9 @@ export function RadioGroup({ value, defaultValue, onValueChange, variant = "defa
|
|
|
24
24
|
// `form.reset()` restores native controls; a group of role="radio" buttons
|
|
25
25
|
// holds its answer in React, so it has to listen (#245). A controlled group
|
|
26
26
|
// is left alone — the value is the consumer's.
|
|
27
|
+
// A `<label for>` cannot name a div, so a FormField above this group named
|
|
28
|
+
// nothing until now (#247).
|
|
29
|
+
useLabelAssociation(groupRef);
|
|
27
30
|
useFormReset(groupRef, () => {
|
|
28
31
|
if (value === undefined)
|
|
29
32
|
setInternal(defaultValue);
|
|
@@ -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 { useLabelAssociation } from "../../core/index.js";
|
|
3
4
|
import { useRadioGroupKeyboard } from "../radiogroup/index.js";
|
|
4
5
|
import { segmentedRecipe } from "./segmented.recipe.js";
|
|
5
6
|
/**
|
|
@@ -33,6 +34,9 @@ export function Segmented({ options, value, defaultValue, onValueChange, "aria-l
|
|
|
33
34
|
// replaces closed over its own index, so it selected a neighbour without
|
|
34
35
|
// moving focus and every press after the first recomputed the same step.
|
|
35
36
|
const { onKeyDown } = useRadioGroupKeyboard({ select, groupRef: rootRef });
|
|
37
|
+
// A `<label for>` cannot name a div, and this root is a role="radiogroup"
|
|
38
|
+
// (#247).
|
|
39
|
+
useLabelAssociation(rootRef);
|
|
36
40
|
return (_jsxs("div", { ref: rootRef, className: segmentedRecipe({ className }), ...rest, role: "radiogroup", "aria-label": ariaLabel, onKeyDown: onKeyDown, children: [thumb && (_jsx("span", { className: "segmented-thumb", "aria-hidden": "true", style: {
|
|
37
41
|
width: thumb.width,
|
|
38
42
|
transform: `translateX(${thumb.left}px)`,
|
package/dist/esm/core/index.js
CHANGED
|
@@ -8,4 +8,5 @@ export { resolveRecipe } from "./resolve.js";
|
|
|
8
8
|
export * from "./rtl.js";
|
|
9
9
|
export { useFocusAfterRemoval } from "./use-focus-after-removal.js";
|
|
10
10
|
export { useFormReset } from "./use-form-reset.js";
|
|
11
|
+
export { useLabelAssociation } from "./use-label-association.js";
|
|
11
12
|
export { usePrefersReducedMotion } from "./use-prefers-reduced-motion.js";
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
/**
|
|
3
|
+
* Let a `<label for=…>` name a group.
|
|
4
|
+
*
|
|
5
|
+
* `for` only associates with a LABELABLE element — button, input, select,
|
|
6
|
+
* textarea, meter, output, progress. A `role="radiogroup"` or `role="group"`
|
|
7
|
+
* is a `<div>`, so a FormField label sitting above one names nothing at all:
|
|
8
|
+
* the markup looks right, the label is clickable, and a screen reader
|
|
9
|
+
* announces "group" with no name (#247).
|
|
10
|
+
*
|
|
11
|
+
* ARIA's answer for a group is `aria-labelledby`, which is exactly what this
|
|
12
|
+
* repairs: when the element carries neither `aria-label` nor
|
|
13
|
+
* `aria-labelledby`, and a label points at its id, that label becomes its
|
|
14
|
+
* name. Done from the DOM rather than through a prop because the label is
|
|
15
|
+
* rendered by whoever wrote the markup — crisp's FormField, or a hand-written
|
|
16
|
+
* `<label>` — and neither can reach into the group's props.
|
|
17
|
+
*
|
|
18
|
+
* The label is given an id if it has none, derived from the control's own id
|
|
19
|
+
* so two groups on a page cannot collide.
|
|
20
|
+
*/
|
|
21
|
+
export function useLabelAssociation(ref) {
|
|
22
|
+
React.useEffect(() => {
|
|
23
|
+
const el = ref.current;
|
|
24
|
+
if (!el?.id)
|
|
25
|
+
return;
|
|
26
|
+
if (el.getAttribute("aria-label") || el.getAttribute("aria-labelledby"))
|
|
27
|
+
return;
|
|
28
|
+
const label = el.ownerDocument.querySelector(`label[for="${CSS.escape(el.id)}"]`);
|
|
29
|
+
if (!label)
|
|
30
|
+
return;
|
|
31
|
+
if (!label.id)
|
|
32
|
+
label.id = `${el.id}-label`;
|
|
33
|
+
el.setAttribute("aria-labelledby", label.id);
|
|
34
|
+
return () => {
|
|
35
|
+
// only take back what this put there: a consumer who set their own
|
|
36
|
+
// aria-labelledby in the meantime keeps it
|
|
37
|
+
if (el.getAttribute("aria-labelledby") === label.id)
|
|
38
|
+
el.removeAttribute("aria-labelledby");
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
}
|
|
@@ -36,7 +36,7 @@ export type MultiSelectProps = Omit<React.ComponentPropsWithoutRef<"div">, "valu
|
|
|
36
36
|
* popover with a search field over rows of "[chip] … [checkbox]" that toggle on
|
|
37
37
|
* click (the menu stays open). Keyboard: ↑/↓ move, Enter toggles, Esc closes.
|
|
38
38
|
*/
|
|
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;
|
|
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;
|
|
40
40
|
export declare namespace MultiSelect {
|
|
41
41
|
var displayName: string;
|
|
42
42
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type QuantityPickerRootProps } from "@seed-design/react-quantity-picker";
|
|
2
|
+
import * as React from "react";
|
|
2
3
|
/** seed's icon box per size, read from their stylesheet. */
|
|
3
4
|
declare const ICON: {
|
|
4
5
|
readonly small: 16;
|
|
@@ -36,7 +37,7 @@ export type QuantityPickerProps = QuantityPickerRootProps & {
|
|
|
36
37
|
* Slider documents. Here it also decides child ORDER, since the engine
|
|
37
38
|
* reverses its visible children rather than relying on CSS.
|
|
38
39
|
*/
|
|
39
|
-
export declare function QuantityPicker({ size, fill, className, name, decrementLabel, incrementLabel, ...props }: QuantityPickerProps):
|
|
40
|
+
export declare function QuantityPicker({ size, fill, className, name, decrementLabel, incrementLabel, ...props }: QuantityPickerProps): React.JSX.Element;
|
|
40
41
|
export declare namespace QuantityPicker {
|
|
41
42
|
var displayName: string;
|
|
42
43
|
}
|
|
@@ -9,4 +9,5 @@ export * from "./rtl";
|
|
|
9
9
|
export type * from "./types";
|
|
10
10
|
export { useFocusAfterRemoval } from "./use-focus-after-removal";
|
|
11
11
|
export { useFormReset } from "./use-form-reset";
|
|
12
|
+
export { useLabelAssociation } from "./use-label-association";
|
|
12
13
|
export { usePrefersReducedMotion } from "./use-prefers-reduced-motion";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
/**
|
|
3
|
+
* Let a `<label for=…>` name a group.
|
|
4
|
+
*
|
|
5
|
+
* `for` only associates with a LABELABLE element — button, input, select,
|
|
6
|
+
* textarea, meter, output, progress. A `role="radiogroup"` or `role="group"`
|
|
7
|
+
* is a `<div>`, so a FormField label sitting above one names nothing at all:
|
|
8
|
+
* the markup looks right, the label is clickable, and a screen reader
|
|
9
|
+
* announces "group" with no name (#247).
|
|
10
|
+
*
|
|
11
|
+
* ARIA's answer for a group is `aria-labelledby`, which is exactly what this
|
|
12
|
+
* repairs: when the element carries neither `aria-label` nor
|
|
13
|
+
* `aria-labelledby`, and a label points at its id, that label becomes its
|
|
14
|
+
* name. Done from the DOM rather than through a prop because the label is
|
|
15
|
+
* rendered by whoever wrote the markup — crisp's FormField, or a hand-written
|
|
16
|
+
* `<label>` — and neither can reach into the group's props.
|
|
17
|
+
*
|
|
18
|
+
* The label is given an id if it has none, derived from the control's own id
|
|
19
|
+
* so two groups on a page cannot collide.
|
|
20
|
+
*/
|
|
21
|
+
export declare function useLabelAssociation(ref: React.RefObject<HTMLElement | null>): void;
|