UNPKG

@accessible/disclosure

Version:

🅰 An accessible and versatile disclosure component for React

284 lines (249 loc) • 9.37 kB
"use strict"; exports.__esModule = true; exports.useDisclosure = useDisclosure; exports.Disclosure = Disclosure; exports.useA11yTarget = useA11yTarget; exports.Target = Target; exports.useA11yCloseButton = useA11yCloseButton; exports.CloseButton = CloseButton; exports.useA11yTrigger = useA11yTrigger; exports.Trigger = Trigger; var React = /*#__PURE__*/_interopRequireWildcard( /*#__PURE__*/require("react")); var _useKey = /*#__PURE__*/_interopRequireDefault( /*#__PURE__*/require("@accessible/use-key")); var _useConditionalFocus = /*#__PURE__*/_interopRequireDefault( /*#__PURE__*/require("@accessible/use-conditional-focus")); var _switch = /*#__PURE__*/_interopRequireDefault( /*#__PURE__*/require("@react-hook/switch")); var _mergedRef = /*#__PURE__*/_interopRequireDefault( /*#__PURE__*/require("@react-hook/merged-ref")); var _previous = /*#__PURE__*/_interopRequireDefault( /*#__PURE__*/require("@react-hook/previous")); var _useId = /*#__PURE__*/_interopRequireDefault( /*#__PURE__*/require("@accessible/use-id")); var _button = /*#__PURE__*/require("@accessible/button"); var _reactPortalize = /*#__PURE__*/_interopRequireDefault( /*#__PURE__*/require("react-portalize")); var _clsx = /*#__PURE__*/_interopRequireDefault( /*#__PURE__*/require("clsx")); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function () { return cache; }; return cache; } function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } const __reactCreateElement__ = React.createElement; const DisclosureContext = /*#__PURE__*/React.createContext({ isOpen: false, open: noop, close: noop, toggle: noop }); /** * This hook provides the current value of the disclosure's context object */ function useDisclosure() { return React.useContext(DisclosureContext); } /** * This component creates the context for your disclosure target and trigger * and contains some configuration options. */ function Disclosure({ id, open, defaultOpen, onChange = noop, children }) { id = (0, _useId.default)(id); const [isOpen, toggle] = (0, _switch.default)(defaultOpen, open, onChange); const context = React.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; const props = { children: Component }; if (typeof portal === 'string') props.container = portal;else Object.assign(props, portal); return __reactCreateElement__(_reactPortalize.default, 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 = {}) { const { preventScroll, closeOnEscape = true, openClass, closedClass, openStyle, closedStyle } = options; const { id, isOpen, close } = useDisclosure(); const prevOpen = (0, _previous.default)(isOpen); // Provides the target focus when it is in a new open state (0, _useConditionalFocus.default)(target, !prevOpen && isOpen, { includeRoot: true, preventScroll }); // Handles closing the modal when the ESC key is pressed (0, _useKey.default)(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({ closeOnEscape = true, portal, openClass, closedClass, openStyle, closedStyle, preventScroll, children }) { const ref = React.useRef(null); const childProps = children.props; const a11yProps = useA11yTarget(ref, { openClass: (0, _clsx.default)(childProps.className, openClass) || void 0, closedClass: (0, _clsx.default)(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__*/React.cloneElement(children, Object.assign(a11yProps, { ref: (0, _mergedRef.default)(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, { onClick } = {}) { const { close, isOpen, id } = useDisclosure(); return Object.assign({ 'aria-controls': id, 'aria-expanded': isOpen, 'aria-label': 'Close' }, (0, _button.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({ children }) { const ref = React.useRef(null); const childProps = children.props; const a11yProps = useA11yCloseButton(ref, { onClick: childProps.onClick }); return /*#__PURE__*/React.cloneElement(children, Object.assign(a11yProps, { onClick: undefined, 'aria-label': childProps.hasOwnProperty('aria-label') ? childProps['aria-label'] : a11yProps['aria-label'], ref: (0, _mergedRef.default)(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 = {}) { const { openClass, closedClass, openStyle, closedStyle, onClick } = options; const { isOpen, id, toggle } = useDisclosure(); const prevOpen = (0, _previous.default)(isOpen); (0, _useConditionalFocus.default)(target, prevOpen && !isOpen, { includeRoot: true }); return Object.assign({ 'aria-controls': id, 'aria-expanded': isOpen, className: isOpen ? openClass : closedClass, style: isOpen ? openStyle : closedStyle }, (0, _button.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({ openClass, closedClass, openStyle, closedStyle, children }) { const ref = React.useRef(null); const childProps = children.props; const a11yProps = useA11yTrigger(ref, { openClass: (0, _clsx.default)(childProps.className, openClass) || void 0, closedClass: (0, _clsx.default)(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__*/React.cloneElement(children, Object.assign(a11yProps, { onClick: undefined, ref: (0, _mergedRef.default)(ref, // @ts-expect-error children.ref) })); } function noop() {} /* istanbul ignore next */ if (typeof process !== 'undefined' && process.env.NODE_ENV !== 'production') { Disclosure.displayName = 'Disclosure'; Target.displayName = 'Target'; Trigger.displayName = 'Trigger'; CloseButton.displayName = 'CloseButton'; }