Skip to content

Commit 35d9007

Browse files
committed
#52: Comme back to using forwardRef
1 parent 3beaec5 commit 35d9007

28 files changed

+2088
-1998
lines changed

src/Accordion.tsx

Lines changed: 43 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
"use client";
22

3-
import React, { memo, ReactNode, useId, useState } from "react";
4-
import type { ForwardedRef } from "react";
3+
import React, { forwardRef, memo, ReactNode, useId, useState } from "react";
54
import { assert } from "tsafe";
65
import type { Equals } from "tsafe";
76
import { fr } from "./fr";
@@ -17,7 +16,6 @@ export namespace AccordionProps {
1716
titleAs?: `h${2 | 3 | 4 | 5 | 6}`;
1817
label: ReactNode;
1918
classes?: Partial<Record<"root" | "accordion" | "title" | "collapse", string>>;
20-
ref?: ForwardedRef<HTMLDivElement>;
2119
children: NonNullable<ReactNode>;
2220
};
2321

@@ -41,53 +39,54 @@ export namespace AccordionProps {
4139
}
4240

4341
/** @see <https://react-dsfr-components.etalab.studio/?path=/docs/components-accordion> */
44-
export const Accordion = memo((props: AccordionProps) => {
45-
const {
46-
className,
47-
titleAs: HtmlTitleTag = "h3",
48-
label,
49-
classes = {},
50-
ref,
51-
children,
52-
expanded: expandedProp,
53-
defaultExpanded = false,
54-
onExpandedChange,
55-
...rest
56-
} = props;
42+
export const Accordion = memo(
43+
forwardRef<HTMLDivElement, AccordionProps>((props, ref) => {
44+
const {
45+
className,
46+
titleAs: HtmlTitleTag = "h3",
47+
label,
48+
classes = {},
49+
children,
50+
expanded: expandedProp,
51+
defaultExpanded = false,
52+
onExpandedChange,
53+
...rest
54+
} = props;
5755

58-
assert<Equals<keyof typeof rest, never>>();
56+
assert<Equals<keyof typeof rest, never>>();
5957

60-
const accordionId = `accordion-${useId()}`;
58+
const accordionId = `accordion-${useId()}`;
6159

62-
const [expandedState, setExpandedState] = useState(defaultExpanded);
60+
const [expandedState, setExpandedState] = useState(defaultExpanded);
6361

64-
const value = expandedProp ? expandedProp : expandedState;
62+
const value = expandedProp ? expandedProp : expandedState;
6563

66-
const onExtendButtonClick = useConstCallback(
67-
(event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
68-
setExpandedState(!value);
69-
onExpandedChange?.(!value, event);
70-
}
71-
);
64+
const onExtendButtonClick = useConstCallback(
65+
(event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
66+
setExpandedState(!value);
67+
onExpandedChange?.(!value, event);
68+
}
69+
);
7270

73-
return (
74-
<section className={cx(fr.cx("fr-accordion"), className)} ref={ref}>
75-
<HtmlTitleTag className={cx(fr.cx("fr-accordion__title"), classes.title)}>
76-
<button
77-
className={fr.cx("fr-accordion__btn")}
78-
aria-expanded={value}
79-
aria-controls={accordionId}
80-
onClick={onExtendButtonClick}
81-
>
82-
{label}
83-
</button>
84-
</HtmlTitleTag>
85-
<div className={cx(fr.cx("fr-collapse"), classes.collapse)} id={accordionId}>
86-
{children}
87-
</div>
88-
</section>
89-
);
90-
});
71+
return (
72+
<section className={cx(fr.cx("fr-accordion"), className)} ref={ref} {...rest}>
73+
<HtmlTitleTag className={cx(fr.cx("fr-accordion__title"), classes.title)}>
74+
<button
75+
className={fr.cx("fr-accordion__btn")}
76+
aria-expanded={value}
77+
aria-controls={accordionId}
78+
onClick={onExtendButtonClick}
79+
>
80+
{label}
81+
</button>
82+
</HtmlTitleTag>
83+
<div className={cx(fr.cx("fr-collapse"), classes.collapse)} id={accordionId}>
84+
{children}
85+
</div>
86+
</section>
87+
);
88+
})
89+
);
9190

9291
Accordion.displayName = symToStr({ Accordion });
9392

src/Alert.tsx

Lines changed: 93 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"use client";
22

3-
import React, { memo, useState, useEffect, useRef } from "react";
4-
import type { ReactNode, ForwardedRef } from "react";
3+
import React, { memo, forwardRef, useState, useEffect, useRef } from "react";
4+
import type { ReactNode } from "react";
55
import type { FrClassName } from "./fr/generatedFromCss/classNames";
66
import { symToStr } from "tsafe/symToStr";
77
import { fr } from "./fr";
@@ -16,7 +16,6 @@ export type AlertProps = {
1616
severity: AlertProps.Severity;
1717
/** Default h3 */
1818
as?: `h${2 | 3 | 4 | 5 | 6}`;
19-
ref?: ForwardedRef<HTMLDivElement>;
2019
classes?: Partial<Record<"root" | "title" | "description" | "close", string>>;
2120
} & (AlertProps.DefaultSize | AlertProps.Small) &
2221
(AlertProps.NonClosable | AlertProps.Closable);
@@ -68,102 +67,104 @@ export namespace AlertProps {
6867
}
6968

7069
/** @see <https://react-dsfr-components.etalab.studio/?path=/docs/components-alert> */
71-
export const Alert = memo((props: AlertProps) => {
72-
const {
73-
className,
74-
severity,
75-
as: HtmlTitleTag = "h3",
76-
classes = {},
77-
ref,
78-
small: isSmall,
79-
title,
80-
description,
81-
closable: isClosable = false,
82-
isClosed: props_isClosed,
83-
onClose,
84-
...rest
85-
} = props;
86-
87-
assert<Equals<keyof typeof rest, never>>();
88-
89-
const [isClosed, setIsClosed] = useState(props_isClosed ?? false);
90-
91-
const [buttonElement, setButtonElement] = useState<HTMLButtonElement | null>(null);
92-
93-
const refShouldButtonGetFocus = useRef(false);
94-
const refShouldSetRole = useRef(false);
95-
96-
useEffect(() => {
97-
if (props_isClosed === undefined) {
98-
return;
99-
}
100-
setIsClosed(isClosed => {
101-
if (isClosed && !props_isClosed) {
102-
refShouldButtonGetFocus.current = true;
103-
refShouldSetRole.current = true;
70+
export const Alert = memo(
71+
forwardRef<HTMLDivElement, AlertProps>((props, ref) => {
72+
const {
73+
className,
74+
severity,
75+
as: HtmlTitleTag = "h3",
76+
classes = {},
77+
small: isSmall,
78+
title,
79+
description,
80+
closable: isClosable = false,
81+
isClosed: props_isClosed,
82+
onClose,
83+
...rest
84+
} = props;
85+
86+
assert<Equals<keyof typeof rest, never>>();
87+
88+
const [isClosed, setIsClosed] = useState(props_isClosed ?? false);
89+
90+
const [buttonElement, setButtonElement] = useState<HTMLButtonElement | null>(null);
91+
92+
const refShouldButtonGetFocus = useRef(false);
93+
const refShouldSetRole = useRef(false);
94+
95+
useEffect(() => {
96+
if (props_isClosed === undefined) {
97+
return;
98+
}
99+
setIsClosed(isClosed => {
100+
if (isClosed && !props_isClosed) {
101+
refShouldButtonGetFocus.current = true;
102+
refShouldSetRole.current = true;
103+
}
104+
105+
return props_isClosed;
106+
});
107+
}, [props_isClosed]);
108+
109+
useEffect(() => {
110+
if (!refShouldButtonGetFocus.current) {
111+
return;
104112
}
105113

106-
return props_isClosed;
107-
});
108-
}, [props_isClosed]);
114+
if (buttonElement === null) {
115+
//NOTE: This should not be reachable
116+
return;
117+
}
109118

110-
useEffect(() => {
111-
if (!refShouldButtonGetFocus.current) {
112-
return;
113-
}
119+
refShouldButtonGetFocus.current = false;
120+
buttonElement.focus();
121+
}, [buttonElement]);
122+
123+
const onCloseButtonClick = useConstCallback(() => {
124+
if (props_isClosed === undefined) {
125+
//Uncontrolled
126+
setIsClosed(true);
127+
onClose?.();
128+
} else {
129+
//Controlled
130+
onClose();
131+
}
132+
});
114133

115-
if (buttonElement === null) {
116-
//NOTE: This should not be reachable
117-
return;
118-
}
134+
const { t } = useTranslation();
119135

120-
refShouldButtonGetFocus.current = false;
121-
buttonElement.focus();
122-
}, [buttonElement]);
123-
124-
const onCloseButtonClick = useConstCallback(() => {
125-
if (props_isClosed === undefined) {
126-
//Uncontrolled
127-
setIsClosed(true);
128-
onClose?.();
129-
} else {
130-
//Controlled
131-
onClose();
136+
if (isClosed) {
137+
return null;
132138
}
133-
});
134-
135-
const { t } = useTranslation();
136139

137-
if (isClosed) {
138-
return null;
139-
}
140-
141-
return (
142-
<div
143-
className={cx(
144-
fr.cx("fr-alert", `fr-alert--${severity}`, { "fr-alert--sm": isSmall }),
145-
classes.root,
146-
className
147-
)}
148-
{...(refShouldSetRole.current && { "role": "alert" })}
149-
ref={ref}
150-
>
151-
<HtmlTitleTag className={cx(fr.cx("fr-alert__title"), classes.title)}>
152-
{title}
153-
</HtmlTitleTag>
154-
<p className={classes.description}>{description}</p>
155-
{isClosable && (
156-
<button
157-
ref={setButtonElement}
158-
className={cx(fr.cx("fr-link--close", "fr-link"), classes.close)}
159-
onClick={onCloseButtonClick}
160-
>
161-
{t("hide message")}
162-
</button>
163-
)}
164-
</div>
165-
);
166-
});
140+
return (
141+
<div
142+
className={cx(
143+
fr.cx("fr-alert", `fr-alert--${severity}`, { "fr-alert--sm": isSmall }),
144+
classes.root,
145+
className
146+
)}
147+
{...(refShouldSetRole.current && { "role": "alert" })}
148+
ref={ref}
149+
{...rest}
150+
>
151+
<HtmlTitleTag className={cx(fr.cx("fr-alert__title"), classes.title)}>
152+
{title}
153+
</HtmlTitleTag>
154+
<p className={classes.description}>{description}</p>
155+
{isClosable && (
156+
<button
157+
ref={setButtonElement}
158+
className={cx(fr.cx("fr-link--close", "fr-link"), classes.close)}
159+
onClick={onCloseButtonClick}
160+
>
161+
{t("hide message")}
162+
</button>
163+
)}
164+
</div>
165+
);
166+
})
167+
);
167168

168169
Alert.displayName = symToStr({ Alert });
169170

0 commit comments

Comments
 (0)