@accessible/disclosure
Version:
🅰 An accessible and versatile disclosure component for React
1 lines • 15.7 kB
Source Map (JSON)
{"version":3,"file":"index.mjs","sources":["../../src/index.tsx"],"sourcesContent":["import * as React from 'react'\nimport useKey from '@accessible/use-key'\nimport useConditionalFocus from '@accessible/use-conditional-focus'\nimport useSwitch from '@react-hook/switch'\nimport useMergedRef from '@react-hook/merged-ref'\nimport usePrevious from '@react-hook/previous'\nimport useId from '@accessible/use-id'\nimport {useA11yButton} from '@accessible/button'\nimport Portalize from 'react-portalize'\nimport type {PortalizeProps} from 'react-portalize'\nimport clsx from 'clsx'\n\nconst DisclosureContext = React.createContext<DisclosureContextValue>({\n isOpen: false,\n open: noop,\n close: noop,\n toggle: noop,\n})\n\n/**\n * This hook provides the current value of the disclosure's context object\n */\nexport function useDisclosure() {\n return React.useContext(DisclosureContext)\n}\n\n/**\n * This component creates the context for your disclosure target and trigger\n * and contains some configuration options.\n */\nexport function Disclosure({\n id,\n open,\n defaultOpen,\n onChange = noop,\n children,\n}: DisclosureProps) {\n id = useId(id)\n const [isOpen, toggle] = useSwitch(defaultOpen, open, onChange)\n\n const context = React.useMemo(\n () => ({\n id,\n open: toggle.on,\n close: toggle.off,\n toggle,\n isOpen,\n }),\n [id, isOpen, toggle]\n )\n\n return (\n <DisclosureContext.Provider value={context}>\n {children}\n </DisclosureContext.Provider>\n )\n}\n\nfunction portalize(\n Component: React.ReactElement,\n portal: boolean | undefined | null | string | Omit<PortalizeProps, 'children'>\n) {\n if (!portal) return Component\n const props: PortalizeProps = {children: Component}\n if (typeof portal === 'string') props.container = portal\n else Object.assign(props, portal)\n return React.createElement(Portalize, props)\n}\n\n/**\n * A React hook for creating a headless disclosure target to [WAI-ARIA authoring practices](https://www.w3.org/TR/wai-aria-practices-1.1/examples/disclosure/disclosure-faq.html).\n *\n * @param target A React ref or HTML element\n * @param options Configuration options\n */\nexport function useA11yTarget<T extends HTMLElement>(\n target: React.RefObject<T> | T | null,\n options: UseA11yTargetOptions = {}\n) {\n const {\n preventScroll,\n closeOnEscape = true,\n openClass,\n closedClass,\n openStyle,\n closedStyle,\n } = options\n const {id, isOpen, close} = useDisclosure()\n const prevOpen = usePrevious(isOpen)\n // Provides the target focus when it is in a new open state\n useConditionalFocus(target, !prevOpen && isOpen, {\n includeRoot: true,\n preventScroll,\n })\n // Handles closing the modal when the ESC key is pressed\n useKey(target, {Escape: () => closeOnEscape && close()})\n\n return {\n 'aria-hidden': !isOpen,\n id,\n className: isOpen ? openClass : closedClass,\n style: Object.assign(\n {visibility: isOpen ? 'visible' : 'hidden'} as const,\n isOpen ? openStyle : closedStyle\n ),\n } as const\n}\n\n/**\n * This component wraps any React element and turns it into a\n * disclosure target.\n */\nexport function Target({\n closeOnEscape = true,\n portal,\n openClass,\n closedClass,\n openStyle,\n closedStyle,\n preventScroll,\n children,\n}: TargetProps) {\n const ref = React.useRef<HTMLElement>(null)\n const childProps = children.props\n const a11yProps = useA11yTarget(ref, {\n openClass: clsx(childProps.className, openClass) || void 0,\n closedClass: clsx(childProps.className, closedClass) || void 0,\n openStyle: childProps.style\n ? Object.assign({}, childProps.style, openStyle)\n : openStyle,\n closedStyle: childProps.style\n ? Object.assign({}, childProps.style, closedStyle)\n : closedStyle,\n closeOnEscape,\n preventScroll,\n })\n\n return portalize(\n React.cloneElement(\n children,\n Object.assign(a11yProps, {\n ref: useMergedRef(\n ref,\n // @ts-expect-error\n children.ref\n ),\n })\n ),\n portal\n )\n}\n\n/**\n * A React hook for creating a headless close button to [WAI-ARIA authoring practices](https://www.w3.org/TR/wai-aria-practices-1.1/examples/disclosure/disclosure-faq.html).\n * In addition to providing accessibility props to your component, this\n * hook will add events for interoperability between actual <button> elements\n * and fake ones e.g. <a> and <div> to the target element.\n *\n * @param target A React ref or HTML element\n * @param options Configuration options\n */\nexport function useA11yCloseButton<T extends HTMLElement>(\n target: React.RefObject<T> | T | null,\n {onClick}: UseA11yCloseButtonOptions = {}\n) {\n const {close, isOpen, id} = useDisclosure()\n return Object.assign(\n {\n 'aria-controls': id,\n 'aria-expanded': isOpen,\n 'aria-label': 'Close',\n } as const,\n useA11yButton<T>(target, (e) => {\n close()\n onClick?.(e)\n })\n )\n}\n\n/**\n * This is a convenience component that wraps any React element and adds\n * an onClick handler which closes the disclosure.\n */\nexport function CloseButton({children}: CloseButtonProps) {\n const ref = React.useRef<HTMLElement>(null)\n const childProps = children.props\n const a11yProps = useA11yCloseButton(ref, {\n onClick: childProps.onClick,\n })\n\n return React.cloneElement(\n children,\n Object.assign(a11yProps, {\n onClick: undefined,\n 'aria-label': childProps.hasOwnProperty('aria-label')\n ? childProps['aria-label']\n : a11yProps['aria-label'],\n ref: useMergedRef(\n ref,\n // @ts-expect-error\n children.ref\n ),\n })\n )\n}\n\n/**\n * A React hook for creating a headless disclosure trigger to [WAI-ARIA authoring practices](https://www.w3.org/TR/wai-aria-practices-1.1/examples/disclosure/disclosure-faq.html).\n * In addition to providing accessibility props to your component, this\n * hook will add events for interoperability between actual <button> elements\n * and fake ones e.g. <a> and <div> to the target element\n *\n * @param target A React ref or HTML element\n * @param options Configuration options\n */\nexport function useA11yTrigger<T extends HTMLElement>(\n target: React.RefObject<T> | T | null,\n options: UseA11yTriggerOptions = {}\n) {\n const {openClass, closedClass, openStyle, closedStyle, onClick} = options\n const {isOpen, id, toggle} = useDisclosure()\n const prevOpen = usePrevious(isOpen)\n useConditionalFocus(target, prevOpen && !isOpen, {includeRoot: true})\n\n return Object.assign(\n {\n 'aria-controls': id,\n 'aria-expanded': isOpen,\n className: isOpen ? openClass : closedClass,\n style: isOpen ? openStyle : closedStyle,\n } as const,\n useA11yButton<T>(target, (e) => {\n toggle()\n onClick?.(e)\n })\n )\n}\n\n/**\n * This component wraps any React element and adds an `onClick` handler\n * which toggles the open state of the disclosure target.\n */\nexport function Trigger({\n openClass,\n closedClass,\n openStyle,\n closedStyle,\n children,\n}: TriggerProps) {\n const ref = React.useRef<HTMLElement>(null)\n const childProps = children.props\n const a11yProps = useA11yTrigger(ref, {\n openClass: clsx(childProps.className, openClass) || void 0,\n closedClass: clsx(childProps.className, closedClass) || void 0,\n openStyle: childProps.style\n ? Object.assign({}, childProps.style, openStyle)\n : openStyle,\n closedStyle: childProps.style\n ? Object.assign({}, childProps.style, closedStyle)\n : closedStyle,\n onClick: childProps.onClick,\n })\n\n return React.cloneElement(\n children,\n Object.assign(a11yProps, {\n onClick: undefined,\n ref: useMergedRef(\n ref,\n // @ts-expect-error\n children.ref\n ),\n })\n )\n}\n\nfunction noop() {}\n\nexport interface DisclosureContextValue {\n /**\n * The open state of the disclosure\n */\n isOpen: boolean\n /**\n * Opens the disclosure\n */\n open: () => void\n /**\n * Closes the disclosure\n */\n close: () => void\n /**\n * Toggles the open state of the disclosure\n */\n toggle: () => void\n /**\n * A unique ID for the disclosure target\n */\n id?: string\n}\n\nexport interface DisclosureProps {\n /**\n * This creates a controlled disclosure component where the open state of the\n * disclosure is controlled by this property.\n */\n open?: boolean\n /**\n * This sets the default open state of the disclosure. By default the disclosure\n * is closed.\n * @default false\n */\n defaultOpen?: boolean\n /**\n * By default this component creates a unique id for you, as it is required\n * for certain aria attributes. Supplying an id here overrides the auto id feature.\n */\n id?: string\n /**\n * This callback is invoked any time the `open` state of the disclosure changes.\n */\n onChange?: (open: boolean) => void\n /**\n * By default this component creates a unique id for you, as it is required for\n * certain aria attributes. Supplying an id here overrides the auto id feature.\n */\n children: React.ReactNode\n}\n\nexport interface UseA11yTriggerOptions {\n /**\n * Adds this class name to props when the disclosure is open\n */\n openClass?: string\n /**\n * Adds this class name to props when the disclosure is closed\n */\n closedClass?: string\n /**\n * Adds this style to props when the disclosure is open\n */\n openStyle?: React.CSSProperties\n /**\n * Adds this style to props when the disclosure is closed\n */\n closedStyle?: React.CSSProperties\n /**\n * Adds an onClick handler in addition to the default one that\n * toggles the disclosure's open state.\n */\n onClick?: (e: MouseEvent) => any\n}\n\nexport interface TriggerProps extends Omit<UseA11yTriggerOptions, 'onClick'> {\n /**\n * The child is cloned by this component and has aria attributes injected\n * into its props as well as the events defined above.\n */\n children: JSX.Element | React.ReactElement\n}\n\nexport interface UseA11yTargetOptions {\n /**\n * Adds this class name to props when the disclosure is open\n */\n openClass?: string\n /**\n * Adds this class name to props when the disclosure is closed\n */\n closedClass?: string\n /**\n * Adds this style to props when the disclosure is open\n */\n openStyle?: React.CSSProperties\n /**\n * Adds this style to props when the disclosure is closed\n */\n closedStyle?: React.CSSProperties\n /**\n * Prevents the `window` from scrolling when the target is\n * focused after opening.\n */\n preventScroll?: boolean\n /**\n * When `true`, this closes the target element when the `Escape`\n * key is pressed.\n * @default true\n */\n closeOnEscape?: boolean\n}\n\nexport interface TargetProps extends UseA11yTargetOptions {\n /**\n * When `true` this will render the disclosure into a React portal with the\n * id `#portals`. You can render it into any portal by providing its query\n * selector here, e.g. `#foobar`, `[data-portal=true]`, or `.foobar`.\n * @default false\n */\n portal?:\n | boolean\n | undefined\n | null\n | string\n | Omit<PortalizeProps, 'children'>\n /**\n * The child is cloned by this component and has aria attributes injected into its\n * props as well events.\n */\n children: JSX.Element | React.ReactElement\n}\n\nexport interface UseA11yCloseButtonOptions {\n /**\n * Adds an onClick handler in addition to the default one that\n * closes the disclosure.\n */\n onClick?: (e: MouseEvent) => any\n}\n\nexport interface CloseButtonProps {\n /**\n * The child is cloned by this component and has aria attributes injected into its\n * props as well events.\n */\n children: JSX.Element | React.ReactElement\n}\n\n/* istanbul ignore next */\nif (typeof process !== 'undefined' && process.env.NODE_ENV !== 'production') {\n Disclosure.displayName = 'Disclosure'\n Target.displayName = 'Target'\n Trigger.displayName = 'Trigger'\n CloseButton.displayName = 'CloseButton'\n}\n"],"names":["useDisclosure","React","DisclosureContext","Disclosure","id","open","defaultOpen","onChange","noop","children","useId","isOpen","toggle","useSwitch","context","on","close","off","__reactCreateElement__","Provider","value","useA11yTarget","target","options","preventScroll","closeOnEscape","openClass","closedClass","openStyle","closedStyle","prevOpen","usePrevious","useConditionalFocus","includeRoot","useKey","Escape","className","style","Object","assign","visibility","Target","portal","ref","childProps","props","a11yProps","clsx","Component","container","Portalize","portalize","useMergedRef","useA11yCloseButton","onClick","useA11yButton","e","CloseButton","undefined","hasOwnProperty","useA11yTrigger","Trigger"],"mappings":"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"}