import * as React from 'react' import useKey from '@accessible/use-key' import useConditionalFocus from '@accessible/use-conditional-focus' import useSwitch from '@react-hook/switch' import useMergedRef from '@react-hook/merged-ref' import usePrevious from '@react-hook/previous' import useId from '@accessible/use-id' import {useA11yButton} from '@accessible/button' import Portalize from 'react-portalize' import type {PortalizeProps} from 'react-portalize' import clsx from 'clsx' const DisclosureContext = React.createContext({ isOpen: false, open: noop, close: noop, toggle: noop, }) /** * This hook provides the current value of the disclosure's context object */ export function useDisclosure() { return React.useContext(DisclosureContext) } /** * This component creates the context for your disclosure target and trigger * and contains some configuration options. */ export function Disclosure({ id, open, defaultOpen, onChange = noop, children, }: DisclosureProps) { id = useId(id) const [isOpen, toggle] = useSwitch(defaultOpen, open, onChange) const context = React.useMemo( () => ({ id, open: toggle.on, close: toggle.off, toggle, isOpen, }), [id, isOpen, toggle] ) return ( {children} ) } function portalize( Component: React.ReactElement, portal: boolean | undefined | null | string | Omit ) { if (!portal) return Component const props: PortalizeProps = {children: Component} if (typeof portal === 'string') props.container = portal else Object.assign(props, portal) return React.createElement(Portalize, props) } /** * 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). * * @param target A React ref or HTML element * @param options Configuration options */ export function useA11yTarget( target: React.RefObject | T | null, options: UseA11yTargetOptions = {} ) { const { preventScroll, closeOnEscape = true, openClass, closedClass, openStyle, closedStyle, } = options const {id, isOpen, close} = useDisclosure() const prevOpen = usePrevious(isOpen) // Provides the target focus when it is in a new open state useConditionalFocus(target, !prevOpen && isOpen, { includeRoot: true, preventScroll, }) // Handles closing the modal when the ESC key is pressed useKey(target, {Escape: () => closeOnEscape && close()}) return { 'aria-hidden': !isOpen, id, className: isOpen ? openClass : closedClass, style: Object.assign( {visibility: isOpen ? 'visible' : 'hidden'} as const, isOpen ? openStyle : closedStyle ), } as const } /** * This component wraps any React element and turns it into a * disclosure target. */ export function Target({ closeOnEscape = true, portal, openClass, closedClass, openStyle, closedStyle, preventScroll, children, }: TargetProps) { const ref = React.useRef(null) const childProps = children.props const a11yProps = useA11yTarget(ref, { openClass: clsx(childProps.className, openClass) || void 0, closedClass: clsx(childProps.className, closedClass) || void 0, openStyle: childProps.style ? Object.assign({}, childProps.style, openStyle) : openStyle, closedStyle: childProps.style ? Object.assign({}, childProps.style, closedStyle) : closedStyle, closeOnEscape, preventScroll, }) return portalize( React.cloneElement( children, Object.assign(a11yProps, { ref: useMergedRef( ref, // @ts-expect-error children.ref ), }) ), portal ) } /** * 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). * In addition to providing accessibility props to your component, this * hook will add events for interoperability between actual