UNPKG

@procore/core-react

Version:
335 lines (322 loc) • 13.1 kB
function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); } function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; } function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : i + ""; } function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } import { useId, useLabels } from '@react-aria/utils'; var emptyObj = {}; var returnEmpty = function returnEmpty() { return {}; }; var a11yPresets = { haspopup: { getOverlayProps: function getOverlayProps(_ref) { var role = _ref.role, overlayId = _ref.overlayId; return { id: overlayId, role: role }; }, getTriggerProps: function getTriggerProps(_ref2) { var role = _ref2.role, isVisible = _ref2.isVisible, overlayId = _ref2.overlayId; return { 'aria-expanded': isVisible, 'aria-controls': isVisible ? overlayId : undefined, 'aria-haspopup': role }; } }, tooltip: { getOverlayProps: function getOverlayProps(_ref3) { var overlayId = _ref3.overlayId; return { role: 'tooltip', id: overlayId }; }, getTriggerProps: function getTriggerProps(_ref4) { var isVisible = _ref4.isVisible, overlayId = _ref4.overlayId; return { 'aria-describedby': isVisible ? overlayId : undefined // 'aria-expanded': isVisible, }; } } }; export function getA11yPreset(role) { switch (role) { case 'alertdialog': case 'dialog': case 'listbox': case 'menu': return a11yPresets.haspopup; case 'tooltip': return a11yPresets.tooltip; case 'none': return { getOverlayProps: function () { return { role: 'none' }; }, getTriggerProps: returnEmpty }; default: return { getOverlayProps: returnEmpty, getTriggerProps: returnEmpty }; } } /** * Manages labelling for an element and the other DOM. Defaults an ID for `aria-labelledby` usage. * * When `aria-label` and `aria-labelledby` both exist, it combines them into `aria-labelledby` for a screen reader chain. * @link [W3 naming with aria-labelledby](https://www.w3.org/WAI/ARIA/apg/practices/names-and-descriptions/#naming_with_aria-labelledby) */ export function useLabelled(props) { var _ref5 = props || emptyObj, ariaDescribedby = _ref5['aria-describedby'], ariaDetails = _ref5['aria-details'], ariaLabelledby = _ref5['aria-labelledby'], ariaLabel = _ref5['aria-label'], id = _ref5.id; var ariaLabelOnly = ariaLabel && !ariaLabelledby; // Generate an ID. We want to use this unless they are using only aria-label var labelledId = useId(ariaLabelledby); // Merges aria-label and aria-labelledby into aria-labelledby when both exist var widgetId = useId(id); var fieldProps = useLabels({ 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelOnly ? undefined : labelledId, id: widgetId }); return { descriptionProps: { id: ariaDescribedby }, labelProps: { id: ariaLabelOnly ? undefined : labelledId }, widgetProps: _objectSpread(_objectSpread({}, fieldProps), {}, { 'aria-describedby': ariaDescribedby, 'aria-details': ariaDetails }) }; } /** * Cover the label links for the trigger (button), the popup element (dialog), and the popup element title (heading). * Similar to [React Aria useOverlayTrigger](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-aria/overlays/src/useOverlayTrigger.ts) * but with element title support. * @link [MDN aria-haspopup](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-haspopup) */ export function useLabelledPopup(_ref6) { var _getA11yPreset$getTri, _getA11yPreset, _getA11yPreset$getOve, _getA11yPreset2; var ariaDescribedby = _ref6['aria-describedby'], ariaDetails = _ref6['aria-details'], ariaLabelledby = _ref6['aria-labelledby'], ariaLabel = _ref6['aria-label'], id_ = _ref6.id, isOpen = _ref6.isOpen, _ref6$type = _ref6.type, type = _ref6$type === void 0 ? 'button' : _ref6$type, popupRole = _ref6.popupRole, popupId_ = _ref6.popupId; /** Web spec default for aria-haspopup=true is menu, unless element has role combobox, which have an implicit aria-haspopup value of listbox. */ // const popupRole = popupRole_ || type === 'combobox' ? 'listbox' : 'menu' var id = useId(id_); var popupId = useId(popupId_); var presetArgs = { isVisible: isOpen, role: popupRole, overlayId: popupId }; var triggerProps = (_getA11yPreset$getTri = (_getA11yPreset = getA11yPreset(popupRole)).getTriggerProps) === null || _getA11yPreset$getTri === void 0 ? void 0 : _getA11yPreset$getTri.call(_getA11yPreset, presetArgs); var overlayProps = (_getA11yPreset$getOve = (_getA11yPreset2 = getA11yPreset(popupRole)).getOverlayProps) === null || _getA11yPreset$getOve === void 0 ? void 0 : _getA11yPreset$getOve.call(_getA11yPreset2, presetArgs); var _useLabelled = useLabelled({ 'aria-describedby': ariaDescribedby, 'aria-details': ariaDetails, 'aria-labelledby': ariaLabelledby, 'aria-label': ariaLabel, id: id }), labelProps = _useLabelled.labelProps, widgetProps = _useLabelled.widgetProps; return { labelProps: labelProps, popupProps: _objectSpread(_objectSpread({ role: popupRole }, widgetProps), overlayProps), triggerProps: triggerProps }; } /** * For dialog experiences: * - Has role dialog and aria linked title props * - Focus management props to work with FocusScope * * For modal dialog experiences (full screen locked experiences): * - what dialog above does * - adds aria-modal="true" * - this hook does not determine where to mount or how many modals are open * * For either experiences, you still MUST support a way to close by mouse and keyboard. * * Using `aria-modal`, you need to keep additional overlay content inside, so * portals must be smart enough to know where other overlays * like tooltips or toasts should go to remain visible to screen readers (inside the aria-modal). * * Setting aria-modal="true" tells assistive technologies to let the user know the ability to interact with, * or access other content on the page requires the modal dialog to be closed or otherwise lose focus. * Modal dialogs are when content is displayed and the user's interaction is limited to only that section until it is dismissed. * [MDN aria modal](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-modal). */ export function useModalDialogLike(_ref7) { var ariaDescribedby = _ref7['aria-describedby'], ariaDetails = _ref7['aria-details'], ariaLabelledby = _ref7['aria-labelledby'], ariaLabel = _ref7['aria-label'], ariaModal = _ref7['aria-modal'], id = _ref7.id, isOpen = _ref7.isOpen, _ref7$role = _ref7.role, role = _ref7$role === void 0 ? 'dialog' : _ref7$role; var _useLabelled2 = useLabelled({ 'aria-describedby': ariaDescribedby, 'aria-details': ariaDetails, 'aria-labelledby': ariaLabelledby, 'aria-label': ariaLabel }), labelProps = _useLabelled2.labelProps, widgetProps = _useLabelled2.widgetProps; var dialogProps = _objectSpread(_objectSpread({ 'aria-modal': ariaModal }, widgetProps), {}, { id: id, role: role, tabIndex: -1 }); // usePreventScroll({ // isDisabled: ariaModal ? !isOpen : true, // }) // Fills aria-modal=true // import { ariaHideOutside } from '@react-aria/overlays' // React.useLayoutEffect(() => { // if (isModal && isOpen && ref.current) { // // Could add additional visible element refs here // return ariaHideOutside([ref.current]) // } // }, [isModal, isOpen, ref]) return { dialogProps: dialogProps, labelProps: labelProps, focusScopeProps: { autoFocus: true, contain: true, restoreFocus: true } }; } /** * Takes many roles and determines props necessary for DOM/components. * **Side effect of role="dialog"**, current and any content added later * outside of the element (like by portals) will get `aria-hidden=true` * to replace the `aria-modal=true` `inert` nature. * @see useModalDialogLike */ export function useOverlayTriggerA11y(_ref8) { var alwaysAriaVisible = _ref8.alwaysAriaVisible, ariaDescribedby = _ref8['aria-describedby'], ariaDetails = _ref8['aria-details'], ariaLabelledby = _ref8['aria-labelledby'], ariaLabel = _ref8['aria-label'], canPropOverlayUp = _ref8.canPropOverlayUp, id_ = _ref8.id, isOpen = _ref8.isOpen, role = _ref8.role; if ( // @ts-expect-error Checking for invalid usage role === 'alertdialog') { console.error('@procore/core-react: useOverlayTriggerA11y role alertdialog is not supported because it requires aria-modal to be true. This is intended for non-modal dialogs.'); } var isDialog = role === 'dialog'; var id = useId(id_); var _getA11yPreset3 = getA11yPreset(role), getTriggerProps = _getA11yPreset3.getTriggerProps, getOverlayProps = _getA11yPreset3.getOverlayProps; var triggerProps = getTriggerProps === null || getTriggerProps === void 0 ? void 0 : getTriggerProps({ isVisible: isOpen, role: role, overlayId: id }); var overlayA11yProps = getOverlayProps === null || getOverlayProps === void 0 ? void 0 : getOverlayProps({ isVisible: isOpen, role: role, overlayId: id }); var wrapperA11yProps = !canPropOverlayUp ? overlayA11yProps : emptyObj; var overlayProps = canPropOverlayUp ? overlayA11yProps : emptyObj; var _useModalDialogLike = useModalDialogLike({ 'aria-describedby': ariaDescribedby, 'aria-details': ariaDetails, 'aria-labelledby': ariaLabelledby, 'aria-label': ariaLabel, 'aria-modal': undefined, // OverlayTrigger only supports non-modal dialogs id: id, isOpen: isOpen }), dialogProps = _useModalDialogLike.dialogProps, labelProps = _useModalDialogLike.labelProps, focusScopeProps = _useModalDialogLike.focusScopeProps; /** This is a code side-effect from ariaHideOutside hiding everthing */ var portalProps = role === 'tooltip' || alwaysAriaVisible // || !role ? { 'data-react-aria-top-layer': true, 'data-live-announcer': true } : emptyObj; /** * If it is a dialog, we can merge the dialog props with any haspopup props */ var wrapperFinalProps = isDialog ? _objectSpread(_objectSpread({}, wrapperA11yProps), dialogProps) : wrapperA11yProps; /** * If it is a dialog, we have opinions on `FocusScope` props. */ var focusScopeFinalProps = isDialog ? focusScopeProps : emptyObj; return { focusScopeProps: focusScopeFinalProps, labelProps: labelProps, overlayProps: overlayProps, portalProps: portalProps, triggerProps: triggerProps, wrapperProps: wrapperFinalProps }; } // WIP Example combobox // function useCombox({ // controls, // isOpen = false, // }: { // controls: 'dialog' | 'menu' | 'listbox' // isOpen: boolean // }) { // const { labelProps, popupProps, triggerProps } = useLabelledPopup({ // role: controls, // type: 'combobox', // isOpen, // }) // dispatch between elements. // NOTE combobox has two labels, that could be different // one for the trigger input (typically the form label) // second for the thing it controls (listbox, dialog) // - label id=1 // - input role='combobox' aria-labelledby=1 // - div role={controls} aria-label // Also, any icon only buttons like clear or open should have a // label and -1 tabindex. No nested interactive roles. // } //# sourceMappingURL=a11yPresets.js.map