Skip to content

Commit

Permalink
fix(Accordion): controlled onfound
Browse files Browse the repository at this point in the history
  • Loading branch information
eirikbacker committed Sep 11, 2024
1 parent 0b13e2d commit 0b47c46
Show file tree
Hide file tree
Showing 3 changed files with 36 additions and 25 deletions.
9 changes: 8 additions & 1 deletion packages/css/accordion.css
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,14 @@
padding: var(--ds-spacing-5, 1rem);
}

&[open] > :is(summary, u-summary) {
/* Used to prevent glitchy toggle on beforematch when controlled */
&[data-controlled]:not([data-open]) > :not(summary, u-summary) {
position: absolute;
opacity: 0;
}

/* data-open is used to prevent glitchy toggle on beforematch when controlled */
&[data-open] > :is(summary, u-summary) {
background: var(--dsc-accordion-heading-background--open);

&::before {
Expand Down
19 changes: 10 additions & 9 deletions packages/react/src/components/Accordion/Accordion.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export const AccordionBorder: StoryFn<typeof Accordion> = () => (

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

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

return (
<>
<Button onClick={() => setOpen(!open)}>Toggle Accordions</Button>
<Button onClick={toggleOpen}>Toggle Accordions</Button>
<br />
<Accordion>
<Accordion.Item open={open}>
<Accordion.Heading onClick={() => setOpen(!open)}>
<Accordion.Item open={open} onFound={toggleOpen}>
<Accordion.Heading onClick={toggleOpen}>
Enkeltpersonforetak
</Accordion.Heading>
<Accordion.Content>
Expand All @@ -97,8 +98,8 @@ export const Controlled: StoryFn<typeof Accordion> = () => {
økonomien.
</Accordion.Content>
</Accordion.Item>
<Accordion.Item open={open}>
<Accordion.Heading onClick={() => setOpen(!open)}>
<Accordion.Item open={open} onFound={toggleOpen}>
<Accordion.Heading onClick={toggleOpen}>
Aksjeselskap (AS)
</Accordion.Heading>
<Accordion.Content>
Expand All @@ -109,8 +110,8 @@ export const Controlled: StoryFn<typeof Accordion> = () => {
hensiktsmessig organisasjonsform.
</Accordion.Content>
</Accordion.Item>
<Accordion.Item open={open}>
<Accordion.Heading onClick={() => setOpen(!open)}>
<Accordion.Item open={open} onFound={toggleOpen}>
<Accordion.Heading onClick={toggleOpen}>
Ansvarlig selskap (ANS/DA)
</Accordion.Heading>
<Accordion.Content>
Expand Down
33 changes: 18 additions & 15 deletions packages/react/src/components/Accordion/AccordionItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ export type AccordionItemProps = {
* @default false
*/
defaultOpen?: boolean;
/** Callback function when AccordionItem toggles */
onToggle?: () => void;
/** Callback function when AccordionItem toggles due to a find in page */
onFound?: () => void;
/** Content should be one `<Accordion.Header>` and `<Accordion.Content>` */
children: ReactNode;
} & HTMLAttributes<HTMLDetailsElement>;
Expand All @@ -33,7 +33,7 @@ export type AccordionItemProps = {
* </AccordionItem>
*/
export const AccordionItem = forwardRef<HTMLDetailsElement, AccordionItemProps>(
({ className, open, defaultOpen = false, onToggle, ...rest }, ref) => {
({ className, open, defaultOpen = false, onFound, ...rest }, ref) => {
const isControlled = open !== undefined;
const internalOpen = useRef(open ?? defaultOpen); // Only render open state on server, let <details> handle state in browser
const detailsRef = useRef<HTMLDetailsElement>(null);
Expand All @@ -42,18 +42,17 @@ export const AccordionItem = forwardRef<HTMLDetailsElement, AccordionItemProps>(
// Control state with a useEffect to animate on prop change and prevent native <details> toggle
useEffect(() => {
const details = detailsRef.current;
const summary = details?.querySelector(':scope > u-summary');
const summary = details?.querySelector(':scope > :is(summary,u-summary)');
const handleSummaryClick = (event: Event) => {
event?.preventDefault(); // Prevent native <details> toggle so we can animate
if (!isControlled && details) animateToggle(details);
};
const handleToggle = () => {
if (isControlled && details && details?.open !== open) {
setTimeout(() => {
details.open = open;
onToggle?.();
});
} else onToggle?.();
setTimeout(() => {
if (details?.open === details?.hasAttribute('data-open')) return;
if (isControlled) details?.toggleAttribute('open', open);
onFound?.();
});
};

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

AccordionItem.displayName = 'AccordionItem';

function animateToggle(details: HTMLDetailsElement, open = !details.open) {
const content = details.querySelector<HTMLElement>(
':scope > :not(summary, u-summary)',
);
const animateToggle = (details: HTMLDetailsElement, open = !details.open) => {
const isAnimateSupported = 'animate' in details;
const isReducedMotion = window.matchMedia?.(
'(prefers-reduced-motion: reduce)',
).matches;
const content = details.querySelector<HTMLElement>(
':scope > :not(summary, u-summary)',
);

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

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

0 comments on commit 0b47c46

Please sign in to comment.