AbiirSkincare/Hydrogen/web/app/components/Aside.tsx

102 lines
2.1 KiB
TypeScript

import {
createContext,
type ReactNode,
useContext,
useEffect,
useState,
} from 'react';
import {useId} from 'react';
type AsideType = 'search' | 'cart' | 'mobile' | 'closed';
type AsideContextValue = {
type: AsideType;
open: (mode: AsideType) => void;
close: () => void;
};
/**
* A side bar component with Overlay
* @example
* ```jsx
* <Aside type="search" heading="SEARCH">
* <input type="search" />
* ...
* </Aside>
* ```
*/
export function Aside({
children,
heading,
type,
}: {
children?: React.ReactNode;
type: AsideType;
heading: React.ReactNode;
}) {
const {type: activeType, close} = useAside();
const expanded = type === activeType;
const id = useId();
useEffect(() => {
const abortController = new AbortController();
if (expanded) {
document.addEventListener(
'keydown',
function handler(event: KeyboardEvent) {
if (event.key === 'Escape') {
close();
}
},
{signal: abortController.signal},
);
}
return () => abortController.abort();
}, [close, expanded]);
return (
<div
aria-modal
className={`overlay ${expanded ? 'expanded' : ''}`}
role="dialog"
aria-labelledby={id}
>
<button className="close-outside" onClick={close} />
<aside data-aside-type={type}>
<header>
<h3 id={id}>{heading}</h3>
<button className="close reset" onClick={close} aria-label="Close">
&times;
</button>
</header>
<main>{children}</main>
</aside>
</div>
);
}
const AsideContext = createContext<AsideContextValue | null>(null);
Aside.Provider = function AsideProvider({children}: {children: ReactNode}) {
const [type, setType] = useState<AsideType>('closed');
return (
<AsideContext.Provider
value={{
type,
open: setType,
close: () => setType('closed'),
}}
>
{children}
</AsideContext.Provider>
);
};
export function useAside() {
const aside = useContext(AsideContext);
if (!aside) {
throw new Error('useAside must be used within an AsideProvider');
}
return aside;
}