UNPKG

@hypothesis/frontend-shared

Version:

Shared components, styles and utilities for Hypothesis projects

198 lines (194 loc) 6.71 kB
var _jsxFileName = "/home/runner/work/frontend-shared/frontend-shared/src/components/feedback/Dialog.tsx"; import classnames from 'classnames'; import { Fragment } from 'preact'; import { useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from 'preact/hooks'; import { useClickAway } from '../../hooks/use-click-away'; import { useFocusAway } from '../../hooks/use-focus-away'; import { useKeyPress } from '../../hooks/use-key-press'; import { useSyncedRef } from '../../hooks/use-synced-ref'; import { downcastRef } from '../../util/typing'; import CloseableContext from '../CloseableContext'; import Panel from '../layout/Panel'; // This component forwards a number of props on to `Panel` but always sets the // `fullWidthHeader` prop to `true`. import { jsxDEV as _jsxDEV, Fragment as _Fragment } from "preact/jsx-dev-runtime"; function isPanelProps(props) { return props.variant !== 'custom'; } /** * Show a dialog */ export default function Dialog({ closeOnClickAway = false, closeOnEscape = false, closeOnFocusAway = false, children, initialFocus = 'auto', restoreFocus = false, transitionComponent: TransitionComponent, classes, elementRef, onClose, closeTitle, ...rest }) { const isPanel = isPanelProps(rest); const { buttons, icon, paddingSize = 'md', title = '', scrollable = true, ...htmlAttributes } = isPanel ? rest : { buttons: undefined, icon: undefined, paddingSize: undefined, title: undefined, scrollable: undefined, ...rest }; const modalRef = useSyncedRef(elementRef); const restoreFocusEl = useRef(document.activeElement); const [transitionComponentVisible, setTransitionComponentVisible] = useState(false); const closeHandler = useCallback(() => { if (TransitionComponent) { // When a TransitionComponent is provided, the actual "onClose" will be // called by that component, once the "out" transition has finished setTransitionComponentVisible(false); } else { onClose === null || onClose === void 0 || onClose(); } }, [onClose, TransitionComponent]); const initializeDialog = useCallback(() => { if (initialFocus === 'manual') { return; } if (initialFocus === 'auto') { var _modalRef$current; // An explicit `initialFocus` has not been set, so use automatic focus // handling. Modern accessibility guidance is to focus the dialog itself // rather than trying to be smart about focusing a particular control // within the dialog. (_modalRef$current = modalRef.current) === null || _modalRef$current === void 0 || _modalRef$current.focus(); return; } const focusEl = initialFocus === null || initialFocus === void 0 ? void 0 : initialFocus.current; if (focusEl && !focusEl.disabled) { focusEl.focus(); } else { var _modalRef$current2; // Fall back to focusing the modal itself (_modalRef$current2 = modalRef.current) === null || _modalRef$current2 === void 0 || _modalRef$current2.focus(); } }, [initialFocus, modalRef]); const onTransitionEnd = direction => { if (direction === 'in') { initializeDialog(); } else { onClose === null || onClose === void 0 || onClose(); } }; useClickAway(modalRef, closeHandler, { enabled: closeOnClickAway }); useKeyPress(['Escape'], closeHandler, { enabled: closeOnEscape }); useFocusAway(modalRef, closeHandler, { enabled: closeOnFocusAway }); const dialogDescriptionId = useId(); const Wrapper = useMemo(() => TransitionComponent !== null && TransitionComponent !== void 0 ? TransitionComponent : Fragment, [TransitionComponent]); useEffect(() => { setTransitionComponentVisible(true); if (!TransitionComponent) { initializeDialog(); } // We only want to run this effect once when the dialog is mounted. // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useLayoutEffect( /** * Restore focus when component is unmounted, if `restoreFocus` is set. */ () => { const restoreFocusTo = restoreFocusEl.current; return () => { if (restoreFocus && restoreFocusTo) { restoreFocusTo.focus(); } }; }, // We only want to run this effect once when the dialog is mounted. // eslint-disable-next-line react-hooks/exhaustive-deps []); useLayoutEffect( /** * Try to assign the dialog an accessible description, using the content of * the first paragraph of text within it. * * A limitation of this approach is that it doesn't update if the dialog's * content changes after the initial render. */ () => { var _modalRef$current3; const description = modalRef === null || modalRef === void 0 || (_modalRef$current3 = modalRef.current) === null || _modalRef$current3 === void 0 ? void 0 : _modalRef$current3.querySelector('p'); if (description) { description.id = dialogDescriptionId; modalRef.current.setAttribute('aria-describedby', dialogDescriptionId); } }, [dialogDescriptionId, modalRef]); // Provide a close handler to descendant components const closeableContext = { onClose: onClose ? closeHandler : undefined, title: closeTitle }; return _jsxDEV(CloseableContext.Provider, { value: closeableContext, children: _jsxDEV(Wrapper, { direction: transitionComponentVisible ? 'in' : 'out', onTransitionEnd: onTransitionEnd, children: _jsxDEV("div", { "data-component": "Dialog", tabIndex: -1 // NB: Role can be overridden with an HTML attribute; this is purposeful , role: "dialog", ...htmlAttributes, className: classnames( // Column-flex layout to constrain content to max-height 'flex flex-col', classes), ref: downcastRef(modalRef), children: isPanel ? _jsxDEV(Panel, { buttons: buttons, fullWidthHeader: true, icon: icon, paddingSize: paddingSize, title: title, scrollable: scrollable, children: children }, void 0, false, { fileName: _jsxFileName, lineNumber: 257, columnNumber: 13 }, this) : _jsxDEV(_Fragment, { children: children }, void 0, false) }, void 0, false, { fileName: _jsxFileName, lineNumber: 243, columnNumber: 9 }, this) }, void 0, false, { fileName: _jsxFileName, lineNumber: 239, columnNumber: 7 }, this) }, void 0, false, { fileName: _jsxFileName, lineNumber: 238, columnNumber: 5 }, this); } //# sourceMappingURL=Dialog.js.map