@developer-overheid-nl/don-register-components 1.1.0 → 1.2.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/CHANGELOG.md +42 -0
- package/package.json +3 -3
- package/src/alignBox/AlignBox.tsx +3 -0
- package/src/alignBox/styles.module.css +4 -0
- package/src/amountLabel/AmountLabel.tsx +16 -0
- package/src/amountLabel/styles.module.css +11 -0
- package/src/analytics/PiwikPro.tsx +27 -5
- package/src/badgeList/BadgeList.tsx +26 -0
- package/src/badgeList/styles.module.css +24 -0
- package/src/client.ts +5 -0
- package/src/dataBadgeLink/DataBadgeLink.tsx +3 -0
- package/src/dataBadgeLink/styles.module.css +26 -0
- package/src/dataSummary/styles.module.css +10 -5
- package/src/facetFilters/FacetFilters.tsx +313 -0
- package/src/facetFilters/styles.module.css +3 -0
- package/src/formFieldCheckboxGroup/FormFieldCheckboxGroup.tsx +16 -0
- package/src/formFieldCheckboxGroup/styles.module.css +5 -0
- package/src/formFieldCheckboxOption/FormFieldCheckboxOption.tsx +26 -0
- package/src/formFieldCheckboxOption/styles.module.css +22 -0
- package/src/formFieldRadioGroup/FormFieldRadioGroup.tsx +13 -0
- package/src/formFieldRadioGroup/styles.module.css +5 -0
- package/src/formFieldRadioOption/FormFieldRadioOption.tsx +75 -0
- package/src/formFieldRadioOption/styles.module.css +49 -0
- package/src/formFieldSwitch/FormFieldSwitch.tsx +79 -0
- package/src/formFieldSwitch/styles.module.css +32 -0
- package/src/heading/Heading.tsx +5 -0
- package/src/iconBadge/IconBadge.tsx +3 -0
- package/src/iconBadge/getAppearance.ts +7 -1
- package/src/index.ts +27 -1
- package/src/infoButton/InfoButton.tsx +45 -0
- package/src/infoButton/styles.module.css +43 -0
- package/src/overlay/Overlay.tsx +18 -0
- package/src/overlay/styles.module.css +15 -0
- package/src/paragraph/Paragraph.tsx +16 -2
- package/src/paragraph/styles.module.css +6 -0
- package/src/pillBadge/styles.module.css +20 -0
- package/src/scoreBadge/ScoreBadge.tsx +1 -0
- package/src/switch/Switch.tsx +86 -0
- package/src/switch/styles.module.css +7 -0
- package/src/toolTip/ToolTip.tsx +55 -0
- package/src/toolTip/styles.module.css +48 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,47 @@
|
|
|
1
1
|
# @developer-overheid-nl/don-register-components
|
|
2
2
|
|
|
3
|
+
## 1.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- b404400: Nieuwe components:
|
|
8
|
+
|
|
9
|
+
- FacetFilters, met de types:
|
|
10
|
+
- toggle: boolean filter (switch)
|
|
11
|
+
- multi-select: multiple options, multiple selectable (checkboxes)
|
|
12
|
+
- single-select: multiple options, single selectable (radiobuttons)
|
|
13
|
+
- AmountLabel: label with amount, used in FacetFilters
|
|
14
|
+
- FormFieldCheckboxGroup
|
|
15
|
+
- FormFieldCheckboxOption, with amount label
|
|
16
|
+
- FormFieldRadioGroup
|
|
17
|
+
- FormFieldRadioOption, with amount label
|
|
18
|
+
- Switch: toggle (checkbox)
|
|
19
|
+
- FormFieldSwitch, with amount label
|
|
20
|
+
- InfoButton
|
|
21
|
+
- Tooltip: html popover tooltip; default met InfoButton
|
|
22
|
+
- Overlay: overlay component, blurs the background
|
|
23
|
+
- BadgeList, list for Badge components
|
|
24
|
+
|
|
25
|
+
- Changed components:
|
|
26
|
+
|
|
27
|
+
- AlignBox: wrap toegevoegd
|
|
28
|
+
- DataBadgeLink: tabIndex toegevoegd met default `undefined`, omdat RHC tabindex=0 op `span` had gezet, fixed Focus styling.
|
|
29
|
+
- Heading: HeadingProps, clampHeadingLevel exported
|
|
30
|
+
- Paragraph: `short` property en styling toegevoegd
|
|
31
|
+
- PillBadge: focus and hover styling toegevoegd
|
|
32
|
+
|
|
33
|
+
- Nieuwe translation keys toegevoegd
|
|
34
|
+
|
|
35
|
+
- 4bc38b3: Refactor PiwikPro component to deprecate pageTitle prop and introduce customEvent for event tracking
|
|
36
|
+
|
|
37
|
+
### Patch Changes
|
|
38
|
+
|
|
39
|
+
- a7a1405: Small bugfixes, server error details in action, future test file setup
|
|
40
|
+
- Updated dependencies [b404400]
|
|
41
|
+
- Updated dependencies [b404400]
|
|
42
|
+
- Updated dependencies [a7a1405]
|
|
43
|
+
- @developer-overheid-nl/don-register-locales@1.1.0
|
|
44
|
+
|
|
3
45
|
## 1.1.0
|
|
4
46
|
|
|
5
47
|
### Minor Changes
|
package/package.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"license": "EUPL-1.2",
|
|
6
6
|
"private": false,
|
|
7
7
|
"type": "module",
|
|
8
|
-
"version": "1.
|
|
8
|
+
"version": "1.2.0",
|
|
9
9
|
"keywords": [],
|
|
10
10
|
"publishConfig": {
|
|
11
11
|
"access": "public"
|
|
@@ -34,13 +34,13 @@
|
|
|
34
34
|
"react-i18next": "16.5.4",
|
|
35
35
|
"react-markdown": "10.1.0",
|
|
36
36
|
"remark-behead": "^3.1.0",
|
|
37
|
-
"@developer-overheid-nl/don-register-locales": "1.
|
|
37
|
+
"@developer-overheid-nl/don-register-locales": "1.1.0"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@vitejs/plugin-react": "^5.1.2",
|
|
41
41
|
"rimraf": "^6.1.2",
|
|
42
42
|
"typescript": "5.9.3",
|
|
43
|
-
"vite": "7.
|
|
43
|
+
"vite": "7.1.7",
|
|
44
44
|
"vite-plugin-dts": "^4.5.4"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
@@ -25,6 +25,7 @@ export interface AlignBoxProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
|
25
25
|
align?: Align;
|
|
26
26
|
gap?: Gap;
|
|
27
27
|
direction?: "row" | "column";
|
|
28
|
+
wrap?: boolean;
|
|
28
29
|
display?: "block" | "inline";
|
|
29
30
|
children: ReactNode;
|
|
30
31
|
}
|
|
@@ -83,6 +84,7 @@ const AlignBox: React.FC<AlignBoxProps> = ({
|
|
|
83
84
|
align = "center",
|
|
84
85
|
gap = "none",
|
|
85
86
|
direction = "row",
|
|
87
|
+
wrap = false,
|
|
86
88
|
display = "block",
|
|
87
89
|
className,
|
|
88
90
|
children,
|
|
@@ -96,6 +98,7 @@ const AlignBox: React.FC<AlignBoxProps> = ({
|
|
|
96
98
|
className={clsx(
|
|
97
99
|
styles.alignBox,
|
|
98
100
|
styles[direction],
|
|
101
|
+
wrap && styles.wrap,
|
|
99
102
|
styles[display],
|
|
100
103
|
alignmentClass,
|
|
101
104
|
gapClass,
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import styles from "./styles.module.css";
|
|
3
|
+
|
|
4
|
+
export interface AmountLabelProps {
|
|
5
|
+
label: ReactNode;
|
|
6
|
+
amount?: string | number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const AmountLabel = ({ label, amount }: AmountLabelProps) => (
|
|
10
|
+
<div className={styles.amountLabel}>
|
|
11
|
+
<span className={styles.labelText}>{label}</span>
|
|
12
|
+
{amount ? <span className={styles.amountText}>({amount})</span> : null}
|
|
13
|
+
</div>
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
export default AmountLabel;
|
|
@@ -2,21 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
import PiwikPro, {
|
|
4
4
|
ContentTracking,
|
|
5
|
+
CustomEvent,
|
|
5
6
|
type Dimensions,
|
|
6
7
|
DownloadAndOutlink,
|
|
7
8
|
ErrorTracking,
|
|
8
|
-
PageViews,
|
|
9
9
|
SiteSearch,
|
|
10
10
|
} from "@piwikpro/react-piwik-pro";
|
|
11
11
|
import { useEffect } from "react";
|
|
12
12
|
|
|
13
|
+
declare global {
|
|
14
|
+
interface Window {
|
|
15
|
+
_paq: unknown[];
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
13
19
|
interface AnalyticsProps {
|
|
14
20
|
siteId: string;
|
|
15
21
|
accountAddress: string;
|
|
16
22
|
dataLayerName?: string;
|
|
17
23
|
trackJSErrors?: boolean;
|
|
18
24
|
downloadExtensions?: string[];
|
|
25
|
+
/**
|
|
26
|
+
* @deprecated This property is deprecated.
|
|
27
|
+
*/
|
|
19
28
|
pageTitle?: string;
|
|
29
|
+
customEvent?: { category?: string; action?: string; name: string };
|
|
20
30
|
isSearchPage?: boolean;
|
|
21
31
|
searchObject?: {
|
|
22
32
|
keyword: string;
|
|
@@ -37,6 +47,7 @@ export default function Analytics(props: AnalyticsProps) {
|
|
|
37
47
|
dataLayerName,
|
|
38
48
|
trackJSErrors = true,
|
|
39
49
|
pageTitle,
|
|
50
|
+
customEvent,
|
|
40
51
|
isSearchPage,
|
|
41
52
|
searchObject,
|
|
42
53
|
} = props;
|
|
@@ -45,6 +56,12 @@ export default function Analytics(props: AnalyticsProps) {
|
|
|
45
56
|
...(props.downloadExtensions || []),
|
|
46
57
|
];
|
|
47
58
|
|
|
59
|
+
if (pageTitle) {
|
|
60
|
+
console.warn(
|
|
61
|
+
"The 'pageTitle' property is deprecated and will be removed in future versions.",
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
48
65
|
PiwikPro.initialize(siteId, accountAddress, {
|
|
49
66
|
/* nonce, */
|
|
50
67
|
dataLayerName,
|
|
@@ -60,13 +77,18 @@ export default function Analytics(props: AnalyticsProps) {
|
|
|
60
77
|
}
|
|
61
78
|
|
|
62
79
|
useEffect(() => {
|
|
63
|
-
if (
|
|
64
|
-
|
|
65
|
-
|
|
80
|
+
if (customEvent) {
|
|
81
|
+
CustomEvent.trackEvent(
|
|
82
|
+
customEvent.category || "Pages",
|
|
83
|
+
customEvent.action || "view",
|
|
84
|
+
customEvent.name,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
if (isSearchPage && searchObject?.keyword) {
|
|
66
88
|
const { keyword, category, searchCount, dimensions } = searchObject;
|
|
67
89
|
SiteSearch.trackSiteSearch(keyword, category, searchCount, dimensions);
|
|
68
90
|
}
|
|
69
|
-
}, [
|
|
91
|
+
}, [customEvent, isSearchPage, searchObject]);
|
|
70
92
|
|
|
71
93
|
return null;
|
|
72
94
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import clsx from "clsx";
|
|
2
|
+
import type { HTMLAttributes } from "react";
|
|
3
|
+
import styles from "./styles.module.css";
|
|
4
|
+
|
|
5
|
+
export interface BadgeListProps extends HTMLAttributes<HTMLUListElement> {
|
|
6
|
+
direction?: "row" | "column";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const BadgeList = (props: BadgeListProps) => {
|
|
10
|
+
const { direction = "row", className, children, ...restProps } = props;
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<ul
|
|
14
|
+
className={clsx(
|
|
15
|
+
styles.badgelist,
|
|
16
|
+
styles[`badgelist--${direction}`],
|
|
17
|
+
className,
|
|
18
|
+
)}
|
|
19
|
+
{...restProps}
|
|
20
|
+
>
|
|
21
|
+
{children}
|
|
22
|
+
</ul>
|
|
23
|
+
);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export default BadgeList;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
.badgelist {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-wrap: wrap;
|
|
4
|
+
gap: 0.25rem;
|
|
5
|
+
padding: 0;
|
|
6
|
+
list-style: none;
|
|
7
|
+
|
|
8
|
+
&:empty {
|
|
9
|
+
display: none;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.badgelist--row {
|
|
14
|
+
flex-direction: row;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.badgelist--row > * {
|
|
18
|
+
display: inline-flex;
|
|
19
|
+
gap: 0.25rem;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.badgelist--column {
|
|
23
|
+
flex-direction: column;
|
|
24
|
+
}
|
package/src/client.ts
CHANGED
|
@@ -24,6 +24,7 @@ const DataBadgeLink = (props: DataBadgeLinkProps) => {
|
|
|
24
24
|
children,
|
|
25
25
|
className,
|
|
26
26
|
role,
|
|
27
|
+
tabIndex = undefined,
|
|
27
28
|
appearance = "primary",
|
|
28
29
|
...restProps
|
|
29
30
|
} = props;
|
|
@@ -33,6 +34,8 @@ const DataBadgeLink = (props: DataBadgeLinkProps) => {
|
|
|
33
34
|
/* biome-ignore lint/a11y/useValidAriaValues: {null} is needed to remove it */
|
|
34
35
|
aria-pressed={undefined}
|
|
35
36
|
role={role}
|
|
37
|
+
/* RHC DataBadgeButton had a tabindex=0 on the span, and a focus ring, but no interaction */
|
|
38
|
+
tabIndex={tabIndex}
|
|
36
39
|
{...restProps}
|
|
37
40
|
>
|
|
38
41
|
<a
|
|
@@ -33,6 +33,13 @@
|
|
|
33
33
|
background-color: var(--rhc-color-cool-grey-500);
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
|
+
|
|
37
|
+
&:focus-within {
|
|
38
|
+
outline-width: var(--utrecht-focus-outline-width, revert);
|
|
39
|
+
outline-style: var(--utrecht-focus-outline-style, revert);
|
|
40
|
+
outline-color: var(--utrecht-focus-outline-color, revert);
|
|
41
|
+
outline-offset: var(--utrecht-focus-outline-offset, revert);
|
|
42
|
+
}
|
|
36
43
|
}
|
|
37
44
|
|
|
38
45
|
.badgeLink {
|
|
@@ -40,4 +47,23 @@
|
|
|
40
47
|
padding-block: 0.25rem;
|
|
41
48
|
color: inherit;
|
|
42
49
|
text-decoration: inherit;
|
|
50
|
+
|
|
51
|
+
&:focus-visible {
|
|
52
|
+
outline: none;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* fix for non-clickable icons bug */
|
|
56
|
+
&::before {
|
|
57
|
+
position: absolute;
|
|
58
|
+
top: 0;
|
|
59
|
+
left: 0;
|
|
60
|
+
display: block;
|
|
61
|
+
width: 100%;
|
|
62
|
+
height: 100%;
|
|
63
|
+
content: '';
|
|
64
|
+
border-radius: var(
|
|
65
|
+
--utrecht-data-badge-border-radius,
|
|
66
|
+
var(--utrecht-badge-border-radius, 0.5ch)
|
|
67
|
+
);
|
|
68
|
+
}
|
|
43
69
|
}
|
|
@@ -14,15 +14,15 @@
|
|
|
14
14
|
--rhc-data-summary-item-key-row-padding-inline-start: 0;
|
|
15
15
|
--rhc-data-summary-item-key-row-padding-inline-end: 0;
|
|
16
16
|
--rhc-data-summary-item-key-row-padding-block-start: 0;
|
|
17
|
-
--rhc-data-summary-item-key-row-padding-block-end: 0.
|
|
17
|
+
--rhc-data-summary-item-key-row-padding-block-end: 0.5rem;
|
|
18
18
|
--rhc-data-summary-item-value-row-padding-inline-start: 0;
|
|
19
19
|
--rhc-data-summary-item-value-row-padding-inline-end: 0;
|
|
20
20
|
--rhc-data-summary-item-value-row-padding-block-start: 0;
|
|
21
|
-
--rhc-data-summary-item-value-row-padding-block-end: 0.
|
|
21
|
+
--rhc-data-summary-item-value-row-padding-block-end: 0.5rem;
|
|
22
22
|
--rhc-data-summary-item-action-row-padding-inline-start: 0;
|
|
23
23
|
--rhc-data-summary-item-action-row-padding-inline-end: 0;
|
|
24
24
|
--rhc-data-summary-item-action-row-padding-block-start: 0;
|
|
25
|
-
--rhc-data-summary-item-action-row-padding-block-end: 0.
|
|
25
|
+
--rhc-data-summary-item-action-row-padding-block-end: 0.5rem;
|
|
26
26
|
display: flex;
|
|
27
27
|
flex-direction: column;
|
|
28
28
|
row-gap: 0.75rem;
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
:global(.rhc-data-summary__item) {
|
|
34
|
-
row-gap: 0.
|
|
34
|
+
row-gap: 0.25rem;
|
|
35
35
|
column-gap: 1rem;
|
|
36
36
|
}
|
|
37
37
|
|
|
@@ -62,9 +62,14 @@
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
.isContained {
|
|
65
|
-
|
|
65
|
+
&:last-child {
|
|
66
|
+
margin-block-end: 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
66
69
|
:global(.rhc-data-summary__item:last-child) {
|
|
67
70
|
--rhc-data-summary-item-border-block-end-style: none;
|
|
68
71
|
--rhc-data-summary-item-key-row-padding-block-end: 0;
|
|
72
|
+
--rhc-data-summary-item-value-row-padding-block-end: 0;
|
|
73
|
+
--rhc-data-summary-item-action-row-padding-block-end: 0;
|
|
69
74
|
}
|
|
70
75
|
}
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import clsx from "clsx";
|
|
4
|
+
import type { ChangeEvent } from "react";
|
|
5
|
+
import { type HTMLProps, type PropsWithChildren, useRef } from "react";
|
|
6
|
+
import { I18nextProvider, useTranslation } from "react-i18next";
|
|
7
|
+
import FormFieldCheckboxGroup from "../formFieldCheckboxGroup/FormFieldCheckboxGroup";
|
|
8
|
+
import FormFieldCheckboxOption from "../formFieldCheckboxOption/FormFieldCheckboxOption";
|
|
9
|
+
import FormFieldRadioGroup from "../formFieldRadioGroup/FormFieldRadioGroup";
|
|
10
|
+
import FormFieldRadioOption from "../formFieldRadioOption/FormFieldRadioOption";
|
|
11
|
+
import FormFieldSwitch from "../formFieldSwitch/FormFieldSwitch";
|
|
12
|
+
import Heading, {
|
|
13
|
+
clampHeadingLevel,
|
|
14
|
+
type HeadingProps,
|
|
15
|
+
} from "../heading/Heading";
|
|
16
|
+
import i18n from "../i18n";
|
|
17
|
+
import Paragraph from "../paragraph/Paragraph";
|
|
18
|
+
import ToolTip from "../toolTip/ToolTip";
|
|
19
|
+
import styles from "./styles.module.css";
|
|
20
|
+
|
|
21
|
+
enum FilterType {
|
|
22
|
+
Toggle = "toggle",
|
|
23
|
+
Date = "date",
|
|
24
|
+
Multi = "multi-select",
|
|
25
|
+
Single = "single-select",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface SelectOption {
|
|
29
|
+
value: string;
|
|
30
|
+
label: string;
|
|
31
|
+
description?: string | null;
|
|
32
|
+
count: number;
|
|
33
|
+
selected: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface BaseFilterItems {
|
|
37
|
+
key: string;
|
|
38
|
+
label: string;
|
|
39
|
+
description: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface ToggleFilterData extends BaseFilterItems {
|
|
43
|
+
type: FilterType.Toggle;
|
|
44
|
+
value?: boolean;
|
|
45
|
+
count?: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface DateFilterData extends BaseFilterItems {
|
|
49
|
+
type: FilterType.Date;
|
|
50
|
+
value?: string;
|
|
51
|
+
count?: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface MultiSelectFilterData extends BaseFilterItems {
|
|
55
|
+
type: FilterType.Multi;
|
|
56
|
+
options: SelectOption[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface SingleSelectFilterData extends BaseFilterItems {
|
|
60
|
+
type: FilterType.Single;
|
|
61
|
+
options: SelectOption[];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export type FilterData =
|
|
65
|
+
| ToggleFilterData
|
|
66
|
+
| DateFilterData
|
|
67
|
+
| MultiSelectFilterData
|
|
68
|
+
| SingleSelectFilterData;
|
|
69
|
+
|
|
70
|
+
type SelectedFilters = Record<string, Array<string | [string, string]>>;
|
|
71
|
+
|
|
72
|
+
export interface FacetFiltersProps extends HTMLProps<HTMLDivElement> {
|
|
73
|
+
title?: string;
|
|
74
|
+
startHeadingLevel?: HeadingProps["level"];
|
|
75
|
+
filters: FilterData[] | null | undefined;
|
|
76
|
+
/**
|
|
77
|
+
* Callback function called when filter option has changed.
|
|
78
|
+
* @param selectedFilters current selected filters at the moment of change
|
|
79
|
+
* @returns
|
|
80
|
+
*/
|
|
81
|
+
onFilterChange?: (selectedFilters: SelectedFilters) => void;
|
|
82
|
+
/**
|
|
83
|
+
* Types of filters for which the onFilterChange callback should be debounced. Default: ["date", "single-select"]
|
|
84
|
+
*/
|
|
85
|
+
debounceTypes?: Array<FilterType>;
|
|
86
|
+
/**
|
|
87
|
+
* Delay for debouncing the onFilterChange callback, in milliseconds. Default: 600
|
|
88
|
+
*/
|
|
89
|
+
debounceDelay?: number;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export const getSelectedFilters = (
|
|
93
|
+
filters: FilterData[] | null | undefined,
|
|
94
|
+
withLabels = false,
|
|
95
|
+
): SelectedFilters => {
|
|
96
|
+
return (
|
|
97
|
+
filters?.reduce((acc, filter) => {
|
|
98
|
+
if (filter.type === FilterType.Multi) {
|
|
99
|
+
const keys: (string | [string, string])[] = filter.options
|
|
100
|
+
?.filter((option) => option.selected)
|
|
101
|
+
.map((option) =>
|
|
102
|
+
withLabels ? [option.value, option.label] : option.value,
|
|
103
|
+
);
|
|
104
|
+
if (keys?.length > 0) {
|
|
105
|
+
acc[filter.key] = keys;
|
|
106
|
+
}
|
|
107
|
+
} else if (filter.type === FilterType.Toggle && filter.value) {
|
|
108
|
+
acc[filter.key] = withLabels ? [["true", filter.label]] : ["true"];
|
|
109
|
+
} else if (filter.type === FilterType.Date && filter.value) {
|
|
110
|
+
acc[filter.key] = withLabels
|
|
111
|
+
? [[filter.value, filter.label]]
|
|
112
|
+
: [filter.value];
|
|
113
|
+
} else if (filter.type === FilterType.Single) {
|
|
114
|
+
const selectedOption = filter.options?.find(
|
|
115
|
+
(option) => option.selected,
|
|
116
|
+
);
|
|
117
|
+
if (selectedOption) {
|
|
118
|
+
acc[filter.key] = withLabels
|
|
119
|
+
? [[selectedOption.value, selectedOption.label]]
|
|
120
|
+
: [selectedOption.value];
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return acc;
|
|
124
|
+
}, {} as SelectedFilters) ?? {}
|
|
125
|
+
);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const updateFilters = (
|
|
129
|
+
selectedFilters: SelectedFilters,
|
|
130
|
+
filterKey: string,
|
|
131
|
+
filterValue: string,
|
|
132
|
+
checked: boolean,
|
|
133
|
+
) => {
|
|
134
|
+
const newSelectedFilters = {
|
|
135
|
+
...selectedFilters,
|
|
136
|
+
[filterKey]: checked
|
|
137
|
+
? [...(selectedFilters[filterKey] || []), filterValue]
|
|
138
|
+
: (selectedFilters[filterKey] || []).filter(
|
|
139
|
+
(value) => value !== filterValue,
|
|
140
|
+
),
|
|
141
|
+
};
|
|
142
|
+
return newSelectedFilters;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const debounce = <T extends unknown[]>(
|
|
146
|
+
fn: (...args: T) => void,
|
|
147
|
+
delay: number,
|
|
148
|
+
) => {
|
|
149
|
+
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
150
|
+
return (...args: T) => {
|
|
151
|
+
if (timeoutId) {
|
|
152
|
+
clearTimeout(timeoutId);
|
|
153
|
+
}
|
|
154
|
+
timeoutId = setTimeout(() => {
|
|
155
|
+
fn(...args);
|
|
156
|
+
}, delay);
|
|
157
|
+
};
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const FacetFilters = (props: PropsWithChildren<FacetFiltersProps>) => {
|
|
161
|
+
const { t } = useTranslation();
|
|
162
|
+
const thisRef = useRef<HTMLDivElement>(null);
|
|
163
|
+
const {
|
|
164
|
+
title,
|
|
165
|
+
startHeadingLevel = 2,
|
|
166
|
+
filters,
|
|
167
|
+
onFilterChange = () => {},
|
|
168
|
+
debounceTypes = [FilterType.Date, FilterType.Single],
|
|
169
|
+
debounceDelay = 600,
|
|
170
|
+
...restProps
|
|
171
|
+
} = props;
|
|
172
|
+
|
|
173
|
+
const selectedFilters = getSelectedFilters(filters);
|
|
174
|
+
|
|
175
|
+
// Callback function (for React) and CustomEvent (for non-React) to notify about filter changes
|
|
176
|
+
const emitChange = (newSelectedFilters: SelectedFilters) => {
|
|
177
|
+
onFilterChange(newSelectedFilters);
|
|
178
|
+
const customEvent = new CustomEvent("onFilterChange", {
|
|
179
|
+
detail: newSelectedFilters,
|
|
180
|
+
});
|
|
181
|
+
if (thisRef.current) thisRef.current.dispatchEvent(customEvent);
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const debouncedEmitChange = useRef(
|
|
185
|
+
debounce(emitChange, debounceDelay),
|
|
186
|
+
).current;
|
|
187
|
+
|
|
188
|
+
const handleChange = (
|
|
189
|
+
event: ChangeEvent<HTMLInputElement>,
|
|
190
|
+
type: FilterType,
|
|
191
|
+
) => {
|
|
192
|
+
const newSelectedFilters = updateFilters(
|
|
193
|
+
selectedFilters,
|
|
194
|
+
event.target.name,
|
|
195
|
+
event.target.value,
|
|
196
|
+
event.target.checked,
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
if (debounceTypes.includes(type)) {
|
|
200
|
+
debouncedEmitChange(newSelectedFilters);
|
|
201
|
+
} else {
|
|
202
|
+
emitChange(newSelectedFilters);
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
return (
|
|
207
|
+
<div
|
|
208
|
+
ref={thisRef}
|
|
209
|
+
className={clsx([styles.facetFilters, props.className])}
|
|
210
|
+
{...restProps}
|
|
211
|
+
>
|
|
212
|
+
{title ? (
|
|
213
|
+
<Heading level={startHeadingLevel} appearanceLevel={3}>
|
|
214
|
+
{title}
|
|
215
|
+
</Heading>
|
|
216
|
+
) : null}
|
|
217
|
+
{/* Temporary filter on `date` types */}
|
|
218
|
+
{filters
|
|
219
|
+
?.filter(({ type }) => type !== "date")
|
|
220
|
+
.map((facet) => (
|
|
221
|
+
<div className={styles.facet} key={facet.key}>
|
|
222
|
+
<Heading
|
|
223
|
+
level={
|
|
224
|
+
title
|
|
225
|
+
? clampHeadingLevel(startHeadingLevel + 1)
|
|
226
|
+
: startHeadingLevel
|
|
227
|
+
}
|
|
228
|
+
appearanceLevel={5}
|
|
229
|
+
>
|
|
230
|
+
{facet.label}
|
|
231
|
+
</Heading>
|
|
232
|
+
<Paragraph id={`${facet.key}-label`} purpose="short">
|
|
233
|
+
{facet.description}
|
|
234
|
+
</Paragraph>
|
|
235
|
+
{facet.type === "toggle" ? (
|
|
236
|
+
<FormFieldSwitch
|
|
237
|
+
label=""
|
|
238
|
+
labelledById={`${facet.key}-label`}
|
|
239
|
+
name={facet.key}
|
|
240
|
+
defaultChecked={facet.value}
|
|
241
|
+
value="true"
|
|
242
|
+
amount={facet.count || 0}
|
|
243
|
+
labels={{
|
|
244
|
+
on: t("components.filter-on"),
|
|
245
|
+
off: t("components.filter-off"),
|
|
246
|
+
}}
|
|
247
|
+
onChange={(event) => handleChange(event, facet.type)}
|
|
248
|
+
/>
|
|
249
|
+
) : null}
|
|
250
|
+
{/* TODO: Date selector
|
|
251
|
+
{facet.type === "date" ? <div>TODO</div> : null}
|
|
252
|
+
*/}
|
|
253
|
+
{facet.type === "multi-select" ? (
|
|
254
|
+
<FormFieldCheckboxGroup>
|
|
255
|
+
{facet.options?.map((option) => (
|
|
256
|
+
<FormFieldCheckboxOption
|
|
257
|
+
key={option.value}
|
|
258
|
+
label={option.label}
|
|
259
|
+
name={facet.key}
|
|
260
|
+
value={option.value}
|
|
261
|
+
defaultChecked={option.selected}
|
|
262
|
+
amount={option.count}
|
|
263
|
+
onChange={(event) => handleChange(event, facet.type)}
|
|
264
|
+
>
|
|
265
|
+
{option.description ? (
|
|
266
|
+
<ToolTip
|
|
267
|
+
text={option.description}
|
|
268
|
+
aria-label={t("components.info-about", {
|
|
269
|
+
subject: option.label,
|
|
270
|
+
})}
|
|
271
|
+
/>
|
|
272
|
+
) : null}
|
|
273
|
+
</FormFieldCheckboxOption>
|
|
274
|
+
))}
|
|
275
|
+
</FormFieldCheckboxGroup>
|
|
276
|
+
) : null}
|
|
277
|
+
{facet.type === "single-select" ? (
|
|
278
|
+
<FormFieldRadioGroup>
|
|
279
|
+
<FormFieldRadioOption
|
|
280
|
+
key="none"
|
|
281
|
+
label={t("components.filter-all")}
|
|
282
|
+
name={facet.key}
|
|
283
|
+
value=""
|
|
284
|
+
onChange={(event) => handleChange(event, facet.type)}
|
|
285
|
+
/>
|
|
286
|
+
{facet.options?.map((option) => (
|
|
287
|
+
<FormFieldRadioOption
|
|
288
|
+
key={option.value}
|
|
289
|
+
label={option.label}
|
|
290
|
+
name={facet.key}
|
|
291
|
+
value={option.value}
|
|
292
|
+
defaultChecked={option.selected}
|
|
293
|
+
amount={option.count}
|
|
294
|
+
onChange={(event) => handleChange(event, facet.type)}
|
|
295
|
+
/>
|
|
296
|
+
))}
|
|
297
|
+
</FormFieldRadioGroup>
|
|
298
|
+
) : null}
|
|
299
|
+
</div>
|
|
300
|
+
))}
|
|
301
|
+
</div>
|
|
302
|
+
);
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
const TranslatedFacetFilters = (props: FacetFiltersProps) => {
|
|
306
|
+
return (
|
|
307
|
+
<I18nextProvider i18n={i18n}>
|
|
308
|
+
<FacetFilters {...props} />
|
|
309
|
+
</I18nextProvider>
|
|
310
|
+
);
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
export default TranslatedFacetFilters;
|