@procore/core-react
Version:
React library of Procore Design Guidelines
335 lines (322 loc) • 13.1 kB
JavaScript
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