reshaped 3.4.5 → 3.4.7

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 (71) hide show
  1. package/CHANGELOG.md +17 -2
  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 +15 -1
  8. package/dist/components/Autocomplete/tests/Autocomplete.stories.js +193 -64
  9. package/dist/components/Avatar/Avatar.js +2 -2
  10. package/dist/components/Avatar/Avatar.types.d.ts +4 -1
  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 +2 -2
  19. package/dist/components/FormControl/FormControlLabel.js +2 -7
  20. package/dist/components/Image/Image.js +4 -4
  21. package/dist/components/Image/Image.types.d.ts +4 -1
  22. package/dist/components/NumberField/NumberField.d.ts +6 -0
  23. package/dist/components/NumberField/NumberField.js +11 -0
  24. package/dist/components/NumberField/NumberField.module.css +1 -0
  25. package/dist/components/NumberField/NumberField.types.d.ts +19 -0
  26. package/dist/components/NumberField/NumberField.types.js +1 -0
  27. package/dist/components/NumberField/NumberFieldControlled.d.ts +6 -0
  28. package/dist/components/NumberField/NumberFieldControlled.js +146 -0
  29. package/dist/components/NumberField/NumberFieldUncontrolled.d.ts +6 -0
  30. package/dist/components/NumberField/NumberFieldUncontrolled.js +16 -0
  31. package/dist/components/NumberField/index.d.ts +2 -0
  32. package/dist/components/NumberField/index.js +1 -0
  33. package/dist/components/NumberField/tests/NumberField.stories.d.ts +29 -0
  34. package/dist/components/NumberField/tests/NumberField.stories.js +215 -0
  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/hooks/tests/useScrollLock.stories.d.ts +1 -0
  57. package/dist/hooks/tests/useScrollLock.stories.js +29 -0
  58. package/dist/icons/ChevronUp.d.ts +2 -0
  59. package/dist/icons/ChevronUp.js +5 -0
  60. package/dist/icons/Minus.d.ts +2 -0
  61. package/dist/icons/Minus.js +3 -0
  62. package/dist/icons/Plus.d.ts +2 -2
  63. package/dist/icons/Plus.js +2 -2
  64. package/dist/index.d.ts +2 -0
  65. package/dist/index.js +1 -0
  66. package/dist/utilities/scroll/lock.js +15 -7
  67. package/dist/utilities/scroll/lockStandard.d.ts +1 -2
  68. package/dist/utilities/scroll/lockStandard.js +2 -9
  69. package/package.json +1 -1
  70. package/dist/components/Autocomplete/tests/Autocomplete.test.stories.d.ts +0 -27
  71. package/dist/components/Autocomplete/tests/Autocomplete.test.stories.js +0 -86
@@ -36,8 +36,8 @@ const Image = (props) => {
36
36
  if (typeof fallback === "string") {
37
37
  const imageAttributes = {
38
38
  ...attributes,
39
- src: fallback,
40
- alt,
39
+ src: fallback ?? "",
40
+ alt: alt ?? "",
41
41
  role: alt ? undefined : "presentation",
42
42
  className: fallbackClassNames,
43
43
  style,
@@ -50,8 +50,8 @@ const Image = (props) => {
50
50
  const imageAttributes = {
51
51
  ...attributes,
52
52
  ...passedImageAttributes,
53
- src,
54
- alt,
53
+ src: src ?? "",
54
+ alt: alt ?? "",
55
55
  role: alt ? undefined : "presentation",
56
56
  onLoad: handleLoad,
57
57
  onError: handleError,
@@ -11,7 +11,10 @@ export type Props = {
11
11
  onLoad?: (e: React.SyntheticEvent) => void;
12
12
  onError?: (e: React.SyntheticEvent) => void;
13
13
  fallback?: string | React.ReactNode | boolean;
14
- renderImage?: (attributes: G.Attributes<"img">) => React.ReactNode;
14
+ renderImage?: (attributes: Omit<G.Attributes<"img">, "src" | "alt"> & {
15
+ src: string;
16
+ alt: string;
17
+ }) => React.ReactNode;
15
18
  imageAttributes?: G.Attributes<"img">;
16
19
  className?: G.ClassName;
17
20
  attributes?: G.Attributes<"div"> & G.Attributes<"img">;
@@ -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{--rs-number-field-control-border-color:var(--rs-color-border-neutral);border-inline-start:1px solid var(--rs-number-field-control-border-color);display:flex;flex-direction:column;inset-block:calc(var(--rs-text-field-p-v) * -1 + var(--rs-unit-x1));inset-inline-end:1px;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)}.control{align-items:center;aspect-ratio:5/4;display:flex;flex-grow:1;height:50%;justify-content:center;touch-action:manipulation;transition:color var(--rs-duration-fast) var(--rs-easing-standard)}.control:first-child{box-shadow:0 1px var(--rs-number-field-control-border-color)}.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}@media (pointer:coarse) and (hover:none){.controls{flex-direction:row-reverse}.control{aspect-ratio:1;box-sizing:content-box;height:100%}.control:first-child{border-inline-start:1px solid var(--rs-color-border-neutral);box-shadow:none}.icon--touch{display:block}.icon--mouse{display:none}}
@@ -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"> & {
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,146 @@
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 IconChevronUp from "../../icons/ChevronUp.js";
8
+ import IconChevronDown from "../../icons/ChevronDown.js";
9
+ import IconPlus from "../../icons/Plus.js";
10
+ import IconMinus from "../../icons/Minus.js";
11
+ import useElementId from "../../hooks/useElementId.js";
12
+ import useHotkeys from "../../hooks/useHotkeys.js";
13
+ import * as keys from "../../constants/keys.js";
14
+ import s from "./NumberField.module.css";
15
+ import useHandlerRef from "../../hooks/useHandlerRef.js";
16
+ import { useFormControl } from "../FormControl/index.js";
17
+ const NumberFieldControlled = (props) => {
18
+ const { increaseAriaLabel, decreaseAriaLabel, min, max, step = 1, name, value, onChange, ...textFieldProps } = props;
19
+ const formControl = useFormControl();
20
+ const id = useElementId(textFieldProps.id);
21
+ const inputId = formControl?.attributes.id || props.inputAttributes?.id || id;
22
+ const disabled = formControl?.disabled || props.disabled;
23
+ const hasError = formControl?.hasError || props.hasError;
24
+ const increaseDisabled = disabled || (value && max ? value >= max : false);
25
+ const decreaseDisabled = disabled || (value && min ? value <= min : false);
26
+ const inputRef = React.useRef(null);
27
+ const rootRef = React.useRef(null);
28
+ const [textValue, setTextValue] = React.useState(value?.toString() || "");
29
+ // Sync value to a ref to handle holding controlss pressed
30
+ // And changing it without waiting for a rerender
31
+ const valueRef = React.useRef(value);
32
+ const onChangeRef = useHandlerRef(onChange);
33
+ const pressedTimeoutRef = React.useRef(null);
34
+ const changeIntervalRef = React.useRef(null);
35
+ const calculateDirectionalChange = React.useCallback((direction) => {
36
+ const delta = step * direction;
37
+ const value = valueRef.current;
38
+ let nextValue = value === null ? delta : value + delta;
39
+ if (max !== undefined && nextValue > max)
40
+ nextValue = max;
41
+ if (min !== undefined && nextValue < min)
42
+ nextValue = min;
43
+ // Keep the right precision and avoid JS rounding errors
44
+ const floatPartLength = value?.toString().split(".")[1]?.length || 0;
45
+ return Number(nextValue.toFixed(floatPartLength));
46
+ }, [step, min, max]);
47
+ const commitValue = React.useCallback((value, options) => {
48
+ onChangeRef.current?.({ value, name });
49
+ // Only update the ref here when typing in the input
50
+ // Otherwise it will be updated when value changes
51
+ if (!options?.programmatic)
52
+ valueRef.current = value;
53
+ }, [name, onChangeRef]);
54
+ const handleIncrease = React.useCallback(() => {
55
+ const nextValue = calculateDirectionalChange(1);
56
+ commitValue(nextValue, { programmatic: true });
57
+ }, [calculateDirectionalChange, commitValue]);
58
+ const handleDecrease = React.useCallback(() => {
59
+ const nextValue = calculateDirectionalChange(-1);
60
+ commitValue(nextValue, { programmatic: true });
61
+ }, [calculateDirectionalChange, commitValue]);
62
+ const handleChange = (args) => {
63
+ if (!args.value.match(/^(-?)[0-9]*(\.?)[0-9]*$/))
64
+ return;
65
+ setTextValue(args.value);
66
+ const numberValue = parseFloat(args.value);
67
+ if (isNaN(numberValue))
68
+ return;
69
+ commitValue(numberValue);
70
+ };
71
+ const handleControlPointerDown = (e, callback) => {
72
+ if (disabled)
73
+ return;
74
+ callback();
75
+ if (e.pointerType !== "touch") {
76
+ inputRef.current?.focus();
77
+ }
78
+ pressedTimeoutRef.current = setTimeout(() => {
79
+ changeIntervalRef.current = setInterval(() => {
80
+ callback();
81
+ }, 50);
82
+ }, 500);
83
+ };
84
+ const handleControlPointerUp = () => {
85
+ if (disabled)
86
+ return;
87
+ if (pressedTimeoutRef.current) {
88
+ clearTimeout(pressedTimeoutRef.current);
89
+ pressedTimeoutRef.current = null;
90
+ }
91
+ if (changeIntervalRef.current) {
92
+ clearTimeout(changeIntervalRef.current);
93
+ changeIntervalRef.current = null;
94
+ }
95
+ };
96
+ useHotkeys({
97
+ [keys.UP]: handleIncrease,
98
+ [keys.DOWN]: handleDecrease,
99
+ }, [handleIncrease, handleDecrease], {
100
+ preventDefault: true,
101
+ ref: rootRef,
102
+ });
103
+ React.useEffect(() => {
104
+ valueRef.current = value;
105
+ setTextValue(value?.toString() ?? "");
106
+ }, [value]);
107
+ const controlsNode = (_jsxs("span", { className: s.controls, children: [_jsxs(Actionable, { className: s.control, disabled: increaseDisabled, disableFocusRing: true, as: "span", attributes: {
108
+ "aria-label": increaseAriaLabel,
109
+ "aria-controls": id,
110
+ role: "button",
111
+ tabIndex: increaseDisabled ? undefined : -1,
112
+ onPointerDown: (e) => handleControlPointerDown(e, handleIncrease),
113
+ onPointerUp: handleControlPointerUp,
114
+ onPointerLeave: handleControlPointerUp,
115
+ // Prevent menu from opening on long press
116
+ onContextMenu: (e) => e.preventDefault(),
117
+ }, children: [_jsx(Icon, { svg: IconChevronUp, size: 3, className: s["icon--mouse"] }), _jsx(Icon, { svg: IconPlus, size: 4, className: s["icon--touch"] })] }), _jsxs(Actionable, { className: s.control, disabled: decreaseDisabled, disableFocusRing: true, as: "span", attributes: {
118
+ "aria-label": decreaseAriaLabel,
119
+ "aria-controls": id,
120
+ role: "button",
121
+ tabIndex: decreaseDisabled ? undefined : -1,
122
+ onPointerDown: (e) => handleControlPointerDown(e, handleDecrease),
123
+ onPointerUp: handleControlPointerUp,
124
+ onPointerLeave: handleControlPointerUp,
125
+ // Prevent menu from opening on long press
126
+ onContextMenu: (e) => e.preventDefault(),
127
+ }, children: [_jsx(Icon, { svg: IconChevronDown, size: 3, className: s["icon--mouse"] }), _jsx(Icon, { svg: IconMinus, size: 4, className: s["icon--touch"] })] })] }));
128
+ return (_jsx(TextField, { attributes: {
129
+ ...textFieldProps.attributes,
130
+ role: "group",
131
+ ref: rootRef,
132
+ }, id: inputId, hasError: hasError, inputAttributes: {
133
+ ...textFieldProps.inputAttributes,
134
+ ref: inputRef,
135
+ inputMode: "numeric",
136
+ autoComplete: "off",
137
+ autoCorrect: "off",
138
+ spellCheck: "false",
139
+ min,
140
+ max,
141
+ step,
142
+ className: s.field,
143
+ }, ...textFieldProps, disabled: disabled, value: textValue, onChange: handleChange, name: name, endSlot: controlsNode }));
144
+ };
145
+ NumberFieldControlled.displayName = "NumberFieldControlled";
146
+ 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,29 @@
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 base: StoryObj;
17
+ export declare const disabled: StoryObj<{
18
+ handleChange: Mock;
19
+ }>;
20
+ export declare const defaultValue: StoryObj<{
21
+ handleChange: Mock;
22
+ }>;
23
+ export declare const value: StoryObj<{
24
+ handleChange: Mock;
25
+ }>;
26
+ export declare const minMax: StoryObj;
27
+ export declare const className: StoryObj;
28
+ export declare const formControl: StoryObj;
29
+ export declare const valueChanges: StoryObj;
@@ -0,0 +1,215 @@
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 base = {
15
+ name: "base",
16
+ render: () => {
17
+ return (<Example>
18
+ <Example.Item title="base">
19
+ <NumberField name="test-name" increaseAriaLabel="Increase" decreaseAriaLabel="Decrease" inputAttributes={{ "aria-label": "Label" }}/>
20
+ </Example.Item>
21
+ </Example>);
22
+ },
23
+ play: async ({ canvas }) => {
24
+ const input = canvas.getByRole("textbox");
25
+ const [increaseButton, decreaseButton] = canvas.getAllByRole("button");
26
+ expect(input).toHaveAttribute("name", "test-name");
27
+ expect(increaseButton).toHaveAccessibleName("Increase");
28
+ expect(decreaseButton).toHaveAccessibleName("Decrease");
29
+ },
30
+ };
31
+ export const disabled = {
32
+ name: "disabled",
33
+ args: {
34
+ handleChange: fn(),
35
+ },
36
+ render: (args) => (<NumberField name="test-name" onChange={args.handleChange} increaseAriaLabel="Increase" decreaseAriaLabel="Decrease" disabled inputAttributes={{ "aria-label": "Label" }}/>),
37
+ play: async ({ canvas, args }) => {
38
+ const input = canvas.getByRole("textbox");
39
+ const increaseButton = document.querySelector('[aria-label="Increase"]');
40
+ const decreaseButton = document.querySelector('[aria-label="Decrease"]');
41
+ expect(input).toBeDisabled();
42
+ await userEvent.click(increaseButton);
43
+ expect(args.handleChange).not.toHaveBeenCalled();
44
+ await userEvent.click(decreaseButton);
45
+ expect(args.handleChange).not.toHaveBeenCalled();
46
+ },
47
+ };
48
+ export const defaultValue = {
49
+ name: "defaultValue, uncontrolled",
50
+ args: {
51
+ handleChange: fn(),
52
+ },
53
+ render: (args) => (<NumberField name="test-name" defaultValue={2} onChange={args.handleChange} increaseAriaLabel="Increase" decreaseAriaLabel="Decrease" inputAttributes={{ "aria-label": "Label" }}/>),
54
+ play: async ({ canvas, args }) => {
55
+ const input = canvas.getByRole("textbox");
56
+ const [increaseButton, decreaseButton] = canvas.getAllByRole("button");
57
+ expect(input).toHaveValue("2");
58
+ input.focus();
59
+ await userEvent.keyboard("3");
60
+ expect(args.handleChange).toBeCalledTimes(1);
61
+ expect(args.handleChange).toHaveBeenLastCalledWith({
62
+ name: "test-name",
63
+ value: 23,
64
+ });
65
+ expect(input).toHaveValue("23");
66
+ await userEvent.click(increaseButton);
67
+ expect(args.handleChange).toBeCalledTimes(2);
68
+ expect(args.handleChange).toHaveBeenLastCalledWith({
69
+ name: "test-name",
70
+ value: 24,
71
+ });
72
+ expect(input).toHaveValue("24");
73
+ await userEvent.click(decreaseButton);
74
+ expect(args.handleChange).toBeCalledTimes(3);
75
+ expect(args.handleChange).toHaveBeenLastCalledWith({
76
+ name: "test-name",
77
+ value: 23,
78
+ });
79
+ expect(input).toHaveValue("23");
80
+ },
81
+ };
82
+ export const value = {
83
+ name: "value, controlled",
84
+ args: {
85
+ handleChange: fn(),
86
+ },
87
+ render: (args) => (<NumberField name="test-name" value={2} onChange={args.handleChange} increaseAriaLabel="Increase" decreaseAriaLabel="Decrease" inputAttributes={{ "aria-label": "Label" }}/>),
88
+ play: async ({ canvas, args }) => {
89
+ const input = canvas.getByRole("textbox");
90
+ const [increaseButton, decreaseButton] = canvas.getAllByRole("button");
91
+ expect(input).toHaveValue("2");
92
+ input.focus();
93
+ await userEvent.keyboard("3");
94
+ expect(args.handleChange).toBeCalledTimes(1);
95
+ expect(args.handleChange).toHaveBeenLastCalledWith({
96
+ name: "test-name",
97
+ value: 23,
98
+ });
99
+ expect(input).toHaveValue("23");
100
+ await userEvent.click(increaseButton);
101
+ expect(args.handleChange).toBeCalledTimes(2);
102
+ expect(args.handleChange).toHaveBeenLastCalledWith({
103
+ name: "test-name",
104
+ value: 24,
105
+ });
106
+ expect(input).toHaveValue("23");
107
+ await userEvent.click(decreaseButton);
108
+ expect(args.handleChange).toBeCalledTimes(3);
109
+ expect(args.handleChange).toHaveBeenLastCalledWith({
110
+ name: "test-name",
111
+ value: 22,
112
+ });
113
+ expect(input).toHaveValue("23");
114
+ },
115
+ };
116
+ export const minMax = {
117
+ name: "min, max, step",
118
+ render: () => (<NumberField name="test-name" defaultValue={6} min={5} max={15} step={5} increaseAriaLabel="Increase" decreaseAriaLabel="Decrease" inputAttributes={{ "aria-label": "Label" }}/>),
119
+ play: async ({ canvas }) => {
120
+ const input = canvas.getByRole("textbox");
121
+ const [increaseButton, decreaseButton] = canvas.getAllByRole("button");
122
+ expect(input).toHaveValue("6");
123
+ await userEvent.click(increaseButton);
124
+ expect(input).toHaveValue("11");
125
+ await userEvent.click(decreaseButton);
126
+ await userEvent.click(decreaseButton);
127
+ expect(input).toHaveValue("5");
128
+ await userEvent.click(increaseButton);
129
+ await userEvent.click(increaseButton);
130
+ await userEvent.click(increaseButton);
131
+ expect(input).toHaveValue("15");
132
+ },
133
+ };
134
+ export const className = {
135
+ name: "className, attributes",
136
+ render: () => (<div data-testid="root">
137
+ <NumberField className="test-classname" attributes={{ id: "test-id" }} name="name" inputAttributes={{ id: "test-input-id", "aria-label": "Label" }} increaseAriaLabel="Increase" decreaseAriaLabel="Decrease"/>
138
+ </div>),
139
+ play: async ({ canvas }) => {
140
+ const root = canvas.getByTestId("root").firstChild;
141
+ const input = canvas.getByRole("textbox");
142
+ expect(root).toHaveClass("test-classname");
143
+ expect(root).toHaveAttribute("id", "test-id");
144
+ expect(input).toHaveAttribute("id", "test-input-id");
145
+ },
146
+ };
147
+ export const formControl = {
148
+ name: "test: FormControl",
149
+ render: () => (<Example>
150
+ <Example.Item title="FormControl">
151
+ <FormControl>
152
+ <FormControl.Label>Label</FormControl.Label>
153
+ <NumberField name="name" increaseAriaLabel="Increase" decreaseAriaLabel="Decrease"/>
154
+ <FormControl.Helper>Helper</FormControl.Helper>
155
+ </FormControl>
156
+ </Example.Item>
157
+
158
+ <Example.Item title="FormControl, disabled">
159
+ <FormControl disabled>
160
+ <FormControl.Label>Label</FormControl.Label>
161
+ <NumberField name="name" increaseAriaLabel="Increase" decreaseAriaLabel="Decrease"/>
162
+ <FormControl.Helper>Helper</FormControl.Helper>
163
+ </FormControl>
164
+ </Example.Item>
165
+
166
+ <Example.Item title="FormControl, error">
167
+ <FormControl hasError>
168
+ <FormControl.Label>Label</FormControl.Label>
169
+ <NumberField name="name" increaseAriaLabel="Increase" decreaseAriaLabel="Decrease"/>
170
+ <FormControl.Error>Error</FormControl.Error>
171
+ </FormControl>
172
+ </Example.Item>
173
+ </Example>),
174
+ };
175
+ export const valueChanges = {
176
+ name: "test: keyboard",
177
+ render: () => (<Example>
178
+ <Example.Item title="keyboard">
179
+ <NumberField name="name" increaseAriaLabel="Increase" decreaseAriaLabel="Decrease"/>
180
+ </Example.Item>
181
+ </Example>),
182
+ play: async ({ canvas }) => {
183
+ const input = canvas.getByRole("textbox");
184
+ input.focus();
185
+ await userEvent.keyboard("-");
186
+ expect(input).toHaveValue("-");
187
+ await userEvent.keyboard("1");
188
+ expect(input).toHaveValue("-1");
189
+ await userEvent.keyboard("-");
190
+ expect(input).toHaveValue("-1");
191
+ await userEvent.keyboard("2");
192
+ expect(input).toHaveValue("-12");
193
+ await userEvent.keyboard("{ArrowUp}");
194
+ expect(input).toHaveValue("-11");
195
+ await userEvent.keyboard("{ArrowDown}");
196
+ expect(input).toHaveValue("-12");
197
+ await userEvent.keyboard(".");
198
+ expect(input).toHaveValue("-12.");
199
+ await userEvent.keyboard("3");
200
+ expect(input).toHaveValue("-12.3");
201
+ await userEvent.keyboard(".");
202
+ expect(input).toHaveValue("-12.3");
203
+ await userEvent.keyboard("-");
204
+ expect(input).toHaveValue("-12.3");
205
+ await userEvent.keyboard("{ArrowUp}");
206
+ await userEvent.keyboard("{ArrowUp}");
207
+ await userEvent.keyboard("{ArrowUp}");
208
+ await userEvent.keyboard("{ArrowUp}");
209
+ await userEvent.keyboard("{ArrowUp}");
210
+ expect(input).toHaveValue("-7.3");
211
+ await userEvent.keyboard("{ArrowDown}");
212
+ expect(input).toHaveValue("-8.3");
213
+ },
214
+ };
215
+ // Value change edge cases
@@ -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)}}
@@ -6,6 +6,7 @@ type BaseProps = {
6
6
  disabled?: boolean;
7
7
  hasError?: boolean;
8
8
  value: string;
9
+ size?: G.Responsive<"small" | "medium" | "large">;
9
10
  onChange?: G.ChangeHandler<boolean>;
10
11
  onFocus?: (e: React.FocusEvent) => void;
11
12
  onBlur?: (e: React.FocusEvent) => void;
@@ -12,5 +12,6 @@ declare const _default: {
12
12
  };
13
13
  export default _default;
14
14
  export declare const selection: () => import("react").JSX.Element;
15
+ export declare const size: () => import("react").JSX.Element;
15
16
  export declare const error: () => import("react").JSX.Element;
16
17
  export declare const disabled: () => import("react").JSX.Element;