@crisp-ui-kit/crisp 0.16.1 → 0.17.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/radiogroup/index.js +3 -1
- package/dist/cjs/components/radiogroup/radiogroup-keyboard.js +67 -0
- package/dist/cjs/components/radiogroup/radiogroup.js +2 -13
- package/dist/cjs/components/segmented/segmented.js +9 -21
- package/dist/esm/components/radiogroup/index.js +1 -0
- package/dist/esm/components/radiogroup/radiogroup-keyboard.js +64 -0
- package/dist/esm/components/radiogroup/radiogroup.js +2 -13
- package/dist/esm/components/segmented/segmented.js +9 -21
- package/dist/types/components/radiogroup/index.d.ts +1 -0
- package/dist/types/components/radiogroup/radiogroup-keyboard.d.ts +32 -0
- package/package.json +1 -1
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.radiogroupRecipe = exports.RadioGroup = exports.Radio = void 0;
|
|
3
|
+
exports.useRadioGroupKeyboard = exports.radiogroupRecipe = exports.RadioGroup = exports.Radio = void 0;
|
|
4
4
|
var radiogroup_1 = require("./radiogroup");
|
|
5
5
|
Object.defineProperty(exports, "Radio", { enumerable: true, get: function () { return radiogroup_1.Radio; } });
|
|
6
6
|
Object.defineProperty(exports, "RadioGroup", { enumerable: true, get: function () { return radiogroup_1.RadioGroup; } });
|
|
7
7
|
var radiogroup_recipe_1 = require("./radiogroup.recipe");
|
|
8
8
|
Object.defineProperty(exports, "radiogroupRecipe", { enumerable: true, get: function () { return radiogroup_recipe_1.radiogroupRecipe; } });
|
|
9
|
+
var radiogroup_keyboard_1 = require("./radiogroup-keyboard");
|
|
10
|
+
Object.defineProperty(exports, "useRadioGroupKeyboard", { enumerable: true, get: function () { return radiogroup_keyboard_1.useRadioGroupKeyboard; } });
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.useRadioGroupKeyboard = useRadioGroupKeyboard;
|
|
4
|
+
/**
|
|
5
|
+
* The WAI-ARIA radio group keyboard model: ←/→/↑/↓ move focus to the next or
|
|
6
|
+
* previous enabled radio, wrapping, and check it on arrival. Home/End jump to
|
|
7
|
+
* the ends.
|
|
8
|
+
*
|
|
9
|
+
* Mount the returned handler on the element carrying `role="radiogroup"`. It
|
|
10
|
+
* reads the radios out of the DOM rather than an options array, so a surface
|
|
11
|
+
* that renders its own chrome — `Segmented`'s pill switcher — shares the model
|
|
12
|
+
* without sharing a data shape.
|
|
13
|
+
*
|
|
14
|
+
* Extracted from `RadioGroup`, whose version was the working one. `Segmented`
|
|
15
|
+
* mounted its handler per button and closed over that button's index, so it
|
|
16
|
+
* selected a neighbour without moving focus and every later press recomputed
|
|
17
|
+
* the same step (#200). The same reason `useMenuKeyboard`,
|
|
18
|
+
* `useTablistKeyboard` and `useListboxKeyboard` exist.
|
|
19
|
+
*
|
|
20
|
+
* Moving focus is not a detail: roving `tabIndex` follows the selection, so a
|
|
21
|
+
* handler that selects without focusing leaves focus on a `tabindex="-1"`
|
|
22
|
+
* element while the tab stop is somewhere else.
|
|
23
|
+
*
|
|
24
|
+
* Home/End are not in APG's radio group pattern. They are kept because
|
|
25
|
+
* `Segmented` already offered them, and the shared model should be the union
|
|
26
|
+
* rather than quietly dropping behaviour one surface had.
|
|
27
|
+
*/
|
|
28
|
+
function useRadioGroupKeyboard({ select, }) {
|
|
29
|
+
const onKeyDown = (e) => {
|
|
30
|
+
const KEYS = [
|
|
31
|
+
"ArrowDown",
|
|
32
|
+
"ArrowRight",
|
|
33
|
+
"ArrowUp",
|
|
34
|
+
"ArrowLeft",
|
|
35
|
+
"Home",
|
|
36
|
+
"End",
|
|
37
|
+
];
|
|
38
|
+
if (!KEYS.includes(e.key))
|
|
39
|
+
return;
|
|
40
|
+
const radios = Array.from(e.currentTarget.querySelectorAll('[role="radio"]:not([disabled])'));
|
|
41
|
+
if (!radios.length)
|
|
42
|
+
return;
|
|
43
|
+
const idx = radios.indexOf(document.activeElement);
|
|
44
|
+
let next;
|
|
45
|
+
if (e.key === "Home")
|
|
46
|
+
next = radios[0];
|
|
47
|
+
else if (e.key === "End")
|
|
48
|
+
next = radios[radios.length - 1];
|
|
49
|
+
else {
|
|
50
|
+
const fwd = e.key === "ArrowDown" || e.key === "ArrowRight";
|
|
51
|
+
// focus outside the group: enter at the near end rather than stepping
|
|
52
|
+
// off an index of -1, which would land two from the back going backwards
|
|
53
|
+
if (idx < 0)
|
|
54
|
+
next = fwd ? radios[0] : radios[radios.length - 1];
|
|
55
|
+
else
|
|
56
|
+
next = radios[(idx + (fwd ? 1 : -1) + radios.length) % radios.length];
|
|
57
|
+
}
|
|
58
|
+
if (!next)
|
|
59
|
+
return;
|
|
60
|
+
e.preventDefault();
|
|
61
|
+
next.focus();
|
|
62
|
+
const value = next.dataset.value;
|
|
63
|
+
if (value !== undefined)
|
|
64
|
+
select(value);
|
|
65
|
+
};
|
|
66
|
+
return { onKeyDown };
|
|
67
|
+
}
|
|
@@ -38,6 +38,7 @@ exports.Radio = Radio;
|
|
|
38
38
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
39
39
|
const React = __importStar(require("react"));
|
|
40
40
|
const radiogroup_recipe_1 = require("./radiogroup.recipe");
|
|
41
|
+
const radiogroup_keyboard_1 = require("./radiogroup-keyboard");
|
|
41
42
|
const RadioContext = React.createContext(null);
|
|
42
43
|
/**
|
|
43
44
|
* A radio group — measured from Attio's settings. `default` renders rows (a ring
|
|
@@ -55,19 +56,7 @@ function RadioGroup({ value, defaultValue, onValueChange, variant = "default", n
|
|
|
55
56
|
setInternal(v);
|
|
56
57
|
onValueChange?.(v);
|
|
57
58
|
}, [value, onValueChange]);
|
|
58
|
-
const onKeyDown = (
|
|
59
|
-
if (!["ArrowDown", "ArrowRight", "ArrowUp", "ArrowLeft"].includes(e.key))
|
|
60
|
-
return;
|
|
61
|
-
const radios = Array.from(e.currentTarget.querySelectorAll('[role="radio"]:not([disabled])'));
|
|
62
|
-
const idx = radios.indexOf(document.activeElement);
|
|
63
|
-
const fwd = e.key === "ArrowDown" || e.key === "ArrowRight";
|
|
64
|
-
const next = radios[(idx + (fwd ? 1 : -1) + radios.length) % radios.length];
|
|
65
|
-
if (next) {
|
|
66
|
-
e.preventDefault();
|
|
67
|
-
next.focus();
|
|
68
|
-
next.dataset.value && select(next.dataset.value);
|
|
69
|
-
}
|
|
70
|
-
};
|
|
59
|
+
const { onKeyDown } = (0, radiogroup_keyboard_1.useRadioGroupKeyboard)({ select });
|
|
71
60
|
return ((0, jsx_runtime_1.jsx)(RadioContext.Provider, { value: { value: selected, select, name: groupName, variant }, children: (0, jsx_runtime_1.jsx)("div", { role: "radiogroup", "aria-label": ariaLabel, "data-variant": variant, className: (0, radiogroup_recipe_1.radiogroupRecipe)({ className }), ...rest, onKeyDown: onKeyDown, children: children }) }));
|
|
72
61
|
}
|
|
73
62
|
RadioGroup.displayName = "RadioGroup";
|
|
@@ -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 radiogroup_1 = require("../radiogroup");
|
|
39
40
|
const segmented_recipe_1 = require("./segmented.recipe");
|
|
40
41
|
/**
|
|
41
42
|
* A segmented control (single-choice toggle) — measured from Attio's view
|
|
@@ -64,32 +65,19 @@ function Segmented({ options, value, defaultValue, onValueChange, "aria-label":
|
|
|
64
65
|
setInternal(v);
|
|
65
66
|
onValueChange?.(v);
|
|
66
67
|
};
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
next = (pos + 1) % enabled.length;
|
|
73
|
-
else if (e.key === "ArrowLeft" || e.key === "ArrowUp")
|
|
74
|
-
next = (pos - 1 + enabled.length) % enabled.length;
|
|
75
|
-
else if (e.key === "Home")
|
|
76
|
-
next = 0;
|
|
77
|
-
else if (e.key === "End")
|
|
78
|
-
next = enabled.length - 1;
|
|
79
|
-
if (next < 0)
|
|
80
|
-
return;
|
|
81
|
-
e.preventDefault();
|
|
82
|
-
select(enabled[next].value);
|
|
83
|
-
};
|
|
84
|
-
return ((0, jsx_runtime_1.jsxs)("div", { ref: rootRef, className: (0, segmented_recipe_1.segmentedRecipe)({ className }), ...rest, role: "radiogroup", "aria-label": ariaLabel, children: [thumb && ((0, jsx_runtime_1.jsx)("span", { className: "segmented-thumb", "aria-hidden": "true", style: {
|
|
68
|
+
// crisp's shared radio group model (#200). The per-button handler this
|
|
69
|
+
// replaces closed over its own index, so it selected a neighbour without
|
|
70
|
+
// moving focus and every press after the first recomputed the same step.
|
|
71
|
+
const { onKeyDown } = (0, radiogroup_1.useRadioGroupKeyboard)({ select });
|
|
72
|
+
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: {
|
|
85
73
|
width: thumb.width,
|
|
86
74
|
transform: `translateX(${thumb.left}px)`,
|
|
87
|
-
} })), options.map((opt
|
|
75
|
+
} })), options.map((opt) => {
|
|
88
76
|
const on = opt.value === selected;
|
|
89
77
|
const iconOnly = opt.label == null && opt.icon != null;
|
|
90
|
-
return ((0, jsx_runtime_1.jsxs)("button", { type: "button", role: "radio", "aria-checked": on, "aria-label": opt["aria-label"], disabled: opt.disabled, "data-selected": on ? "" : undefined, tabIndex: on ? 0 : -1, className: iconOnly
|
|
78
|
+
return ((0, jsx_runtime_1.jsxs)("button", { type: "button", role: "radio", "aria-checked": on, "aria-label": opt["aria-label"], disabled: opt.disabled, "data-selected": on ? "" : undefined, "data-value": opt.value, tabIndex: on ? 0 : -1, className: iconOnly
|
|
91
79
|
? "segmented-item segmented-item--icon"
|
|
92
|
-
: "segmented-item", onClick: () => select(opt.value),
|
|
80
|
+
: "segmented-item", onClick: () => select(opt.value), children: [opt.icon, opt.label] }, opt.value));
|
|
93
81
|
})] }));
|
|
94
82
|
}
|
|
95
83
|
Segmented.displayName = "Segmented";
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The WAI-ARIA radio group keyboard model: ←/→/↑/↓ move focus to the next or
|
|
3
|
+
* previous enabled radio, wrapping, and check it on arrival. Home/End jump to
|
|
4
|
+
* the ends.
|
|
5
|
+
*
|
|
6
|
+
* Mount the returned handler on the element carrying `role="radiogroup"`. It
|
|
7
|
+
* reads the radios out of the DOM rather than an options array, so a surface
|
|
8
|
+
* that renders its own chrome — `Segmented`'s pill switcher — shares the model
|
|
9
|
+
* without sharing a data shape.
|
|
10
|
+
*
|
|
11
|
+
* Extracted from `RadioGroup`, whose version was the working one. `Segmented`
|
|
12
|
+
* mounted its handler per button and closed over that button's index, so it
|
|
13
|
+
* selected a neighbour without moving focus and every later press recomputed
|
|
14
|
+
* the same step (#200). The same reason `useMenuKeyboard`,
|
|
15
|
+
* `useTablistKeyboard` and `useListboxKeyboard` exist.
|
|
16
|
+
*
|
|
17
|
+
* Moving focus is not a detail: roving `tabIndex` follows the selection, so a
|
|
18
|
+
* handler that selects without focusing leaves focus on a `tabindex="-1"`
|
|
19
|
+
* element while the tab stop is somewhere else.
|
|
20
|
+
*
|
|
21
|
+
* Home/End are not in APG's radio group pattern. They are kept because
|
|
22
|
+
* `Segmented` already offered them, and the shared model should be the union
|
|
23
|
+
* rather than quietly dropping behaviour one surface had.
|
|
24
|
+
*/
|
|
25
|
+
export function useRadioGroupKeyboard({ select, }) {
|
|
26
|
+
const onKeyDown = (e) => {
|
|
27
|
+
const KEYS = [
|
|
28
|
+
"ArrowDown",
|
|
29
|
+
"ArrowRight",
|
|
30
|
+
"ArrowUp",
|
|
31
|
+
"ArrowLeft",
|
|
32
|
+
"Home",
|
|
33
|
+
"End",
|
|
34
|
+
];
|
|
35
|
+
if (!KEYS.includes(e.key))
|
|
36
|
+
return;
|
|
37
|
+
const radios = Array.from(e.currentTarget.querySelectorAll('[role="radio"]:not([disabled])'));
|
|
38
|
+
if (!radios.length)
|
|
39
|
+
return;
|
|
40
|
+
const idx = radios.indexOf(document.activeElement);
|
|
41
|
+
let next;
|
|
42
|
+
if (e.key === "Home")
|
|
43
|
+
next = radios[0];
|
|
44
|
+
else if (e.key === "End")
|
|
45
|
+
next = radios[radios.length - 1];
|
|
46
|
+
else {
|
|
47
|
+
const fwd = e.key === "ArrowDown" || e.key === "ArrowRight";
|
|
48
|
+
// focus outside the group: enter at the near end rather than stepping
|
|
49
|
+
// off an index of -1, which would land two from the back going backwards
|
|
50
|
+
if (idx < 0)
|
|
51
|
+
next = fwd ? radios[0] : radios[radios.length - 1];
|
|
52
|
+
else
|
|
53
|
+
next = radios[(idx + (fwd ? 1 : -1) + radios.length) % radios.length];
|
|
54
|
+
}
|
|
55
|
+
if (!next)
|
|
56
|
+
return;
|
|
57
|
+
e.preventDefault();
|
|
58
|
+
next.focus();
|
|
59
|
+
const value = next.dataset.value;
|
|
60
|
+
if (value !== undefined)
|
|
61
|
+
select(value);
|
|
62
|
+
};
|
|
63
|
+
return { onKeyDown };
|
|
64
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
import { radiogroupRecipe } from "./radiogroup.recipe.js";
|
|
4
|
+
import { useRadioGroupKeyboard } from "./radiogroup-keyboard.js";
|
|
4
5
|
const RadioContext = React.createContext(null);
|
|
5
6
|
/**
|
|
6
7
|
* A radio group — measured from Attio's settings. `default` renders rows (a ring
|
|
@@ -18,19 +19,7 @@ export function RadioGroup({ value, defaultValue, onValueChange, variant = "defa
|
|
|
18
19
|
setInternal(v);
|
|
19
20
|
onValueChange?.(v);
|
|
20
21
|
}, [value, onValueChange]);
|
|
21
|
-
const onKeyDown = (
|
|
22
|
-
if (!["ArrowDown", "ArrowRight", "ArrowUp", "ArrowLeft"].includes(e.key))
|
|
23
|
-
return;
|
|
24
|
-
const radios = Array.from(e.currentTarget.querySelectorAll('[role="radio"]:not([disabled])'));
|
|
25
|
-
const idx = radios.indexOf(document.activeElement);
|
|
26
|
-
const fwd = e.key === "ArrowDown" || e.key === "ArrowRight";
|
|
27
|
-
const next = radios[(idx + (fwd ? 1 : -1) + radios.length) % radios.length];
|
|
28
|
-
if (next) {
|
|
29
|
-
e.preventDefault();
|
|
30
|
-
next.focus();
|
|
31
|
-
next.dataset.value && select(next.dataset.value);
|
|
32
|
-
}
|
|
33
|
-
};
|
|
22
|
+
const { onKeyDown } = useRadioGroupKeyboard({ select });
|
|
34
23
|
return (_jsx(RadioContext.Provider, { value: { value: selected, select, name: groupName, variant }, children: _jsx("div", { role: "radiogroup", "aria-label": ariaLabel, "data-variant": variant, className: radiogroupRecipe({ className }), ...rest, onKeyDown: onKeyDown, children: children }) }));
|
|
35
24
|
}
|
|
36
25
|
RadioGroup.displayName = "RadioGroup";
|
|
@@ -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 { useRadioGroupKeyboard } from "../radiogroup/index.js";
|
|
3
4
|
import { segmentedRecipe } from "./segmented.recipe.js";
|
|
4
5
|
/**
|
|
5
6
|
* A segmented control (single-choice toggle) — measured from Attio's view
|
|
@@ -28,32 +29,19 @@ export function Segmented({ options, value, defaultValue, onValueChange, "aria-l
|
|
|
28
29
|
setInternal(v);
|
|
29
30
|
onValueChange?.(v);
|
|
30
31
|
};
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
next = (pos + 1) % enabled.length;
|
|
37
|
-
else if (e.key === "ArrowLeft" || e.key === "ArrowUp")
|
|
38
|
-
next = (pos - 1 + enabled.length) % enabled.length;
|
|
39
|
-
else if (e.key === "Home")
|
|
40
|
-
next = 0;
|
|
41
|
-
else if (e.key === "End")
|
|
42
|
-
next = enabled.length - 1;
|
|
43
|
-
if (next < 0)
|
|
44
|
-
return;
|
|
45
|
-
e.preventDefault();
|
|
46
|
-
select(enabled[next].value);
|
|
47
|
-
};
|
|
48
|
-
return (_jsxs("div", { ref: rootRef, className: segmentedRecipe({ className }), ...rest, role: "radiogroup", "aria-label": ariaLabel, children: [thumb && (_jsx("span", { className: "segmented-thumb", "aria-hidden": "true", style: {
|
|
32
|
+
// crisp's shared radio group model (#200). The per-button handler this
|
|
33
|
+
// replaces closed over its own index, so it selected a neighbour without
|
|
34
|
+
// moving focus and every press after the first recomputed the same step.
|
|
35
|
+
const { onKeyDown } = useRadioGroupKeyboard({ select });
|
|
36
|
+
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: {
|
|
49
37
|
width: thumb.width,
|
|
50
38
|
transform: `translateX(${thumb.left}px)`,
|
|
51
|
-
} })), options.map((opt
|
|
39
|
+
} })), options.map((opt) => {
|
|
52
40
|
const on = opt.value === selected;
|
|
53
41
|
const iconOnly = opt.label == null && opt.icon != null;
|
|
54
|
-
return (_jsxs("button", { type: "button", role: "radio", "aria-checked": on, "aria-label": opt["aria-label"], disabled: opt.disabled, "data-selected": on ? "" : undefined, tabIndex: on ? 0 : -1, className: iconOnly
|
|
42
|
+
return (_jsxs("button", { type: "button", role: "radio", "aria-checked": on, "aria-label": opt["aria-label"], disabled: opt.disabled, "data-selected": on ? "" : undefined, "data-value": opt.value, tabIndex: on ? 0 : -1, className: iconOnly
|
|
55
43
|
? "segmented-item segmented-item--icon"
|
|
56
|
-
: "segmented-item", onClick: () => select(opt.value),
|
|
44
|
+
: "segmented-item", onClick: () => select(opt.value), children: [opt.icon, opt.label] }, opt.value));
|
|
57
45
|
})] }));
|
|
58
46
|
}
|
|
59
47
|
Segmented.displayName = "Segmented";
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type * as React from "react";
|
|
2
|
+
export type UseRadioGroupKeyboardOptions = {
|
|
3
|
+
/** Select a radio by its `data-value`. */
|
|
4
|
+
select: (value: string) => void;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* The WAI-ARIA radio group keyboard model: ←/→/↑/↓ move focus to the next or
|
|
8
|
+
* previous enabled radio, wrapping, and check it on arrival. Home/End jump to
|
|
9
|
+
* the ends.
|
|
10
|
+
*
|
|
11
|
+
* Mount the returned handler on the element carrying `role="radiogroup"`. It
|
|
12
|
+
* reads the radios out of the DOM rather than an options array, so a surface
|
|
13
|
+
* that renders its own chrome — `Segmented`'s pill switcher — shares the model
|
|
14
|
+
* without sharing a data shape.
|
|
15
|
+
*
|
|
16
|
+
* Extracted from `RadioGroup`, whose version was the working one. `Segmented`
|
|
17
|
+
* mounted its handler per button and closed over that button's index, so it
|
|
18
|
+
* selected a neighbour without moving focus and every later press recomputed
|
|
19
|
+
* the same step (#200). The same reason `useMenuKeyboard`,
|
|
20
|
+
* `useTablistKeyboard` and `useListboxKeyboard` exist.
|
|
21
|
+
*
|
|
22
|
+
* Moving focus is not a detail: roving `tabIndex` follows the selection, so a
|
|
23
|
+
* handler that selects without focusing leaves focus on a `tabindex="-1"`
|
|
24
|
+
* element while the tab stop is somewhere else.
|
|
25
|
+
*
|
|
26
|
+
* Home/End are not in APG's radio group pattern. They are kept because
|
|
27
|
+
* `Segmented` already offered them, and the shared model should be the union
|
|
28
|
+
* rather than quietly dropping behaviour one surface had.
|
|
29
|
+
*/
|
|
30
|
+
export declare function useRadioGroupKeyboard({ select, }: UseRadioGroupKeyboardOptions): {
|
|
31
|
+
onKeyDown: (e: React.KeyboardEvent) => void;
|
|
32
|
+
};
|