@agregio-solutions/design-system 1.101.0 → 1.101.2
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 +461 -431
- package/dist/design-system.js +9201 -8752
- package/dist/packages/components/Breadcrumbs/doc.md +1 -1
- package/dist/packages/components/Button/Button.d.ts +12 -4
- package/dist/packages/components/Button/doc.md +200 -8
- package/dist/packages/components/Checkbox/Checkbox.d.ts +1 -1
- package/dist/packages/components/Checkbox/doc.md +45 -0
- package/dist/packages/components/Combobox/doc.md +22 -0
- package/dist/packages/components/I18nProvider/doc.md +1 -1
- package/dist/packages/components/NavigationItem/doc.md +6 -5
- package/dist/packages/components/RouterProvider/RouterProvider.d.ts +4 -4
- package/dist/packages/components/SearchBar/components/SearchBarInput/SearchBarInput.d.ts +3 -7
- package/dist/packages/components/Select/doc.md +1 -1
- package/dist/packages/components/Switch/doc.md +48 -1
- package/dist/packages/index.d.ts +1 -1
- package/package.json +14 -4
|
@@ -138,7 +138,7 @@ Here are some more advanced stories with more testing coverage and examples that
|
|
|
138
138
|
import { Meta, StoryObj } from "@storybook/react-vite";
|
|
139
139
|
import { userEvent, within, expect, screen } from "storybook/test";
|
|
140
140
|
import { Route, Routes } from "react-router-dom";
|
|
141
|
-
import { I18nProvider } from "react-aria";
|
|
141
|
+
import { I18nProvider } from "react-aria-components";
|
|
142
142
|
|
|
143
143
|
import { expectNotPresent } from "@internal/test-utils-storybook/test-utils-storybook";
|
|
144
144
|
import { FiveItems } from "../Breadcrumbs.stories";
|
|
@@ -4,7 +4,7 @@ import { HTMLAttributeAnchorTarget } from 'react';
|
|
|
4
4
|
import { Props as TooltipProps } from '../Tooltip/Tooltip';
|
|
5
5
|
import { Props as BadgeProps } from '../Badge/Badge';
|
|
6
6
|
export type ButtonSize = "small" | "medium" | "large";
|
|
7
|
-
export type Props = Omit<ButtonProps, "onPress" | "isDisabled" | "children"> & {
|
|
7
|
+
export type Props = Omit<ButtonProps, "onPress" | "isDisabled" | "isPending" | "children"> & {
|
|
8
8
|
/**
|
|
9
9
|
* Handler that is called when the press is released over the target.
|
|
10
10
|
*/
|
|
@@ -45,7 +45,9 @@ export type Props = Omit<ButtonProps, "onPress" | "isDisabled" | "children"> & {
|
|
|
45
45
|
*/
|
|
46
46
|
fullWidth?: boolean;
|
|
47
47
|
/**
|
|
48
|
-
* Show loading state
|
|
48
|
+
* Show loading state. On a button, this maps to react-aria's `isPending`: the button
|
|
49
|
+
* keeps the focus and announces the state change to screen readers instead of being
|
|
50
|
+
* removed from the tab order. On a link (`href`), it disables the element.
|
|
49
51
|
*/
|
|
50
52
|
isLoading?: boolean;
|
|
51
53
|
/**
|
|
@@ -69,6 +71,8 @@ export type Props = Omit<ButtonProps, "onPress" | "isDisabled" | "children"> & {
|
|
|
69
71
|
* before `onClick` is called. While pressed, text and icons are hidden (the button
|
|
70
72
|
* keeps its size) and a circular progress fills up. Releasing early resets
|
|
71
73
|
* everything without calling `onClick`.
|
|
74
|
+
* Works with `type="submit"`: the owner form is submitted once the hold completes,
|
|
75
|
+
* and never on a short click.
|
|
72
76
|
* Omit it (or pass 0) to get a regular click button. Not supported with `href`.
|
|
73
77
|
*/
|
|
74
78
|
holdToSubmitDuration?: number;
|
|
@@ -78,7 +82,7 @@ export type Props = Omit<ButtonProps, "onPress" | "isDisabled" | "children"> & {
|
|
|
78
82
|
*/
|
|
79
83
|
badgeProps?: BadgeProps;
|
|
80
84
|
};
|
|
81
|
-
declare const Button: import('react').ForwardRefExoticComponent<Omit<ButtonProps, "children" | "isDisabled" | "onPress"> & {
|
|
85
|
+
declare const Button: import('react').ForwardRefExoticComponent<Omit<ButtonProps, "children" | "isDisabled" | "onPress" | "isPending"> & {
|
|
82
86
|
/**
|
|
83
87
|
* Handler that is called when the press is released over the target.
|
|
84
88
|
*/
|
|
@@ -119,7 +123,9 @@ declare const Button: import('react').ForwardRefExoticComponent<Omit<ButtonProps
|
|
|
119
123
|
*/
|
|
120
124
|
fullWidth?: boolean;
|
|
121
125
|
/**
|
|
122
|
-
* Show loading state
|
|
126
|
+
* Show loading state. On a button, this maps to react-aria's `isPending`: the button
|
|
127
|
+
* keeps the focus and announces the state change to screen readers instead of being
|
|
128
|
+
* removed from the tab order. On a link (`href`), it disables the element.
|
|
123
129
|
*/
|
|
124
130
|
isLoading?: boolean;
|
|
125
131
|
/**
|
|
@@ -143,6 +149,8 @@ declare const Button: import('react').ForwardRefExoticComponent<Omit<ButtonProps
|
|
|
143
149
|
* before `onClick` is called. While pressed, text and icons are hidden (the button
|
|
144
150
|
* keeps its size) and a circular progress fills up. Releasing early resets
|
|
145
151
|
* everything without calling `onClick`.
|
|
152
|
+
* Works with `type="submit"`: the owner form is submitted once the hold completes,
|
|
153
|
+
* and never on a short click.
|
|
146
154
|
* Omit it (or pass 0) to get a regular click button. Not supported with `href`.
|
|
147
155
|
*/
|
|
148
156
|
holdToSubmitDuration?: number;
|
|
@@ -13,7 +13,7 @@ Here are the Storybook Stories.
|
|
|
13
13
|
Base stories:
|
|
14
14
|
|
|
15
15
|
```tsx
|
|
16
|
-
import { Fragment } from "react";
|
|
16
|
+
import { Fragment, useState } from "react";
|
|
17
17
|
import { Meta, StoryObj } from "@storybook/react-vite";
|
|
18
18
|
import { userEvent, within, waitFor, expect, fn } from "storybook/test";
|
|
19
19
|
|
|
@@ -119,17 +119,36 @@ export const Loading: StoryObj<typeof Button> = {
|
|
|
119
119
|
},
|
|
120
120
|
};
|
|
121
121
|
|
|
122
|
-
export const
|
|
122
|
+
export const HoldToSubmitInAForm: StoryObj<typeof Button> = {
|
|
123
123
|
args: {
|
|
124
124
|
...Playground.args,
|
|
125
|
-
text: "
|
|
126
|
-
nature: "negative",
|
|
125
|
+
text: "Envoyer",
|
|
127
126
|
holdToSubmitDuration: 1000,
|
|
127
|
+
type: "submit",
|
|
128
|
+
},
|
|
129
|
+
render: (args) => {
|
|
130
|
+
const Form = () => {
|
|
131
|
+
const [submitCount, setSubmitCount] = useState(0);
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
<form
|
|
135
|
+
onSubmit={(event) => {
|
|
136
|
+
event.preventDefault();
|
|
137
|
+
setSubmitCount((count) => count + 1);
|
|
138
|
+
}}
|
|
139
|
+
>
|
|
140
|
+
<Button {...args} />
|
|
141
|
+
<p>Submitted {submitCount} time(s)</p>
|
|
142
|
+
</form>
|
|
143
|
+
);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
return <Form />;
|
|
128
147
|
},
|
|
129
|
-
play: async ({ canvasElement
|
|
148
|
+
play: async ({ canvasElement }) => {
|
|
130
149
|
const canvas = within(canvasElement);
|
|
131
|
-
|
|
132
|
-
await expect(
|
|
150
|
+
// Hold it by hand in the sidebar: a short click leaves the count at 0
|
|
151
|
+
await expect(canvas.getByText("Submitted 0 time(s)")).toBeVisible();
|
|
133
152
|
},
|
|
134
153
|
};
|
|
135
154
|
|
|
@@ -346,7 +365,14 @@ Here are some more advanced stories with more testing coverage and examples that
|
|
|
346
365
|
|
|
347
366
|
```tsx
|
|
348
367
|
import { Meta, StoryObj } from "@storybook/react-vite";
|
|
349
|
-
import {
|
|
368
|
+
import {
|
|
369
|
+
within,
|
|
370
|
+
expect,
|
|
371
|
+
userEvent,
|
|
372
|
+
waitFor,
|
|
373
|
+
fireEvent,
|
|
374
|
+
fn,
|
|
375
|
+
} from "storybook/test";
|
|
350
376
|
|
|
351
377
|
import Button from "../Button";
|
|
352
378
|
import ErrorBoundary from "@packages/internal-components/ErrorBoundary/ErrorBoundary";
|
|
@@ -412,6 +438,47 @@ export const LinkWithTarget: StoryObj<typeof Button> = {
|
|
|
412
438
|
},
|
|
413
439
|
};
|
|
414
440
|
|
|
441
|
+
export const LoadingKeepsTheButtonFocusable: StoryObj<typeof Button> = {
|
|
442
|
+
args: {
|
|
443
|
+
...Playground.args,
|
|
444
|
+
isLoading: true,
|
|
445
|
+
},
|
|
446
|
+
play: async ({ canvasElement, args }) => {
|
|
447
|
+
const canvas = within(canvasElement);
|
|
448
|
+
const user = userEvent.setup();
|
|
449
|
+
const button = canvas.getByRole("button");
|
|
450
|
+
|
|
451
|
+
// react-aria's isPending uses aria-disabled, so the button keeps its place in
|
|
452
|
+
// the tab order instead of dropping the focus back to the body
|
|
453
|
+
await expect(button).toHaveAttribute("aria-disabled", "true");
|
|
454
|
+
await expect(button).toBeEnabled();
|
|
455
|
+
|
|
456
|
+
await user.tab();
|
|
457
|
+
await expect(button).toHaveFocus();
|
|
458
|
+
|
|
459
|
+
await user.click(button);
|
|
460
|
+
await expect(args.onClick).not.toHaveBeenCalled();
|
|
461
|
+
},
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
export const LoadingLinkIsDisabled: StoryObj<typeof Button> = {
|
|
465
|
+
args: {
|
|
466
|
+
...Playground.args,
|
|
467
|
+
href: "/link",
|
|
468
|
+
isLoading: true,
|
|
469
|
+
},
|
|
470
|
+
play: async ({ canvasElement }) => {
|
|
471
|
+
const canvas = within(canvasElement);
|
|
472
|
+
const link = await canvas.findByRole("link");
|
|
473
|
+
|
|
474
|
+
// Link has no isPending equivalent, so the loading state still disables it:
|
|
475
|
+
// react-aria drops the anchor for a span, left out of the tab order
|
|
476
|
+
await expect(link.tagName).toBe("SPAN");
|
|
477
|
+
await expect(link).toHaveAttribute("data-disabled");
|
|
478
|
+
await expect(link).not.toHaveAttribute("tabindex");
|
|
479
|
+
},
|
|
480
|
+
};
|
|
481
|
+
|
|
415
482
|
const HOLD_DURATION = 300;
|
|
416
483
|
|
|
417
484
|
// `userEvent.pointer` does not drive React Aria's press events here, so we
|
|
@@ -494,6 +561,131 @@ export const HoldByKeyboard: StoryObj<typeof Button> = {
|
|
|
494
561
|
await waitFor(() => expect(args.onClick).toHaveBeenCalledTimes(1));
|
|
495
562
|
},
|
|
496
563
|
};
|
|
564
|
+
|
|
565
|
+
const onFormSubmit = fn();
|
|
566
|
+
|
|
567
|
+
/** Wraps the button in a real form so the native submit path is exercised, not simulated. */
|
|
568
|
+
const HoldToSubmitForm = (props: React.ComponentProps<typeof Button>) => (
|
|
569
|
+
<form
|
|
570
|
+
onSubmit={(event) => {
|
|
571
|
+
event.preventDefault();
|
|
572
|
+
onFormSubmit();
|
|
573
|
+
}}
|
|
574
|
+
>
|
|
575
|
+
<Button {...props} />
|
|
576
|
+
</form>
|
|
577
|
+
);
|
|
578
|
+
|
|
579
|
+
const holdSubmitStory = {
|
|
580
|
+
args: { ...holdArgs, type: "submit" as const },
|
|
581
|
+
render: (args: React.ComponentProps<typeof Button>) => (
|
|
582
|
+
<HoldToSubmitForm {...args} />
|
|
583
|
+
),
|
|
584
|
+
beforeEach: () => {
|
|
585
|
+
onFormSubmit.mockClear();
|
|
586
|
+
},
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
export const HoldSubmitsTheFormOnlyOnceCompleted: StoryObj<typeof Button> = {
|
|
590
|
+
...holdSubmitStory,
|
|
591
|
+
play: async ({ canvasElement }) => {
|
|
592
|
+
const canvas = within(canvasElement);
|
|
593
|
+
const button = canvas.getByRole("button");
|
|
594
|
+
|
|
595
|
+
// A short click must not submit — this is exactly what a raw `type="submit"` would do
|
|
596
|
+
await userEvent.setup().click(button);
|
|
597
|
+
await someTime(HOLD_DURATION * 2);
|
|
598
|
+
await expect(onFormSubmit).not.toHaveBeenCalled();
|
|
599
|
+
|
|
600
|
+
await pressWithoutReleasing(button);
|
|
601
|
+
await waitFor(() => expect(onFormSubmit).toHaveBeenCalledTimes(1));
|
|
602
|
+
},
|
|
603
|
+
};
|
|
604
|
+
|
|
605
|
+
export const HoldReleasedTooEarlyDoesNotSubmitTheForm: StoryObj<typeof Button> =
|
|
606
|
+
{
|
|
607
|
+
...holdSubmitStory,
|
|
608
|
+
play: async ({ canvasElement }) => {
|
|
609
|
+
const canvas = within(canvasElement);
|
|
610
|
+
const button = canvas.getByRole("button");
|
|
611
|
+
|
|
612
|
+
await pressWithoutReleasing(button);
|
|
613
|
+
await someTime(HOLD_DURATION / 3);
|
|
614
|
+
await release(button);
|
|
615
|
+
await someTime(HOLD_DURATION * 2);
|
|
616
|
+
|
|
617
|
+
await expect(onFormSubmit).not.toHaveBeenCalled();
|
|
618
|
+
},
|
|
619
|
+
};
|
|
620
|
+
|
|
621
|
+
const submitters: Array<string | undefined> = [];
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* Two hold buttons in one form: a plain `type="submit"` would let `onSubmit` tell them apart
|
|
625
|
+
* through `event.submitter`, and honour the `formAction` it carries. So must the hold.
|
|
626
|
+
*/
|
|
627
|
+
const TwoHoldSubmitButtonsForm = () => (
|
|
628
|
+
<form
|
|
629
|
+
action="/default"
|
|
630
|
+
onSubmit={(event) => {
|
|
631
|
+
event.preventDefault();
|
|
632
|
+
const submitter = (event.nativeEvent as SubmitEvent)
|
|
633
|
+
.submitter as HTMLButtonElement | null;
|
|
634
|
+
submitters.push(submitter?.formAction);
|
|
635
|
+
}}
|
|
636
|
+
>
|
|
637
|
+
<Button
|
|
638
|
+
{...holdArgs}
|
|
639
|
+
text="Save"
|
|
640
|
+
type="submit"
|
|
641
|
+
formAction="/save"
|
|
642
|
+
onClick={undefined}
|
|
643
|
+
/>
|
|
644
|
+
<Button
|
|
645
|
+
{...holdArgs}
|
|
646
|
+
text="Delete"
|
|
647
|
+
type="submit"
|
|
648
|
+
formAction="/delete"
|
|
649
|
+
onClick={undefined}
|
|
650
|
+
/>
|
|
651
|
+
</form>
|
|
652
|
+
);
|
|
653
|
+
|
|
654
|
+
export const HoldKeepsTheButtonAsTheFormSubmitter: StoryObj<typeof Button> = {
|
|
655
|
+
render: () => <TwoHoldSubmitButtonsForm />,
|
|
656
|
+
beforeEach: () => {
|
|
657
|
+
submitters.length = 0;
|
|
658
|
+
},
|
|
659
|
+
play: async ({ canvasElement }) => {
|
|
660
|
+
const canvas = within(canvasElement);
|
|
661
|
+
|
|
662
|
+
await pressWithoutReleasing(canvas.getByRole("button", { name: "Delete" }));
|
|
663
|
+
await waitFor(() => expect(submitters).toHaveLength(1));
|
|
664
|
+
await expect(submitters[0]).toMatch(/\/delete$/);
|
|
665
|
+
|
|
666
|
+
await pressWithoutReleasing(canvas.getByRole("button", { name: "Save" }));
|
|
667
|
+
await waitFor(() => expect(submitters).toHaveLength(2));
|
|
668
|
+
await expect(submitters[1]).toMatch(/\/save$/);
|
|
669
|
+
},
|
|
670
|
+
};
|
|
671
|
+
|
|
672
|
+
export const HoldRestoresTheButtonTypeAfterSubmitting: StoryObj<typeof Button> =
|
|
673
|
+
{
|
|
674
|
+
...holdSubmitStory,
|
|
675
|
+
play: async ({ canvasElement }) => {
|
|
676
|
+
const canvas = within(canvasElement);
|
|
677
|
+
const button = canvas.getByRole("button") as HTMLButtonElement;
|
|
678
|
+
|
|
679
|
+
// Rendered as "button" so a short click cannot submit
|
|
680
|
+
await expect(button.type).toBe("button");
|
|
681
|
+
|
|
682
|
+
await pressWithoutReleasing(button);
|
|
683
|
+
await waitFor(() => expect(onFormSubmit).toHaveBeenCalledTimes(1));
|
|
684
|
+
|
|
685
|
+
// ...and back to "button" right after, ready for the next hold
|
|
686
|
+
await expect(button.type).toBe("button");
|
|
687
|
+
},
|
|
688
|
+
};
|
|
497
689
|
```
|
|
498
690
|
|
|
499
691
|
## Developer notes
|
|
@@ -49,5 +49,5 @@ export type Props = {
|
|
|
49
49
|
*/
|
|
50
50
|
required?: boolean;
|
|
51
51
|
};
|
|
52
|
-
declare const Checkbox: React.ForwardRefExoticComponent<Props & React.RefAttributes<
|
|
52
|
+
declare const Checkbox: React.ForwardRefExoticComponent<Props & React.RefAttributes<HTMLInputElement>>;
|
|
53
53
|
export default Checkbox;
|
|
@@ -277,6 +277,51 @@ export const TestWithReactHookForm: StoryObj<typeof Checkbox> = {
|
|
|
277
277
|
await expect(canvas.getByLabelText("Label")).toBeChecked();
|
|
278
278
|
},
|
|
279
279
|
};
|
|
280
|
+
|
|
281
|
+
// The ref targets the native input, and react-aria's inputRef only accepts a ref
|
|
282
|
+
// object, so both forms of forwarded ref must still reach that input.
|
|
283
|
+
const RefProbe = ({ kind }: { kind: "object" | "callback" }) => {
|
|
284
|
+
const objectRef = React.useRef<HTMLInputElement>(null);
|
|
285
|
+
const [resolved, setResolved] = React.useState("none");
|
|
286
|
+
|
|
287
|
+
return (
|
|
288
|
+
<>
|
|
289
|
+
<div>Resolved: {resolved}</div>
|
|
290
|
+
<Checkbox
|
|
291
|
+
label="Label"
|
|
292
|
+
ref={
|
|
293
|
+
kind === "object"
|
|
294
|
+
? objectRef
|
|
295
|
+
: (element) => setResolved(element?.type ?? "none")
|
|
296
|
+
}
|
|
297
|
+
/>
|
|
298
|
+
<button
|
|
299
|
+
type="button"
|
|
300
|
+
onClick={() => setResolved(objectRef.current?.type ?? "none")}
|
|
301
|
+
>
|
|
302
|
+
Read object ref
|
|
303
|
+
</button>
|
|
304
|
+
</>
|
|
305
|
+
);
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
export const TestObjectRefReachesTheInput: StoryObj<typeof Checkbox> = {
|
|
309
|
+
render: () => <RefProbe kind="object" />,
|
|
310
|
+
play: async ({ canvasElement }) => {
|
|
311
|
+
const canvas = within(canvasElement);
|
|
312
|
+
fireEvent.click(canvas.getByText("Read object ref"));
|
|
313
|
+
await expect(canvas.findByText("Resolved: checkbox")).resolves.toBeTruthy();
|
|
314
|
+
},
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
export const TestCallbackRefReachesTheInput: StoryObj<typeof Checkbox> = {
|
|
318
|
+
render: () => <RefProbe kind="callback" />,
|
|
319
|
+
play: async ({ canvasElement }) => {
|
|
320
|
+
const canvas = within(canvasElement);
|
|
321
|
+
// A callback ref is what react-hook-form's register() hands over
|
|
322
|
+
await expect(canvas.findByText("Resolved: checkbox")).resolves.toBeTruthy();
|
|
323
|
+
},
|
|
324
|
+
};
|
|
280
325
|
```
|
|
281
326
|
|
|
282
327
|
## Developer notes
|
|
@@ -681,6 +681,28 @@ export const ShouldLayOutTheFieldShellAroundItsContent: StoryObj<
|
|
|
681
681
|
await expect(Math.round(listboxBox.left)).toBe(Math.round(fieldBox.left));
|
|
682
682
|
},
|
|
683
683
|
};
|
|
684
|
+
|
|
685
|
+
// Current behaviour, without react-aria's allowsEmptyCollection: when the filter
|
|
686
|
+
// matches nothing, the popover closes and the user is left with no feedback.
|
|
687
|
+
export const ShouldCloseTheDropdownWhenNoOptionMatches: StoryObj<
|
|
688
|
+
typeof Combobox
|
|
689
|
+
> = {
|
|
690
|
+
args: {
|
|
691
|
+
...Playground.args,
|
|
692
|
+
},
|
|
693
|
+
play: async ({ canvasElement }) => {
|
|
694
|
+
const canvas = within(canvasElement);
|
|
695
|
+
const user = userEvent.setup({ delay: 50 });
|
|
696
|
+
const input = canvas.getByLabelText("[Insert label]");
|
|
697
|
+
|
|
698
|
+
await user.type(input, "Option");
|
|
699
|
+
await screen.findByRole("listbox");
|
|
700
|
+
|
|
701
|
+
await user.type(input, " that matches nothing");
|
|
702
|
+
await expectNotPresent(() => screen.queryByRole("listbox"));
|
|
703
|
+
await expect(input).toHaveValue("Option that matches nothing");
|
|
704
|
+
},
|
|
705
|
+
};
|
|
684
706
|
```
|
|
685
707
|
|
|
686
708
|
## Developer notes
|
|
@@ -14,7 +14,7 @@ Base stories:
|
|
|
14
14
|
|
|
15
15
|
```tsx
|
|
16
16
|
import { Meta, StoryObj } from "@storybook/react-vite";
|
|
17
|
-
import { I18nProvider as AriaI18nProvider } from "react-aria";
|
|
17
|
+
import { I18nProvider as AriaI18nProvider } from "react-aria-components";
|
|
18
18
|
import { fn, within } from "storybook/test";
|
|
19
19
|
import Chip from "@components/Chip/Chip";
|
|
20
20
|
import React from "react";
|
|
@@ -90,9 +90,10 @@ export const Dropdown: Story = {
|
|
|
90
90
|
play: async ({ canvasElement, args }) => {
|
|
91
91
|
const canvas = within(canvasElement);
|
|
92
92
|
await canvas.findByText(args.label as string);
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
).
|
|
93
|
+
// A dropdown is a button, not a link: it must announce that it expands content
|
|
94
|
+
const trigger = canvas.getByRole("button", { name: args.label as string });
|
|
95
|
+
await expect(trigger).toHaveAttribute("aria-expanded", "false");
|
|
96
|
+
await expect(trigger).not.toHaveAttribute("href");
|
|
96
97
|
await expectNotPresent(() => canvas.queryByText("Sous-libellé 1"));
|
|
97
98
|
await expectNotPresent(() => canvas.queryByText("Sous-libellé 2"));
|
|
98
99
|
},
|
|
@@ -107,8 +108,8 @@ export const DropdownOpenByDefault: Story = {
|
|
|
107
108
|
const canvas = within(canvasElement);
|
|
108
109
|
await canvas.findByText(args.label as string);
|
|
109
110
|
await expect(
|
|
110
|
-
canvas.
|
|
111
|
-
).
|
|
111
|
+
canvas.getByRole("button", { name: args.label as string }),
|
|
112
|
+
).toHaveAttribute("aria-expanded", "true");
|
|
112
113
|
await expect(
|
|
113
114
|
canvas.getByText("Sous-libellé 1").parentElement,
|
|
114
115
|
).toHaveAttribute("href", "/some-url-1");
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { ReactNode } from 'react';
|
|
2
|
-
import {
|
|
1
|
+
import { ComponentProps, ReactNode } from 'react';
|
|
2
|
+
import { RouterProvider as AriaRouterProvider } from 'react-aria-components';
|
|
3
3
|
export type Props = {
|
|
4
4
|
/**
|
|
5
5
|
* See the React Aria docs for more details on the RouterProvider usage
|
|
6
6
|
* https://react-spectrum.adobe.com/react-aria/routing.html
|
|
7
7
|
*/
|
|
8
|
-
navigate:
|
|
9
|
-
useHref?:
|
|
8
|
+
navigate: ComponentProps<typeof AriaRouterProvider>["navigate"];
|
|
9
|
+
useHref?: ComponentProps<typeof AriaRouterProvider>["useHref"];
|
|
10
10
|
children: ReactNode;
|
|
11
11
|
};
|
|
12
12
|
declare const RouterProvider: ({ children, ...props }: Props) => import("react").JSX.Element;
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { default as React } from 'react';
|
|
2
2
|
export interface Props extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "type" | "value" | "onChange"> {
|
|
3
3
|
/**
|
|
4
|
-
* The value of the input
|
|
4
|
+
* The value of the input. Omit it inside an `Autocomplete`, which owns the value.
|
|
5
5
|
*/
|
|
6
|
-
value
|
|
6
|
+
value?: string;
|
|
7
7
|
/**
|
|
8
8
|
* The function to call when the value changes
|
|
9
9
|
*/
|
|
10
|
-
onChange
|
|
10
|
+
onChange?: (value: string) => void;
|
|
11
11
|
/**
|
|
12
12
|
* The function to call when the clear button is clicked
|
|
13
13
|
*/
|
|
@@ -16,10 +16,6 @@ export interface Props extends Omit<React.InputHTMLAttributes<HTMLInputElement>,
|
|
|
16
16
|
* Whether the input has results (mainly to remove the bottom radius)
|
|
17
17
|
*/
|
|
18
18
|
withResults?: boolean;
|
|
19
|
-
/**
|
|
20
|
-
* Props to pass to the wrapper container
|
|
21
|
-
*/
|
|
22
|
-
wrapperProps?: React.HTMLProps<HTMLLabelElement>;
|
|
23
19
|
}
|
|
24
20
|
declare const _default: React.NamedExoticComponent<Props & React.RefAttributes<HTMLInputElement>>;
|
|
25
21
|
export default _default;
|
|
@@ -220,7 +220,7 @@ import { expect, fn, within } from "storybook/test";
|
|
|
220
220
|
import Select from "../Select";
|
|
221
221
|
import SelectItem from "@components/SelectItem/SelectItem";
|
|
222
222
|
import { useState } from "react";
|
|
223
|
-
import { I18nProvider } from "react-aria";
|
|
223
|
+
import { I18nProvider } from "react-aria-components";
|
|
224
224
|
import Badge from "@packages/components/Badge/Badge";
|
|
225
225
|
|
|
226
226
|
const meta: Meta<typeof Select> = {
|
|
@@ -155,7 +155,7 @@ Here are some more advanced stories with more testing coverage and examples that
|
|
|
155
155
|
import { Meta, StoryObj } from "@storybook/react-vite";
|
|
156
156
|
import Switch from "../Switch";
|
|
157
157
|
import { userEvent, within, expect } from "storybook/test";
|
|
158
|
-
import { useState } from "react";
|
|
158
|
+
import { useRef, useState } from "react";
|
|
159
159
|
import { useController, useForm } from "react-hook-form";
|
|
160
160
|
|
|
161
161
|
const meta: Meta<typeof Switch> = {
|
|
@@ -244,6 +244,53 @@ export const TestWithReactHookForm: StoryObj<typeof Switch> = {
|
|
|
244
244
|
await canvas.findByText("Is selected: No");
|
|
245
245
|
},
|
|
246
246
|
};
|
|
247
|
+
|
|
248
|
+
// react-aria's inputRef only accepts a ref object, so the forwarded ref is replayed
|
|
249
|
+
// through useImperativeHandle. Both ref forms must reach the native input.
|
|
250
|
+
const RefProbe = ({ kind }: { kind: "object" | "callback" }) => {
|
|
251
|
+
const objectRef = useRef<HTMLInputElement>(null);
|
|
252
|
+
const [tagName, setTagName] = useState("none");
|
|
253
|
+
|
|
254
|
+
return (
|
|
255
|
+
<>
|
|
256
|
+
<div>Resolved: {tagName}</div>
|
|
257
|
+
<Switch
|
|
258
|
+
label="Wi-Fi"
|
|
259
|
+
ref={
|
|
260
|
+
kind === "object"
|
|
261
|
+
? objectRef
|
|
262
|
+
: (element) => setTagName(element?.tagName ?? "none")
|
|
263
|
+
}
|
|
264
|
+
/>
|
|
265
|
+
<button
|
|
266
|
+
type="button"
|
|
267
|
+
onClick={() => setTagName(objectRef.current?.tagName ?? "none")}
|
|
268
|
+
>
|
|
269
|
+
Read object ref
|
|
270
|
+
</button>
|
|
271
|
+
</>
|
|
272
|
+
);
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
export const TestObjectRefReachesTheInput: StoryObj<typeof Switch> = {
|
|
276
|
+
render: () => <RefProbe kind="object" />,
|
|
277
|
+
play: async ({ canvasElement }) => {
|
|
278
|
+
const canvas = within(canvasElement);
|
|
279
|
+
const user = userEvent.setup();
|
|
280
|
+
|
|
281
|
+
await user.click(canvas.getByText("Read object ref"));
|
|
282
|
+
await expect(canvas.getByText("Resolved: INPUT")).toBeInTheDocument();
|
|
283
|
+
},
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
export const TestCallbackRefReachesTheInput: StoryObj<typeof Switch> = {
|
|
287
|
+
render: () => <RefProbe kind="callback" />,
|
|
288
|
+
play: async ({ canvasElement }) => {
|
|
289
|
+
const canvas = within(canvasElement);
|
|
290
|
+
// A callback ref is what react-hook-form's register() hands over
|
|
291
|
+
await expect(canvas.getByText("Resolved: INPUT")).toBeInTheDocument();
|
|
292
|
+
},
|
|
293
|
+
};
|
|
247
294
|
```
|
|
248
295
|
|
|
249
296
|
## Developer notes
|
package/dist/packages/index.d.ts
CHANGED
|
@@ -72,7 +72,7 @@ import { default as YearMonthPicker } from './components/YearMonthPicker/YearMon
|
|
|
72
72
|
import { default as RouterProvider } from './components/RouterProvider/RouterProvider';
|
|
73
73
|
import { default as createMuiTheme } from './mui/mui-theme.ts';
|
|
74
74
|
import { tokens, fontSize, lineHeight } from './ds-design-tokens/index';
|
|
75
|
-
import { I18nProvider } from 'react-aria';
|
|
75
|
+
import { I18nProvider } from 'react-aria-components';
|
|
76
76
|
import { toast } from 'sonner';
|
|
77
77
|
export { Accordion, Badge, Breadcrumbs, Button, Calendar, CalendarCell, Card, 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, };
|
|
78
78
|
export type { TypeTableColumnWithFilter };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agregio-solutions/design-system",
|
|
3
|
-
"version": "1.101.
|
|
3
|
+
"version": "1.101.2",
|
|
4
4
|
"description": "React Component library and Storybook that is part of the Design System for Agregio Solutions",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"module": "dist/design-system.js",
|
|
@@ -85,7 +85,9 @@
|
|
|
85
85
|
"storybook": "10.5.5",
|
|
86
86
|
"typescript": "5.9.3",
|
|
87
87
|
"typescript-eslint": "8.65.0",
|
|
88
|
+
"vite": "7.3.3",
|
|
88
89
|
"vite-plugin-dts": "5.0.3",
|
|
90
|
+
"vite-tsconfig-paths": "6.1.1",
|
|
89
91
|
"vitest": "4.1.10"
|
|
90
92
|
},
|
|
91
93
|
"dependencies": {
|
|
@@ -93,9 +95,17 @@
|
|
|
93
95
|
"normalize.css": "8.0.1",
|
|
94
96
|
"react-aria-components": "1.17.0",
|
|
95
97
|
"sonner": "2.0.7",
|
|
96
|
-
"styled-components": "6.4.4"
|
|
97
|
-
|
|
98
|
-
|
|
98
|
+
"styled-components": "6.4.4"
|
|
99
|
+
},
|
|
100
|
+
"peerDependencies": {
|
|
101
|
+
"@tanstack/react-table": "^8.0.0",
|
|
102
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
103
|
+
"react-dom": "^18.0.0 || ^19.0.0"
|
|
104
|
+
},
|
|
105
|
+
"peerDependenciesMeta": {
|
|
106
|
+
"@tanstack/react-table": {
|
|
107
|
+
"optional": true
|
|
108
|
+
}
|
|
99
109
|
},
|
|
100
110
|
"overrides": {
|
|
101
111
|
"storybook": "$storybook"
|