Skip to content

Commit

Permalink
feat: expand animation component for accordions
Browse files Browse the repository at this point in the history
  • Loading branch information
jeremyagabriel committed Feb 28, 2025
1 parent 62ad969 commit 2130376
Show file tree
Hide file tree
Showing 4 changed files with 172 additions and 48 deletions.
160 changes: 160 additions & 0 deletions app/components/Expand.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import {useCallback, useEffect, useMemo, useRef, useState} from 'react';

// In Firefox, setTimeout w/ duration 0 too short for browser to notice changes in DOM
const initialTransitDuration = 20;

interface Phase {
CLOSE: 'close';
CLOSING: 'closing';
CLOSED: 'closed';
OPEN: 'open';
OPENING: 'opening';
OPENED: 'opened';
}

type Status =
| Phase['CLOSE']
| Phase['CLOSING']
| Phase['CLOSED']
| Phase['OPEN']
| Phase['OPENING']
| Phase['OPENED'];

const PHASE = {
CLOSE: 'close',
CLOSING: 'closing',
CLOSED: 'closed',
OPEN: 'open',
OPENING: 'opening',
OPENED: 'opened',
} as Phase;

const GROUP = {
[PHASE.CLOSE]: PHASE.CLOSE,
[PHASE.CLOSED]: PHASE.CLOSE,
[PHASE.OPENING]: PHASE.CLOSE,
[PHASE.CLOSING]: PHASE.OPEN,
[PHASE.OPEN]: PHASE.OPEN,
[PHASE.OPENED]: PHASE.OPEN,
};

export function Expand({
children,
className = '',
duration = 200,
easing = 'ease-in-out',
open,
styles,
transitions = ['height', 'opacity'],
}: {
children: React.ReactNode;
className?: string;
duration?: number;
easing?: string;
open: boolean;
styles?: {
[PHASE.OPEN]: Record<string, string | number>;
[PHASE.CLOSE]: Record<string, string | number>;
};
transitions?: string[];
}) {
const ref = useRef<HTMLDivElement | null>(null);
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
const [status, setStatus] = useState<Status>(open ? PHASE.OPEN : PHASE.CLOSE);

const delay = useCallback((fn: () => void, time: number) => {
const timeout = setTimeout(fn, time);
timeoutRef.current = timeout;
}, []);

const clearDelay = useCallback(() => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
}, []);

const transit = useCallback(
(
entering: Phase['OPENING'] | Phase['CLOSING'],
entered: Phase['OPENED'] | Phase['CLOSED'],
enter: Phase['OPEN'] | Phase['CLOSE'],
) => {
setStatus(entering);

delay(() => {
setStatus(entered);

delay(() => {
setStatus(enter);
}, duration);
}, initialTransitDuration);
},
[duration],
);

const toggle = useCallback(
(toggleOpen: boolean) => {
clearDelay();
if (toggleOpen) {
transit(PHASE.OPENING, PHASE.OPENED, PHASE.OPEN);
} else {
transit(PHASE.CLOSING, PHASE.CLOSED, PHASE.CLOSE);
}
},
[transit],
);

const defaultExpandStyle = useMemo(() => {
switch (status) {
case PHASE.OPENING:
case PHASE.CLOSE:
case PHASE.CLOSED:
return {height: 0, opacity: 0, overflow: 'hidden'};
case PHASE.OPENED:
case PHASE.CLOSING:
return {
height: ref.current?.scrollHeight,
opacity: 1,
overflow: 'hidden',
};
default:
return {height: 'auto', opacity: 1, overflow: 'unset'};
}
}, [status]);

const expandStyle = useMemo(() => {
return {
...defaultExpandStyle,
...(styles?.[GROUP[status]] || {}),
};
}, [defaultExpandStyle, JSON.stringify(styles)]);

const style = useMemo(() => {
const transition = transitions
.map((attr) => `${attr} ${duration}ms ${easing}`)
.join(',');
return {
...expandStyle,
transition,
};
}, [duration, easing, expandStyle, JSON.stringify(transitions)]);

useEffect(() => {
toggle(open);

return () => {
clearDelay();
if (open) {
transit(PHASE.OPENING, PHASE.OPENED, PHASE.OPEN);
} else {
transit(PHASE.CLOSING, PHASE.CLOSED, PHASE.CLOSE);
}
};
}, [open]);

return (
<div ref={ref} className={className} style={style}>
{children}
</div>
);
}
20 changes: 4 additions & 16 deletions app/components/Footer/MobileMenuItem.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import {
Disclosure,
DisclosureButton,
DisclosurePanel,
Transition,
} from '@headlessui/react';
import {Disclosure, DisclosureButton, DisclosurePanel} from '@headlessui/react';

import {Expand} from '~/components/Expand';
import {Link} from '~/components/Link';
import {Svg} from '~/components/Svg';
import type {Settings} from '~/lib/types';
Expand Down Expand Up @@ -34,15 +30,7 @@ export function MobileMenuItem({
/>
</DisclosureButton>

<Transition
show={open}
enter="transition duration-100 ease-out"
enterFrom="transform scale-97 opacity-0"
enterTo="transform scale-100 opacity-100"
leave="transition duration-75 ease-out"
leaveFrom="transform scale-100 opacity-100"
leaveTo="transform scale-97 opacity-0"
>
<Expand open={open}>
<DisclosurePanel
as="ul"
className="flex flex-col items-start gap-2 px-4 pb-6"
Expand All @@ -65,7 +53,7 @@ export function MobileMenuItem({
);
})}
</DisclosurePanel>
</Transition>
</Expand>
</>
)}
</Disclosure>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
import {
Disclosure,
DisclosureButton,
DisclosurePanel,
Transition,
} from '@headlessui/react';
import {Disclosure, DisclosureButton, DisclosurePanel} from '@headlessui/react';
import type {Metafield} from '@shopify/hydrogen/storefront-api-types';
import startCase from 'lodash/startCase';

import {Expand} from '~/components/Expand';
import {Markdown} from '~/components/Markdown';
import {Svg} from '~/components/Svg';

Expand Down Expand Up @@ -49,22 +45,14 @@ export function ProductMetafieldsAccordion({
)}
</DisclosureButton>

<Transition
show={open}
enter="transition duration-100 ease-out"
enterFrom="transform scale-97 opacity-0"
enterTo="transform scale-100 opacity-100"
leave="transition duration-75 ease-out"
leaveFrom="transform scale-100 opacity-100"
leaveTo="transform scale-97 opacity-0"
>
<Expand open={open}>
<DisclosurePanel
className="px-4 pt-4 [&_h1]:mb-3 [&_h1]:text-sm [&_h2]:mb-3 [&_h2]:text-sm [&_h3]:mb-3 [&_h3]:text-sm [&_h4]:mb-3 [&_h4]:text-sm [&_h5]:mb-3 [&_h5]:text-sm [&_h6]:mb-3 [&_h6]:text-sm [&_ol]:!pl-4 [&_ol]:text-sm [&_p]:mb-3 [&_p]:text-sm [&_ul]:!pl-4 [&_ul]:text-sm"
static
>
<Markdown>{value}</Markdown>
</DisclosurePanel>
</Transition>
</Expand>
</>
)}
</Disclosure>
Expand Down
20 changes: 4 additions & 16 deletions app/sections/Accordions/Accordion.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import {
Disclosure,
DisclosureButton,
DisclosurePanel,
Transition,
} from '@headlessui/react';
import {Disclosure, DisclosureButton, DisclosurePanel} from '@headlessui/react';

import {Expand} from '~/components/Expand';
import {Markdown} from '~/components/Markdown';
import {Svg} from '~/components/Svg';

Expand Down Expand Up @@ -46,19 +42,11 @@ export function Accordion({
)}
</DisclosureButton>

<Transition
show={open}
enter="transition duration-100 ease-out"
enterFrom="transform scale-97 opacity-0"
enterTo="transform scale-100 opacity-100"
leave="transition duration-75 ease-out"
leaveFrom="transform scale-100 opacity-100"
leaveTo="transform scale-97 opacity-0"
>
<Expand open={open}>
<DisclosurePanel className="p-4 xs:px-6" static>
<Markdown>{body}</Markdown>
</DisclosurePanel>
</Transition>
</Expand>
</>
)}
</Disclosure>
Expand Down

0 comments on commit 2130376

Please sign in to comment.