@agregio-solutions/design-system 1.92.1 → 1.93.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 +400 -361
- package/dist/design-system.js +5862 -5685
- package/dist/packages/components/TimePicker/TimePicker.d.ts +101 -0
- package/dist/packages/components/TimePicker/doc.md +628 -0
- package/dist/packages/components/TimePicker/utils/buildOptions/buildOptions.d.ts +8 -0
- package/dist/packages/components/TimePicker/utils/buildOptions/buildOptions.test.d.ts +1 -0
- package/dist/packages/components/TimePicker/utils/getSegment/getSegment.d.ts +9 -0
- package/dist/packages/components/TimePicker/utils/getSegment/getSegment.test.d.ts +1 -0
- package/dist/packages/index.d.ts +2 -1
- package/dist/public_docs/components.md +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
# TimePicker
|
|
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/TimePicker/TimePicker.d.ts
|
|
8
|
+
|
|
9
|
+
## Example usage
|
|
10
|
+
|
|
11
|
+
Here are the Storybook Stories.
|
|
12
|
+
|
|
13
|
+
Base stories:
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
import { useState } from "react";
|
|
17
|
+
import { Meta, StoryObj } from "@storybook/react-vite";
|
|
18
|
+
import TimePicker, { TimePickerValue } from "./TimePicker";
|
|
19
|
+
import DatePicker from "@components/DatePicker/DatePicker";
|
|
20
|
+
import { I18nProvider } from "react-aria-components";
|
|
21
|
+
import { fn, within, expect } from "storybook/test";
|
|
22
|
+
|
|
23
|
+
const meta: Meta<typeof TimePicker> = {
|
|
24
|
+
component: TimePicker,
|
|
25
|
+
argTypes: {
|
|
26
|
+
label: { control: "text" },
|
|
27
|
+
value: { control: false },
|
|
28
|
+
units: { control: false },
|
|
29
|
+
helperText: { control: "text" },
|
|
30
|
+
errorHelperText: { control: "text" },
|
|
31
|
+
successHelperText: { control: "text" },
|
|
32
|
+
warningHelperText: { control: "text" },
|
|
33
|
+
},
|
|
34
|
+
parameters: {
|
|
35
|
+
layout: "centered",
|
|
36
|
+
},
|
|
37
|
+
decorators: [
|
|
38
|
+
(Story, context) => {
|
|
39
|
+
const [value, setValue] = useState<TimePickerValue | null>(
|
|
40
|
+
context.args.value as TimePickerValue | null,
|
|
41
|
+
);
|
|
42
|
+
return (
|
|
43
|
+
<I18nProvider locale={context.parameters.locale || "en-EN"}>
|
|
44
|
+
<Story
|
|
45
|
+
args={{
|
|
46
|
+
...context.args,
|
|
47
|
+
value,
|
|
48
|
+
onChange: (next: TimePickerValue) => {
|
|
49
|
+
context.args.onChange?.(next);
|
|
50
|
+
setValue(next);
|
|
51
|
+
},
|
|
52
|
+
}}
|
|
53
|
+
/>
|
|
54
|
+
</I18nProvider>
|
|
55
|
+
);
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
};
|
|
59
|
+
export default meta;
|
|
60
|
+
|
|
61
|
+
export const Playground: StoryObj<typeof TimePicker> = {
|
|
62
|
+
args: {
|
|
63
|
+
label: "Time Picker",
|
|
64
|
+
value: { hours: "12", minutes: "34", seconds: "56" },
|
|
65
|
+
onChange: fn(),
|
|
66
|
+
labelIconRight: "help_outline",
|
|
67
|
+
labelIconRightTooltip: "Additional information",
|
|
68
|
+
},
|
|
69
|
+
play: async ({ canvasElement }) => {
|
|
70
|
+
const canvas = within(canvasElement);
|
|
71
|
+
expect(canvas.getByLabelText("Time Picker : Hours")).toHaveTextContent(
|
|
72
|
+
"12h",
|
|
73
|
+
);
|
|
74
|
+
expect(canvas.getByLabelText("Time Picker : Minutes")).toHaveTextContent(
|
|
75
|
+
"34m",
|
|
76
|
+
);
|
|
77
|
+
expect(canvas.getByLabelText("Time Picker : Seconds")).toHaveTextContent(
|
|
78
|
+
"56s",
|
|
79
|
+
);
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export const NoValue: StoryObj<typeof TimePicker> = {
|
|
84
|
+
args: {
|
|
85
|
+
...Playground.args,
|
|
86
|
+
value: null,
|
|
87
|
+
},
|
|
88
|
+
play: async ({ canvasElement }) => {
|
|
89
|
+
const canvas = within(canvasElement);
|
|
90
|
+
expect(canvas.getByLabelText("Time Picker : Hours")).toHaveTextContent(
|
|
91
|
+
"HH",
|
|
92
|
+
);
|
|
93
|
+
expect(canvas.getByLabelText("Time Picker : Minutes")).toHaveTextContent(
|
|
94
|
+
"MM",
|
|
95
|
+
);
|
|
96
|
+
expect(canvas.getByLabelText("Time Picker : Seconds")).toHaveTextContent(
|
|
97
|
+
"SS",
|
|
98
|
+
);
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export const WithoutSeconds: StoryObj<typeof TimePicker> = {
|
|
103
|
+
args: {
|
|
104
|
+
...Playground.args,
|
|
105
|
+
value: { hours: "12", minutes: "34" },
|
|
106
|
+
units: ["hours", "minutes"],
|
|
107
|
+
},
|
|
108
|
+
play: async ({ canvasElement }) => {
|
|
109
|
+
const canvas = within(canvasElement);
|
|
110
|
+
expect(canvas.getByLabelText("Time Picker : Hours")).toHaveTextContent(
|
|
111
|
+
"12h",
|
|
112
|
+
);
|
|
113
|
+
expect(canvas.getByLabelText("Time Picker : Minutes")).toHaveTextContent(
|
|
114
|
+
"34m",
|
|
115
|
+
);
|
|
116
|
+
expect(
|
|
117
|
+
canvas.queryByLabelText("Time Picker : Seconds"),
|
|
118
|
+
).not.toBeInTheDocument();
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export const OnlyHours: StoryObj<typeof TimePicker> = {
|
|
123
|
+
args: {
|
|
124
|
+
...Playground.args,
|
|
125
|
+
value: { hours: "12" },
|
|
126
|
+
units: ["hours"],
|
|
127
|
+
},
|
|
128
|
+
play: async ({ canvasElement }) => {
|
|
129
|
+
const canvas = within(canvasElement);
|
|
130
|
+
expect(canvas.getByLabelText("Time Picker : Hours")).toHaveTextContent(
|
|
131
|
+
"12h",
|
|
132
|
+
);
|
|
133
|
+
expect(
|
|
134
|
+
canvas.queryByLabelText("Time Picker : Minutes"),
|
|
135
|
+
).not.toBeInTheDocument();
|
|
136
|
+
expect(
|
|
137
|
+
canvas.queryByLabelText("Time Picker : Seconds"),
|
|
138
|
+
).not.toBeInTheDocument();
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
export const OnlyMinutesSeconds: StoryObj<typeof TimePicker> = {
|
|
143
|
+
args: {
|
|
144
|
+
...Playground.args,
|
|
145
|
+
value: { minutes: "34", seconds: "56" },
|
|
146
|
+
units: ["minutes", "seconds"],
|
|
147
|
+
},
|
|
148
|
+
play: async ({ canvasElement }) => {
|
|
149
|
+
const canvas = within(canvasElement);
|
|
150
|
+
expect(
|
|
151
|
+
canvas.queryByLabelText("Time Picker : Hours"),
|
|
152
|
+
).not.toBeInTheDocument();
|
|
153
|
+
expect(canvas.getByLabelText("Time Picker : Minutes")).toHaveTextContent(
|
|
154
|
+
"34m",
|
|
155
|
+
);
|
|
156
|
+
expect(canvas.getByLabelText("Time Picker : Seconds")).toHaveTextContent(
|
|
157
|
+
"56s",
|
|
158
|
+
);
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
export const Horizontal: StoryObj<typeof TimePicker> = {
|
|
163
|
+
args: {
|
|
164
|
+
...Playground.args,
|
|
165
|
+
orientation: "horizontal",
|
|
166
|
+
required: true,
|
|
167
|
+
helperText: "Pick a time during business hours",
|
|
168
|
+
helperTextIcon: "help_outline",
|
|
169
|
+
},
|
|
170
|
+
play: async ({ canvasElement }) => {
|
|
171
|
+
const canvas = within(canvasElement);
|
|
172
|
+
const root = canvasElement.querySelector(
|
|
173
|
+
'[data-design-system-component="TimePicker"]',
|
|
174
|
+
);
|
|
175
|
+
expect(root).toHaveAttribute("data-orientation", "horizontal");
|
|
176
|
+
expect(
|
|
177
|
+
canvas.getByText("Pick a time during business hours"),
|
|
178
|
+
).toBeInTheDocument();
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
export const HorizontalWithoutSeconds: StoryObj<typeof TimePicker> = {
|
|
183
|
+
args: {
|
|
184
|
+
...Horizontal.args,
|
|
185
|
+
value: { hours: "12", minutes: "34" },
|
|
186
|
+
units: ["hours", "minutes"],
|
|
187
|
+
},
|
|
188
|
+
play: async ({ canvasElement }) => {
|
|
189
|
+
const canvas = within(canvasElement);
|
|
190
|
+
const root = canvasElement.querySelector(
|
|
191
|
+
'[data-design-system-component="TimePicker"]',
|
|
192
|
+
);
|
|
193
|
+
expect(root).toHaveAttribute("data-orientation", "horizontal");
|
|
194
|
+
expect(
|
|
195
|
+
canvas.queryByLabelText("Time Picker : Seconds"),
|
|
196
|
+
).not.toBeInTheDocument();
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
export const Disabled: StoryObj<typeof TimePicker> = {
|
|
201
|
+
args: {
|
|
202
|
+
...Playground.args,
|
|
203
|
+
isDisabled: true,
|
|
204
|
+
},
|
|
205
|
+
play: async ({ canvasElement }) => {
|
|
206
|
+
const canvas = within(canvasElement);
|
|
207
|
+
const triggers = canvas.getAllByLabelText(/^Time Picker : /);
|
|
208
|
+
expect(triggers).toHaveLength(3);
|
|
209
|
+
triggers.forEach((trigger) => {
|
|
210
|
+
expect(trigger).toBeDisabled();
|
|
211
|
+
});
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
export const ReadOnly: StoryObj<typeof TimePicker> = {
|
|
216
|
+
args: {
|
|
217
|
+
...Playground.args,
|
|
218
|
+
isReadOnly: true,
|
|
219
|
+
},
|
|
220
|
+
play: async ({ canvasElement }) => {
|
|
221
|
+
const canvas = within(canvasElement);
|
|
222
|
+
await expect(
|
|
223
|
+
canvas.queryByRole("button", { name: /Hours/ }),
|
|
224
|
+
).not.toBeInTheDocument();
|
|
225
|
+
const readOnly = canvasElement.querySelector(
|
|
226
|
+
'[data-design-system-component="TimePicker"] [data-readonly="true"]',
|
|
227
|
+
);
|
|
228
|
+
await expect(readOnly).toHaveTextContent("12:34:56");
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
export const WithError: StoryObj<typeof TimePicker> = {
|
|
233
|
+
args: {
|
|
234
|
+
...Playground.args,
|
|
235
|
+
errorHelperText: "Error message",
|
|
236
|
+
errorHelperTextIcon: "error_outline",
|
|
237
|
+
},
|
|
238
|
+
play: async ({ canvasElement }) => {
|
|
239
|
+
const canvas = within(canvasElement);
|
|
240
|
+
const triggers = canvas.getAllByLabelText(/^Time Picker : /);
|
|
241
|
+
expect(triggers).toHaveLength(3);
|
|
242
|
+
triggers.forEach((trigger) => {
|
|
243
|
+
expect(trigger).toHaveAttribute("data-nature", "negative");
|
|
244
|
+
});
|
|
245
|
+
},
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
export const WithWarning: StoryObj<typeof TimePicker> = {
|
|
249
|
+
args: {
|
|
250
|
+
...Playground.args,
|
|
251
|
+
warningHelperText: "Warning message",
|
|
252
|
+
warningHelperTextIcon: "warning_amber",
|
|
253
|
+
},
|
|
254
|
+
play: async ({ canvasElement }) => {
|
|
255
|
+
const canvas = within(canvasElement);
|
|
256
|
+
expect(canvas.getByLabelText("Time Picker : Hours")).toHaveAttribute(
|
|
257
|
+
"data-nature",
|
|
258
|
+
"warning",
|
|
259
|
+
);
|
|
260
|
+
expect(canvas.getByText("Warning message")).toBeInTheDocument();
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
export const WithSuccess: StoryObj<typeof TimePicker> = {
|
|
265
|
+
args: {
|
|
266
|
+
...Playground.args,
|
|
267
|
+
successHelperText: "Success message",
|
|
268
|
+
successHelperTextIcon: "check",
|
|
269
|
+
},
|
|
270
|
+
play: async ({ canvasElement }) => {
|
|
271
|
+
const canvas = within(canvasElement);
|
|
272
|
+
const triggers = canvas.getAllByLabelText(/^Time Picker : /);
|
|
273
|
+
expect(triggers).toHaveLength(3);
|
|
274
|
+
triggers.forEach((trigger) => {
|
|
275
|
+
expect(trigger).toHaveAttribute("data-nature", "positive");
|
|
276
|
+
});
|
|
277
|
+
},
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
export const WithStep: StoryObj<typeof TimePicker> = {
|
|
281
|
+
args: {
|
|
282
|
+
...Playground.args,
|
|
283
|
+
value: { hours: "12", minutes: "30", seconds: "40" },
|
|
284
|
+
minutesStep: 5,
|
|
285
|
+
secondsStep: 10,
|
|
286
|
+
},
|
|
287
|
+
play: async ({ canvasElement }) => {
|
|
288
|
+
const canvas = within(canvasElement);
|
|
289
|
+
expect(canvas.getByLabelText("Time Picker : Hours")).toHaveTextContent(
|
|
290
|
+
"12h",
|
|
291
|
+
);
|
|
292
|
+
expect(canvas.getByLabelText("Time Picker : Minutes")).toHaveTextContent(
|
|
293
|
+
"30m",
|
|
294
|
+
);
|
|
295
|
+
expect(canvas.getByLabelText("Time Picker : Seconds")).toHaveTextContent(
|
|
296
|
+
"40s",
|
|
297
|
+
);
|
|
298
|
+
},
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Demonstrates assembling a full date-time without a dedicated component:
|
|
303
|
+
* a `DatePicker` (granularity "day") for the date and a `TimePicker` for the
|
|
304
|
+
* time. The two wall-clock values are combined into a native `Date` via
|
|
305
|
+
* `combineToDate`, which is DST-safe in the browser's local timezone.
|
|
306
|
+
*/
|
|
307
|
+
export const WithDatePicker: StoryObj<typeof TimePicker> = {
|
|
308
|
+
render: () => {
|
|
309
|
+
/**
|
|
310
|
+
* Combines a date (wall-clock "YYYY-MM-DD" from DatePicker) and a time
|
|
311
|
+
* (TimePickerValue) into a native Date in the browser's local timezone.
|
|
312
|
+
*
|
|
313
|
+
* The multi-argument Date constructor interprets its parts as local
|
|
314
|
+
* wall-clock time and applies the local timezone's DST rules automatically
|
|
315
|
+
* — no manual offset math, so no DST bugs. Note: never use
|
|
316
|
+
* `new Date("YYYY-MM-DD")` for the date part, as a date-only ISO string is
|
|
317
|
+
* parsed as UTC midnight, not local.
|
|
318
|
+
*/
|
|
319
|
+
const combineToDate = (
|
|
320
|
+
date: string | null,
|
|
321
|
+
time: TimePickerValue,
|
|
322
|
+
): Date | null => {
|
|
323
|
+
if (!date) return null;
|
|
324
|
+
const [year, month, day] = date.split("-").map(Number);
|
|
325
|
+
const hours = Number(time.hours ?? "0");
|
|
326
|
+
const minutes = Number(time.minutes ?? "0");
|
|
327
|
+
const seconds = Number(time.seconds ?? "0");
|
|
328
|
+
return new Date(year, month - 1, day, hours, minutes, seconds);
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
const DateTimeExample = () => {
|
|
332
|
+
const [date, setDate] = useState<string | null>("2020-01-15");
|
|
333
|
+
const [time, setTime] = useState<TimePickerValue>({
|
|
334
|
+
hours: "09",
|
|
335
|
+
minutes: "30",
|
|
336
|
+
seconds: "00",
|
|
337
|
+
});
|
|
338
|
+
const combined = combineToDate(date, time);
|
|
339
|
+
return (
|
|
340
|
+
<div style={{ display: "flex", flexDirection: "column", gap: "16px" }}>
|
|
341
|
+
<div style={{ display: "flex", gap: "16px", alignItems: "flex-end" }}>
|
|
342
|
+
<DatePicker
|
|
343
|
+
id="date-time-date"
|
|
344
|
+
label="Date"
|
|
345
|
+
granularity="day"
|
|
346
|
+
value={date}
|
|
347
|
+
onChange={setDate}
|
|
348
|
+
/>
|
|
349
|
+
<TimePicker label="Time" value={time} onChange={setTime} />
|
|
350
|
+
</div>
|
|
351
|
+
<div style={{ fontFamily: "monospace" }}>
|
|
352
|
+
<div data-testid="combined-local">
|
|
353
|
+
Date (local): {combined?.toString() ?? ""}
|
|
354
|
+
</div>
|
|
355
|
+
<div data-testid="combined-iso">
|
|
356
|
+
ISO (UTC): {combined?.toISOString() ?? ""}
|
|
357
|
+
</div>
|
|
358
|
+
</div>
|
|
359
|
+
</div>
|
|
360
|
+
);
|
|
361
|
+
};
|
|
362
|
+
return <DateTimeExample />;
|
|
363
|
+
},
|
|
364
|
+
};
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
## How to test this component
|
|
368
|
+
|
|
369
|
+
Here are some more advanced stories with more testing coverage and examples that you can read to understand how to test this component.
|
|
370
|
+
|
|
371
|
+
```tsx
|
|
372
|
+
import { Meta, StoryObj } from "@storybook/react-vite";
|
|
373
|
+
import TimePicker, { TimePickerValue, TimeUnit } from "../TimePicker";
|
|
374
|
+
import {
|
|
375
|
+
userEvent,
|
|
376
|
+
within,
|
|
377
|
+
waitFor,
|
|
378
|
+
screen,
|
|
379
|
+
fireEvent,
|
|
380
|
+
expect,
|
|
381
|
+
fn,
|
|
382
|
+
} from "storybook/test";
|
|
383
|
+
import { I18nProvider } from "react-aria-components";
|
|
384
|
+
import { useState } from "react";
|
|
385
|
+
import { Playground, WithDatePicker } from "../TimePicker.stories";
|
|
386
|
+
|
|
387
|
+
const meta: Meta<typeof TimePicker> = {
|
|
388
|
+
component: TimePicker,
|
|
389
|
+
parameters: {
|
|
390
|
+
layout: "centered",
|
|
391
|
+
chromatic: { disableSnapshot: true },
|
|
392
|
+
},
|
|
393
|
+
decorators: [
|
|
394
|
+
(Story) => (
|
|
395
|
+
<I18nProvider locale="en-EN">
|
|
396
|
+
<Story />
|
|
397
|
+
</I18nProvider>
|
|
398
|
+
),
|
|
399
|
+
],
|
|
400
|
+
};
|
|
401
|
+
export default meta;
|
|
402
|
+
|
|
403
|
+
const optionByText = async (text: string) => {
|
|
404
|
+
return screen.findByText(text, { selector: "span" });
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
const onChangeSpy = fn();
|
|
408
|
+
|
|
409
|
+
const ControlledHarness = ({
|
|
410
|
+
initialValue = {},
|
|
411
|
+
units,
|
|
412
|
+
minutesStep,
|
|
413
|
+
}: {
|
|
414
|
+
initialValue?: TimePickerValue;
|
|
415
|
+
units?: Array<TimeUnit>;
|
|
416
|
+
minutesStep?: number;
|
|
417
|
+
}) => {
|
|
418
|
+
const [value, setValue] = useState<TimePickerValue>(initialValue);
|
|
419
|
+
return (
|
|
420
|
+
<TimePicker
|
|
421
|
+
{...Playground.args}
|
|
422
|
+
label="Time Picker"
|
|
423
|
+
units={units}
|
|
424
|
+
minutesStep={minutesStep}
|
|
425
|
+
value={value}
|
|
426
|
+
onChange={(next) => {
|
|
427
|
+
onChangeSpy(next);
|
|
428
|
+
setValue(next);
|
|
429
|
+
}}
|
|
430
|
+
/>
|
|
431
|
+
);
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
export const ShouldSelectAllUnits: StoryObj<typeof TimePicker> = {
|
|
435
|
+
render: () => {
|
|
436
|
+
onChangeSpy.mockClear();
|
|
437
|
+
return <ControlledHarness />;
|
|
438
|
+
},
|
|
439
|
+
play: async ({ canvasElement }) => {
|
|
440
|
+
const canvas = within(canvasElement);
|
|
441
|
+
const user = userEvent.setup({ delay: 50 });
|
|
442
|
+
|
|
443
|
+
await user.click(canvas.getByRole("button", { name: /Hours/ }));
|
|
444
|
+
await user.click(await optionByText("11h"));
|
|
445
|
+
await waitFor(() =>
|
|
446
|
+
expect(onChangeSpy).toHaveBeenLastCalledWith({ hours: "11" }),
|
|
447
|
+
);
|
|
448
|
+
|
|
449
|
+
await user.click(canvas.getByRole("button", { name: /Minutes/ }));
|
|
450
|
+
await user.click(await optionByText("59m"));
|
|
451
|
+
await waitFor(() =>
|
|
452
|
+
expect(onChangeSpy).toHaveBeenLastCalledWith({
|
|
453
|
+
hours: "11",
|
|
454
|
+
minutes: "59",
|
|
455
|
+
}),
|
|
456
|
+
);
|
|
457
|
+
|
|
458
|
+
await user.click(canvas.getByRole("button", { name: /Seconds/ }));
|
|
459
|
+
await user.click(await optionByText("30s"));
|
|
460
|
+
await waitFor(() =>
|
|
461
|
+
expect(onChangeSpy).toHaveBeenLastCalledWith({
|
|
462
|
+
hours: "11",
|
|
463
|
+
minutes: "59",
|
|
464
|
+
seconds: "30",
|
|
465
|
+
}),
|
|
466
|
+
);
|
|
467
|
+
},
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
export const ShouldRespectMinutesStep: StoryObj<typeof TimePicker> = {
|
|
471
|
+
render: () => {
|
|
472
|
+
onChangeSpy.mockClear();
|
|
473
|
+
return <ControlledHarness minutesStep={15} />;
|
|
474
|
+
},
|
|
475
|
+
play: async ({ canvasElement }) => {
|
|
476
|
+
const canvas = within(canvasElement);
|
|
477
|
+
const user = userEvent.setup({ delay: 50 });
|
|
478
|
+
|
|
479
|
+
await user.click(canvas.getByRole("button", { name: /Minutes/ }));
|
|
480
|
+
await screen.findByText("00m", { selector: "span" });
|
|
481
|
+
const optionTexts = screen
|
|
482
|
+
.getAllByRole("option")
|
|
483
|
+
.map((option) => option.textContent?.trim());
|
|
484
|
+
expect(optionTexts).toEqual(["00m", "15m", "30m", "45m"]);
|
|
485
|
+
},
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
export const ShouldOmitSecondsWhenUnitsExcludeIt: StoryObj<typeof TimePicker> =
|
|
489
|
+
{
|
|
490
|
+
render: () => {
|
|
491
|
+
onChangeSpy.mockClear();
|
|
492
|
+
return <ControlledHarness units={["hours", "minutes"]} />;
|
|
493
|
+
},
|
|
494
|
+
play: async ({ canvasElement }) => {
|
|
495
|
+
const canvas = within(canvasElement);
|
|
496
|
+
const user = userEvent.setup({ delay: 50 });
|
|
497
|
+
|
|
498
|
+
await user.click(canvas.getByRole("button", { name: /Hours/ }));
|
|
499
|
+
await user.click(await optionByText("08h"));
|
|
500
|
+
await user.click(canvas.getByRole("button", { name: /Minutes/ }));
|
|
501
|
+
await user.click(await optionByText("45m"));
|
|
502
|
+
|
|
503
|
+
await waitFor(() =>
|
|
504
|
+
expect(onChangeSpy).toHaveBeenLastCalledWith({
|
|
505
|
+
hours: "08",
|
|
506
|
+
minutes: "45",
|
|
507
|
+
}),
|
|
508
|
+
);
|
|
509
|
+
},
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
export const ReadOnlyShouldRenderText: StoryObj<typeof TimePicker> = {
|
|
513
|
+
args: {
|
|
514
|
+
...Playground.args,
|
|
515
|
+
isReadOnly: true,
|
|
516
|
+
value: { hours: "01", minutes: "02", seconds: "03" },
|
|
517
|
+
},
|
|
518
|
+
play: async ({ canvasElement }) => {
|
|
519
|
+
const canvas = within(canvasElement);
|
|
520
|
+
await expect(
|
|
521
|
+
canvas.queryByRole("button", { name: /Hours/ }),
|
|
522
|
+
).not.toBeInTheDocument();
|
|
523
|
+
const readOnly = canvasElement.querySelector(
|
|
524
|
+
'[data-design-system-component="TimePicker"] [data-readonly="true"]',
|
|
525
|
+
);
|
|
526
|
+
await expect(readOnly).toHaveTextContent("01:02:03");
|
|
527
|
+
},
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
export const ShouldCombineDateAndTime: StoryObj<typeof TimePicker> = {
|
|
531
|
+
...WithDatePicker,
|
|
532
|
+
play: async ({ canvasElement }) => {
|
|
533
|
+
const canvas = within(canvasElement);
|
|
534
|
+
const user = userEvent.setup({ delay: 50 });
|
|
535
|
+
|
|
536
|
+
await user.click(canvas.getByLabelText("Open calendar Date"));
|
|
537
|
+
await screen.findByLabelText("Monday, January 20, 2020");
|
|
538
|
+
await fireEvent.click(screen.getByLabelText("Monday, January 20, 2020"));
|
|
539
|
+
|
|
540
|
+
await user.click(canvas.getByLabelText("Time : Hours"));
|
|
541
|
+
await user.click(await optionByText("11h"));
|
|
542
|
+
await user.click(canvas.getByLabelText("Time : Minutes"));
|
|
543
|
+
await user.click(await optionByText("45m"));
|
|
544
|
+
await user.click(canvas.getByLabelText("Time : Seconds"));
|
|
545
|
+
await user.click(await optionByText("30s"));
|
|
546
|
+
|
|
547
|
+
await waitFor(() =>
|
|
548
|
+
expect(canvas.getByTestId("combined-local")).toHaveTextContent(
|
|
549
|
+
"Jan 20 2020 11:45:30",
|
|
550
|
+
),
|
|
551
|
+
);
|
|
552
|
+
},
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
export const ExampleControlled: StoryObj<typeof TimePicker> = {
|
|
556
|
+
render: () => {
|
|
557
|
+
const ParentComponent = () => {
|
|
558
|
+
const [value, setValue] = useState<TimePickerValue>({
|
|
559
|
+
hours: "12",
|
|
560
|
+
minutes: "34",
|
|
561
|
+
seconds: "56",
|
|
562
|
+
});
|
|
563
|
+
return (
|
|
564
|
+
<>
|
|
565
|
+
<div>Selected time : {JSON.stringify(value)}</div>
|
|
566
|
+
<TimePicker label="Time Picker" value={value} onChange={setValue} />
|
|
567
|
+
</>
|
|
568
|
+
);
|
|
569
|
+
};
|
|
570
|
+
return <ParentComponent />;
|
|
571
|
+
},
|
|
572
|
+
};
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
## Developer notes
|
|
576
|
+
|
|
577
|
+
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.
|
|
578
|
+
|
|
579
|
+
```mdx
|
|
580
|
+
import {
|
|
581
|
+
Canvas,
|
|
582
|
+
Meta,
|
|
583
|
+
Stories,
|
|
584
|
+
Controls,
|
|
585
|
+
Source,
|
|
586
|
+
} from "@storybook/addon-docs/blocks";
|
|
587
|
+
|
|
588
|
+
import * as TimePicker from "./TimePicker.stories";
|
|
589
|
+
import * as TimePickerTests from "./tests/TimePicker.stories";
|
|
590
|
+
|
|
591
|
+
<Meta of={TimePicker} />
|
|
592
|
+
|
|
593
|
+
# TimePicker
|
|
594
|
+
|
|
595
|
+
A time picker allows users to pick a time using one Select per unit (hours, minutes, seconds). Unlike `TimeField` which is keyboard-segmented input, `TimePicker` is more tabular and exploratory.
|
|
596
|
+
|
|
597
|
+
⚠️ Warning: This component **does not handle timezones natively**. It exposes values as an object so you can rebuild a correct date depending on your specific needs. See below for more details.
|
|
598
|
+
|
|
599
|
+
<Canvas of={TimePicker.Playground} />
|
|
600
|
+
<Controls of={TimePicker.Playground} />
|
|
601
|
+
|
|
602
|
+
## Example
|
|
603
|
+
|
|
604
|
+
<Source of={TimePickerTests.ExampleControlled} type="code" dark />
|
|
605
|
+
|
|
606
|
+
## Value
|
|
607
|
+
|
|
608
|
+
The value is an object of type `TimePickerValue = { hours?: string; minutes?: string; seconds?: string }`, where each segment is a 2-digit padded string in 24h format. Use `null` for the initial empty state.
|
|
609
|
+
|
|
610
|
+
- All units set: `{ hours: "12", minutes: "34", seconds: "56" }`
|
|
611
|
+
- Without seconds: `{ hours: "12", minutes: "34" }`
|
|
612
|
+
- Without hours: `{ minutes: "34", seconds: "56" }`
|
|
613
|
+
- Empty initial state: `null`
|
|
614
|
+
|
|
615
|
+
`onChange` is called every time a unit changes and emits the full object with only the units that have a value (units the user hasn't selected yet are simply absent from the object).
|
|
616
|
+
|
|
617
|
+
## Combining with DatePicker (date + time)
|
|
618
|
+
|
|
619
|
+
`TimePicker` only holds wall-clock time, so to obtain a full date-time you assemble it with a `DatePicker` (granularity `"day"`). This avoids a dedicated component and keeps each picker focused on one concern.
|
|
620
|
+
|
|
621
|
+
The assembly is done with **native JS only** and stays correct across daylight saving time (DST) changes in the browser's local timezone. See the `combineToDate` helper in the story source below.
|
|
622
|
+
|
|
623
|
+
Why this is DST-safe: the multi-argument `Date` constructor interprets its parts as **local wall-clock time** and applies the local timezone's DST rules automatically — you never add an offset by hand, so no DST edge case can break it. This yields the same result as a `DatePicker` used at `granularity="second"` in the local timezone.
|
|
624
|
+
|
|
625
|
+
⚠️ Do **not** build the date part with `new Date("YYYY-MM-DD")`: a date-only ISO string is parsed as **UTC midnight**, not local time. Always split the string and pass numbers to the constructor.
|
|
626
|
+
|
|
627
|
+
<Canvas of={TimePicker.WithDatePicker} sourceState="shown" />
|
|
628
|
+
```
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds the list of 2-digit padded string options for a time unit.
|
|
3
|
+
*
|
|
4
|
+
* @param max - Maximum inclusive value (e.g. 23 for hours, 59 for minutes/seconds).
|
|
5
|
+
* @param step - Increment between consecutive options. Values < 1 or non-integers are clamped to 1.
|
|
6
|
+
* @returns Ascending list of padded strings from "00" up to the last value ≤ max.
|
|
7
|
+
*/
|
|
8
|
+
export declare function buildOptions(max: number, step: number): Array<string>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { TimePickerValue, TimeUnit } from '../../TimePicker';
|
|
2
|
+
/**
|
|
3
|
+
* Extracts a unit segment from a TimePickerValue, normalized as a 2-digit padded string.
|
|
4
|
+
*
|
|
5
|
+
* @param value - The current TimePicker value, or `null` for the empty state.
|
|
6
|
+
* @param unit - The unit to extract (`"hours"`, `"minutes"`, or `"seconds"`).
|
|
7
|
+
* @returns The padded 2-digit string for the unit, or `null` when absent or empty.
|
|
8
|
+
*/
|
|
9
|
+
export declare function getSegment(value: TimePickerValue | null, unit: TimeUnit): string | null;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/packages/index.d.ts
CHANGED
|
@@ -61,6 +61,7 @@ import { default as Tabs } from './components/Tabs/Tabs/Tabs';
|
|
|
61
61
|
import { default as Tag } from './components/Tag/Tag';
|
|
62
62
|
import { default as TextInput } from './components/TextInput/TextInput';
|
|
63
63
|
import { default as TimeField } from './components/TimeField/TimeField';
|
|
64
|
+
import { default as TimePicker } from './components/TimePicker/TimePicker';
|
|
64
65
|
import { default as Timeline } from './components/Timeline/Timeline';
|
|
65
66
|
import { default as Toaster } from './components/Toaster/Toaster';
|
|
66
67
|
import { default as ToggleButton } from './components/ToggleButton/ToggleButton';
|
|
@@ -72,5 +73,5 @@ import { default as createMuiTheme } from './mui/mui-theme.ts';
|
|
|
72
73
|
import { tokens, fontSize, lineHeight } from './ds-design-tokens/index';
|
|
73
74
|
import { I18nProvider } from 'react-aria';
|
|
74
75
|
import { toast } from 'sonner';
|
|
75
|
-
export { Accordion, Badge, Breadcrumbs, Button, Calendar, CalendarCell, ChartLegend, ChartTooltip, Checkbox, CheckboxGroup, Chip, Combobox, createMuiTheme, DataTable, DataTableCell, DataTableHeader, DataTableRoot, DataTableRow, DatePicker, DateRangePicker, Drawer, Dropdown, DropdownListItem, EmptyState, FileUpload, Filter, fontSize, Header, I18nProvider, Icon, Label, LinearProgressBar, lineHeight, Link, List, Loader, Menu, MenuItem, Message, Modal, ModalContent, ModalActions, ModalForm, Navigation, NavigationItem, Notifications, NotificationCard, NumberField, PageLayout, Pagination, Popover, PopoverTrigger, Radio, RouterProvider, SearchBar, SegmentedControl, SegmentedControlButton, SegmentedControlList, SegmentedControlPanel, Select, SelectItem, Skeleton, Slider, Stepper, Step, Switch, Tab, TabList, TabPanel, Tabs, Tag, TextInput, TimeField, Timeline, toast, Toaster, ToggleButton, ToggleButtonGroup, Tooltip, TooltipTrigger, tokens, YearMonthPicker, };
|
|
76
|
+
export { Accordion, Badge, Breadcrumbs, Button, Calendar, CalendarCell, ChartLegend, ChartTooltip, Checkbox, CheckboxGroup, Chip, Combobox, createMuiTheme, DataTable, DataTableCell, DataTableHeader, DataTableRoot, DataTableRow, DatePicker, DateRangePicker, Drawer, Dropdown, DropdownListItem, EmptyState, FileUpload, Filter, fontSize, Header, I18nProvider, Icon, Label, LinearProgressBar, lineHeight, Link, List, Loader, Menu, MenuItem, Message, Modal, ModalContent, ModalActions, ModalForm, Navigation, NavigationItem, Notifications, NotificationCard, NumberField, PageLayout, Pagination, Popover, PopoverTrigger, Radio, RouterProvider, SearchBar, SegmentedControl, SegmentedControlButton, SegmentedControlList, SegmentedControlPanel, Select, SelectItem, Skeleton, Slider, Stepper, Step, Switch, Tab, TabList, TabPanel, Tabs, Tag, TextInput, TimeField, TimePicker, Timeline, toast, Toaster, ToggleButton, ToggleButtonGroup, Tooltip, TooltipTrigger, tokens, YearMonthPicker, };
|
|
76
77
|
export type { TypeTableColumnWithFilter };
|
|
@@ -60,6 +60,7 @@ title: Exhaustive list of all the Design System Components and their props
|
|
|
60
60
|
- [Tag](node_modules/@agregio-solutions/design-system/dist/packages/components/Tag/doc.md) : A small colored label with optional left icon, supporting different nature/semantic colors — use it to categorize, label, or mark content with semantic meaning.
|
|
61
61
|
- [TextInput](node_modules/@agregio-solutions/design-system/dist/packages/components/TextInput/doc.md) : A text input field (or textarea) with optional label, icon, character counter, and helper text — use it for single-line or multi-line text entry with validation feedback.
|
|
62
62
|
- [TimeField](node_modules/@agregio-solutions/design-system/dist/packages/components/TimeField/doc.md) : A time input field with segmented controls for hours/minutes/seconds and optional 12/24 format — use it to let users input or edit a specific time of day.
|
|
63
|
+
- [TimePicker](node_modules/@agregio-solutions/design-system/dist/packages/components/TimePicker/doc.md) : A time picker using one dropdown Select per unit (hours, minutes, seconds) with configurable units, steps, and read-only mode — use it for a tabular, exploratory time selection as an alternative to the keyboard-segmented TimeField.
|
|
63
64
|
- [Timeline](node_modules/@agregio-solutions/design-system/dist/packages/components/Timeline/doc.md) : A horizontal or vertical timeline displaying blocks representing events or periods on a time axis — use it to visualize events, activities, or states across a time range.
|
|
64
65
|
- [Toaster](node_modules/@agregio-solutions/design-system/dist/packages/components/Toaster/doc.md) : A toast notification container positioning and managing multiple toast messages with auto-dismiss — use it to display temporary success, error, info, or warning notifications.
|
|
65
66
|
- [ToggleButton](node_modules/@agregio-solutions/design-system/dist/packages/components/ToggleButton/doc.md) : A button that toggles between selected/unselected states, optionally with icon and tooltip — use it as a child of ToggleButtonGroup for button-like toggles.
|
package/package.json
CHANGED