@fluentui/react-northstar
Version:
A themable React component library.
578 lines (567 loc) • 24.4 kB
JavaScript
import _includes from "lodash/includes";
import _isArray from "lodash/isArray";
import _some from "lodash/some";
import _invoke from "lodash/invoke";
import { popupBehavior, getCode, keyboardKey, SpacebarKey } from '@fluentui/accessibility';
import { useAccessibility, useAutoControlled, useTelemetry, useFluentContext, useTriggerElement, useOnIFrameFocus } from '@fluentui/react-bindings';
import { EventListener } from '@fluentui/react-component-event-listener';
import { Unstable_NestingAuto } from '@fluentui/react-component-nesting-registry';
import { handleRef, 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 { elementContains, setVirtualParent } from '@fluentui/dom-utilities';
import { commonPropTypes, isFromKeyboard, doesNodeContainClick, setWhatInputSource } from '../../utils';
import { ALIGNMENTS, POSITIONS, createReferenceFromClick, Popper, AUTOSIZES } from '../../utils/positioner';
import { PopupContent } from './PopupContent';
import { createShorthandFactory } from '../../utils/factories';
import { isRightClick } from '../../utils/isRightClick';
import { PortalInner } from '../Portal/PortalInner';
import { Animation } from '../Animation/Animation';
function getRealEventProps(element) {
if (element.type === Ref) {
return getRealEventProps(element.props.children);
}
return Object.keys(element.props).reduce(function (acc, propName) {
var _Object$assign;
return propName.startsWith('on') ? Object.assign({}, acc, (_Object$assign = {}, _Object$assign[propName] = element.props[propName], _Object$assign)) : acc;
}, {});
}
export var popupClassName = 'ui-popup';
/**
* A Popup displays a non-modal, often rich content, on top of its target element.
*/
export var Popup = /*#__PURE__*/function () {
var Popup = function Popup(props) {
var context = useFluentContext();
var _useTelemetry = useTelemetry(Popup.displayName, context.telemetry),
setStart = _useTelemetry.setStart,
setEnd = _useTelemetry.setEnd;
setStart();
var accessibility = props.accessibility,
align = props.align,
autoFocus = props.autoFocus,
inline = props.inline,
contentRef = props.contentRef,
flipBoundary = props.flipBoundary,
on = props.on,
mountNode = props.mountNode,
mouseLeaveDelay = props.mouseLeaveDelay,
offset = props.offset,
overflowBoundary = props.overflowBoundary,
pointing = props.pointing,
popperRef = props.popperRef,
position = props.position,
positionFixed = props.positionFixed,
renderContent = props.renderContent,
tabbableTrigger = props.tabbableTrigger,
target = props.target,
trapFocus = props.trapFocus,
trigger = props.trigger,
unstable_disableTether = props.unstable_disableTether,
unstable_pinned = props.unstable_pinned,
autoSize = props.autoSize,
closeOnScroll = props.closeOnScroll;
var _useAutoControlled = useAutoControlled({
initialValue: false,
defaultValue: props.defaultOpen,
value: props.open
}),
open = _useAutoControlled[0],
setOpen = _useAutoControlled[1];
var _React$useState = React.useState(false),
isOpenedByRightClick = _React$useState[0],
setIsOpenedByRightClick = _React$useState[1];
var closeTimeoutId = React.useRef();
var mouseDownEventRef = React.useRef();
var popupContentRef = React.useRef();
var pointerTargetRef = React.useRef();
var triggerRef = React.useRef();
// focusable element which has triggered Popup, can be either triggerDomElement or the element inside it
var triggerFocusableRef = React.useRef();
var rightClickReferenceObject = React.useRef();
useOnIFrameFocus(open, context.target, function (e) {
var iframeInsidePopup = elementContains(popupContentRef.current, e.target);
if (iframeInsidePopup) {
return;
}
setOpen(function (__) {
_invoke(props, 'onOpenChange', e, Object.assign({}, props, {
open: false
}));
return false;
});
});
var getA11yProps = useAccessibility(accessibility, {
debugName: Popup.displayName,
actionHandlers: {
closeAndFocusTrigger: function closeAndFocusTrigger(e) {
e.preventDefault();
close(e, function () {
return _invoke(triggerFocusableRef.current, 'focus');
});
},
close: function (_close) {
function close(_x) {
return _close.apply(this, arguments);
}
close.toString = function () {
return _close.toString();
};
return close;
}(function (e) {
close(e);
}),
toggle: function toggle(e) {
e.preventDefault();
trySetOpen(!open, e);
},
open: function open(e) {
e.preventDefault();
setPopupOpen(true, e);
},
click: function click(e) {
_invoke(triggerRef.current, 'click');
},
preventScroll: function preventScroll(e) {
e.preventDefault();
},
stopPropagation: function stopPropagation(e) {
e.stopPropagation();
}
},
mapPropsToBehavior: function mapPropsToBehavior() {
return {
isOpenedByRightClick: isOpenedByRightClick,
on: on,
trapFocus: trapFocus,
tabbableTrigger: tabbableTrigger,
trigger: trigger,
inline: inline
};
},
rtl: context.rtl
});
var handleDocumentClick = function handleDocumentClick(getRefs) {
return function (e) {
var currentMouseDownEvent = mouseDownEventRef.current;
mouseDownEventRef.current = null;
if (currentMouseDownEvent && !isOutsidePopupElement(getRefs(), currentMouseDownEvent)) {
return;
}
if (isOpenedByRightClick && isOutsidePopupElement(getRefs(), e)) {
trySetOpen(false, e);
rightClickReferenceObject.current = null;
return;
}
if (isOutsidePopupElementAndOutsideTriggerElement(getRefs(), e)) {
trySetOpen(false, e);
}
};
};
var handleMouseDown = function handleMouseDown(e) {
mouseDownEventRef.current = e;
};
var handleDocumentKeyDown = function handleDocumentKeyDown(getRefs) {
return function (e) {
var keyCode = getCode(e);
var isMatchingKey = keyCode === keyboardKey.Enter || keyCode === SpacebarKey;
if (isMatchingKey && isOutsidePopupElementAndOutsideTriggerElement(getRefs(), e)) {
trySetOpen(false, e);
}
// if focus was lost from Popup and moved to body, for e.g. when click on popup content
// and ESC is pressed, the last opened Popup should get closed and the trigger should get focus
var lastContentRef = getRefs().pop();
var isLastOpenedPopup = lastContentRef && lastContentRef.current === popupContentRef.current;
var activeDocument = context.target;
var bodyHasFocus = activeDocument.activeElement === activeDocument.body;
if (keyCode === keyboardKey.Escape && bodyHasFocus && isLastOpenedPopup) {
close(e, function () {
return _invoke(triggerFocusableRef.current, 'focus');
});
}
};
};
var isOutsidePopupElementAndOutsideTriggerElement = function isOutsidePopupElementAndOutsideTriggerElement(refs, e) {
var isOutsidePopup = isOutsidePopupElement(refs, e);
var isInsideTrigger = triggerRef.current && doesNodeContainClick(triggerRef.current, e, context.target);
return isOutsidePopup && !isInsideTrigger;
};
var isOutsidePopupElement = function isOutsidePopupElement(refs, e) {
var isInsideNested = _some(refs, function (childRef) {
return doesNodeContainClick(childRef.current, e, context.target);
});
var isOutsidePopup = popupContentRef.current && !isInsideNested;
return isOutsidePopup;
};
var getTriggerProps = function getTriggerProps(triggerElement) {
var triggerElementEventProps = triggerElement ? getRealEventProps(triggerElement) : {};
var triggerProps = {};
var normalizedOn = _isArray(on) ? on : [on];
/**
* The focus is adding the focus, blur and click event (always opening on click)
* If focus and context are provided, there is no need to add onClick
*/
if (_includes(normalizedOn, 'focus')) {
triggerProps.onFocus = function (e) {
if (isFromKeyboard()) {
trySetOpen(true, e);
}
for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
args[_key - 1] = arguments[_key];
}
_invoke.apply(void 0, [triggerElementEventProps, 'onFocus', e].concat(args));
};
triggerProps.onBlur = function (e) {
if (shouldBlurClose(e)) {
trySetOpen(false, e);
}
for (var _len2 = arguments.length, args = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {
args[_key2 - 1] = arguments[_key2];
}
_invoke.apply(void 0, [triggerElementEventProps, 'onBlur', e].concat(args));
};
if (!_includes(normalizedOn, 'context')) {
triggerProps.onClick = function (e) {
setPopupOpen(true, e);
for (var _len3 = arguments.length, args = new Array(_len3 > 1 ? _len3 - 1 : 0), _key3 = 1; _key3 < _len3; _key3++) {
args[_key3 - 1] = arguments[_key3];
}
_invoke.apply(void 0, [triggerElementEventProps, 'onClick', e].concat(args));
};
}
}
/**
* The click is toggling the open state of the popup
*/
if (_includes(normalizedOn, 'click')) {
triggerProps.onClick = function (e) {
trySetOpen(!open, e);
for (var _len4 = arguments.length, args = new Array(_len4 > 1 ? _len4 - 1 : 0), _key4 = 1; _key4 < _len4; _key4++) {
args[_key4 - 1] = arguments[_key4];
}
_invoke.apply(void 0, [triggerElementEventProps, 'onClick', e].concat(args));
};
}
/**
* The context is opening the popup
*/
if (_includes(normalizedOn, 'context')) {
triggerProps.onContextMenu = function (e) {
setPopupOpen(!open, e);
for (var _len5 = arguments.length, args = new Array(_len5 > 1 ? _len5 - 1 : 0), _key5 = 1; _key5 < _len5; _key5++) {
args[_key5 - 1] = arguments[_key5];
}
_invoke.apply(void 0, [triggerElementEventProps, 'onContextMenu', e].concat(args));
e.preventDefault();
};
}
/**
* The hover is adding the mouseEnter, mouseLeave, blur and click event (always opening on click)
* If hover and context are provided, there is no need to add onClick
*/
if (_includes(normalizedOn, 'hover')) {
triggerProps.onMouseEnter = function (e) {
setPopupOpen(true, e);
setWhatInputSource(context.target, 'mouse');
for (var _len6 = arguments.length, args = new Array(_len6 > 1 ? _len6 - 1 : 0), _key6 = 1; _key6 < _len6; _key6++) {
args[_key6 - 1] = arguments[_key6];
}
_invoke.apply(void 0, [triggerElementEventProps, 'onMouseEnter', e].concat(args));
};
triggerProps.onMouseLeave = function (e) {
setPopupOpen(false, e);
for (var _len7 = arguments.length, args = new Array(_len7 > 1 ? _len7 - 1 : 0), _key7 = 1; _key7 < _len7; _key7++) {
args[_key7 - 1] = arguments[_key7];
}
_invoke.apply(void 0, [triggerElementEventProps, 'onMouseLeave', e].concat(args));
};
if (!_includes(normalizedOn, 'context')) {
triggerProps.onClick = function (e) {
setPopupOpen(true, e);
for (var _len8 = arguments.length, args = new Array(_len8 > 1 ? _len8 - 1 : 0), _key8 = 1; _key8 < _len8; _key8++) {
args[_key8 - 1] = arguments[_key8];
}
_invoke.apply(void 0, [triggerElementEventProps, 'onClick', e].concat(args));
};
}
triggerProps.onBlur = function (e) {
if (shouldBlurClose(e)) {
trySetOpen(false, e);
}
for (var _len9 = arguments.length, args = new Array(_len9 > 1 ? _len9 - 1 : 0), _key9 = 1; _key9 < _len9; _key9++) {
args[_key9 - 1] = arguments[_key9];
}
_invoke.apply(void 0, [triggerElementEventProps, 'onBlur', e].concat(args));
};
}
return Object.assign({}, triggerElementEventProps, triggerProps);
};
var getContentProps = function getContentProps(predefinedProps) {
var contentHandlerProps = {};
var normalizedOn = _isArray(on) ? on : [on];
/**
* The focus is adding the focus and blur events on the content
*/
if (_includes(normalizedOn, 'focus')) {
contentHandlerProps.onFocus = function (e, contentProps) {
trySetOpen(true, e);
predefinedProps && _invoke(predefinedProps, 'onFocus', e, contentProps);
};
contentHandlerProps.onBlur = function (e, contentProps) {
if (shouldBlurClose(e)) {
trySetOpen(false, e);
}
predefinedProps && _invoke(predefinedProps, 'onBlur', e, contentProps);
};
}
/**
* The hover is adding the mouseEnter, mouseLeave
*/
if (_includes(normalizedOn, 'hover')) {
contentHandlerProps.onMouseEnter = function (e, contentProps) {
setPopupOpen(true, e);
predefinedProps && _invoke(predefinedProps, 'onMouseEnter', e, contentProps);
};
contentHandlerProps.onMouseLeave = function (e, contentProps) {
setPopupOpen(false, e);
predefinedProps && _invoke(predefinedProps, 'onMouseLeave', e, contentProps);
};
}
return contentHandlerProps;
};
var shouldBlurClose = function shouldBlurClose(e) {
var relatedTarget = e.relatedTarget;
var isInsideContent = elementContains(popupContentRef.current, relatedTarget);
var isInsideTarget = elementContains(e.currentTarget, relatedTarget);
// When clicking in the popup content that has no tabIndex focus goes to body
// We shouldn't close the popup in this case
return relatedTarget && !(isInsideContent || isInsideTarget);
};
var renderPopperChildren = function renderPopperChildren(classes) {
return function (_ref) {
var placement = _ref.placement,
scheduleUpdate = _ref.scheduleUpdate;
var content = renderContent ? renderContent(scheduleUpdate) : props.content;
var popupContent = Popup.Content.create(content || {}, {
defaultProps: function defaultProps() {
return getA11yProps('popup', Object.assign({}, getContentProps(), {
placement: placement,
pointing: pointing,
pointerRef: pointerTargetRef,
trapFocus: trapFocus,
autoFocus: autoFocus,
autoSize: autoSize,
className: classes
}));
},
overrideProps: getContentProps
});
return /*#__PURE__*/React.createElement(Unstable_NestingAuto, null, function (getRefs, nestingRef) {
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Ref, {
innerRef: function innerRef(domElement) {
popupContentRef.current = domElement;
handleRef(contentRef, domElement);
nestingRef.current = domElement;
}
}, popupContent), context.target && /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(EventListener, {
listener: handleMouseDown,
target: context.target,
type: "mousedown"
}), /*#__PURE__*/React.createElement(EventListener, {
listener: handleDocumentClick(getRefs),
target: context.target,
type: "click",
capture: true
}), /*#__PURE__*/React.createElement(EventListener, {
listener: handleDocumentClick(getRefs),
target: context.target,
type: "contextmenu",
capture: true
}), /*#__PURE__*/React.createElement(EventListener, {
listener: handleDocumentKeyDown(getRefs),
target: context.target,
type: "keydown",
capture: true
}), (isOpenedByRightClick || closeOnScroll) && /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(EventListener, {
listener: dismissOnScroll,
target: context.target,
type: "wheel",
capture: true
}), /*#__PURE__*/React.createElement(EventListener, {
listener: dismissOnScroll,
target: context.target,
type: "touchmove",
capture: true
}))));
});
};
};
var dismissOnScroll = function dismissOnScroll(e) {
// we only need to dismiss if the scroll happens outside the popup
if (!elementContains(popupContentRef.current, e.target)) {
trySetOpen(false, e);
}
};
var trySetOpen = function trySetOpen(newValue, event) {
var isOpenedByRightClick = newValue && isRightClick(event);
// when new state 'open' === 'true', save the last focused element
if (newValue) {
updateTriggerFocusableRef();
updateContextPosition(isOpenedByRightClick && event.nativeEvent);
}
setOpen(newValue);
setIsOpenedByRightClick(isOpenedByRightClick);
_invoke(props, 'onOpenChange', event, Object.assign({}, props, {
open: newValue
}));
};
var setPopupOpen = function setPopupOpen(newOpen, e) {
clearTimeout(closeTimeoutId.current);
newOpen ? trySetOpen(true, e) : schedulePopupClose(e);
};
var schedulePopupClose = function schedulePopupClose(e) {
closeTimeoutId.current = setTimeout(function () {
trySetOpen(false, e);
}, mouseLeaveDelay);
};
var close = function close(e, onClose) {
if (open) {
trySetOpen(false, e);
onClose && onClose();
e.stopPropagation();
}
};
/**
* Save DOM element which had focus before Popup opens.
* Can be either trigger DOM element itself or the element inside it.
*/
var updateTriggerFocusableRef = function updateTriggerFocusableRef() {
var _context$target;
var activeElement = (_context$target = context.target) == null ? void 0 : _context$target.activeElement;
if (activeElement) {
triggerFocusableRef.current = triggerRef.current && elementContains(triggerRef.current, activeElement) ? activeElement : triggerRef.current;
}
};
var updateContextPosition = function updateContextPosition(nativeEvent) {
rightClickReferenceObject.current = nativeEvent ? createReferenceFromClick(nativeEvent) : null;
};
if (process.env.NODE_ENV !== 'production') {
// This is fine to violate there conditional rule as environment variables will never change during component
// lifecycle
// eslint-disable-next-line react-hooks/rules-of-hooks
React.useEffect(function () {
if (inline && trapFocus) {
// eslint-disable-next-line no-console
console.warn('Using "trapFocus" in inline popup leads to broken behavior for screen reader users.');
}
if (!inline && autoFocus) {
// eslint-disable-next-line no-console
console.warn('Beware, "autoFocus" prop will just grab focus at the moment of mount and will not trap it. As user is able to TAB out from popup, better use "inline" prop to keep correct tab order.');
}
}, [autoFocus, inline, trapFocus]);
}
React.useEffect(function () {
if (open) {
// when new state 'open' === 'true', save the last focused element
updateTriggerFocusableRef();
}
});
var triggerNode = useTriggerElement(props);
var triggerProps = getTriggerProps(triggerNode);
React.useEffect(function () {
if (open) {
setVirtualParent(popupContentRef.current, triggerRef.current);
}
return function () {
if (open && popupContentRef.current) {
setVirtualParent(popupContentRef.current, null);
}
};
}, [open]);
var contentElement = /*#__PURE__*/React.createElement(Animation, {
mountOnEnter: true,
unmountOnExit: true,
visible: open,
name: open ? 'popup-show' : 'popup-hide'
}, function (_ref2) {
var classes = _ref2.classes;
var content = /*#__PURE__*/React.createElement(Popper, {
pointerTargetRef: pointerTargetRef,
align: align,
flipBoundary: flipBoundary,
popperRef: popperRef,
position: position,
positionFixed: positionFixed,
offset: offset,
overflowBoundary: overflowBoundary,
rtl: context.rtl,
unstable_disableTether: unstable_disableTether,
unstable_pinned: unstable_pinned,
autoSize: autoSize,
targetRef: rightClickReferenceObject.current || target || triggerRef
}, renderPopperChildren(classes));
return inline ? content : /*#__PURE__*/React.createElement(PortalInner, {
mountNode: mountNode
}, content);
});
var triggerElement = triggerNode && /*#__PURE__*/React.createElement(Ref, {
innerRef: triggerRef
}, /*#__PURE__*/React.cloneElement(triggerNode, getA11yProps('trigger', triggerProps)));
var element = /*#__PURE__*/React.createElement(React.Fragment, null, triggerElement, contentElement);
setEnd();
return element;
};
Popup.displayName = 'Popup';
Popup.propTypes = Object.assign({}, commonPropTypes.createCommon({
as: false,
content: false
}), {
align: PropTypes.oneOf(ALIGNMENTS),
defaultOpen: PropTypes.bool,
inline: PropTypes.bool,
mountNode: customPropTypes.domNode,
mouseLeaveDelay: PropTypes.number,
offset: PropTypes.oneOfType([PropTypes.func, PropTypes.arrayOf(PropTypes.number)]),
popperRef: customPropTypes.ref,
flipBoundary: PropTypes.oneOfType([PropTypes.object, PropTypes.arrayOf(PropTypes.object), PropTypes.oneOf(['clippingParents', 'window', 'scrollParent'])]),
overflowBoundary: PropTypes.oneOfType([PropTypes.object, PropTypes.arrayOf(PropTypes.object), PropTypes.oneOf(['clippingParents', 'window', 'scrollParent'])]),
on: PropTypes.oneOfType([PropTypes.oneOf(['hover', 'click', 'focus', 'context']), PropTypes.arrayOf(PropTypes.oneOf(['click', 'focus', 'context'])), PropTypes.arrayOf(PropTypes.oneOf(['hover', 'focus', 'context']))]),
open: PropTypes.bool,
onOpenChange: PropTypes.func,
pointing: PropTypes.bool,
position: PropTypes.oneOf(POSITIONS),
positionFixed: PropTypes.bool,
renderContent: PropTypes.func,
target: PropTypes.any,
trigger: customPropTypes.every([customPropTypes.disallow(['children']), PropTypes.any]),
tabbableTrigger: PropTypes.bool,
unstable_disableTether: PropTypes.oneOf([true, false, 'all']),
unstable_pinned: PropTypes.bool,
autoSize: PropTypes.oneOf(AUTOSIZES),
content: customPropTypes.shorthandAllowingChildren,
contentRef: customPropTypes.ref,
trapFocus: PropTypes.oneOfType([PropTypes.bool, PropTypes.object]),
autoFocus: PropTypes.oneOfType([PropTypes.bool, PropTypes.object]),
closeOnScroll: PropTypes.bool
});
Popup.defaultProps = {
accessibility: popupBehavior,
align: 'start',
position: 'above',
on: 'click',
mouseLeaveDelay: 500,
tabbableTrigger: true
};
Popup.handledProps = Object.keys(Popup.propTypes);
Popup.Content = PopupContent;
Popup.create = createShorthandFactory({
Component: Popup,
mappedProp: 'content'
});
Popup.shorthandConfig = {
mappedProp: 'content'
};
return Popup;
}();
//# sourceMappingURL=Popup.js.map