@fluentui/react-northstar
Version:
A themable React component library.
343 lines (341 loc) • 13 kB
JavaScript
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