Skip to content

Commit 0b47c46

Browse files
committed
fix(Accordion): controlled onfound
1 parent 0b13e2d commit 0b47c46

File tree

3 files changed

+36
-25
lines changed

3 files changed

+36
-25
lines changed

packages/css/accordion.css

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,14 @@
106106
padding: var(--ds-spacing-5, 1rem);
107107
}
108108

109-
&[open] > :is(summary, u-summary) {
109+
/* Used to prevent glitchy toggle on beforematch when controlled */
110+
&[data-controlled]:not([data-open]) > :not(summary, u-summary) {
111+
position: absolute;
112+
opacity: 0;
113+
}
114+
115+
/* data-open is used to prevent glitchy toggle on beforematch when controlled */
116+
&[data-open] > :is(summary, u-summary) {
110117
background: var(--dsc-accordion-heading-background--open);
111118

112119
&::before {

packages/react/src/components/Accordion/Accordion.stories.tsx

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export const AccordionBorder: StoryFn<typeof Accordion> = () => (
5050

5151
export const AccordionColor: StoryFn<typeof Accordion> = () => (
5252
<Accordion border color='brand2'>
53-
<Accordion.Item>
53+
<Accordion.Item onFound={() => console.log('a')}>
5454
<Accordion.Heading>
5555
Hvordan får jeg tildelt et jegernummer?
5656
</Accordion.Heading>
@@ -59,7 +59,7 @@ export const AccordionColor: StoryFn<typeof Accordion> = () => (
5959
Jegerregisteret når du har bestått jegerprøven.
6060
</Accordion.Content>
6161
</Accordion.Item>
62-
<Accordion.Item>
62+
<Accordion.Item onFound={() => console.log('b')}>
6363
<Accordion.Heading>
6464
Jeg har glemt jegernummeret mitt. Hvor finner jeg dette?
6565
</Accordion.Heading>
@@ -79,14 +79,15 @@ Preview.args = {
7979

8080
export const Controlled: StoryFn<typeof Accordion> = () => {
8181
const [open, setOpen] = useState(false);
82+
const toggleOpen = () => setOpen(!open);
8283

8384
return (
8485
<>
85-
<Button onClick={() => setOpen(!open)}>Toggle Accordions</Button>
86+
<Button onClick={toggleOpen}>Toggle Accordions</Button>
8687
<br />
8788
<Accordion>
88-
<Accordion.Item open={open}>
89-
<Accordion.Heading onClick={() => setOpen(!open)}>
89+
<Accordion.Item open={open} onFound={toggleOpen}>
90+
<Accordion.Heading onClick={toggleOpen}>
9091
Enkeltpersonforetak
9192
</Accordion.Heading>
9293
<Accordion.Content>
@@ -97,8 +98,8 @@ export const Controlled: StoryFn<typeof Accordion> = () => {
9798
økonomien.
9899
</Accordion.Content>
99100
</Accordion.Item>
100-
<Accordion.Item open={open}>
101-
<Accordion.Heading onClick={() => setOpen(!open)}>
101+
<Accordion.Item open={open} onFound={toggleOpen}>
102+
<Accordion.Heading onClick={toggleOpen}>
102103
Aksjeselskap (AS)
103104
</Accordion.Heading>
104105
<Accordion.Content>
@@ -109,8 +110,8 @@ export const Controlled: StoryFn<typeof Accordion> = () => {
109110
hensiktsmessig organisasjonsform.
110111
</Accordion.Content>
111112
</Accordion.Item>
112-
<Accordion.Item open={open}>
113-
<Accordion.Heading onClick={() => setOpen(!open)}>
113+
<Accordion.Item open={open} onFound={toggleOpen}>
114+
<Accordion.Heading onClick={toggleOpen}>
114115
Ansvarlig selskap (ANS/DA)
115116
</Accordion.Heading>
116117
<Accordion.Content>

packages/react/src/components/Accordion/AccordionItem.tsx

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ export type AccordionItemProps = {
1818
* @default false
1919
*/
2020
defaultOpen?: boolean;
21-
/** Callback function when AccordionItem toggles */
22-
onToggle?: () => void;
21+
/** Callback function when AccordionItem toggles due to a find in page */
22+
onFound?: () => void;
2323
/** Content should be one `<Accordion.Header>` and `<Accordion.Content>` */
2424
children: ReactNode;
2525
} & HTMLAttributes<HTMLDetailsElement>;
@@ -33,7 +33,7 @@ export type AccordionItemProps = {
3333
* </AccordionItem>
3434
*/
3535
export const AccordionItem = forwardRef<HTMLDetailsElement, AccordionItemProps>(
36-
({ className, open, defaultOpen = false, onToggle, ...rest }, ref) => {
36+
({ className, open, defaultOpen = false, onFound, ...rest }, ref) => {
3737
const isControlled = open !== undefined;
3838
const internalOpen = useRef(open ?? defaultOpen); // Only render open state on server, let <details> handle state in browser
3939
const detailsRef = useRef<HTMLDetailsElement>(null);
@@ -42,18 +42,17 @@ export const AccordionItem = forwardRef<HTMLDetailsElement, AccordionItemProps>(
4242
// Control state with a useEffect to animate on prop change and prevent native <details> toggle
4343
useEffect(() => {
4444
const details = detailsRef.current;
45-
const summary = details?.querySelector(':scope > u-summary');
45+
const summary = details?.querySelector(':scope > :is(summary,u-summary)');
4646
const handleSummaryClick = (event: Event) => {
4747
event?.preventDefault(); // Prevent native <details> toggle so we can animate
4848
if (!isControlled && details) animateToggle(details);
4949
};
5050
const handleToggle = () => {
51-
if (isControlled && details && details?.open !== open) {
52-
setTimeout(() => {
53-
details.open = open;
54-
onToggle?.();
55-
});
56-
} else onToggle?.();
51+
setTimeout(() => {
52+
if (details?.open === details?.hasAttribute('data-open')) return;
53+
if (isControlled) details?.toggleAttribute('open', open);
54+
onFound?.();
55+
});
5756
};
5857

5958
details?.addEventListener('toggle', handleToggle, true);
@@ -69,6 +68,7 @@ export const AccordionItem = forwardRef<HTMLDetailsElement, AccordionItemProps>(
6968
<u-details
7069
class={cl('ds-accordion__item', className)} // Using class since React does not translate className on custom elements
7170
open={internalOpen.current || undefined} // Fallback to undefined to prevent rendering open="false"
71+
data-controlled={isControlled || undefined} // Used to prevent glitchy toggle on beforematch when controlled
7272
ref={mergedRefs}
7373
{...rest}
7474
/>
@@ -78,18 +78,20 @@ export const AccordionItem = forwardRef<HTMLDetailsElement, AccordionItemProps>(
7878

7979
AccordionItem.displayName = 'AccordionItem';
8080

81-
function animateToggle(details: HTMLDetailsElement, open = !details.open) {
82-
const content = details.querySelector<HTMLElement>(
83-
':scope > :not(summary, u-summary)',
84-
);
81+
const animateToggle = (details: HTMLDetailsElement, open = !details.open) => {
8582
const isAnimateSupported = 'animate' in details;
8683
const isReducedMotion = window.matchMedia?.(
8784
'(prefers-reduced-motion: reduce)',
8885
).matches;
86+
const content = details.querySelector<HTMLElement>(
87+
':scope > :not(summary, u-summary)',
88+
);
8989

9090
if (isReducedMotion || !isAnimateSupported || !content) {
91+
details.toggleAttribute('data-open', open); // Used to prevent glitchy toggle on beforematch when controlled
9192
details.open = open;
9293
} else if (details.open !== open) {
94+
details.toggleAttribute('data-open', true); // Used to prevent glitchy toggle on beforematch when controlled
9395
details.open = true;
9496
const opened = `${content.scrollHeight}px`;
9597

@@ -102,7 +104,8 @@ function animateToggle(details: HTMLDetailsElement, open = !details.open) {
102104
{ duration: 400, easing: 'ease-in-out' },
103105
).onfinish = () => {
104106
content.style.removeProperty('overflow'); // Restore overlow
107+
details.toggleAttribute('data-open', open); // Used to prevent glitchy toggle on beforematch when controlled
105108
details.open = open;
106109
};
107110
}
108-
}
111+
};

0 commit comments

Comments
 (0)