102 lines
2.1 KiB
TypeScript
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">
|
|
×
|
|
</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;
|
|
}
|