reshaped 3.4.6 → 3.5.0-rc.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.
Files changed (76) hide show
  1. package/CHANGELOG.md +0 -13
  2. package/dist/bundle.css +1 -1
  3. package/dist/bundle.d.ts +2 -0
  4. package/dist/bundle.js +11 -11
  5. package/dist/components/Autocomplete/Autocomplete.js +25 -8
  6. package/dist/components/Autocomplete/Autocomplete.module.css +1 -0
  7. package/dist/components/Autocomplete/tests/Autocomplete.stories.d.ts +14 -1
  8. package/dist/components/Autocomplete/tests/Autocomplete.stories.js +181 -64
  9. package/dist/components/Badge/Badge.types.d.ts +4 -2
  10. package/dist/components/Button/Button.types.d.ts +4 -2
  11. package/dist/components/Calendar/Calendar.module.css +1 -1
  12. package/dist/components/Checkbox/Checkbox.js +17 -4
  13. package/dist/components/Checkbox/Checkbox.module.css +1 -1
  14. package/dist/components/Checkbox/Checkbox.types.d.ts +1 -0
  15. package/dist/components/Checkbox/tests/Checkbox.stories.d.ts +1 -0
  16. package/dist/components/Checkbox/tests/Checkbox.stories.js +40 -0
  17. package/dist/components/Dismissible/Dismissible.js +1 -0
  18. package/dist/components/DropdownMenu/DropdownMenu.js +1 -1
  19. package/dist/components/FormControl/FormControlLabel.js +2 -7
  20. package/dist/components/MenuItem/MenuItem.types.d.ts +4 -2
  21. package/dist/components/NumberField/NumberField.d.ts +6 -0
  22. package/dist/components/NumberField/NumberField.js +11 -0
  23. package/dist/components/NumberField/NumberField.module.css +1 -0
  24. package/dist/components/NumberField/NumberField.types.d.ts +19 -0
  25. package/dist/components/NumberField/NumberField.types.js +1 -0
  26. package/dist/components/NumberField/NumberFieldControlled.d.ts +6 -0
  27. package/dist/components/NumberField/NumberFieldControlled.js +162 -0
  28. package/dist/components/NumberField/NumberFieldUncontrolled.d.ts +6 -0
  29. package/dist/components/NumberField/NumberFieldUncontrolled.js +16 -0
  30. package/dist/components/NumberField/index.d.ts +2 -0
  31. package/dist/components/NumberField/index.js +1 -0
  32. package/dist/components/NumberField/tests/NumberField.stories.d.ts +30 -0
  33. package/dist/components/NumberField/tests/NumberField.stories.js +240 -0
  34. package/dist/components/Overlay/Overlay.js +2 -2
  35. package/dist/components/PinField/PinField.module.css +1 -1
  36. package/dist/components/PinField/PinField.types.d.ts +1 -1
  37. package/dist/components/PinField/PinFieldControlled.js +1 -0
  38. package/dist/components/PinField/tests/PinField.stories.js +8 -0
  39. package/dist/components/Radio/Radio.js +11 -4
  40. package/dist/components/Radio/Radio.module.css +1 -1
  41. package/dist/components/Radio/Radio.types.d.ts +1 -0
  42. package/dist/components/Radio/tests/Radio.stories.d.ts +1 -0
  43. package/dist/components/Radio/tests/Radio.stories.js +31 -0
  44. package/dist/components/Select/Select.module.css +1 -1
  45. package/dist/components/Select/Select.types.d.ts +1 -1
  46. package/dist/components/Select/tests/Select.stories.js +42 -16
  47. package/dist/components/Switch/Switch.js +10 -4
  48. package/dist/components/Switch/Switch.module.css +1 -1
  49. package/dist/components/Switch/Switch.types.d.ts +1 -1
  50. package/dist/components/Switch/tests/Switch.stories.js +30 -15
  51. package/dist/components/TextField/TextField.js +1 -1
  52. package/dist/components/TextField/TextField.module.css +1 -1
  53. package/dist/components/TextField/TextField.types.d.ts +2 -2
  54. package/dist/components/TextField/index.d.ts +1 -1
  55. package/dist/components/TextField/tests/TextField.stories.js +4 -0
  56. package/dist/components/Toast/ToastContainer.js +2 -2
  57. package/dist/components/_private/Flyout/FlyoutControlled.js +2 -2
  58. package/dist/hooks/tests/useScrollLock.stories.d.ts +1 -0
  59. package/dist/hooks/tests/useScrollLock.stories.js +29 -0
  60. package/dist/icons/ChevronUp.d.ts +2 -0
  61. package/dist/icons/ChevronUp.js +5 -0
  62. package/dist/icons/Minus.d.ts +2 -0
  63. package/dist/icons/Minus.js +3 -0
  64. package/dist/icons/Plus.d.ts +2 -2
  65. package/dist/icons/Plus.js +2 -2
  66. package/dist/index.d.ts +2 -0
  67. package/dist/index.js +1 -0
  68. package/dist/utilities/a11y/TrapFocus.d.ts +3 -17
  69. package/dist/utilities/a11y/TrapFocus.js +54 -49
  70. package/dist/utilities/a11y/tests/TrapFocus.stories.js +20 -20
  71. package/dist/utilities/scroll/lock.js +15 -7
  72. package/dist/utilities/scroll/lockStandard.d.ts +1 -2
  73. package/dist/utilities/scroll/lockStandard.js +2 -9
  74. package/package.json +31 -31
  75. package/dist/components/Autocomplete/tests/Autocomplete.test.stories.d.ts +0 -27
  76. package/dist/components/Autocomplete/tests/Autocomplete.test.stories.js +0 -86
@@ -0,0 +1,6 @@
1
+ import type * as T from "./NumberField.types";
2
+ declare const NumberField: {
3
+ (props: T.Props): import("react/jsx-runtime").JSX.Element;
4
+ displayName: string;
5
+ };
6
+ export default NumberField;
@@ -0,0 +1,11 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import NumberFieldControlled from "./NumberFieldControlled.js";
3
+ import NumberFieldUncontrolled from "./NumberFieldUncontrolled.js";
4
+ const NumberField = (props) => {
5
+ const { value } = props;
6
+ if (value !== undefined)
7
+ return _jsx(NumberFieldControlled, { ...props });
8
+ return _jsx(NumberFieldUncontrolled, { ...props });
9
+ };
10
+ NumberField.displayName = "NumberField";
11
+ export default NumberField;
@@ -0,0 +1 @@
1
+ .field{font-variant-numeric:tabular-nums}.controls-wrapper{display:block}.controls{--rs-number-field-control-border-color:var(--rs-color-border-neutral);display:flex;flex-direction:column;inset-block:calc(var(--rs-text-field-p-v) * -1 + var(--rs-unit-x1));inset-inline-end:0;position:absolute;transition:border-color var(--rs-duration-fast) var(--rs-easing-standard)}.controls:has([aria-disabled]+[aria-disabled]){--rs-number-field-control-border-color:var(--rs-color-border-disabled)}.controls--size-small .control,.controls--size-small .controls-wrapper{width:var(--rs-unit-x4)}@media (pointer:coarse) and (hover:none){.controls--size-small .controls-wrapper{width:54px}}.controls--size-medium .control,.controls--size-medium .controls-wrapper{width:var(--rs-unit-x5)}@media (pointer:coarse) and (hover:none){.controls--size-medium .controls-wrapper{width:70px}}.controls--size-large .control{width:var(--rs-unit-x6)}.controls--size-large .controls-wrapper{width:var(--rs-unit-x5)}@media (pointer:coarse) and (hover:none){.controls--size-large .controls-wrapper{width:94px}}.controls--size-xlarge .control{width:var(--rs-unit-x7)}.controls--size-xlarge .controls-wrapper{width:var(--rs-unit-x6)}@media (pointer:coarse) and (hover:none){.controls--size-xlarge .controls-wrapper{width:110px}}.control{align-items:center;display:flex;flex-grow:1;height:50%;justify-content:center;touch-action:manipulation;transition:color var(--rs-duration-fast) var(--rs-easing-standard)}.control[aria-disabled]{color:var(--rs-color-foreground-disabled)}.control:not([aria-disabled]):hover{background:rgba(var(--rs-color-rgb-background-neutral),32%)}.icon--touch{display:none!important}.--outline .controls{border-inline-start:1px solid var(--rs-number-field-control-border-color);inset-inline-end:1px}.--outline .control:first-child{box-shadow:0 1px var(--rs-number-field-control-border-color)}@media (pointer:coarse) and (hover:none){.controls{flex-direction:row-reverse}.control{aspect-ratio:1;box-sizing:content-box;height:100%;touch-action:manipulation;width:auto!important}.icon--touch{display:block!important}.icon--mouse{display:none!important}.--outline .control:first-child{border-inline-start:1px solid var(--rs-color-border-neutral);box-shadow:none}}@media (--rs-viewport-m ){.controls--size-small--m .control,.controls--size-small--m .controls-wrapper{width:var(--rs-unit-x4)}@media (pointer:coarse) and (hover:none){.controls--size-small--m .controls-wrapper{width:54px}}.controls--size-medium--m .control,.controls--size-medium--m .controls-wrapper{width:var(--rs-unit-x5)}@media (pointer:coarse) and (hover:none){.controls--size-medium--m .controls-wrapper{width:70px}}.controls--size-large--m .control{width:var(--rs-unit-x6)}.controls--size-large--m .controls-wrapper{width:var(--rs-unit-x5)}@media (pointer:coarse) and (hover:none){.controls--size-large--m .controls-wrapper{width:94px}}.controls--size-xlarge--m .control{width:var(--rs-unit-x7)}.controls--size-xlarge--m .controls-wrapper{width:var(--rs-unit-x6)}@media (pointer:coarse) and (hover:none){.controls--size-xlarge--m .controls-wrapper{width:110px}}}@media (--rs-viewport-l ){.controls--size-small--l .control,.controls--size-small--l .controls-wrapper{width:var(--rs-unit-x4)}@media (pointer:coarse) and (hover:none){.controls--size-small--l .controls-wrapper{width:54px}}.controls--size-medium--l .control,.controls--size-medium--l .controls-wrapper{width:var(--rs-unit-x5)}@media (pointer:coarse) and (hover:none){.controls--size-medium--l .controls-wrapper{width:70px}}.controls--size-large--l .control{width:var(--rs-unit-x6)}.controls--size-large--l .controls-wrapper{width:var(--rs-unit-x5)}@media (pointer:coarse) and (hover:none){.controls--size-large--l .controls-wrapper{width:94px}}.controls--size-xlarge--l .control{width:var(--rs-unit-x7)}.controls--size-xlarge--l .controls-wrapper{width:var(--rs-unit-x6)}@media (pointer:coarse) and (hover:none){.controls--size-xlarge--l .controls-wrapper{width:110px}}}@media (--rs-viewport-xl ){.controls--size-small--xl .control,.controls--size-small--xl .controls-wrapper{width:var(--rs-unit-x4)}@media (pointer:coarse) and (hover:none){.controls--size-small--xl .controls-wrapper{width:54px}}.controls--size-medium--xl .control,.controls--size-medium--xl .controls-wrapper{width:var(--rs-unit-x5)}@media (pointer:coarse) and (hover:none){.controls--size-medium--xl .controls-wrapper{width:70px}}.controls--size-large--xl .control{width:var(--rs-unit-x6)}.controls--size-large--xl .controls-wrapper{width:var(--rs-unit-x5)}@media (pointer:coarse) and (hover:none){.controls--size-large--xl .controls-wrapper{width:94px}}.controls--size-xlarge--xl .control{width:var(--rs-unit-x7)}.controls--size-xlarge--xl .controls-wrapper{width:var(--rs-unit-x6)}@media (pointer:coarse) and (hover:none){.controls--size-xlarge--xl .controls-wrapper{width:110px}}}
@@ -0,0 +1,19 @@
1
+ import type { TextFieldBaseProps } from "../TextField";
2
+ import type * as G from "../../types/global";
3
+ export type BaseProps = Omit<TextFieldBaseProps, "endSlot" | "onChange" | "rounded" | "multiline"> & {
4
+ onChange?: G.ChangeHandler<number>;
5
+ increaseAriaLabel: string;
6
+ decreaseAriaLabel: string;
7
+ min?: number;
8
+ max?: number;
9
+ step?: number;
10
+ };
11
+ export type ControlledProps = BaseProps & {
12
+ value: number | null;
13
+ defaultValue?: never;
14
+ };
15
+ export type UncontrolledProps = BaseProps & {
16
+ value?: never;
17
+ defaultValue?: number;
18
+ };
19
+ export type Props = ControlledProps | UncontrolledProps;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,6 @@
1
+ import type * as T from "./NumberField.types";
2
+ declare const NumberFieldControlled: {
3
+ (props: T.ControlledProps): import("react/jsx-runtime").JSX.Element;
4
+ displayName: string;
5
+ };
6
+ export default NumberFieldControlled;
@@ -0,0 +1,162 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import React from "react";
4
+ import Actionable from "../Actionable/index.js";
5
+ import Icon from "../Icon/index.js";
6
+ import TextField from "../TextField/index.js";
7
+ import { useFormControl } from "../FormControl/index.js";
8
+ import IconChevronUp from "../../icons/ChevronUp.js";
9
+ import IconChevronDown from "../../icons/ChevronDown.js";
10
+ import IconPlus from "../../icons/Plus.js";
11
+ import IconMinus from "../../icons/Minus.js";
12
+ import useElementId from "../../hooks/useElementId.js";
13
+ import useHotkeys from "../../hooks/useHotkeys.js";
14
+ import useHandlerRef from "../../hooks/useHandlerRef.js";
15
+ import * as keys from "../../constants/keys.js";
16
+ import { responsiveClassNames, responsivePropDependency } from "../../utilities/helpers.js";
17
+ import s from "./NumberField.module.css";
18
+ const NumberFieldControlled = (props) => {
19
+ const { increaseAriaLabel, decreaseAriaLabel, min, max, step = 1, name, value, onChange, size = "medium", ...textFieldProps } = props;
20
+ const formControl = useFormControl();
21
+ const id = useElementId(textFieldProps.id);
22
+ const inputId = formControl?.attributes.id || props.inputAttributes?.id || id;
23
+ const disabled = formControl?.disabled || props.disabled;
24
+ const hasError = formControl?.hasError || props.hasError;
25
+ const increaseDisabled = disabled || (value && max ? value >= max : false);
26
+ const decreaseDisabled = disabled || (value && min ? value <= min : false);
27
+ const inputRef = React.useRef(null);
28
+ const rootRef = React.useRef(null);
29
+ const [textValue, setTextValue] = React.useState(value?.toString() || "");
30
+ // Sync value to a ref to handle holding controlss pressed
31
+ // And changing it without waiting for a rerender
32
+ const valueRef = React.useRef(value);
33
+ const onChangeRef = useHandlerRef(onChange);
34
+ const pressedTimeoutRef = React.useRef(null);
35
+ const changeIntervalRef = React.useRef(null);
36
+ const calculateDirectionalChange = React.useCallback((direction) => {
37
+ const delta = step * direction;
38
+ const value = valueRef.current;
39
+ let nextValue = value === null ? delta : value + delta;
40
+ if (max !== undefined && nextValue > max)
41
+ nextValue = max;
42
+ if (min !== undefined && nextValue < min)
43
+ nextValue = min;
44
+ // Keep the right precision and avoid JS rounding errors
45
+ const floatPartLength = value?.toString().split(".")[1]?.length || 0;
46
+ return Number(nextValue.toFixed(floatPartLength));
47
+ }, [step, min, max]);
48
+ const commitValue = React.useCallback((value, options) => {
49
+ onChangeRef.current?.({ value, name });
50
+ // Only update the ref here when typing in the input
51
+ // Otherwise it will be updated when value changes
52
+ if (!options?.programmatic)
53
+ valueRef.current = value;
54
+ }, [name, onChangeRef]);
55
+ const handleIncrease = React.useCallback(() => {
56
+ const nextValue = calculateDirectionalChange(1);
57
+ commitValue(nextValue, { programmatic: true });
58
+ }, [calculateDirectionalChange, commitValue]);
59
+ const handleDecrease = React.useCallback(() => {
60
+ const nextValue = calculateDirectionalChange(-1);
61
+ commitValue(nextValue, { programmatic: true });
62
+ }, [calculateDirectionalChange, commitValue]);
63
+ const handleChange = (args) => {
64
+ if (!args.value.match(/^(-?)[0-9]*(\.?)[0-9]*$/))
65
+ return;
66
+ const numberValue = parseFloat(args.value);
67
+ if (numberValue > Number.MAX_SAFE_INTEGER)
68
+ return;
69
+ if (numberValue < Number.MIN_SAFE_INTEGER)
70
+ return;
71
+ setTextValue(args.value);
72
+ if (isNaN(numberValue))
73
+ return;
74
+ commitValue(numberValue);
75
+ };
76
+ const handleControlPointerDown = (e, callback) => {
77
+ if (disabled)
78
+ return;
79
+ callback();
80
+ if (e.pointerType !== "touch") {
81
+ inputRef.current?.focus();
82
+ }
83
+ pressedTimeoutRef.current = setTimeout(() => {
84
+ changeIntervalRef.current = setInterval(() => {
85
+ callback();
86
+ }, 50);
87
+ }, 500);
88
+ };
89
+ const handleControlPointerUp = () => {
90
+ if (disabled)
91
+ return;
92
+ if (pressedTimeoutRef.current) {
93
+ clearTimeout(pressedTimeoutRef.current);
94
+ pressedTimeoutRef.current = null;
95
+ }
96
+ if (changeIntervalRef.current) {
97
+ clearTimeout(changeIntervalRef.current);
98
+ changeIntervalRef.current = null;
99
+ }
100
+ };
101
+ useHotkeys({
102
+ [keys.UP]: handleIncrease,
103
+ [keys.DOWN]: handleDecrease,
104
+ }, [handleIncrease, handleDecrease], {
105
+ preventDefault: true,
106
+ ref: rootRef,
107
+ });
108
+ React.useEffect(() => {
109
+ valueRef.current = value;
110
+ setTextValue(value?.toString() ?? "");
111
+ }, [value]);
112
+ const mouseIconSize = responsivePropDependency(size, (size) => {
113
+ return size === "large" || size === "xlarge" ? 4 : 3;
114
+ });
115
+ const touchIconSize = responsivePropDependency(size, (size) => {
116
+ return size === "small" ? 3 : 4;
117
+ });
118
+ const controlsNode = (_jsx("span", { className: s["controls-wrapper"], children: _jsxs("span", { className: s.controls, children: [_jsxs(Actionable, { className: s.control, disabled: increaseDisabled, disableFocusRing: true, as: "span", attributes: {
119
+ "aria-label": increaseAriaLabel,
120
+ "aria-controls": inputId,
121
+ role: "button",
122
+ tabIndex: increaseDisabled ? undefined : -1,
123
+ onPointerDown: (e) => handleControlPointerDown(e, handleIncrease),
124
+ onPointerUp: handleControlPointerUp,
125
+ onPointerLeave: handleControlPointerUp,
126
+ // Prevent menu from opening on long press
127
+ onContextMenu: (e) => e.preventDefault(),
128
+ }, children: [_jsx(Icon, { svg: IconChevronUp, size: mouseIconSize, className: s["icon--mouse"] }), _jsx(Icon, { svg: IconPlus, size: touchIconSize, className: s["icon--touch"] })] }), _jsxs(Actionable, { className: s.control, disabled: decreaseDisabled, disableFocusRing: true, as: "span", attributes: {
129
+ "aria-label": decreaseAriaLabel,
130
+ "aria-controls": inputId,
131
+ role: "button",
132
+ tabIndex: decreaseDisabled ? undefined : -1,
133
+ onPointerDown: (e) => handleControlPointerDown(e, handleDecrease),
134
+ onPointerUp: handleControlPointerUp,
135
+ onPointerLeave: handleControlPointerUp,
136
+ // Prevent menu from opening on long press
137
+ onContextMenu: (e) => e.preventDefault(),
138
+ }, children: [_jsx(Icon, { svg: IconChevronDown, size: mouseIconSize, className: s["icon--mouse"] }), _jsx(Icon, { svg: IconMinus, size: touchIconSize, className: s["icon--touch"] })] })] }) }));
139
+ return (_jsx(TextField, { ...textFieldProps, className: [
140
+ textFieldProps.className,
141
+ responsiveClassNames(s, "controls--size", size),
142
+ !(textFieldProps.variant === "faded" || textFieldProps.variant === "headless") &&
143
+ s["--outline"],
144
+ ], attributes: {
145
+ ...textFieldProps.attributes,
146
+ role: "group",
147
+ ref: rootRef,
148
+ }, inputAttributes: {
149
+ ...textFieldProps.inputAttributes,
150
+ ref: inputRef,
151
+ inputMode: "numeric",
152
+ autoComplete: "off",
153
+ autoCorrect: "off",
154
+ spellCheck: "false",
155
+ min,
156
+ max,
157
+ step,
158
+ className: s.field,
159
+ }, size: size, id: inputId, hasError: hasError, disabled: disabled, value: textValue, onChange: handleChange, name: name, endSlot: controlsNode }));
160
+ };
161
+ NumberFieldControlled.displayName = "NumberFieldControlled";
162
+ export default NumberFieldControlled;
@@ -0,0 +1,6 @@
1
+ import type * as T from "./NumberField.types";
2
+ declare const NumberFieldUncontrolled: {
3
+ (props: T.UncontrolledProps): import("react/jsx-runtime").JSX.Element;
4
+ displayName: string;
5
+ };
6
+ export default NumberFieldUncontrolled;
@@ -0,0 +1,16 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import React from "react";
4
+ import NumberFieldControlled from "./NumberFieldControlled.js";
5
+ const NumberFieldUncontrolled = (props) => {
6
+ const { defaultValue, onChange } = props;
7
+ const [value, setValue] = React.useState(defaultValue ?? null);
8
+ const handleChange = (args) => {
9
+ setValue(args.value);
10
+ if (onChange)
11
+ onChange(args);
12
+ };
13
+ return (_jsx(NumberFieldControlled, { ...props, value: value, defaultValue: undefined, onChange: handleChange }));
14
+ };
15
+ NumberFieldUncontrolled.displayName = "NumberFieldUncontrolled";
16
+ export default NumberFieldUncontrolled;
@@ -0,0 +1,2 @@
1
+ export { default } from "./NumberField";
2
+ export type { Props as NumberFieldProps } from "./NumberField.types";
@@ -0,0 +1 @@
1
+ export { default } from "./NumberField.js";
@@ -0,0 +1,30 @@
1
+ import { StoryObj } from "@storybook/react";
2
+ import { Mock } from "@storybook/test";
3
+ declare const _default: {
4
+ title: string;
5
+ component: {
6
+ (props: import("./..").NumberFieldProps): import("react").JSX.Element;
7
+ displayName: string;
8
+ };
9
+ parameters: {
10
+ iframe: {
11
+ url: string;
12
+ };
13
+ };
14
+ };
15
+ export default _default;
16
+ export declare const variant: StoryObj;
17
+ export declare const size: StoryObj;
18
+ export declare const disabled: StoryObj<{
19
+ handleChange: Mock;
20
+ }>;
21
+ export declare const defaultValue: StoryObj<{
22
+ handleChange: Mock;
23
+ }>;
24
+ export declare const value: StoryObj<{
25
+ handleChange: Mock;
26
+ }>;
27
+ export declare const minMax: StoryObj;
28
+ export declare const className: StoryObj;
29
+ export declare const formControl: StoryObj;
30
+ export declare const valueChanges: StoryObj;
@@ -0,0 +1,240 @@
1
+ import { expect, fn, userEvent } from "@storybook/test";
2
+ import FormControl from "../../FormControl/index.js";
3
+ import NumberField from "../index.js";
4
+ import { Example } from "../../../utilities/storybook/index.js";
5
+ export default {
6
+ title: "Components/NumberField",
7
+ component: NumberField,
8
+ parameters: {
9
+ iframe: {
10
+ url: "https://reshaped.so/docs/components/number-field",
11
+ },
12
+ },
13
+ };
14
+ export const variant = {
15
+ name: "variant",
16
+ render: () => {
17
+ return (<Example>
18
+ <Example.Item title="variant: outline">
19
+ <NumberField name="test-name" increaseAriaLabel="Increase" decreaseAriaLabel="Decrease" inputAttributes={{ "aria-label": "Label" }}/>
20
+ </Example.Item>
21
+ <Example.Item title="variant: faded">
22
+ <NumberField variant="faded" name="test-name" increaseAriaLabel="Increase" decreaseAriaLabel="Decrease" inputAttributes={{ "aria-label": "Label" }}/>
23
+ </Example.Item>
24
+ <Example.Item title="variant: headless">
25
+ <NumberField variant="headless" name="test-name" increaseAriaLabel="Increase" decreaseAriaLabel="Decrease" inputAttributes={{ "aria-label": "Label" }}/>
26
+ </Example.Item>
27
+ </Example>);
28
+ },
29
+ play: async ({ canvas }) => {
30
+ const input = canvas.getAllByRole("textbox")[0];
31
+ const [increaseButton, decreaseButton] = canvas.getAllByRole("button");
32
+ expect(input).toHaveAttribute("name", "test-name");
33
+ expect(increaseButton).toHaveAccessibleName("Increase");
34
+ expect(decreaseButton).toHaveAccessibleName("Decrease");
35
+ },
36
+ };
37
+ export const size = {
38
+ name: "size",
39
+ render: () => {
40
+ return (<Example>
41
+ <Example.Item title="size: small">
42
+ <NumberField size="small" name="test-name" increaseAriaLabel="Increase" decreaseAriaLabel="Decrease" suffix="m2" inputAttributes={{ "aria-label": "Label" }}/>
43
+ </Example.Item>
44
+ <Example.Item title="size: medium">
45
+ <NumberField size="medium" name="test-name" suffix="m2" increaseAriaLabel="Increase" decreaseAriaLabel="Decrease" inputAttributes={{ "aria-label": "Label" }}/>
46
+ </Example.Item>
47
+ <Example.Item title="size: large">
48
+ <NumberField size="large" name="test-name" suffix="m2" increaseAriaLabel="Increase" decreaseAriaLabel="Decrease" inputAttributes={{ "aria-label": "Label" }}/>
49
+ </Example.Item>
50
+ <Example.Item title="size: xlarge">
51
+ <NumberField size="xlarge" name="test-name" suffix="m2" increaseAriaLabel="Increase" decreaseAriaLabel="Decrease" inputAttributes={{ "aria-label": "Label" }}/>
52
+ </Example.Item>
53
+ </Example>);
54
+ },
55
+ };
56
+ export const disabled = {
57
+ name: "disabled",
58
+ args: {
59
+ handleChange: fn(),
60
+ },
61
+ render: (args) => (<NumberField name="test-name" onChange={args.handleChange} increaseAriaLabel="Increase" decreaseAriaLabel="Decrease" disabled inputAttributes={{ "aria-label": "Label" }}/>),
62
+ play: async ({ canvas, args }) => {
63
+ const input = canvas.getByRole("textbox");
64
+ const increaseButton = document.querySelector('[aria-label="Increase"]');
65
+ const decreaseButton = document.querySelector('[aria-label="Decrease"]');
66
+ expect(input).toBeDisabled();
67
+ await userEvent.click(increaseButton);
68
+ expect(args.handleChange).not.toHaveBeenCalled();
69
+ await userEvent.click(decreaseButton);
70
+ expect(args.handleChange).not.toHaveBeenCalled();
71
+ },
72
+ };
73
+ export const defaultValue = {
74
+ name: "defaultValue, uncontrolled",
75
+ args: {
76
+ handleChange: fn(),
77
+ },
78
+ render: (args) => (<NumberField name="test-name" defaultValue={2} onChange={args.handleChange} increaseAriaLabel="Increase" decreaseAriaLabel="Decrease" inputAttributes={{ "aria-label": "Label" }}/>),
79
+ play: async ({ canvas, args }) => {
80
+ const input = canvas.getByRole("textbox");
81
+ const [increaseButton, decreaseButton] = canvas.getAllByRole("button");
82
+ expect(input).toHaveValue("2");
83
+ input.focus();
84
+ await userEvent.keyboard("3");
85
+ expect(args.handleChange).toBeCalledTimes(1);
86
+ expect(args.handleChange).toHaveBeenLastCalledWith({
87
+ name: "test-name",
88
+ value: 23,
89
+ });
90
+ expect(input).toHaveValue("23");
91
+ await userEvent.click(increaseButton);
92
+ expect(args.handleChange).toBeCalledTimes(2);
93
+ expect(args.handleChange).toHaveBeenLastCalledWith({
94
+ name: "test-name",
95
+ value: 24,
96
+ });
97
+ expect(input).toHaveValue("24");
98
+ await userEvent.click(decreaseButton);
99
+ expect(args.handleChange).toBeCalledTimes(3);
100
+ expect(args.handleChange).toHaveBeenLastCalledWith({
101
+ name: "test-name",
102
+ value: 23,
103
+ });
104
+ expect(input).toHaveValue("23");
105
+ },
106
+ };
107
+ export const value = {
108
+ name: "value, controlled",
109
+ args: {
110
+ handleChange: fn(),
111
+ },
112
+ render: (args) => (<NumberField name="test-name" value={2} onChange={args.handleChange} increaseAriaLabel="Increase" decreaseAriaLabel="Decrease" inputAttributes={{ "aria-label": "Label" }}/>),
113
+ play: async ({ canvas, args }) => {
114
+ const input = canvas.getByRole("textbox");
115
+ const [increaseButton, decreaseButton] = canvas.getAllByRole("button");
116
+ expect(input).toHaveValue("2");
117
+ input.focus();
118
+ await userEvent.keyboard("3");
119
+ expect(args.handleChange).toBeCalledTimes(1);
120
+ expect(args.handleChange).toHaveBeenLastCalledWith({
121
+ name: "test-name",
122
+ value: 23,
123
+ });
124
+ expect(input).toHaveValue("23");
125
+ await userEvent.click(increaseButton);
126
+ expect(args.handleChange).toBeCalledTimes(2);
127
+ expect(args.handleChange).toHaveBeenLastCalledWith({
128
+ name: "test-name",
129
+ value: 24,
130
+ });
131
+ expect(input).toHaveValue("23");
132
+ await userEvent.click(decreaseButton);
133
+ expect(args.handleChange).toBeCalledTimes(3);
134
+ expect(args.handleChange).toHaveBeenLastCalledWith({
135
+ name: "test-name",
136
+ value: 22,
137
+ });
138
+ expect(input).toHaveValue("23");
139
+ },
140
+ };
141
+ export const minMax = {
142
+ name: "min, max, step",
143
+ render: () => (<NumberField name="test-name" defaultValue={6} min={5} max={15} step={5} increaseAriaLabel="Increase" decreaseAriaLabel="Decrease" inputAttributes={{ "aria-label": "Label" }}/>),
144
+ play: async ({ canvas }) => {
145
+ const input = canvas.getByRole("textbox");
146
+ const [increaseButton, decreaseButton] = canvas.getAllByRole("button");
147
+ expect(input).toHaveValue("6");
148
+ await userEvent.click(increaseButton);
149
+ expect(input).toHaveValue("11");
150
+ await userEvent.click(decreaseButton);
151
+ await userEvent.click(decreaseButton);
152
+ expect(input).toHaveValue("5");
153
+ await userEvent.click(increaseButton);
154
+ await userEvent.click(increaseButton);
155
+ await userEvent.click(increaseButton);
156
+ expect(input).toHaveValue("15");
157
+ },
158
+ };
159
+ export const className = {
160
+ name: "className, attributes",
161
+ render: () => (<div data-testid="root">
162
+ <NumberField className="test-classname" attributes={{ id: "test-id" }} name="name" inputAttributes={{ id: "test-input-id", "aria-label": "Label" }} increaseAriaLabel="Increase" decreaseAriaLabel="Decrease"/>
163
+ </div>),
164
+ play: async ({ canvas }) => {
165
+ const root = canvas.getByTestId("root").firstChild;
166
+ const input = canvas.getByRole("textbox");
167
+ expect(root).toHaveClass("test-classname");
168
+ expect(root).toHaveAttribute("id", "test-id");
169
+ expect(input).toHaveAttribute("id", "test-input-id");
170
+ },
171
+ };
172
+ export const formControl = {
173
+ name: "test: FormControl",
174
+ render: () => (<Example>
175
+ <Example.Item title="FormControl">
176
+ <FormControl>
177
+ <FormControl.Label>Label</FormControl.Label>
178
+ <NumberField name="name" increaseAriaLabel="Increase" decreaseAriaLabel="Decrease"/>
179
+ <FormControl.Helper>Helper</FormControl.Helper>
180
+ </FormControl>
181
+ </Example.Item>
182
+
183
+ <Example.Item title="FormControl, disabled">
184
+ <FormControl disabled>
185
+ <FormControl.Label>Label</FormControl.Label>
186
+ <NumberField name="name" increaseAriaLabel="Increase" decreaseAriaLabel="Decrease"/>
187
+ <FormControl.Helper>Helper</FormControl.Helper>
188
+ </FormControl>
189
+ </Example.Item>
190
+
191
+ <Example.Item title="FormControl, error">
192
+ <FormControl hasError>
193
+ <FormControl.Label>Label</FormControl.Label>
194
+ <NumberField name="name" increaseAriaLabel="Increase" decreaseAriaLabel="Decrease"/>
195
+ <FormControl.Error>Error</FormControl.Error>
196
+ </FormControl>
197
+ </Example.Item>
198
+ </Example>),
199
+ };
200
+ export const valueChanges = {
201
+ name: "test: keyboard",
202
+ render: () => (<Example>
203
+ <Example.Item title="keyboard">
204
+ <NumberField name="name" increaseAriaLabel="Increase" decreaseAriaLabel="Decrease" inputAttributes={{ "aria-label": "Label" }}/>
205
+ </Example.Item>
206
+ </Example>),
207
+ play: async ({ canvas }) => {
208
+ const input = canvas.getByRole("textbox");
209
+ input.focus();
210
+ await userEvent.keyboard("-");
211
+ expect(input).toHaveValue("-");
212
+ await userEvent.keyboard("1");
213
+ expect(input).toHaveValue("-1");
214
+ await userEvent.keyboard("-");
215
+ expect(input).toHaveValue("-1");
216
+ await userEvent.keyboard("2");
217
+ expect(input).toHaveValue("-12");
218
+ await userEvent.keyboard("{ArrowUp}");
219
+ expect(input).toHaveValue("-11");
220
+ await userEvent.keyboard("{ArrowDown}");
221
+ expect(input).toHaveValue("-12");
222
+ await userEvent.keyboard(".");
223
+ expect(input).toHaveValue("-12.");
224
+ await userEvent.keyboard("3");
225
+ expect(input).toHaveValue("-12.3");
226
+ await userEvent.keyboard(".");
227
+ expect(input).toHaveValue("-12.3");
228
+ await userEvent.keyboard("-");
229
+ expect(input).toHaveValue("-12.3");
230
+ await userEvent.keyboard("{ArrowUp}");
231
+ await userEvent.keyboard("{ArrowUp}");
232
+ await userEvent.keyboard("{ArrowUp}");
233
+ await userEvent.keyboard("{ArrowUp}");
234
+ await userEvent.keyboard("{ArrowUp}");
235
+ expect(input).toHaveValue("-7.3");
236
+ await userEvent.keyboard("{ArrowDown}");
237
+ expect(input).toHaveValue("-8.3");
238
+ },
239
+ };
240
+ // Value change edge cases
@@ -97,7 +97,7 @@ const Overlay = (props) => {
97
97
  React.useEffect(() => {
98
98
  if (!rendered || !contentRef.current)
99
99
  return;
100
- const trapFocus = new TrapFocus(contentRef.current);
100
+ const trapFocus = new TrapFocus();
101
101
  const containerEl = containerRef?.current;
102
102
  if (containerEl) {
103
103
  originalOverflowRef.current = containerEl.style.overflow;
@@ -105,7 +105,7 @@ const Overlay = (props) => {
105
105
  containerEl.style.isolation = "isolate";
106
106
  setOffset([containerEl.scrollLeft, containerEl.scrollTop]);
107
107
  }
108
- trapFocus.trap({
108
+ trapFocus.trap(contentRef.current, {
109
109
  initialFocusEl: contentRef.current.querySelector("[role=dialog][tabindex='-1']"),
110
110
  });
111
111
  onOpenRef.current?.();
@@ -1 +1 @@
1
- .root{display:inline-flex;margin:-1px 0;overflow-y:clip;padding:1px 0}.input,.root{vertical-align:top}.input{background:transparent;border:transparent;caret-color:transparent;color:transparent;font-size:16px;inset:0;outline:none;padding-left:100%;position:absolute}.item{cursor:text}.item--focused{border-color:var(--rs-color-border-primary);box-shadow:0 0 0 1px var(--rs-color-border-primary)}.item--focused:empty:before{animation:rs-pin-field-caret 1s ease-out infinite;background:var(--rs-color-foreground-neutral);border-radius:999px;content:"";height:var(--rs-font-size-body-2);left:50%;position:absolute;top:50%;transform:translate(-50%,-50%);width:1px}@media (hover:hover){.root:hover .input{pointer-events:none}}@keyframes rs-pin-field-caret{0%,49.9%,to{opacity:1}50%,99.9%{opacity:0}}
1
+ .root{display:inline-flex}.input,.root{vertical-align:top}.input{background:transparent;border:transparent;caret-color:transparent;color:transparent;font-size:16px;inset:0;outline:none;padding-left:100%;position:absolute}.item{box-sizing:border-box;cursor:text}.item--focused{border-color:var(--rs-color-border-primary);box-shadow:0 0 0 1px var(--rs-color-border-primary)}.item--focused:empty:before{animation:rs-pin-field-caret 1s ease-out infinite;background:var(--rs-color-foreground-neutral);border-radius:999px;content:"";height:var(--rs-font-size-body-2);left:50%;position:absolute;top:50%;transform:translate(-50%,-50%);width:1px}@media (hover:hover){.root:hover .input{pointer-events:none}}@keyframes rs-pin-field-caret{0%,49.9%,to{opacity:1}50%,99.9%{opacity:0}}
@@ -1,5 +1,5 @@
1
1
  import type * as G from "../../types/global";
2
- export type Size = "medium" | "large" | "xlarge";
2
+ export type Size = "small" | "medium" | "large" | "xlarge";
3
3
  type BaseProps = {
4
4
  name: string;
5
5
  valueLength?: number;
@@ -11,6 +11,7 @@ import * as keys from "../../constants/keys.js";
11
11
  import { regExpNumericChar, regExpAlphabeticChar, regExpAlphaNumericChar, } from "./PinField.constants.js";
12
12
  import s from "./PinField.module.css";
13
13
  const sizeMap = {
14
+ small: 7,
14
15
  medium: 9,
15
16
  large: 12,
16
17
  xlarge: 14,
@@ -40,6 +40,14 @@ export const variant = () => (<Example>
40
40
  </Example.Item>
41
41
  </Example>);
42
42
  export const size = () => (<Example>
43
+ <Example.Item title="size: small">
44
+ <PinField name="pin" size="small" inputAttributes={{ "aria-label": "Pin" }}/>
45
+ </Example.Item>
46
+
47
+ <Example.Item title="size: medium">
48
+ <PinField name="pin" size="medium" inputAttributes={{ "aria-label": "Pin" }}/>
49
+ </Example.Item>
50
+
43
51
  <Example.Item title="size: large">
44
52
  <PinField name="pin" size="large" inputAttributes={{ "aria-label": "Pin" }}/>
45
53
  </Example.Item>
@@ -1,12 +1,13 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { classNames } from "../../utilities/helpers.js";
3
+ import { classNames, responsiveClassNames, responsivePropDependency } from "../../utilities/helpers.js";
4
4
  import HiddenInput from "../_private/HiddenInput/index.js";
5
+ import Text from "../Text/index.js";
5
6
  import { useRadioGroup } from "../RadioGroup/index.js";
6
7
  import { useFormControl } from "../FormControl/index.js";
7
8
  import s from "./Radio.module.css";
8
9
  const Radio = (props) => {
9
- const { children, value, onChange, onFocus, onBlur, className, attributes, inputAttributes } = props;
10
+ const { children, value, onChange, onFocus, onBlur, size = "medium", className, attributes, inputAttributes, } = props;
10
11
  const formControl = useFormControl();
11
12
  const radioGroup = useRadioGroup();
12
13
  const hasError = formControl?.hasError || props.hasError || radioGroup?.hasError;
@@ -14,7 +15,7 @@ const Radio = (props) => {
14
15
  const checked = radioGroup ? radioGroup.value === value : props.checked;
15
16
  const defaultChecked = radioGroup ? undefined : props.defaultChecked;
16
17
  const name = radioGroup ? radioGroup.name : props.name;
17
- const rootClassName = classNames(s.root, className, hasError && s["--error"], disabled && s["--disabled"]);
18
+ const rootClassName = classNames(s.root, className, hasError && s["--error"], disabled && s["--disabled"], size && responsiveClassNames(s, "--size", size));
18
19
  const handleChange = (event) => {
19
20
  if (!name)
20
21
  return;
@@ -25,7 +26,13 @@ const Radio = (props) => {
25
26
  if (radioGroup?.onChange)
26
27
  radioGroup.onChange(changeArgs);
27
28
  };
28
- return (_jsxs("label", { ...attributes, className: rootClassName, children: [_jsxs("span", { className: s.field, children: [_jsx(HiddenInput, { className: s.input, type: "radio", checked: checked, defaultChecked: defaultChecked, name: name, disabled: disabled, value: value, onChange: handleChange, onFocus: onFocus, onBlur: onBlur, attributes: inputAttributes }), _jsx("div", { className: s.decorator })] }), children && _jsx("span", { className: s.text, children: children })] }));
29
+ return (_jsxs("label", { ...attributes, className: rootClassName, children: [_jsxs("span", { className: s.field, children: [_jsx(HiddenInput, { className: s.input, type: "radio", checked: checked, defaultChecked: defaultChecked, name: name, disabled: disabled, value: value, onChange: handleChange, onFocus: onFocus, onBlur: onBlur, attributes: inputAttributes }), _jsx("div", { className: s.decorator })] }), children && (_jsx(Text, { as: "span", variant: responsivePropDependency(size, (size) => {
30
+ if (size === "large")
31
+ return "body-2";
32
+ if (size === "small")
33
+ return "caption-1";
34
+ return "body-3";
35
+ }), children: children }))] }));
29
36
  };
30
37
  Radio.displayName = "Radio";
31
38
  export default Radio;
@@ -1 +1 @@
1
- .root{align-items:center;cursor:pointer;display:inline-flex;user-select:none;vertical-align:top;-webkit-tap-highlight-color:transparent}.root:hover .input:not(:checked)+.decorator{background:var(--rs-color-background-neutral-faded)}.field{position:relative}.decorator{--rs-radio-decorator-size:var(--rs-line-height-body-3);background:var(--rs-color-background-elevation-base);border:1px solid var(--rs-color-border-neutral);border-radius:50%;height:var(--rs-radio-decorator-size);transition:var(--rs-duration-fast) var(--rs-easing-standard);transition-property:background-color,border-color;width:var(--rs-radio-decorator-size)}.decorator:after{background:var(--rs-color-on-background-primary);border-radius:50%;content:"";height:calc(var(--rs-radio-decorator-size) * .4);left:50%;opacity:0;position:absolute;top:50%;transform:translate(-50%,-50%) scale(0);transition:var(--rs-duration-fast) var(--rs-easing-standard);transition-property:opacity,transform;width:calc(var(--rs-radio-decorator-size) * .4)}[data-rs-keyboard] .input:focus+.decorator{box-shadow:var(--rs-focus-shadow)}.input:checked+.decorator,.root.--error .input:checked+.decorator,.root.--error:hover .input:checked+.decorator{background:var(--rs-color-background-primary);border-color:var(--rs-color-background-primary);border-width:2px}.input:checked+.decorator:after,.root.--error .input:checked+.decorator:after,.root.--error:hover .input:checked+.decorator:after{opacity:1;transform:translate(-50%,-50%) scale(1)}.text{margin-inline-start:var(--rs-unit-x2)}.root.--error .decorator,.root.--error:hover .input+.decorator{border-color:var(--rs-color-border-critical)}.root.--disabled{color:var(--rs-color-foreground-disabled);cursor:not-allowed}.root.--disabled .decorator,.root.--disabled .input:checked+.decorator,.root.--disabled:hover .input+.decorator{background:var(--rs-color-background-disabled-faded);border-color:var(--rs-color-border-disabled)}.root.--disabled .input:checked+.decorator{border-color:transparent}.root.--disabled .input:checked+.decorator:after{background:var(--rs-color-border-disabled)}
1
+ @layer rs.radio.base;@layer rs.radio.error;@layer rs.radio.checked;@layer rs.radio.disabled;@layer rs.radio.base{.root{align-items:center;cursor:pointer;display:inline-flex;gap:var(--rs-radio-gap);user-select:none;vertical-align:top;-webkit-tap-highlight-color:transparent}.root:hover .decorator{background:var(--rs-color-background-neutral-faded)}.field{position:relative}.decorator{background:var(--rs-color-background-elevation-base);border:1px solid var(--rs-color-border-neutral);border-radius:50%;height:var(--rs-radio-line-height);transition:var(--rs-duration-fast) var(--rs-easing-standard);transition-property:background-color,border-color;width:var(--rs-radio-line-height)}.decorator:after{background:var(--rs-color-on-background-primary);border-radius:50%;content:"";height:calc(var(--rs-radio-line-height) * .4);left:50%;opacity:0;position:absolute;top:50%;transform:translate(-50%,-50%) scale(0);transition:var(--rs-duration-fast) var(--rs-easing-standard);transition-property:opacity,transform;width:calc(var(--rs-radio-line-height) * .4)}.--size-small{--rs-radio-line-height:var(--rs-line-height-caption-1);--rs-radio-gap:var(--rs-unit-x1)}.--size-medium{--rs-radio-line-height:var(--rs-line-height-body-3);--rs-radio-gap:var(--rs-unit-x2)}.--size-large{--rs-radio-line-height:var(--rs-line-height-body-2);--rs-radio-gap:var(--rs-unit-x2)}[data-rs-keyboard] .input:focus+.decorator{box-shadow:var(--rs-focus-shadow)}}@layer rs.radio.error{.root.--error .decorator{border-color:var(--rs-color-border-critical)}}@layer rs.radio.checked{.input:checked+.decorator{background:var(--rs-color-background-primary);border-color:var(--rs-color-background-primary)}.input:checked+.decorator:after{opacity:1;transform:translate(-50%,-50%) scale(1)}}@layer rs.radio.disabled{.root.--disabled{color:var(--rs-color-foreground-disabled);cursor:not-allowed}.root.--disabled .decorator{background:var(--rs-color-background-disabled-faded);border-color:var(--rs-color-border-disabled)}.root.--disabled .decorator:after{background-color:var(--rs-color-foreground-disabled)}.root.--disabled .input:checked+.decorator{background:var(--rs-color-background-disabled);border-color:transparent}}@media (--rs-viewport-m ){.--size-small--m{--rs-radio-line-height:var(--rs-line-height-caption-1);--rs-radio-gap:var(--rs-unit-x1)}.--size-medium--m{--rs-radio-line-height:var(--rs-line-height-body-3);--rs-radio-gap:var(--rs-unit-x2)}.--size-large--m{--rs-radio-line-height:var(--rs-line-height-body-2);--rs-radio-gap:var(--rs-unit-x2)}}@media (--rs-viewport-l ){.--size-small--l{--rs-radio-line-height:var(--rs-line-height-caption-1);--rs-radio-gap:var(--rs-unit-x1)}.--size-medium--l{--rs-radio-line-height:var(--rs-line-height-body-3);--rs-radio-gap:var(--rs-unit-x2)}.--size-large--l{--rs-radio-line-height:var(--rs-line-height-body-2);--rs-radio-gap:var(--rs-unit-x2)}}@media (--rs-viewport-xl ){.--size-small--xl{--rs-radio-line-height:var(--rs-line-height-caption-1);--rs-radio-gap:var(--rs-unit-x1)}.--size-medium--xl{--rs-radio-line-height:var(--rs-line-height-body-3);--rs-radio-gap:var(--rs-unit-x2)}.--size-large--xl{--rs-radio-line-height:var(--rs-line-height-body-2);--rs-radio-gap:var(--rs-unit-x2)}}