UNPKG

@accessible/disclosure

Version:

🅰 An accessible and versatile disclosure component for React

268 lines (249 loc) • 7.66 kB
import { useContext, useMemo, useRef, cloneElement, createElement, createContext } 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 clsx from 'clsx'; var __reactCreateElement__ = createElement; var DisclosureContext = /*#__PURE__*/createContext({ isOpen: false, open: noop, close: noop, toggle: noop }); /** * This hook provides the current value of the disclosure's context object */ function useDisclosure() { return useContext(DisclosureContext); } /** * This component creates the context for your disclosure target and trigger * and contains some configuration options. */ function Disclosure(_ref) { var { id, open, defaultOpen, onChange = noop, children } = _ref; id = useId(id); var [isOpen, toggle] = useSwitch(defaultOpen, open, onChange); var context = useMemo(() => ({ id, open: toggle.on, close: toggle.off, toggle, isOpen }), [id, isOpen, toggle]); return /*#__PURE__*/__reactCreateElement__(DisclosureContext.Provider, { value: context }, children); } function portalize(Component, portal) { if (!portal) return Component; var props = { children: Component }; if (typeof portal === 'string') props.container = portal;else Object.assign(props, portal); return __reactCreateElement__(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 */ function useA11yTarget(target, options) { if (options === void 0) { options = {}; } var { preventScroll, closeOnEscape = true, openClass, closedClass, openStyle, closedStyle } = options; var { id, isOpen, close } = useDisclosure(); var 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' }, isOpen ? openStyle : closedStyle) }; } /** * This component wraps any React element and turns it into a * disclosure target. */ function Target(_ref2) { var { closeOnEscape = true, portal, openClass, closedClass, openStyle, closedStyle, preventScroll, children } = _ref2; var ref = useRef(null); var childProps = children.props; var 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( /*#__PURE__*/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 <button> elements * and fake ones e.g. <a> and <div> to the target element. * * @param target A React ref or HTML element * @param options Configuration options */ function useA11yCloseButton(target, _temp) { var { onClick } = _temp === void 0 ? {} : _temp; var { close, isOpen, id } = useDisclosure(); return Object.assign({ 'aria-controls': id, 'aria-expanded': isOpen, 'aria-label': 'Close' }, useA11yButton(target, e => { close(); onClick === null || onClick === void 0 ? void 0 : onClick(e); })); } /** * This is a convenience component that wraps any React element and adds * an onClick handler which closes the disclosure. */ function CloseButton(_ref3) { var { children } = _ref3; var ref = useRef(null); var childProps = children.props; var a11yProps = useA11yCloseButton(ref, { onClick: childProps.onClick }); return /*#__PURE__*/cloneElement(children, Object.assign(a11yProps, { onClick: undefined, 'aria-label': childProps.hasOwnProperty('aria-label') ? childProps['aria-label'] : a11yProps['aria-label'], ref: useMergedRef(ref, // @ts-expect-error children.ref) })); } /** * 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). * In addition to providing accessibility props to your component, this * hook will add events for interoperability between actual <button> elements * and fake ones e.g. <a> and <div> to the target element * * @param target A React ref or HTML element * @param options Configuration options */ function useA11yTrigger(target, options) { if (options === void 0) { options = {}; } var { openClass, closedClass, openStyle, closedStyle, onClick } = options; var { isOpen, id, toggle } = useDisclosure(); var prevOpen = usePrevious(isOpen); useConditionalFocus(target, prevOpen && !isOpen, { includeRoot: true }); return Object.assign({ 'aria-controls': id, 'aria-expanded': isOpen, className: isOpen ? openClass : closedClass, style: isOpen ? openStyle : closedStyle }, useA11yButton(target, e => { toggle(); onClick === null || onClick === void 0 ? void 0 : onClick(e); })); } /** * This component wraps any React element and adds an `onClick` handler * which toggles the open state of the disclosure target. */ function Trigger(_ref4) { var { openClass, closedClass, openStyle, closedStyle, children } = _ref4; var ref = useRef(null); var childProps = children.props; var a11yProps = useA11yTrigger(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, onClick: childProps.onClick }); return /*#__PURE__*/cloneElement(children, Object.assign(a11yProps, { onClick: undefined, ref: useMergedRef(ref, // @ts-expect-error children.ref) })); } function noop() {} /* istanbul ignore next */ if (typeof process !== 'undefined' && "production" !== 'production') { Disclosure.displayName = 'Disclosure'; Target.displayName = 'Target'; Trigger.displayName = 'Trigger'; CloseButton.displayName = 'CloseButton'; } export { CloseButton, Disclosure, Target, Trigger, useA11yCloseButton, useA11yTarget, useA11yTrigger, useDisclosure }; //# sourceMappingURL=index.dev.mjs.map