@accessible/disclosure
Version:
🅰 An accessible and versatile disclosure component for React
284 lines (249 loc) • 9.37 kB
JavaScript
;
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';
}