@agregio-solutions/design-system 1.90.1 → 1.92.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/design-system.cjs +9 -5
- package/dist/design-system.js +14 -6
- package/dist/packages/components/Accordion/doc.md +342 -0
- package/dist/packages/components/Badge/doc.md +192 -0
- package/dist/packages/components/Breadcrumbs/doc.md +332 -0
- package/dist/packages/components/Button/doc.md +425 -0
- package/dist/packages/components/Calendar/doc.md +465 -0
- package/dist/packages/components/ChartLegend/doc.md +151 -0
- package/dist/packages/components/ChartTooltip/doc.md +124 -0
- package/dist/packages/components/Checkbox/doc.md +329 -0
- package/dist/packages/components/CheckboxGroup/doc.md +242 -0
- package/dist/packages/components/Chip/doc.md +99 -0
- package/dist/packages/components/Combobox/Combobox.d.ts +8 -0
- package/dist/packages/components/Combobox/doc.md +680 -0
- package/dist/packages/components/DataTable/doc.md +1124 -0
- package/dist/packages/components/DatePicker/doc.md +579 -0
- package/dist/packages/components/DateRangePicker/doc.md +638 -0
- package/dist/packages/components/Drawer/doc.md +338 -0
- package/dist/packages/components/Dropdown/Dropdown.d.ts +4 -0
- package/dist/packages/components/Dropdown/doc.md +205 -0
- package/dist/packages/components/EmptyState/doc.md +101 -0
- package/dist/packages/components/FileUpload/doc.md +449 -0
- package/dist/packages/components/Filter/doc.md +196 -0
- package/dist/packages/components/Header/doc.md +373 -0
- package/dist/packages/components/I18nProvider/doc.md +187 -0
- package/dist/packages/components/Icon/doc.md +63 -0
- package/dist/packages/components/Label/doc.md +60 -0
- package/dist/packages/components/LinearProgressBar/doc.md +148 -0
- package/dist/packages/components/Link/doc.md +206 -0
- package/dist/packages/components/List/doc.md +481 -0
- package/dist/packages/components/Loader/doc.md +53 -0
- package/dist/packages/components/Menu/Menu.d.ts +5 -1
- package/dist/packages/components/Menu/doc.md +231 -0
- package/dist/packages/components/Message/doc.md +166 -0
- package/dist/packages/components/Modal/doc.md +289 -0
- package/dist/packages/components/Navigation/doc.md +992 -0
- package/dist/packages/components/NavigationItem/doc.md +167 -0
- package/dist/packages/components/NotificationCard/doc.md +206 -0
- package/dist/packages/components/Notifications/doc.md +240 -0
- package/dist/packages/components/NumberField/doc.md +582 -0
- package/dist/packages/components/PageLayout/doc.md +651 -0
- package/dist/packages/components/Pagination/doc.md +227 -0
- package/dist/packages/components/Popover/doc.md +245 -0
- package/dist/packages/components/Radio/doc.md +370 -0
- package/dist/packages/components/RouterProvider/doc.md +64 -0
- package/dist/packages/components/SearchBar/doc.md +504 -0
- package/dist/packages/components/SegmentedControl/doc.md +398 -0
- package/dist/packages/components/Select/Select.d.ts +4 -0
- package/dist/packages/components/Select/doc.md +1133 -0
- package/dist/packages/components/Skeleton/doc.md +129 -0
- package/dist/packages/components/Slider/doc.md +362 -0
- package/dist/packages/components/Stepper/doc.md +104 -0
- package/dist/packages/components/Switch/doc.md +296 -0
- package/dist/packages/components/Tabs/doc.md +295 -0
- package/dist/packages/components/Tag/doc.md +81 -0
- package/dist/packages/components/TextInput/doc.md +490 -0
- package/dist/packages/components/TimeField/doc.md +353 -0
- package/dist/packages/components/Timeline/doc.md +1046 -0
- package/dist/packages/components/Toaster/doc.md +263 -0
- package/dist/packages/components/ToggleButton/doc.md +108 -0
- package/dist/packages/components/ToggleButtonGroup/doc.md +307 -0
- package/dist/packages/components/Tooltip/doc.md +206 -0
- package/dist/packages/components/YearMonthPicker/YearMonthPicker.d.ts +8 -0
- package/dist/packages/components/YearMonthPicker/doc.md +638 -0
- package/dist/public_docs/components.md +68 -0
- package/dist/public_docs/index.md +30 -0
- package/dist/public_docs/tokens.md +121 -0
- package/package.json +3 -2
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
# NumberField
|
|
2
|
+
|
|
3
|
+
## Props
|
|
4
|
+
|
|
5
|
+
The complete Props documentation with JS doc for this component is available at this path:
|
|
6
|
+
|
|
7
|
+
node_modules/@agregio-solutions/design-system/dist/packages/components/NumberField/NumberField.d.ts
|
|
8
|
+
|
|
9
|
+
## Example usage
|
|
10
|
+
|
|
11
|
+
Here are the Storybook Stories.
|
|
12
|
+
|
|
13
|
+
Base stories:
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
import { Meta, StoryObj } from "@storybook/react-vite";
|
|
17
|
+
import { expect, fn, within } from "storybook/test";
|
|
18
|
+
import NumberField from "./NumberField";
|
|
19
|
+
import { STORYBOOK_VIEWPORTS } from "@internal/test-utils-storybook/test-utils-storybook";
|
|
20
|
+
import { I18nProvider } from "react-aria-components";
|
|
21
|
+
|
|
22
|
+
const meta: Meta<typeof NumberField> = {
|
|
23
|
+
component: NumberField,
|
|
24
|
+
argTypes: {
|
|
25
|
+
label: { control: "text" },
|
|
26
|
+
helperText: { control: "text" },
|
|
27
|
+
errorHelperText: { control: "text" },
|
|
28
|
+
successHelperText: { control: "text" },
|
|
29
|
+
warningHelperText: { control: "text" },
|
|
30
|
+
formatOptions: { control: "object" },
|
|
31
|
+
},
|
|
32
|
+
parameters: {
|
|
33
|
+
layout: "centered",
|
|
34
|
+
},
|
|
35
|
+
decorators: [
|
|
36
|
+
(Story) => (
|
|
37
|
+
<I18nProvider locale="fr-FR">
|
|
38
|
+
<Story />
|
|
39
|
+
</I18nProvider>
|
|
40
|
+
),
|
|
41
|
+
],
|
|
42
|
+
};
|
|
43
|
+
export default meta;
|
|
44
|
+
|
|
45
|
+
const getRoot = (canvasElement: HTMLElement) =>
|
|
46
|
+
canvasElement.querySelector('[data-design-system-component="NumberField"]');
|
|
47
|
+
|
|
48
|
+
export const Playground: StoryObj<typeof NumberField> = {
|
|
49
|
+
args: {
|
|
50
|
+
onChange: fn(),
|
|
51
|
+
label: "[Insert label]",
|
|
52
|
+
labelIconRight: "help_outline",
|
|
53
|
+
labelIconRightTooltip: "Additional information",
|
|
54
|
+
helperText: "[Insert helper]",
|
|
55
|
+
helperTextIcon: "help_outline",
|
|
56
|
+
defaultValue: 42,
|
|
57
|
+
step: 1,
|
|
58
|
+
required: true,
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const Default: StoryObj<typeof NumberField> = {
|
|
63
|
+
args: {
|
|
64
|
+
...Playground.args,
|
|
65
|
+
},
|
|
66
|
+
play: async ({ canvasElement }) => {
|
|
67
|
+
const canvas = within(canvasElement);
|
|
68
|
+
await expect(canvas.getByText("[Insert label]")).toBeInTheDocument();
|
|
69
|
+
await expect(getRoot(canvasElement)).toHaveAttribute(
|
|
70
|
+
"data-nature",
|
|
71
|
+
"default",
|
|
72
|
+
);
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export const Disabled: StoryObj<typeof NumberField> = {
|
|
77
|
+
args: {
|
|
78
|
+
...Playground.args,
|
|
79
|
+
isDisabled: true,
|
|
80
|
+
},
|
|
81
|
+
play: async ({ canvasElement }) => {
|
|
82
|
+
const canvas = within(canvasElement);
|
|
83
|
+
await expect(canvas.getByLabelText("[Insert label]")).toBeDisabled();
|
|
84
|
+
await expect(getRoot(canvasElement)).toHaveAttribute("data-disabled");
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export const ReadOnly: StoryObj<typeof NumberField> = {
|
|
89
|
+
args: {
|
|
90
|
+
...Playground.args,
|
|
91
|
+
isReadOnly: true,
|
|
92
|
+
},
|
|
93
|
+
play: async ({ canvasElement }) => {
|
|
94
|
+
const canvas = within(canvasElement);
|
|
95
|
+
await expect(canvas.getByLabelText("[Insert label]")).toHaveAttribute(
|
|
96
|
+
"readonly",
|
|
97
|
+
);
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export const WithError: StoryObj<typeof NumberField> = {
|
|
102
|
+
args: {
|
|
103
|
+
...Playground.args,
|
|
104
|
+
helperText: undefined,
|
|
105
|
+
errorHelperText: "Error message",
|
|
106
|
+
errorHelperTextIcon: "error_outline",
|
|
107
|
+
},
|
|
108
|
+
play: async ({ canvasElement }) => {
|
|
109
|
+
const canvas = within(canvasElement);
|
|
110
|
+
await expect(canvas.getByText("Error message")).toBeInTheDocument();
|
|
111
|
+
await expect(getRoot(canvasElement)).toHaveAttribute(
|
|
112
|
+
"data-nature",
|
|
113
|
+
"negative",
|
|
114
|
+
);
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
export const WithSuccess: StoryObj<typeof NumberField> = {
|
|
119
|
+
args: {
|
|
120
|
+
...Playground.args,
|
|
121
|
+
helperText: undefined,
|
|
122
|
+
successHelperText: "Success message",
|
|
123
|
+
successHelperTextIcon: "check",
|
|
124
|
+
},
|
|
125
|
+
play: async ({ canvasElement }) => {
|
|
126
|
+
const canvas = within(canvasElement);
|
|
127
|
+
await expect(canvas.getByText("Success message")).toBeInTheDocument();
|
|
128
|
+
await expect(getRoot(canvasElement)).toHaveAttribute(
|
|
129
|
+
"data-nature",
|
|
130
|
+
"positive",
|
|
131
|
+
);
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
export const WithWarning: StoryObj<typeof NumberField> = {
|
|
136
|
+
args: {
|
|
137
|
+
...Playground.args,
|
|
138
|
+
helperText: undefined,
|
|
139
|
+
warningHelperText: "Warning message",
|
|
140
|
+
warningHelperTextIcon: "warning_amber",
|
|
141
|
+
},
|
|
142
|
+
play: async ({ canvasElement }) => {
|
|
143
|
+
const canvas = within(canvasElement);
|
|
144
|
+
await expect(canvas.getByText("Warning message")).toBeInTheDocument();
|
|
145
|
+
await expect(getRoot(canvasElement)).toHaveAttribute(
|
|
146
|
+
"data-nature",
|
|
147
|
+
"warning",
|
|
148
|
+
);
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
export const WithMinMax: StoryObj<typeof NumberField> = {
|
|
153
|
+
args: {
|
|
154
|
+
...Playground.args,
|
|
155
|
+
defaultValue: 5,
|
|
156
|
+
minValue: 0,
|
|
157
|
+
maxValue: 10,
|
|
158
|
+
step: 1,
|
|
159
|
+
helperText: "Value between 0 and 10",
|
|
160
|
+
},
|
|
161
|
+
play: async ({ canvasElement }) => {
|
|
162
|
+
const canvas = within(canvasElement);
|
|
163
|
+
await expect(canvas.getByLabelText("[Insert label]")).toHaveValue("5");
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
export const FormattedCurrency: StoryObj<typeof NumberField> = {
|
|
168
|
+
args: {
|
|
169
|
+
...Playground.args,
|
|
170
|
+
label: "Price",
|
|
171
|
+
defaultValue: 1234.5,
|
|
172
|
+
step: 0.01,
|
|
173
|
+
formatOptions: { style: "currency", currency: "EUR" },
|
|
174
|
+
},
|
|
175
|
+
play: async ({ canvasElement }) => {
|
|
176
|
+
const canvas = within(canvasElement);
|
|
177
|
+
const input = canvas.getByLabelText("Price") as HTMLInputElement;
|
|
178
|
+
await expect(input.value).toMatch(/1[,.\s]234[,.\s]50/);
|
|
179
|
+
await expect(input.value).toMatch(/€/);
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
export const FormattedPercent: StoryObj<typeof NumberField> = {
|
|
184
|
+
args: {
|
|
185
|
+
...Playground.args,
|
|
186
|
+
label: "Progress",
|
|
187
|
+
defaultValue: 0.25,
|
|
188
|
+
step: 0.01,
|
|
189
|
+
formatOptions: { style: "percent" },
|
|
190
|
+
},
|
|
191
|
+
play: async ({ canvasElement }) => {
|
|
192
|
+
const canvas = within(canvasElement);
|
|
193
|
+
const input = canvas.getByLabelText("Progress") as HTMLInputElement;
|
|
194
|
+
await expect(input.value).toMatch(/25\s?%/);
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
export const HorizontalOrientation: StoryObj<typeof NumberField> = {
|
|
199
|
+
args: {
|
|
200
|
+
...Playground.args,
|
|
201
|
+
orientation: "horizontal",
|
|
202
|
+
},
|
|
203
|
+
play: async ({ canvasElement }) => {
|
|
204
|
+
const topSection = canvasElement.querySelector("[data-orientation]");
|
|
205
|
+
await expect(topSection).toHaveAttribute("data-orientation", "horizontal");
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
export const Mobile: StoryObj<typeof NumberField> = {
|
|
210
|
+
parameters: {
|
|
211
|
+
...STORYBOOK_VIEWPORTS,
|
|
212
|
+
},
|
|
213
|
+
globals: {
|
|
214
|
+
viewport: { value: "iphone6", isRotated: false },
|
|
215
|
+
},
|
|
216
|
+
args: {
|
|
217
|
+
...Playground.args,
|
|
218
|
+
},
|
|
219
|
+
play: async ({ canvasElement }) => {
|
|
220
|
+
const canvas = within(canvasElement);
|
|
221
|
+
await expect(
|
|
222
|
+
canvas.getByRole("button", { name: "Diminuer : [Insert label]" }),
|
|
223
|
+
).toBeVisible();
|
|
224
|
+
await expect(
|
|
225
|
+
canvas.getByRole("button", { name: "Augmenter : [Insert label]" }),
|
|
226
|
+
).toBeVisible();
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
export const DisplayOnly: StoryObj<typeof NumberField> = {
|
|
231
|
+
args: {
|
|
232
|
+
...Playground.args,
|
|
233
|
+
isDisplayOnly: true,
|
|
234
|
+
value: 1234.56,
|
|
235
|
+
formatOptions: { style: "currency", currency: "EUR" },
|
|
236
|
+
},
|
|
237
|
+
play: async ({ canvasElement }) => {
|
|
238
|
+
const canvas = within(canvasElement);
|
|
239
|
+
await expect(canvas.getByText(/1\s?234,56\s?€/)).toBeVisible();
|
|
240
|
+
await expect(canvas.queryByRole("textbox")).not.toBeInTheDocument();
|
|
241
|
+
await expect(canvas.queryByRole("spinbutton")).not.toBeInTheDocument();
|
|
242
|
+
await expect(
|
|
243
|
+
canvas.queryByRole("button", { name: /Augmenter/i }),
|
|
244
|
+
).not.toBeInTheDocument();
|
|
245
|
+
await expect(
|
|
246
|
+
canvas.queryByRole("button", { name: /Diminuer/i }),
|
|
247
|
+
).not.toBeInTheDocument();
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
export const FullWidth: StoryObj<typeof NumberField> = {
|
|
252
|
+
parameters: {
|
|
253
|
+
layout: "padded",
|
|
254
|
+
},
|
|
255
|
+
args: {
|
|
256
|
+
...Playground.args,
|
|
257
|
+
fullWidth: true,
|
|
258
|
+
},
|
|
259
|
+
play: async ({ canvasElement }) => {
|
|
260
|
+
await expect(getRoot(canvasElement)).toHaveAttribute(
|
|
261
|
+
"data-fullwidth",
|
|
262
|
+
"true",
|
|
263
|
+
);
|
|
264
|
+
},
|
|
265
|
+
};
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
## How to test this component
|
|
269
|
+
|
|
270
|
+
Here are some more advanced stories with more testing coverage and examples that you can read to understand how to test this component.
|
|
271
|
+
|
|
272
|
+
```tsx
|
|
273
|
+
import { Meta, StoryObj } from "@storybook/react-vite";
|
|
274
|
+
import { userEvent, within, expect, fn } from "storybook/test";
|
|
275
|
+
import { I18nProvider } from "react-aria-components";
|
|
276
|
+
import { useState } from "react";
|
|
277
|
+
import NumberField from "../NumberField";
|
|
278
|
+
|
|
279
|
+
const meta: Meta<typeof NumberField> = {
|
|
280
|
+
component: NumberField,
|
|
281
|
+
argTypes: {
|
|
282
|
+
label: { control: "text" },
|
|
283
|
+
},
|
|
284
|
+
parameters: {
|
|
285
|
+
layout: "centered",
|
|
286
|
+
chromatic: { disableSnapshot: true },
|
|
287
|
+
},
|
|
288
|
+
decorators: [
|
|
289
|
+
(Story) => (
|
|
290
|
+
<I18nProvider locale="en-US">
|
|
291
|
+
<Story />
|
|
292
|
+
</I18nProvider>
|
|
293
|
+
),
|
|
294
|
+
],
|
|
295
|
+
};
|
|
296
|
+
export default meta;
|
|
297
|
+
|
|
298
|
+
const baseArgs = {
|
|
299
|
+
label: "Amount",
|
|
300
|
+
id: "number-field-test",
|
|
301
|
+
onChange: fn(),
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
export const ShouldTypeAValue: StoryObj<typeof NumberField> = {
|
|
305
|
+
args: {
|
|
306
|
+
...baseArgs,
|
|
307
|
+
},
|
|
308
|
+
play: async ({ canvasElement, args }) => {
|
|
309
|
+
const canvas = within(canvasElement);
|
|
310
|
+
const user = userEvent.setup({ delay: 50 });
|
|
311
|
+
const input = canvas.getByLabelText("Amount");
|
|
312
|
+
await user.type(input, "42");
|
|
313
|
+
await expect(input).toHaveValue("42");
|
|
314
|
+
// React Aria NumberField commits the value on blur.
|
|
315
|
+
await user.tab();
|
|
316
|
+
await expect(args.onChange).toHaveBeenLastCalledWith(42);
|
|
317
|
+
},
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
export const ShouldIncrementViaButton: StoryObj<typeof NumberField> = {
|
|
321
|
+
args: {
|
|
322
|
+
...baseArgs,
|
|
323
|
+
defaultValue: 10,
|
|
324
|
+
step: 1,
|
|
325
|
+
},
|
|
326
|
+
play: async ({ canvasElement, args }) => {
|
|
327
|
+
const canvas = within(canvasElement);
|
|
328
|
+
const user = userEvent.setup({ delay: 50 });
|
|
329
|
+
const increment = canvas.getByRole("button", {
|
|
330
|
+
name: "Increment : Amount",
|
|
331
|
+
});
|
|
332
|
+
await user.click(increment);
|
|
333
|
+
await expect(args.onChange).toHaveBeenLastCalledWith(11);
|
|
334
|
+
},
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
export const ShouldDecrementViaButton: StoryObj<typeof NumberField> = {
|
|
338
|
+
args: {
|
|
339
|
+
...baseArgs,
|
|
340
|
+
defaultValue: 10,
|
|
341
|
+
step: 1,
|
|
342
|
+
},
|
|
343
|
+
play: async ({ canvasElement, args }) => {
|
|
344
|
+
const canvas = within(canvasElement);
|
|
345
|
+
const user = userEvent.setup({ delay: 50 });
|
|
346
|
+
const decrement = canvas.getByRole("button", {
|
|
347
|
+
name: "Decrement : Amount",
|
|
348
|
+
});
|
|
349
|
+
await user.click(decrement);
|
|
350
|
+
await expect(args.onChange).toHaveBeenLastCalledWith(9);
|
|
351
|
+
},
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
export const ShouldIncrementViaKeyboard: StoryObj<typeof NumberField> = {
|
|
355
|
+
args: {
|
|
356
|
+
...baseArgs,
|
|
357
|
+
defaultValue: 5,
|
|
358
|
+
step: 1,
|
|
359
|
+
},
|
|
360
|
+
play: async ({ canvasElement, args }) => {
|
|
361
|
+
const canvas = within(canvasElement);
|
|
362
|
+
const user = userEvent.setup({ delay: 50 });
|
|
363
|
+
const input = canvas.getByLabelText("Amount");
|
|
364
|
+
await user.click(input);
|
|
365
|
+
await user.keyboard("{ArrowUp}");
|
|
366
|
+
await expect(args.onChange).toHaveBeenLastCalledWith(6);
|
|
367
|
+
await user.keyboard("{ArrowDown}");
|
|
368
|
+
await expect(args.onChange).toHaveBeenLastCalledWith(5);
|
|
369
|
+
},
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
export const ShouldRespectMinValue: StoryObj<typeof NumberField> = {
|
|
373
|
+
args: {
|
|
374
|
+
...baseArgs,
|
|
375
|
+
defaultValue: 0,
|
|
376
|
+
minValue: 0,
|
|
377
|
+
maxValue: 10,
|
|
378
|
+
step: 1,
|
|
379
|
+
},
|
|
380
|
+
play: async ({ canvasElement }) => {
|
|
381
|
+
const canvas = within(canvasElement);
|
|
382
|
+
const decrement = canvas.getByRole("button", {
|
|
383
|
+
name: "Decrement : Amount",
|
|
384
|
+
});
|
|
385
|
+
await expect(decrement).toBeDisabled();
|
|
386
|
+
},
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
export const ShouldRespectMaxValue: StoryObj<typeof NumberField> = {
|
|
390
|
+
args: {
|
|
391
|
+
...baseArgs,
|
|
392
|
+
defaultValue: 10,
|
|
393
|
+
minValue: 0,
|
|
394
|
+
maxValue: 10,
|
|
395
|
+
step: 1,
|
|
396
|
+
},
|
|
397
|
+
play: async ({ canvasElement }) => {
|
|
398
|
+
const canvas = within(canvasElement);
|
|
399
|
+
const increment = canvas.getByRole("button", {
|
|
400
|
+
name: "Increment : Amount",
|
|
401
|
+
});
|
|
402
|
+
await expect(increment).toBeDisabled();
|
|
403
|
+
},
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
export const ShouldNotChangeWhenDisabled: StoryObj<typeof NumberField> = {
|
|
407
|
+
args: {
|
|
408
|
+
...baseArgs,
|
|
409
|
+
defaultValue: 5,
|
|
410
|
+
isDisabled: true,
|
|
411
|
+
},
|
|
412
|
+
play: async ({ canvasElement, args }) => {
|
|
413
|
+
const canvas = within(canvasElement);
|
|
414
|
+
const user = userEvent.setup({ delay: 50 });
|
|
415
|
+
const input = canvas.getByLabelText("Amount");
|
|
416
|
+
await user.type(input, "9");
|
|
417
|
+
await expect(args.onChange).not.toHaveBeenCalled();
|
|
418
|
+
},
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
export const ShouldFormatCurrency: StoryObj<typeof NumberField> = {
|
|
422
|
+
args: {
|
|
423
|
+
...baseArgs,
|
|
424
|
+
defaultValue: 1234.5,
|
|
425
|
+
formatOptions: { style: "currency", currency: "EUR" },
|
|
426
|
+
},
|
|
427
|
+
play: async ({ canvasElement }) => {
|
|
428
|
+
const canvas = within(canvasElement);
|
|
429
|
+
const input = canvas.getByLabelText("Amount") as HTMLInputElement;
|
|
430
|
+
await expect(input.value).toMatch(/€1,234\.50/);
|
|
431
|
+
},
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
export const ShouldExposeNegativeNatureOnError: StoryObj<typeof NumberField> = {
|
|
435
|
+
args: {
|
|
436
|
+
...baseArgs,
|
|
437
|
+
errorHelperText: "Invalid value",
|
|
438
|
+
},
|
|
439
|
+
play: async ({ canvasElement }) => {
|
|
440
|
+
const canvas = within(canvasElement);
|
|
441
|
+
const root = canvasElement.querySelector(
|
|
442
|
+
'[data-design-system-component="NumberField"]',
|
|
443
|
+
);
|
|
444
|
+
await expect(root).toHaveAttribute("data-nature", "negative");
|
|
445
|
+
await expect(canvas.getByText("Invalid value")).toBeInTheDocument();
|
|
446
|
+
},
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
export const ExampleControlled: StoryObj<typeof NumberField> = {
|
|
450
|
+
render: () => {
|
|
451
|
+
const Wrapper = () => {
|
|
452
|
+
const [value, setValue] = useState<number>(0);
|
|
453
|
+
return (
|
|
454
|
+
<>
|
|
455
|
+
<div>Value is: {value}</div>
|
|
456
|
+
<NumberField label="Controlled" value={value} onChange={setValue} />
|
|
457
|
+
</>
|
|
458
|
+
);
|
|
459
|
+
};
|
|
460
|
+
return <Wrapper />;
|
|
461
|
+
},
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
export const ControlledShouldReflectValue: StoryObj<typeof NumberField> = {
|
|
465
|
+
render: ExampleControlled.render,
|
|
466
|
+
play: async ({ canvasElement }) => {
|
|
467
|
+
const canvas = within(canvasElement);
|
|
468
|
+
const user = userEvent.setup({ delay: 50 });
|
|
469
|
+
await canvas.findByText("Value is: 0");
|
|
470
|
+
const increment = canvas.getByRole("button", {
|
|
471
|
+
name: "Increment : Controlled",
|
|
472
|
+
});
|
|
473
|
+
await user.click(increment);
|
|
474
|
+
await canvas.findByText("Value is: 1");
|
|
475
|
+
},
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
export const UncontrolledShouldUpdateInternalValue: StoryObj<
|
|
479
|
+
typeof NumberField
|
|
480
|
+
> = {
|
|
481
|
+
args: {
|
|
482
|
+
...baseArgs,
|
|
483
|
+
defaultValue: 10,
|
|
484
|
+
step: 1,
|
|
485
|
+
},
|
|
486
|
+
play: async ({ canvasElement }) => {
|
|
487
|
+
const canvas = within(canvasElement);
|
|
488
|
+
const user = userEvent.setup({ delay: 50 });
|
|
489
|
+
const input = canvas.getByLabelText("Amount") as HTMLInputElement;
|
|
490
|
+
await expect(input).toHaveValue("10");
|
|
491
|
+
const increment = canvas.getByRole("button", {
|
|
492
|
+
name: "Increment : Amount",
|
|
493
|
+
});
|
|
494
|
+
await user.click(increment);
|
|
495
|
+
await expect(input).toHaveValue("11");
|
|
496
|
+
},
|
|
497
|
+
};
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
## Developer notes
|
|
501
|
+
|
|
502
|
+
Here are the notes available for the developer on the built Storybook, you can read them to understand the component and how to use it.
|
|
503
|
+
|
|
504
|
+
```mdx
|
|
505
|
+
import {
|
|
506
|
+
Canvas,
|
|
507
|
+
Meta,
|
|
508
|
+
Stories,
|
|
509
|
+
Controls,
|
|
510
|
+
Source,
|
|
511
|
+
} from "@storybook/addon-docs/blocks";
|
|
512
|
+
|
|
513
|
+
import * as NumberField from "./NumberField.stories";
|
|
514
|
+
import * as NumberFieldTests from "./tests/NumberField.stories";
|
|
515
|
+
|
|
516
|
+
<Meta of={NumberField} />
|
|
517
|
+
|
|
518
|
+
# NumberField
|
|
519
|
+
|
|
520
|
+
A NumberField allows users to enter a number, and increment or decrement the value using stepper buttons or the keyboard.
|
|
521
|
+
|
|
522
|
+
<Canvas of={NumberField.Playground} />
|
|
523
|
+
<Controls of={NumberField.Playground} />
|
|
524
|
+
|
|
525
|
+
A number field can be built using `<input type="number">`, but this is very limited in functionality, lacking in internationalization capabilities, inconsistent between browsers, and difficult to style.
|
|
526
|
+
NumberField helps achieve accessible and international number fields that can be styled as needed.
|
|
527
|
+
|
|
528
|
+
- **International** – Supports locale-aware formatting, number systems, currencies, units, and right-to-left layouts via `formatOptions`.
|
|
529
|
+
- **Accessible** – Exposed as a spinbutton to assistive technologies, with labelled increment/decrement buttons and full keyboard support (Arrow keys, Page Up/Down, Home/End).
|
|
530
|
+
- **Touch friendly** – Dedicated mobile stepper buttons are rendered on small viewports for easier one-hand usage.
|
|
531
|
+
- **Validated** – Values can be clamped to a `minValue`/`maxValue` range and snapped to a `step`.
|
|
532
|
+
|
|
533
|
+
Read [the React Aria NumberField docs](https://react-aria.adobe.com/NumberField.html) for more details about the internationalization, accessibility, and user experience features implemented under the hood.
|
|
534
|
+
|
|
535
|
+
## Value
|
|
536
|
+
|
|
537
|
+
A `NumberField` accepts a plain JavaScript `number`. An initial, uncontrolled value can be provided via `defaultValue`, or a controlled value via `value` together with `onChange`.
|
|
538
|
+
|
|
539
|
+
<Canvas of={NumberFieldTests.ExampleControlled} sourceState="shown" />
|
|
540
|
+
|
|
541
|
+
## Minimum and maximum
|
|
542
|
+
|
|
543
|
+
The `minValue` and `maxValue` props clamp the allowed range. When the current value reaches a bound, the corresponding stepper button is disabled.
|
|
544
|
+
|
|
545
|
+
<Canvas of={NumberField.WithMinMax} sourceState="shown" />
|
|
546
|
+
|
|
547
|
+
## Step
|
|
548
|
+
|
|
549
|
+
The `step` prop controls the amount added or subtracted on each increment/decrement tick (buttons or keyboard). It also defines the snapping grid when `commitBehavior="snap"`.
|
|
550
|
+
|
|
551
|
+
## Formatting
|
|
552
|
+
|
|
553
|
+
`formatOptions` follows the standard [`Intl.NumberFormat` options](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#options). It controls both the displayed value and which characters are allowed during typing.
|
|
554
|
+
|
|
555
|
+
A currency field:
|
|
556
|
+
|
|
557
|
+
<Canvas of={NumberField.FormattedCurrency} sourceState="shown" />
|
|
558
|
+
|
|
559
|
+
A percentage field (values are expressed as decimals, e.g. `0.25` renders as `25 %`):
|
|
560
|
+
|
|
561
|
+
<Canvas of={NumberField.FormattedPercent} sourceState="shown" />
|
|
562
|
+
|
|
563
|
+
Locale is resolved via the surrounding `I18nProvider`. Wrap your app (or your story) with the locale you want (`fr-FR`, `en-US`, ...) to switch decimal separators, grouping, and currency symbols.
|
|
564
|
+
|
|
565
|
+
## Advanced React Aria options
|
|
566
|
+
|
|
567
|
+
Most common configuration is exposed as top-level props. For less common React Aria options, pass them through `advancedReactAriaOptions`. See the [React Aria NumberField API](https://react-aria.adobe.com/NumberField.html#numberfield) for the full list.
|
|
568
|
+
|
|
569
|
+
## How to test this component
|
|
570
|
+
|
|
571
|
+
The input rendered by NumberField is a real `<input>` (with `role="spinbutton"`) and can be targeted by its label:
|
|
572
|
+
|
|
573
|
+
<Source of={NumberFieldTests.ShouldTypeAValue} type="code" dark />
|
|
574
|
+
|
|
575
|
+
The stepper buttons are labelled using the field's label for screen readers. Target them by their accessible name (the label for increment/decrement depends on the locale — `Increment` / `Decrement` in English, `Augmenter` / `Diminuer` in French):
|
|
576
|
+
|
|
577
|
+
<Source of={NumberFieldTests.ShouldIncrementViaButton} type="code" dark />
|
|
578
|
+
|
|
579
|
+
Note that React Aria commits the typed value on blur, so remember to `tab()` out of the field before asserting on `onChange` calls.
|
|
580
|
+
|
|
581
|
+
<Stories includePrimary={false} />
|
|
582
|
+
```
|