UNPKG

@fluentui/react-northstar

Version:
343 lines (341 loc) 13 kB
import _invoke from "lodash/invoke"; import { dialogBehavior, getCode, keyboardKey } from '@fluentui/accessibility'; import { useAutoControlled, useTelemetry, useAccessibility, useStyles, useFluentContext, useUnhandledProps, getElementType } from '@fluentui/react-bindings'; import { Unstable_NestingAuto } from '@fluentui/react-component-nesting-registry'; import { EventListener } from '@fluentui/react-component-event-listener'; import { Ref } from '@fluentui/react-component-ref'; import * as customPropTypes from '@fluentui/react-proptypes'; import * as PropTypes from 'prop-types'; import * as React from 'react'; import { disableBodyScroll, enableBodyScroll } from 'body-scroll-lock'; import { commonPropTypes, doesNodeContainClick, getOrGenerateIdFromShorthand, createShorthand, createShorthandFactory } from '../../utils'; import { Button } from '../Button/Button'; import { ButtonGroup } from '../Button/ButtonGroup'; import { Box } from '../Box/Box'; import { Header } from '../Header/Header'; import { Portal } from '../Portal/Portal'; import { Flex } from '../Flex/Flex'; import { DialogFooter } from './DialogFooter'; export var dialogClassName = 'ui-dialog'; export var dialogSlotClassNames = { header: dialogClassName + "__header", headerAction: dialogClassName + "__headerAction", content: dialogClassName + "__content", overlay: dialogClassName + "__overlay", footer: dialogClassName + "__footer" }; /** * A Dialog displays important information on top of a page which requires a user's attention, confirmation, or interaction. * Dialogs are purposefully interruptive, so they should be used sparingly. * * @accessibility * Implements [ARIA Dialog (Modal)](https://www.w3.org/TR/wai-aria-practices-1.1/#dialog_modal) design pattern. * @accessibilityIssues * [NVDA narrates dialog title and button twice](https://github.com/nvaccess/nvda/issues/10003) * [NVDA does not recognize the ARIA 1.1 values of aria-haspopup](https://github.com/nvaccess/nvda/issues/8235) * [Jaws does not announce token values of aria-haspopup](https://github.com/FreedomScientific/VFO-standards-support/issues/33) * [Issue 989517: VoiceOver narrates dialog content and button twice](https://bugs.chromium.org/p/chromium/issues/detail?id=989517) */ export var Dialog = /*#__PURE__*/function () { var Dialog = /*#__PURE__*/React.forwardRef(function (props, ref) { var context = useFluentContext(); var _useTelemetry = useTelemetry(Dialog.displayName, context.telemetry), setStart = _useTelemetry.setStart, setEnd = _useTelemetry.setEnd; setStart(); var accessibility = props.accessibility, content = props.content, header = props.header, actions = props.actions, cancelButton = props.cancelButton, closeOnOutsideClick = props.closeOnOutsideClick, confirmButton = props.confirmButton, headerAction = props.headerAction, overlay = props.overlay, trapFocus = props.trapFocus, trigger = props.trigger, footer = props.footer, backdrop = props.backdrop, className = props.className, design = props.design, styles = props.styles, variables = props.variables; var ElementType = getElementType(props); var unhandledProps = useUnhandledProps(Dialog.handledProps, props); var contentRef = React.useRef(); var overlayRef = React.useRef(); var triggerRef = React.useRef(); var contentId = React.useRef(); contentId.current = getOrGenerateIdFromShorthand('dialog-content-', content, contentId.current); var headerId = React.useRef(); headerId.current = getOrGenerateIdFromShorthand('dialog-header-', header, headerId.current); var getA11yProps = useAccessibility(accessibility, { debugName: Dialog.displayName, actionHandlers: { closeAndFocusTrigger: function closeAndFocusTrigger(e) { handleDialogCancel(e); e.stopPropagation(); _invoke(triggerRef, 'current.focus'); }, close: function close(e) { return handleDialogCancel(e); } }, mapPropsToBehavior: function mapPropsToBehavior() { return { headerId: headerId.current, contentId: contentId.current, trapFocus: trapFocus, trigger: trigger }; }, rtl: context.rtl }); var _useStyles = useStyles(Dialog.displayName, { className: dialogClassName, mapPropsToStyles: function mapPropsToStyles() { return { backdrop: backdrop }; }, mapPropsToInlineStyles: function mapPropsToInlineStyles() { return { className: className, design: design, styles: styles, variables: variables }; }, rtl: context.rtl }), classes = _useStyles.classes, resolvedStyles = _useStyles.styles; var _useAutoControlled = useAutoControlled({ defaultValue: props.defaultOpen, value: props.open, initialValue: false }), open = _useAutoControlled[0], setOpen = _useAutoControlled[1]; React.useEffect(function () { var target = contentRef == null ? void 0 : contentRef.current; if (open) { disableBodyScroll(target); } return function () { if (open) { enableBodyScroll(target); } }; }, [open]); var handleDialogCancel = function handleDialogCancel(e) { _invoke(props, 'onCancel', e, Object.assign({}, props, { open: false })); setOpen(false); }; var handleDialogConfirm = function handleDialogConfirm(e) { _invoke(props, 'onConfirm', e, Object.assign({}, props, { open: false })); setOpen(false); }; var handleDialogOpen = function handleDialogOpen(e) { _invoke(props, 'onOpen', e, Object.assign({}, props, { open: true })); setOpen(true); }; var handleCancelButtonOverrides = function handleCancelButtonOverrides(predefinedProps) { return { onClick: function onClick(e, buttonProps) { _invoke(predefinedProps, 'onClick', e, buttonProps); handleDialogCancel(e); } }; }; var handleConfirmButtonOverrides = function handleConfirmButtonOverrides(predefinedProps) { return { onClick: function onClick(e, buttonProps) { _invoke(predefinedProps, 'onClick', e, buttonProps); handleDialogConfirm(e); } }; }; // when press left click on Dialog content and hold, and mouse up on Dialog overlay, Dialog should keep open var isMouseDownInsideContent = React.useRef(false); var registerMouseDownOnDialogContent = function registerMouseDownOnDialogContent(e) { if (e.button === 0) { isMouseDownInsideContent.current = true; } if (unhandledProps.onMouseDown) { _invoke(unhandledProps, 'onMouseDown', e); } }; var handleOverlayClick = function handleOverlayClick(e) { // Dialog has different conditions to close than Popup, so we don't need to iterate across all // refs var isInsideContentClick = isMouseDownInsideContent.current || doesNodeContainClick(contentRef.current, e, context.target); isMouseDownInsideContent.current = false; var isInsideOverlayClick = doesNodeContainClick(overlayRef.current, e, context.target); var shouldClose = !isInsideContentClick && isInsideOverlayClick; if (shouldClose) { handleDialogCancel(e); } }; var handleDocumentKeydown = function handleDocumentKeydown(getRefs) { return function (e) { // if focus was lost from Dialog, for e.g. when click on Dialog's content // and ESC is pressed, the opened Dialog should get closed and the trigger should get focus var lastOverlayRef = getRefs().pop(); var isLastOpenedDialog = lastOverlayRef && lastOverlayRef.current === overlayRef.current; var targetIsBody = e.target.nodeName === 'BODY'; if (targetIsBody && getCode(e) === keyboardKey.Escape && isLastOpenedDialog) { handleDialogCancel(e); _invoke(triggerRef, 'current.focus'); } }; }; var cancelElement = createShorthand(Button, cancelButton, { overrideProps: handleCancelButtonOverrides }); var confirmElement = createShorthand(Button, confirmButton, { defaultProps: function defaultProps() { return { primary: true }; }, overrideProps: handleConfirmButtonOverrides }); var dialogActions = (cancelElement || confirmElement) && ButtonGroup.create(actions, { defaultProps: function defaultProps() { return { styles: resolvedStyles.actions }; }, overrideProps: { content: /*#__PURE__*/React.createElement(Flex, { gap: "gap.smaller", hAlign: "end" }, cancelElement, confirmElement) } }); var dialogContent = /*#__PURE__*/React.createElement(Ref, { innerRef: contentRef }, /*#__PURE__*/React.createElement(ElementType, getA11yProps('popup', Object.assign({ className: classes.root, ref: ref }, unhandledProps, { onMouseDown: registerMouseDownOnDialogContent })), Header.create(header, { defaultProps: function defaultProps() { return getA11yProps('header', { as: 'h2', className: dialogSlotClassNames.header, styles: resolvedStyles.header }); } }), createShorthand(Button, headerAction, { defaultProps: function defaultProps() { return getA11yProps('headerAction', { className: dialogSlotClassNames.headerAction, styles: resolvedStyles.headerAction, text: true, iconOnly: true }); } }), Box.create(content, { defaultProps: function defaultProps() { return getA11yProps('content', { styles: resolvedStyles.content, className: dialogSlotClassNames.content }); } }), DialogFooter.create(footer, { overrideProps: { content: dialogActions, className: dialogSlotClassNames.footer, styles: resolvedStyles.footer } }))); var triggerAccessibility = { // refactor this when unstable_behaviorDefinition gets merged attributes: accessibility(props).attributes.trigger, keyHandlers: accessibility(props).keyActions.trigger }; var element = /*#__PURE__*/React.createElement(Portal, { onTriggerClick: handleDialogOpen, open: open, trapFocus: trapFocus, trigger: trigger, triggerAccessibility: triggerAccessibility, triggerRef: triggerRef }, /*#__PURE__*/React.createElement(Unstable_NestingAuto, null, function (getRefs, nestingRef) { return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Ref, { innerRef: function innerRef(contentNode) { overlayRef.current = contentNode; nestingRef.current = contentNode; } }, Box.create(overlay, { defaultProps: function defaultProps() { return { className: dialogSlotClassNames.overlay, styles: resolvedStyles.overlay }; }, overrideProps: { content: dialogContent } })), closeOnOutsideClick && /*#__PURE__*/React.createElement(EventListener, { listener: handleOverlayClick, target: context.target, type: "click", capture: true }), /*#__PURE__*/React.createElement(EventListener, { listener: handleDocumentKeydown(getRefs), target: context.target, type: "keydown", capture: true })); })); setEnd(); return element; }); Dialog.displayName = 'Dialog'; Dialog.propTypes = Object.assign({}, commonPropTypes.createCommon({ children: false, content: 'shorthand' }), { actions: customPropTypes.itemShorthand, backdrop: PropTypes.bool, headerAction: customPropTypes.itemShorthand, cancelButton: customPropTypes.itemShorthand, closeOnOutsideClick: PropTypes.bool, confirmButton: customPropTypes.itemShorthand, defaultOpen: PropTypes.bool, header: customPropTypes.itemShorthand, onCancel: PropTypes.func, onConfirm: PropTypes.func, onOpen: PropTypes.func, open: PropTypes.bool, overlay: customPropTypes.itemShorthand, trapFocus: PropTypes.oneOfType([PropTypes.bool, PropTypes.object]), trigger: PropTypes.any }); Dialog.defaultProps = { accessibility: dialogBehavior, actions: {}, backdrop: true, closeOnOutsideClick: true, overlay: {}, footer: {}, trapFocus: true }; Dialog.handledProps = Object.keys(Dialog.propTypes); Dialog.Footer = DialogFooter; Dialog.create = createShorthandFactory({ Component: Dialog }); return Dialog; }(); //# sourceMappingURL=Dialog.js.map