@developer-overheid-nl/don-register-components 1.3.0 → 1.6.1

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 (139) hide show
  1. package/CHANGELOG.md +170 -118
  2. package/LICENSE.md +291 -291
  3. package/README.md +20 -20
  4. package/package.json +10 -8
  5. package/src/Introduction.mdx +31 -0
  6. package/src/alert/Alert.stories.tsx +57 -0
  7. package/src/alert/Alert.tsx +19 -19
  8. package/src/alert/styles.module.css +3 -3
  9. package/src/alignBox/AlignBox.stories.tsx +84 -0
  10. package/src/alignBox/AlignBox.tsx +114 -114
  11. package/src/alignBox/styles.module.css +110 -110
  12. package/src/amountLabel/AmountLabel.stories.tsx +28 -0
  13. package/src/amountLabel/AmountLabel.tsx +21 -16
  14. package/src/amountLabel/styles.module.css +11 -11
  15. package/src/analytics/PiwikPro.tsx +94 -94
  16. package/src/analytics/index.ts +1 -1
  17. package/src/article/Article.stories.tsx +53 -0
  18. package/src/article/Article.tsx +10 -10
  19. package/src/badgeList/BadgeList.tsx +34 -34
  20. package/src/badgeList/styles.module.css +37 -37
  21. package/src/block/Block.stories.tsx +58 -0
  22. package/src/block/Block.tsx +35 -35
  23. package/src/block/styles.module.css +29 -29
  24. package/src/button/Button.stories.tsx +56 -0
  25. package/src/button/Button.tsx +3 -3
  26. package/src/button/ButtonAction.stories.tsx +62 -0
  27. package/src/button/ButtonAction.tsx +51 -0
  28. package/src/cardAsLink/CardAsLink.stories.tsx +81 -0
  29. package/src/cardAsLink/CardAsLink.tsx +21 -21
  30. package/src/cardAsLink/styles.module.css +23 -23
  31. package/src/cardsList/CardsList.tsx +56 -54
  32. package/src/cardsList/styles.module.css +18 -19
  33. package/src/client.ts +7 -7
  34. package/src/container/Container.stories.tsx +21 -0
  35. package/src/container/Container.tsx +10 -10
  36. package/src/container/styles.module.css +28 -28
  37. package/src/copyButton/CopyButton.stories.tsx +41 -0
  38. package/src/copyButton/CopyButton.tsx +45 -39
  39. package/src/copyButton/styles.module.css +13 -13
  40. package/src/dataBadgeLink/DataBadgeLink.stories.tsx +137 -0
  41. package/src/dataBadgeLink/DataBadgeLink.tsx +59 -53
  42. package/src/dataBadgeLink/styles.module.css +89 -89
  43. package/src/dataSummary/DataSummary.tsx +22 -22
  44. package/src/dataSummary/DataSummaryItem.tsx +21 -21
  45. package/src/dataSummary/styles.module.css +75 -75
  46. package/src/facetFilters/FacetFilters.stories.tsx +93 -0
  47. package/src/facetFilters/FacetFilters.tsx +424 -367
  48. package/src/facetFilters/styles.module.css +3 -3
  49. package/src/fieldset/FieldSet.stories.tsx +45 -0
  50. package/src/fieldset/FieldSet.tsx +3 -3
  51. package/src/filters/Filters.stories.tsx +45 -0
  52. package/src/filters/Filters.tsx +116 -116
  53. package/src/filters/styles.module.css +26 -26
  54. package/src/footer/Footer.stories.tsx +127 -0
  55. package/src/footer/Footer.tsx +81 -76
  56. package/src/footer/styles.module.css +103 -103
  57. package/src/formFieldCheckboxGroup/FormFieldCheckboxGroup.stories.tsx +48 -0
  58. package/src/formFieldCheckboxGroup/FormFieldCheckboxGroup.tsx +16 -16
  59. package/src/formFieldCheckboxGroup/styles.module.css +5 -5
  60. package/src/formFieldCheckboxOption/FormFieldCheckboxOption.stories.tsx +61 -0
  61. package/src/formFieldCheckboxOption/FormFieldCheckboxOption.tsx +26 -26
  62. package/src/formFieldCheckboxOption/styles.module.css +22 -22
  63. package/src/formFieldLabel/FormFieldLabel.stories.tsx +21 -0
  64. package/src/formFieldLabel/FormFieldLabel.tsx +23 -23
  65. package/src/formFieldRadioGroup/FormFieldRadioGroup.stories.tsx +57 -0
  66. package/src/formFieldRadioGroup/FormFieldRadioGroup.tsx +13 -13
  67. package/src/formFieldRadioGroup/styles.module.css +5 -5
  68. package/src/formFieldRadioOption/FormFieldRadioOption.stories.tsx +48 -0
  69. package/src/formFieldRadioOption/FormFieldRadioOption.tsx +75 -75
  70. package/src/formFieldRadioOption/styles.module.css +49 -49
  71. package/src/formFieldSwitch/FormFieldSwitch.stories.tsx +61 -0
  72. package/src/formFieldSwitch/FormFieldSwitch.tsx +82 -79
  73. package/src/formFieldSwitch/styles.module.css +32 -32
  74. package/src/formFieldTextInput/FormFieldTextInput.stories.tsx +45 -0
  75. package/src/formFieldTextInput/FormFieldTextInput.tsx +3 -3
  76. package/src/grid/Grid.stories.tsx +37 -0
  77. package/src/grid/Grid.tsx +48 -0
  78. package/src/grid/styles.module.css +24 -0
  79. package/src/header/Header.stories.tsx +54 -0
  80. package/src/header/Header.tsx +137 -137
  81. package/src/header/styles.module.css +71 -71
  82. package/src/heading/Heading.stories.tsx +60 -0
  83. package/src/heading/Heading.tsx +15 -15
  84. package/src/headingGroup/HeadingGroup.stories.tsx +57 -0
  85. package/src/headingGroup/HeadingGroup.tsx +48 -48
  86. package/src/headingGroup/styles.module.css +3 -3
  87. package/src/i18n.ts +24 -25
  88. package/src/iconBadge/IconBadge.stories.tsx +46 -0
  89. package/src/iconBadge/IconBadge.tsx +35 -35
  90. package/src/iconBadge/getAppearance.ts +53 -53
  91. package/src/iconBadge/styles.module.css +19 -19
  92. package/src/iconsSprite/Icon.stories.tsx +73 -0
  93. package/src/iconsSprite/Icon.tsx +17 -17
  94. package/src/iconsSprite/IconsSprite.tsx +5 -5
  95. package/src/index.ts +117 -107
  96. package/src/infoButton/InfoButton.stories.tsx +52 -0
  97. package/src/infoButton/InfoButton.tsx +45 -45
  98. package/src/infoButton/styles.module.css +43 -43
  99. package/src/link/Link.stories.tsx +49 -0
  100. package/src/link/Link.tsx +10 -10
  101. package/src/markdown/Markdown.stories.tsx +48 -0
  102. package/src/markdown/Markdown.tsx +42 -42
  103. package/src/overlay/Overlay.stories.tsx +38 -0
  104. package/src/overlay/Overlay.tsx +18 -18
  105. package/src/overlay/styles.module.css +15 -15
  106. package/src/pagination/Pagination.stories.tsx +91 -0
  107. package/src/pagination/Pagination.tsx +267 -267
  108. package/src/pagination/styles.module.css +28 -28
  109. package/src/paragraph/Paragraph.stories.tsx +45 -0
  110. package/src/paragraph/Paragraph.tsx +24 -24
  111. package/src/paragraph/styles.module.css +6 -6
  112. package/src/pillBadge/PillBadge.examples.tsx +107 -107
  113. package/src/pillBadge/PillBadge.stories.tsx +257 -0
  114. package/src/pillBadge/PillBadge.tsx +60 -51
  115. package/src/pillBadge/styles.module.css +214 -214
  116. package/src/readOnlyTextInput/ReadOnlyTextInput.stories.tsx +49 -0
  117. package/src/readOnlyTextInput/ReadOnlyTextInput.tsx +24 -24
  118. package/src/readOnlyTextInput/styles.module.css +19 -19
  119. package/src/scoreBadge/ScoreBadge.stories.tsx +45 -0
  120. package/src/scoreBadge/ScoreBadge.tsx +135 -133
  121. package/src/search/Search.stories.tsx +65 -0
  122. package/src/search/Search.tsx +82 -82
  123. package/src/search/styles.module.css +40 -39
  124. package/src/siteLogo/ExampleLogo.stories.tsx +40 -0
  125. package/src/siteLogo/SiteLogo.stories.tsx +28 -0
  126. package/src/siteLogo/SiteLogo.tsx +24 -24
  127. package/src/sortForm/SortForm.stories.tsx +111 -0
  128. package/src/sortForm/SortForm.tsx +121 -0
  129. package/src/sortForm/styles.module.css +40 -0
  130. package/src/switch/Switch.stories.tsx +68 -0
  131. package/src/switch/Switch.tsx +152 -86
  132. package/src/switch/styles.module.css +54 -7
  133. package/src/toolTip/ToolTip.stories.tsx +131 -0
  134. package/src/toolTip/ToolTip.tsx +55 -55
  135. package/src/toolTip/styles.module.css +49 -49
  136. package/src/topNavigation/TopNavigation.stories.tsx +71 -0
  137. package/src/topNavigation/TopNavigation.tsx +67 -67
  138. package/src/topNavigation/styles.module.css +54 -54
  139. package/tsconfig.json +122 -121
@@ -1,24 +1,24 @@
1
- import { Logo } from "@developer-overheid-nl/proprietary";
2
- import type { JSX } from "react";
3
-
4
- export default function SiteLogo({
5
- isRoot,
6
- href,
7
- }: {
8
- isRoot: boolean;
9
- href: string;
10
- }) {
11
- const element = !isRoot ? "a" : "div";
12
- const props = {
13
- className: "site-logo",
14
- ...(!isRoot ? { href } : {}),
15
- };
16
-
17
- const Element = element as keyof JSX.IntrinsicElements;
18
-
19
- return (
20
- <Element {...props}>
21
- <Logo forced="light" />
22
- </Element>
23
- );
24
- }
1
+ import { Logo } from "@developer-overheid-nl/proprietary";
2
+ import type { JSX } from "react";
3
+
4
+ export default function SiteLogo({
5
+ isRoot,
6
+ href,
7
+ }: {
8
+ isRoot: boolean;
9
+ href: string;
10
+ }) {
11
+ const element = !isRoot ? "a" : "div";
12
+ const props = {
13
+ className: "site-logo",
14
+ ...(!isRoot ? { href } : {}),
15
+ };
16
+
17
+ const Element = element as keyof JSX.IntrinsicElements;
18
+
19
+ return (
20
+ <Element {...props}>
21
+ <Logo forced="light" />
22
+ </Element>
23
+ );
24
+ }
@@ -0,0 +1,111 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { expect, userEvent } from "storybook/test";
3
+ import SortForm from "./SortForm";
4
+
5
+ /**
6
+ * SortForm is een native GET-formulier met één gecombineerde keuze voor het
7
+ * sorteerveld en de sorteervolgorde. De keuze wordt direct toegepast. Actieve
8
+ * zoek- en filterparameters kunnen als verborgen velden behouden blijven.
9
+ */
10
+ const meta = {
11
+ title: "Components/SortForm",
12
+ component: SortForm,
13
+ tags: ["autodocs", "custom"],
14
+ args: {
15
+ action: "/apis",
16
+ options: [
17
+ { sortBy: "title", sortOrder: "asc", label: "Titel A–Z" },
18
+ { sortBy: "title", sortOrder: "desc", label: "Titel Z–A" },
19
+ {
20
+ sortBy: "adrScore",
21
+ sortOrder: "asc",
22
+ label: "ADR-score laag–hoog",
23
+ },
24
+ {
25
+ sortBy: "adrScore",
26
+ sortOrder: "desc",
27
+ label: "ADR-score hoog–laag",
28
+ },
29
+ { sortBy: "version", sortOrder: "asc", label: "Versie laag–hoog" },
30
+ { sortBy: "version", sortOrder: "desc", label: "Versie hoog–laag" },
31
+ ],
32
+ },
33
+ } satisfies Meta<typeof SortForm>;
34
+
35
+ export default meta;
36
+ type Story = StoryObj<typeof meta>;
37
+
38
+ export const Default: Story = {
39
+ play: async ({ canvas }) => {
40
+ await expect(canvas.getByLabelText("Sorteer op")).toHaveValue("title:asc");
41
+ await expect(canvas.queryByLabelText("Volgorde")).not.toBeInTheDocument();
42
+ await expect(canvas.queryByRole("button")).not.toBeInTheDocument();
43
+ },
44
+ };
45
+
46
+ export const WithActiveSortAndFilters: Story = {
47
+ name: "Met actieve sortering en filters",
48
+ args: {
49
+ sortBy: "adrScore",
50
+ sortOrder: "desc",
51
+ hiddenFields: [
52
+ ["organisation", "https://example.nl/a"],
53
+ ["organisation", "https://example.nl/b"],
54
+ ["q", "adressen"],
55
+ ],
56
+ },
57
+ play: async ({ canvas, canvasElement }) => {
58
+ const form = canvasElement.querySelector("form");
59
+ const sortSelect = canvas.getByLabelText("Sorteer op");
60
+ const sortLabel = canvasElement.querySelector(
61
+ `label[for="${sortSelect.id}"]`,
62
+ );
63
+ let submitCount = 0;
64
+
65
+ form?.addEventListener("submit", (event) => {
66
+ event.preventDefault();
67
+ submitCount += 1;
68
+ });
69
+
70
+ await expect(form).toHaveAttribute("action", "/apis");
71
+ await expect(sortSelect).toHaveValue("adrScore:desc");
72
+ await expect(canvas.queryByLabelText("Volgorde")).not.toBeInTheDocument();
73
+ await expect(canvas.queryByRole("button")).not.toBeInTheDocument();
74
+ await expect(canvas.getByRole("heading", { name: "Sorteren" })).toHaveClass(
75
+ "sr-only",
76
+ );
77
+ await expect(getComputedStyle(form as HTMLFormElement).justifyContent).toBe(
78
+ "flex-end",
79
+ );
80
+ await expect(
81
+ Number.parseFloat(getComputedStyle(sortSelect).paddingInlineEnd),
82
+ ).toBeGreaterThanOrEqual(40);
83
+ await expect(sortLabel).not.toBeNull();
84
+ const labelRect = (sortLabel as HTMLLabelElement).getBoundingClientRect();
85
+ const selectRect = sortSelect.getBoundingClientRect();
86
+ await expect(
87
+ Math.abs(
88
+ labelRect.top +
89
+ labelRect.height / 2 -
90
+ (selectRect.top + selectRect.height / 2),
91
+ ),
92
+ ).toBeLessThanOrEqual(1);
93
+ await expect(labelRect.right).toBeLessThanOrEqual(selectRect.left);
94
+ await expect(
95
+ getComputedStyle(sortLabel as HTMLLabelElement).whiteSpace,
96
+ ).toBe("nowrap");
97
+ await expect(
98
+ form?.querySelectorAll('input[name="organisation"]'),
99
+ ).toHaveLength(2);
100
+
101
+ await userEvent.selectOptions(sortSelect, "version:desc");
102
+
103
+ await expect(form?.querySelector('input[name="sortBy"]')).toHaveValue(
104
+ "version",
105
+ );
106
+ await expect(form?.querySelector('input[name="sortOrder"]')).toHaveValue(
107
+ "desc",
108
+ );
109
+ await expect(submitCount).toBe(1);
110
+ },
111
+ };
@@ -0,0 +1,121 @@
1
+ import {
2
+ FormFieldSelect,
3
+ SelectOption,
4
+ } from "@rijkshuisstijl-community/components-react";
5
+ import { type ChangeEvent, useId, useRef } from "react";
6
+ import { I18nextProvider, useTranslation } from "react-i18next";
7
+ import Heading from "../heading/Heading";
8
+ import i18n from "../i18n";
9
+ import styles from "./styles.module.css";
10
+
11
+ export type SortOrder = "asc" | "desc";
12
+
13
+ export interface SortOption {
14
+ sortBy: string;
15
+ sortOrder: SortOrder;
16
+ label: string;
17
+ }
18
+
19
+ export interface SortFormProps {
20
+ action: string;
21
+ options: SortOption[];
22
+ sortBy?: string | null;
23
+ sortOrder?: string | null;
24
+ hiddenFields?: [string, string][];
25
+ className?: string;
26
+ }
27
+
28
+ const SortForm = ({
29
+ action,
30
+ options,
31
+ sortBy = "title",
32
+ sortOrder = "asc",
33
+ hiddenFields = [],
34
+ className,
35
+ }: SortFormProps) => {
36
+ const { t } = useTranslation();
37
+ const headingId = useId();
38
+ const sortByInputRef = useRef<HTMLInputElement>(null);
39
+ const sortOrderInputRef = useRef<HTMLInputElement>(null);
40
+ const selectedOption =
41
+ options.find(
42
+ (option) => option.sortBy === sortBy && option.sortOrder === sortOrder,
43
+ ) ?? options[0];
44
+ const selectedValue = selectedOption
45
+ ? `${selectedOption.sortBy}:${selectedOption.sortOrder}`
46
+ : undefined;
47
+
48
+ const handleChange = (event: ChangeEvent<HTMLSelectElement>) => {
49
+ const option = options.find(
50
+ ({ sortBy, sortOrder }) =>
51
+ `${sortBy}:${sortOrder}` === event.currentTarget.value,
52
+ );
53
+
54
+ if (!option || !sortByInputRef.current || !sortOrderInputRef.current) {
55
+ return;
56
+ }
57
+
58
+ sortByInputRef.current.value = option.sortBy;
59
+ sortOrderInputRef.current.value = option.sortOrder;
60
+ event.currentTarget.form?.requestSubmit();
61
+ };
62
+
63
+ return (
64
+ <section className={className} aria-labelledby={headingId}>
65
+ <Heading id={headingId} level={2} appearanceLevel={3} className="sr-only">
66
+ {t("components.sort")}
67
+ </Heading>
68
+ <form
69
+ id="sort-form"
70
+ action={action}
71
+ method="GET"
72
+ className={styles.form}
73
+ aria-labelledby={headingId}
74
+ >
75
+ {hiddenFields.map(([name, value], index) => (
76
+ <input
77
+ // biome-ignore lint/suspicious/noArrayIndexKey: duplicate query params can share name and value
78
+ key={`${name}-${value}-${index}`}
79
+ type="hidden"
80
+ name={name}
81
+ value={value}
82
+ />
83
+ ))}
84
+ <input
85
+ ref={sortByInputRef}
86
+ type="hidden"
87
+ name="sortBy"
88
+ defaultValue={selectedOption?.sortBy}
89
+ />
90
+ <input
91
+ ref={sortOrderInputRef}
92
+ type="hidden"
93
+ name="sortOrder"
94
+ defaultValue={selectedOption?.sortOrder}
95
+ />
96
+ <FormFieldSelect
97
+ label={t("components.sort-by")}
98
+ defaultValue={selectedValue}
99
+ onChange={handleChange}
100
+ >
101
+ {options.map(({ sortBy, sortOrder, label }) => (
102
+ <SelectOption
103
+ key={`${sortBy}:${sortOrder}`}
104
+ value={`${sortBy}:${sortOrder}`}
105
+ >
106
+ {label}
107
+ </SelectOption>
108
+ ))}
109
+ </FormFieldSelect>
110
+ </form>
111
+ </section>
112
+ );
113
+ };
114
+
115
+ const TranslatedSortForm = (props: SortFormProps) => (
116
+ <I18nextProvider i18n={i18n}>
117
+ <SortForm {...props} />
118
+ </I18nextProvider>
119
+ );
120
+
121
+ export default TranslatedSortForm;
@@ -0,0 +1,40 @@
1
+ .form {
2
+ display: flex;
3
+ flex-wrap: wrap;
4
+ gap: 1rem;
5
+ align-items: end;
6
+ justify-content: flex-end;
7
+ }
8
+
9
+ .form select {
10
+ order: 2;
11
+ min-inline-size: 10rem;
12
+ padding-inline-end: 3rem;
13
+ }
14
+
15
+ .form :global(.utrecht-form-field) {
16
+ display: flex;
17
+ gap: 0.5rem;
18
+ align-items: center;
19
+ }
20
+
21
+ .form :global(.utrecht-form-field__label) {
22
+ flex: none;
23
+ margin-block-end: 0;
24
+ white-space: nowrap;
25
+ }
26
+
27
+ @media (--xs-and-below-viewport) {
28
+ .form {
29
+ flex-direction: column;
30
+ align-items: stretch;
31
+ }
32
+
33
+ .form :global(.utrecht-form-field) {
34
+ display: block;
35
+ }
36
+
37
+ .form :global(.utrecht-form-field__label) {
38
+ margin-block-end: var(--utrecht-form-field-label-margin-block-end);
39
+ }
40
+ }
@@ -0,0 +1,68 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import Switch from "./Switch";
3
+
4
+ /**
5
+ * Switch is een aan/uit-schakelaar. In `explicit`-modus wordt de schakelaar
6
+ * gerenderd als twee losse radio-opties (aan/uit) in plaats van één toggle,
7
+ * met een aparte `aria-labelledby` in plaats van `aria-label`.
8
+ */
9
+ const meta = {
10
+ title: "Components/Form/FormFieldParts/Switch",
11
+ component: Switch,
12
+ tags: ["autodocs", "custom"],
13
+ args: {
14
+ name: "toggle-example",
15
+ value: "true",
16
+ "aria-label": "Toggle voorbeeld",
17
+ },
18
+ argTypes: {
19
+ checked: { control: "boolean" },
20
+ defaultChecked: { control: "boolean" },
21
+ disabled: { control: "boolean" },
22
+ invalid: { control: "boolean" },
23
+ },
24
+ } satisfies Meta<typeof Switch>;
25
+
26
+ export default meta;
27
+ type Story = StoryObj<typeof meta>;
28
+
29
+ export const Default: Story = {};
30
+
31
+ export const Checked: Story = {
32
+ args: {
33
+ defaultChecked: true,
34
+ },
35
+ };
36
+
37
+ export const Disabled: Story = {
38
+ args: {
39
+ disabled: true,
40
+ },
41
+ };
42
+
43
+ export const Invalid: Story = {
44
+ args: {
45
+ invalid: true,
46
+ },
47
+ };
48
+
49
+ export const Explicit: Story = {
50
+ name: "Explicit (aan/uit als losse opties)",
51
+ decorators: [
52
+ (Story) => (
53
+ <>
54
+ <span id="switch-explicit-label" className="utrecht-form-label">
55
+ Heeft publiccode.yml
56
+ </span>
57
+ <Story />
58
+ </>
59
+ ),
60
+ ],
61
+ args: {
62
+ "aria-label": undefined,
63
+ "aria-labelledby": "switch-explicit-label",
64
+ explicit: true,
65
+ value: ["true", "false"],
66
+ labels: { on: "Aan", off: "Uit" },
67
+ },
68
+ };
@@ -1,86 +1,152 @@
1
- import clsx from "clsx";
2
- import {
3
- type ForwardedRef,
4
- forwardRef,
5
- type InputHTMLAttributes,
6
- useId,
7
- } from "react";
8
- import styles from "./styles.module.css";
9
-
10
- export interface SwitchLabels {
11
- on: string;
12
- off: string;
13
- }
14
-
15
- export interface SwitchProps
16
- extends Omit<InputHTMLAttributes<HTMLInputElement>, "type" | "readOnly"> {
17
- invalid?: boolean;
18
- labels?: SwitchLabels;
19
- }
20
-
21
- const Switch = forwardRef(
22
- (props: SwitchProps, ref: ForwardedRef<HTMLInputElement>) => {
23
- const genId = useId();
24
- const {
25
- id = genId,
26
- disabled,
27
- invalid,
28
- required,
29
- hidden,
30
- checked,
31
- defaultChecked,
32
- role,
33
- tabIndex,
34
- className,
35
- labels = { on: "ON", off: "OFF" },
36
- ...restProps
37
- } = props;
38
-
39
- return (
40
- <div
41
- className={clsx(
42
- "utrecht-form-toggle",
43
- "utrecht-form-toggle--html-checkbox",
44
- {
45
- "utrecht-form-toggle--disabled": disabled,
46
- "utrecht-form-toggle--invalid": invalid,
47
- "utrecht-form-toggle--required": required,
48
- },
49
- styles.switch,
50
- className,
51
- )}
52
- hidden={hidden}
53
- >
54
- <input
55
- id={id}
56
- aria-invalid={invalid || undefined}
57
- type="checkbox"
58
- className="utrecht-form-toggle__checkbox"
59
- checked={checked}
60
- defaultChecked={defaultChecked || undefined}
61
- disabled={disabled || undefined}
62
- required={required}
63
- ref={ref}
64
- role={role}
65
- tabIndex={tabIndex}
66
- {...restProps}
67
- />
68
- <label
69
- htmlFor={id}
70
- className={clsx(
71
- "utrecht-form-toggle__track",
72
- "utrecht-form-toggle__track--html-label",
73
- )}
74
- >
75
- <div className="utrecht-form-toggle__thumb">
76
- <span className="sr-only">
77
- {checked || defaultChecked ? labels.on : labels.off}
78
- </span>
79
- </div>
80
- </label>
81
- </div>
82
- );
83
- },
84
- );
85
-
86
- export default Switch;
1
+ import clsx from "clsx";
2
+ import {
3
+ type ForwardedRef,
4
+ forwardRef,
5
+ type InputHTMLAttributes,
6
+ useId,
7
+ } from "react";
8
+ import styles from "./styles.module.css";
9
+
10
+ export interface SwitchLabels {
11
+ on: string;
12
+ off: string;
13
+ }
14
+
15
+ export interface SwitchProps
16
+ extends Omit<InputHTMLAttributes<HTMLInputElement>, "type" | "readOnly"> {
17
+ invalid?: boolean;
18
+ labels?: SwitchLabels;
19
+ explicit?: boolean;
20
+ }
21
+
22
+ const Switch = forwardRef(
23
+ (props: SwitchProps, ref: ForwardedRef<HTMLInputElement>) => {
24
+ const genId = useId();
25
+ const {
26
+ id = genId,
27
+ name,
28
+ value,
29
+ disabled,
30
+ invalid,
31
+ explicit,
32
+ required,
33
+ hidden,
34
+ checked,
35
+ defaultChecked,
36
+ role,
37
+ tabIndex,
38
+ className,
39
+ labels = { on: "ON", off: "OFF" },
40
+ ...restProps
41
+ } = props;
42
+
43
+ return (
44
+ // biome-ignore lint/a11y/useAriaPropsSupportedByRole: okay because of role=group
45
+ <div
46
+ className={clsx(
47
+ "utrecht-form-toggle",
48
+ "utrecht-form-toggle--html-checkbox",
49
+ {
50
+ "utrecht-form-toggle--disabled": disabled,
51
+ "utrecht-form-toggle--invalid": invalid,
52
+ "utrecht-form-toggle--required": required,
53
+ },
54
+ styles.switch,
55
+ explicit && styles.explicit,
56
+ className,
57
+ )}
58
+ hidden={hidden}
59
+ role={explicit ? "group" : undefined}
60
+ aria-labelledby={explicit ? props["aria-labelledby"] : undefined}
61
+ >
62
+ {explicit ? (
63
+ <>
64
+ <input
65
+ id={id}
66
+ aria-invalid={invalid || undefined}
67
+ type="radio"
68
+ name={name}
69
+ value={Array.isArray(value) ? value[0] : value}
70
+ className="utrecht-form-toggle__checkbox"
71
+ defaultChecked={defaultChecked || undefined}
72
+ disabled={disabled || undefined}
73
+ required={required}
74
+ ref={ref}
75
+ role={role}
76
+ tabIndex={tabIndex}
77
+ {...restProps}
78
+ aria-labelledby={undefined}
79
+ />
80
+ <input
81
+ id={`negate_${id}`}
82
+ aria-invalid={invalid || undefined}
83
+ type="radio"
84
+ name={name}
85
+ value={Array.isArray(value) ? value[1] : `Not ${value}`}
86
+ className="utrecht-form-toggle__checkbox"
87
+ defaultChecked={
88
+ (typeof defaultChecked !== "undefined" && !defaultChecked) ||
89
+ undefined
90
+ }
91
+ disabled={disabled || undefined}
92
+ required={required}
93
+ ref={ref}
94
+ role={role}
95
+ tabIndex={tabIndex}
96
+ {...restProps}
97
+ aria-labelledby={undefined}
98
+ />
99
+ </>
100
+ ) : (
101
+ <input
102
+ id={id}
103
+ aria-invalid={invalid || undefined}
104
+ type="checkbox"
105
+ className="utrecht-form-toggle__checkbox"
106
+ name={name}
107
+ value={value}
108
+ defaultChecked={defaultChecked || undefined}
109
+ disabled={disabled || undefined}
110
+ required={required}
111
+ ref={ref}
112
+ role={role}
113
+ tabIndex={tabIndex}
114
+ {...restProps}
115
+ />
116
+ )}
117
+ <label
118
+ htmlFor={id}
119
+ className={clsx(
120
+ "utrecht-form-toggle__track",
121
+ "utrecht-form-toggle__track--html-label",
122
+ )}
123
+ >
124
+ <div className="utrecht-form-toggle__thumb">
125
+ <span className="sr-only">
126
+ {explicit
127
+ ? labels.off
128
+ : checked || defaultChecked
129
+ ? labels.on
130
+ : labels.off}
131
+ </span>
132
+ </div>
133
+ </label>
134
+ {explicit && (
135
+ <label
136
+ htmlFor={`negate_${id}`}
137
+ className={clsx(
138
+ "utrecht-form-toggle__track",
139
+ "utrecht-form-toggle__track--html-label",
140
+ )}
141
+ >
142
+ <div className="utrecht-form-toggle__thumb">
143
+ <span className="sr-only">{labels.on}</span>
144
+ </div>
145
+ </label>
146
+ )}
147
+ </div>
148
+ );
149
+ },
150
+ );
151
+
152
+ export default Switch;
@@ -1,7 +1,54 @@
1
- .switch {
2
- &:hover {
3
- :global(.utrecht-form-toggle__track) {
4
- background-color: var(--utrecht-form-toggle-hover-background-color);
5
- }
6
- }
7
- }
1
+ .switch.switch {
2
+ &:hover {
3
+ :global(.utrecht-form-toggle__track) {
4
+ background-color: var(--utrecht-form-toggle-hover-background-color);
5
+ }
6
+ }
7
+
8
+ &.explicit {
9
+ :global(
10
+ &.utrecht-form-toggle--html-checkbox .utrecht-form-toggle__track
11
+ ):not([for^='negate']) {
12
+ position: absolute;
13
+ z-index: 2;
14
+ display: flex;
15
+
16
+ background-color: var(--utrecht-form-toggle-accent-color);
17
+ :global(.utrecht-form-toggle__thumb) {
18
+ margin-inline-start: var(
19
+ --utrecht-form-toggle-thumb-margin-inline-start,
20
+ 0
21
+ );
22
+ }
23
+ }
24
+
25
+ :global(
26
+ &.utrecht-form-toggle--html-checkbox .utrecht-form-toggle__checkbox
27
+ ):not([id^='negate']):checked
28
+ ~ :global(.utrecht-form-toggle__track):not([for^='negate']) {
29
+ z-index: 1;
30
+ }
31
+
32
+ :global(
33
+ &.utrecht-form-toggle--html-checkbox .utrecht-form-toggle__track
34
+ )[for^='negate'] {
35
+ position: absolute;
36
+ z-index: 2;
37
+ display: flex;
38
+
39
+ :global(.utrecht-form-toggle__thumb) {
40
+ margin-inline-end: var(
41
+ --utrecht-form-toggle-thumb-margin-inline-end,
42
+ 0
43
+ );
44
+ }
45
+ }
46
+
47
+ :global(
48
+ &.utrecht-form-toggle--html-checkbox .utrecht-form-toggle__checkbox
49
+ )[id^='negate']:checked
50
+ ~ :global(.utrecht-form-toggle__track)[for^='negate'] {
51
+ z-index: 1;
52
+ }
53
+ }
54
+ }