@crisp-ui-kit/crisp 0.16.1 → 0.18.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.
@@ -57,13 +57,15 @@ const fromDate = (d) => ({
57
57
  * popover. This is what `DatePicker` used to be; `DatePicker` is now
58
58
  * seed-design's bare calendar, and this composes it.
59
59
  */
60
- function DatePickerField({ value, defaultValue, onChange, placeholder = "Set date…", variant = "default", format = defaultFormat, presets, calendarProps, "aria-label": ariaLabel, className, ...rest }) {
60
+ function DatePickerField({ value, defaultValue, onValueChange, onChange, placeholder = "Set date…", variant = "default", format = defaultFormat, presets, calendarProps, "aria-label": ariaLabel, className, ...rest }) {
61
61
  const [internal, setInternal] = React.useState(defaultValue ?? null);
62
62
  const selected = value ?? internal;
63
63
  const [open, setOpen] = React.useState(false);
64
64
  const pick = (d) => {
65
65
  if (value === undefined)
66
66
  setInternal(d);
67
+ // both, so a consumer passing either name is heard
68
+ onValueChange?.(d);
67
69
  onChange?.(d);
68
70
  setOpen(false);
69
71
  };
@@ -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 = (e) => {
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
- const onKeyDown = (e, i) => {
68
- const enabled = options.filter((o) => !o.disabled);
69
- const pos = enabled.findIndex((o) => o.value === options[i].value);
70
- let next = -1;
71
- if (e.key === "ArrowRight" || e.key === "ArrowDown")
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, i) => {
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), onKeyDown: (e) => onKeyDown(e, i), children: [opt.icon, opt.label] }, 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";
@@ -42,12 +42,15 @@ const viewtoggle_recipe_1 = require("./viewtoggle.recipe");
42
42
  * of icon tiles; the selected tile is raised. Use for view-mode switches (list ⇄
43
43
  * grid) or any small mutually-exclusive icon choice.
44
44
  */
45
- function ViewToggle({ options, value: valueProp, defaultValue, onChange, "aria-label": ariaLabel, className, ...rest }) {
45
+ function ViewToggle({ options, value: valueProp, defaultValue, onValueChange, onChange, "aria-label": ariaLabel, className, ...rest }) {
46
46
  const [internalValue, setInternalValue] = React.useState(defaultValue);
47
47
  const value = valueProp ?? internalValue ?? options[0]?.value;
48
48
  const pick = (next) => {
49
49
  if (valueProp === undefined)
50
50
  setInternalValue(next);
51
+ // both, so a consumer passing either name is heard and one passing both
52
+ // does not silently lose one
53
+ onValueChange?.(next);
51
54
  onChange?.(next);
52
55
  };
53
56
  return ((0, jsx_runtime_1.jsx)("div", { className: (0, viewtoggle_recipe_1.viewtoggleRecipe)({ className }), ...rest, role: "group", "aria-label": ariaLabel, children: options.map((o) => ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "viewtoggle-tile", "aria-pressed": o.value === value, "aria-label": o.label, title: o.label, "data-active": o.value === value ? "" : undefined, onClick: () => pick(o.value), children: o.icon }, o.value))) }));
@@ -21,13 +21,15 @@ const fromDate = (d) => ({
21
21
  * popover. This is what `DatePicker` used to be; `DatePicker` is now
22
22
  * seed-design's bare calendar, and this composes it.
23
23
  */
24
- export function DatePickerField({ value, defaultValue, onChange, placeholder = "Set date…", variant = "default", format = defaultFormat, presets, calendarProps, "aria-label": ariaLabel, className, ...rest }) {
24
+ export function DatePickerField({ value, defaultValue, onValueChange, onChange, placeholder = "Set date…", variant = "default", format = defaultFormat, presets, calendarProps, "aria-label": ariaLabel, className, ...rest }) {
25
25
  const [internal, setInternal] = React.useState(defaultValue ?? null);
26
26
  const selected = value ?? internal;
27
27
  const [open, setOpen] = React.useState(false);
28
28
  const pick = (d) => {
29
29
  if (value === undefined)
30
30
  setInternal(d);
31
+ // both, so a consumer passing either name is heard
32
+ onValueChange?.(d);
31
33
  onChange?.(d);
32
34
  setOpen(false);
33
35
  };
@@ -1,2 +1,3 @@
1
1
  export { Radio, RadioGroup, } from "./radiogroup.js";
2
2
  export { radiogroupRecipe } from "./radiogroup.recipe.js";
3
+ export { useRadioGroupKeyboard, } from "./radiogroup-keyboard.js";
@@ -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 = (e) => {
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
- const onKeyDown = (e, i) => {
32
- const enabled = options.filter((o) => !o.disabled);
33
- const pos = enabled.findIndex((o) => o.value === options[i].value);
34
- let next = -1;
35
- if (e.key === "ArrowRight" || e.key === "ArrowDown")
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, i) => {
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), onKeyDown: (e) => onKeyDown(e, i), children: [opt.icon, opt.label] }, 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";
@@ -6,12 +6,15 @@ import { viewtoggleRecipe } from "./viewtoggle.recipe.js";
6
6
  * of icon tiles; the selected tile is raised. Use for view-mode switches (list ⇄
7
7
  * grid) or any small mutually-exclusive icon choice.
8
8
  */
9
- export function ViewToggle({ options, value: valueProp, defaultValue, onChange, "aria-label": ariaLabel, className, ...rest }) {
9
+ export function ViewToggle({ options, value: valueProp, defaultValue, onValueChange, onChange, "aria-label": ariaLabel, className, ...rest }) {
10
10
  const [internalValue, setInternalValue] = React.useState(defaultValue);
11
11
  const value = valueProp ?? internalValue ?? options[0]?.value;
12
12
  const pick = (next) => {
13
13
  if (valueProp === undefined)
14
14
  setInternalValue(next);
15
+ // both, so a consumer passing either name is heard and one passing both
16
+ // does not silently lose one
17
+ onValueChange?.(next);
15
18
  onChange?.(next);
16
19
  };
17
20
  return (_jsx("div", { className: viewtoggleRecipe({ className }), ...rest, role: "group", "aria-label": ariaLabel, children: options.map((o) => (_jsx("button", { type: "button", className: "viewtoggle-tile", "aria-pressed": o.value === value, "aria-label": o.label, title: o.label, "data-active": o.value === value ? "" : undefined, onClick: () => pick(o.value), children: o.icon }, o.value))) }));
@@ -11,6 +11,14 @@ export type DatePickerFieldProps = Omit<React.ComponentPropsWithoutRef<"div">, "
11
11
  /** Controlled value. Omit to let the field manage it (see defaultValue). */
12
12
  value?: Date | null;
13
13
  defaultValue?: Date | null;
14
+ /** Fires with the picked date, controlled or not. */
15
+ onValueChange?: (date: Date) => void;
16
+ /**
17
+ * The original name for `onValueChange`, kept working so no consumer breaks.
18
+ * Prefer `onValueChange`: it is what every other crisp component calls this,
19
+ * and passing the house name to a component that only knew `onChange` was
20
+ * not a type error — the handler simply never fired (#202).
21
+ */
14
22
  onChange?: (date: Date) => void;
15
23
  placeholder?: string;
16
24
  /** `default` = outlined trigger; `ghost` = transparent (for table cells). */
@@ -29,7 +37,7 @@ export type DatePickerFieldProps = Omit<React.ComponentPropsWithoutRef<"div">, "
29
37
  * popover. This is what `DatePicker` used to be; `DatePicker` is now
30
38
  * seed-design's bare calendar, and this composes it.
31
39
  */
32
- export declare function DatePickerField({ value, defaultValue, onChange, placeholder, variant, format, presets, calendarProps, "aria-label": ariaLabel, className, ...rest }: DatePickerFieldProps): React.JSX.Element;
40
+ export declare function DatePickerField({ value, defaultValue, onValueChange, onChange, placeholder, variant, format, presets, calendarProps, "aria-label": ariaLabel, className, ...rest }: DatePickerFieldProps): React.JSX.Element;
33
41
  export declare namespace DatePickerField {
34
42
  var displayName: string;
35
43
  }
@@ -1,2 +1,3 @@
1
1
  export { Radio, RadioGroup, type RadioGroupProps, type RadioProps, } from "./radiogroup";
2
2
  export { radiogroupRecipe } from "./radiogroup.recipe";
3
+ export { type UseRadioGroupKeyboardOptions, useRadioGroupKeyboard, } from "./radiogroup-keyboard";
@@ -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
+ };
@@ -12,9 +12,15 @@ export type ViewToggleProps<T extends string = string> = Omit<React.ComponentPro
12
12
  value?: T;
13
13
  /** Initially selected value, when uncontrolled. Defaults to the first option. */
14
14
  defaultValue?: T;
15
- /** Fires with the next value on every pick, controlled or not.
16
- * Named `onChange` rather than the house `onValueChange` for historical
17
- * reasons; renaming it would break consumers. */
15
+ /** Fires with the next value on every pick, controlled or not. */
16
+ onValueChange?: (value: T) => void;
17
+ /**
18
+ * The original name for `onValueChange`, kept working so no consumer breaks.
19
+ * Prefer `onValueChange`: it is what every other crisp component calls this,
20
+ * and because these props extend a `<div>`'s, passing the house name to a
21
+ * component that only knew `onChange` was not a type error — it landed on
22
+ * the DOM and React dropped it with a development-only warning (#202).
23
+ */
18
24
  onChange?: (value: T) => void;
19
25
  "aria-label"?: string;
20
26
  className?: string;
@@ -24,7 +30,7 @@ export type ViewToggleProps<T extends string = string> = Omit<React.ComponentPro
24
30
  * of icon tiles; the selected tile is raised. Use for view-mode switches (list ⇄
25
31
  * grid) or any small mutually-exclusive icon choice.
26
32
  */
27
- export declare function ViewToggle<T extends string = string>({ options, value: valueProp, defaultValue, onChange, "aria-label": ariaLabel, className, ...rest }: ViewToggleProps<T>): React.JSX.Element;
33
+ export declare function ViewToggle<T extends string = string>({ options, value: valueProp, defaultValue, onValueChange, onChange, "aria-label": ariaLabel, className, ...rest }: ViewToggleProps<T>): React.JSX.Element;
28
34
  export declare namespace ViewToggle {
29
35
  var displayName: string;
30
36
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crisp-ui-kit/crisp",
3
- "version": "0.16.1",
3
+ "version": "0.18.0",
4
4
  "description": "A lean React design system, measured pixel-for-pixel from Attio.",
5
5
  "license": "MIT",
6
6
  "type": "module",