UNPKG

office-ui-fabric-react

Version:

Reusable React components for building experiences for Microsoft 365.

455 lines • 25 kB
"use strict"; var _a; Object.defineProperty(exports, "__esModule", { value: true }); var tslib_1 = require("tslib"); var React = require("react"); var DirectionalHint_1 = require("../../common/DirectionalHint"); var Utilities_1 = require("../../Utilities"); var positioning_1 = require("../../utilities/positioning"); var Popup_1 = require("../../Popup"); var Utilities_2 = require("../../Utilities"); var Styling_1 = require("../../Styling"); var ANIMATIONS = (_a = {}, _a[positioning_1.RectangleEdge.top] = Styling_1.AnimationClassNames.slideUpIn10, _a[positioning_1.RectangleEdge.bottom] = Styling_1.AnimationClassNames.slideDownIn10, _a[positioning_1.RectangleEdge.left] = Styling_1.AnimationClassNames.slideLeftIn10, _a[positioning_1.RectangleEdge.right] = Styling_1.AnimationClassNames.slideRightIn10, _a); var getClassNames = Utilities_2.classNamesFunction({ disableCaching: true, }); var BEAK_ORIGIN_POSITION = { top: 0, left: 0 }; // Microsoft Edge will overwrite inline styles if there is an animation pertaining to that style. // To help ensure that edge will respect the offscreen style opacity // filter needs to be added as an additional way to set opacity. // Also set pointer-events: none so that the callout will not occlude the element it is // going to be positioned against var OFF_SCREEN_STYLE = { opacity: 0, filter: 'opacity(0)', pointerEvents: 'none' }; // role and role description go hand-in-hand. Both would be included by spreading getNativeProps for a basic element // This constant array can be used to filter these out of native props spread on callout root and apply them together on // calloutMain (the Popup component within the callout) var ARIA_ROLE_ATTRIBUTES = ['role', 'aria-roledescription']; var CalloutContentBase = /** @class */ (function (_super) { tslib_1.__extends(CalloutContentBase, _super); function CalloutContentBase(props) { var _this = _super.call(this, props) || this; _this._hostElement = React.createRef(); _this._calloutElement = React.createRef(); _this._hasListeners = false; _this._disposables = []; _this.dismiss = function (ev) { var onDismiss = _this.props.onDismiss; if (onDismiss) { onDismiss(ev); } }; _this._dismissOnScroll = function (ev) { // eslint-disable-next-line deprecation/deprecation var _a = _this.props, preventDismissOnEvent = _a.preventDismissOnEvent, preventDismissOnScroll = _a.preventDismissOnScroll; if (_this.state.positions && ((preventDismissOnEvent && !preventDismissOnEvent(ev)) || (!preventDismissOnEvent && !preventDismissOnScroll))) { _this._dismissOnClickOrScroll(ev); } }; _this._dismissOnResize = function (ev) { // eslint-disable-next-line deprecation/deprecation var _a = _this.props, preventDismissOnEvent = _a.preventDismissOnEvent, preventDismissOnResize = _a.preventDismissOnResize; if ((preventDismissOnEvent && !preventDismissOnEvent(ev)) || (!preventDismissOnEvent && !preventDismissOnResize)) { _this.dismiss(ev); } }; _this._dismissOnLostFocus = function (ev) { // eslint-disable-next-line deprecation/deprecation var _a = _this.props, preventDismissOnEvent = _a.preventDismissOnEvent, preventDismissOnLostFocus = _a.preventDismissOnLostFocus; if ((preventDismissOnEvent && !preventDismissOnEvent(ev)) || (!preventDismissOnEvent && !preventDismissOnLostFocus)) { _this._dismissOnClickOrScroll(ev); } }; _this._setInitialFocus = function () { if (_this.props.setInitialFocus && !_this._didSetInitialFocus && _this.state.positions && _this._calloutElement.current) { _this._didSetInitialFocus = true; _this._async.requestAnimationFrame(function () { return Utilities_1.focusFirstChild(_this._calloutElement.current); }, _this._calloutElement.current); } }; _this._onComponentDidMount = function () { _this._addListeners(); if (_this.props.onLayerMounted) { _this.props.onLayerMounted(); } _this._updateAsyncPosition(); _this._setHeightOffsetEveryFrame(); }; _this._dismissOnTargetWindowBlur = function (ev) { // eslint-disable-next-line deprecation/deprecation var _a = _this.props, preventDismissOnEvent = _a.preventDismissOnEvent, preventDismissOnLostFocus = _a.preventDismissOnLostFocus, shouldDismissOnWindowFocus = _a.shouldDismissOnWindowFocus; // Do nothing if (!shouldDismissOnWindowFocus) { return; } if (((preventDismissOnEvent && !preventDismissOnEvent(ev)) || (!preventDismissOnEvent && !preventDismissOnLostFocus)) && !_this._targetWindow.document.hasFocus() && ev.relatedTarget === null) { _this.dismiss(ev); } }; _this._mouseDownOnPopup = function () { _this._isMouseDownOnPopup = true; }; _this._mouseUpOnPopup = function () { _this._isMouseDownOnPopup = false; }; _this._async = new Utilities_1.Async(_this); _this._didSetInitialFocus = false; _this.state = { positions: undefined, slideDirectionalClassName: undefined, // @TODO it looks like this is not even being used anymore. calloutElementRect: undefined, heightOffset: 0, }; _this._positionAttempts = 0; return _this; } CalloutContentBase.prototype.componentDidUpdate = function () { if (!this.props.hidden) { this._setInitialFocus(); if (!this._hasListeners) { this._addListeners(); } this._updateAsyncPosition(); } else { if (this._hasListeners) { this._removeListeners(); } } }; CalloutContentBase.prototype.shouldComponentUpdate = function (newProps, newState) { if (!newProps.shouldUpdateWhenHidden && this.props.hidden && newProps.hidden) { // Do not update when hidden. return false; } return !Utilities_1.shallowCompare(this.props, newProps) || !Utilities_1.shallowCompare(this.state, newState); }; CalloutContentBase.prototype.UNSAFE_componentWillMount = function () { this._setTargetWindowAndElement(this._getTarget()); }; CalloutContentBase.prototype.componentWillUnmount = function () { this._async.dispose(); this._disposables.forEach(function (dispose) { return dispose(); }); }; CalloutContentBase.prototype.UNSAFE_componentWillUpdate = function (newProps) { // If the target element changed, find the new one. If we are tracking target with class name, always find element // because we do not know if fabric has rendered a new element and disposed the old element. var newTarget = this._getTarget(newProps); var oldTarget = this._getTarget(); if ((newTarget !== oldTarget || typeof newTarget === 'string' || newTarget instanceof String) && !this._blockResetHeight) { this._maxHeight = undefined; this._setTargetWindowAndElement(newTarget); } if (newProps.gapSpace !== this.props.gapSpace || this.props.beakWidth !== newProps.beakWidth) { this._maxHeight = undefined; } if (newProps.finalHeight !== this.props.finalHeight) { this._setHeightOffsetEveryFrame(); } // Ensure positioning is recalculated when we are about to show a persisted menu. if (this._didPositionPropsChange(newProps, this.props)) { this._maxHeight = undefined; // Target might have been updated while hidden. this._setTargetWindowAndElement(newTarget); this.setState({ positions: undefined, }); this._didSetInitialFocus = false; this._bounds = undefined; } this._blockResetHeight = false; }; CalloutContentBase.prototype.componentDidMount = function () { if (!this.props.hidden) { this._onComponentDidMount(); } }; CalloutContentBase.prototype.render = function () { // If there is no target window then we are likely in server side rendering and we should not render anything. if (!this._targetWindow) { return null; } var target = this.props.target; var _a = this.props, styles = _a.styles, style = _a.style, ariaLabel = _a.ariaLabel, ariaDescribedBy = _a.ariaDescribedBy, ariaLabelledBy = _a.ariaLabelledBy, className = _a.className, isBeakVisible = _a.isBeakVisible, children = _a.children, beakWidth = _a.beakWidth, calloutWidth = _a.calloutWidth, calloutMaxWidth = _a.calloutMaxWidth, calloutMinWidth = _a.calloutMinWidth, finalHeight = _a.finalHeight, _b = _a.hideOverflow, hideOverflow = _b === void 0 ? !!finalHeight : _b, backgroundColor = _a.backgroundColor, calloutMaxHeight = _a.calloutMaxHeight, onScroll = _a.onScroll, // eslint-disable-next-line deprecation/deprecation _c = _a.shouldRestoreFocus, // eslint-disable-next-line deprecation/deprecation shouldRestoreFocus = _c === void 0 ? true : _c, popupProps = _a.popupProps; target = this._getTarget(); var positions = this.state.positions; var getContentMaxHeight = this._getMaxHeight() ? this._getMaxHeight() + this.state.heightOffset : undefined; var contentMaxHeight = calloutMaxHeight && getContentMaxHeight && calloutMaxHeight < getContentMaxHeight ? calloutMaxHeight : getContentMaxHeight; var overflowYHidden = hideOverflow; var beakVisible = isBeakVisible && !!target; this._classNames = getClassNames(styles, { theme: this.props.theme, className: className, overflowYHidden: overflowYHidden, calloutWidth: calloutWidth, positions: positions, beakWidth: beakWidth, backgroundColor: backgroundColor, calloutMinWidth: calloutMinWidth, calloutMaxWidth: calloutMaxWidth, }); var overflowStyle = tslib_1.__assign(tslib_1.__assign(tslib_1.__assign({}, style), { maxHeight: contentMaxHeight }), (overflowYHidden && { overflowY: 'hidden' })); var visibilityStyle = this.props.hidden ? { visibility: 'hidden' } : undefined; // React.CSSProperties does not understand IRawStyle, so the inline animations will need to be cast as any for now. var content = (React.createElement("div", { ref: this._hostElement, className: this._classNames.container, style: visibilityStyle }, React.createElement("div", tslib_1.__assign({}, Utilities_1.getNativeProps(this.props, Utilities_1.divProperties, ARIA_ROLE_ATTRIBUTES), { className: Utilities_1.css(this._classNames.root, positions && positions.targetEdge && ANIMATIONS[positions.targetEdge]), style: positions ? positions.elementPosition : OFF_SCREEN_STYLE, // Safari and Firefox on Mac OS requires this to back-stop click events so focus remains in the Callout. // See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#Clicking_and_focus tabIndex: -1, ref: this._calloutElement }), beakVisible && React.createElement("div", { className: this._classNames.beak, style: this._getBeakPosition() }), beakVisible && React.createElement("div", { className: this._classNames.beakCurtain }), React.createElement(Popup_1.Popup, tslib_1.__assign({}, Utilities_1.getNativeProps(this.props, ARIA_ROLE_ATTRIBUTES), { ariaLabel: ariaLabel, onRestoreFocus: this.props.onRestoreFocus, ariaDescribedBy: ariaDescribedBy, ariaLabelledBy: ariaLabelledBy, className: this._classNames.calloutMain, onDismiss: this.dismiss, onScroll: onScroll, shouldRestoreFocus: shouldRestoreFocus, style: overflowStyle, onMouseDown: this._mouseDownOnPopup, onMouseUp: this._mouseUpOnPopup }, popupProps), children)))); return content; }; CalloutContentBase.prototype._dismissOnClickOrScroll = function (ev) { var target = ev.target; var isEventTargetOutsideCallout = this._hostElement.current && !Utilities_1.elementContains(this._hostElement.current, target); // If mouse is pressed down on callout but moved outside then released, don't dismiss the callout. if (isEventTargetOutsideCallout && this._isMouseDownOnPopup) { this._isMouseDownOnPopup = false; return; } if ((!this._target && isEventTargetOutsideCallout) || (ev.target !== this._targetWindow && isEventTargetOutsideCallout && (this._target.stopPropagation || !this._target || this.props.dismissOnTargetClick || (target !== this._target && !Utilities_1.elementContains(this._target, target))))) { this.dismiss(ev); } }; CalloutContentBase.prototype._addListeners = function () { var _this = this; // This is added so the callout will dismiss when the window is scrolled // but not when something inside the callout is scrolled. The delay seems // to be required to avoid React firing an async focus event in IE from // the target changing focus quickly prior to rendering the callout. this._async.setTimeout(function () { _this._disposables.push(Utilities_1.on(_this._targetWindow, 'scroll', _this._dismissOnScroll, true), Utilities_1.on(_this._targetWindow, 'resize', _this._dismissOnResize, true), Utilities_1.on(_this._targetWindow.document.documentElement, 'focus', _this._dismissOnLostFocus, true), Utilities_1.on(_this._targetWindow.document.documentElement, 'click', _this._dismissOnLostFocus, true), Utilities_1.on(_this._targetWindow, 'blur', _this._dismissOnTargetWindowBlur, true)); _this._hasListeners = true; }, 0); }; CalloutContentBase.prototype._removeListeners = function () { this._disposables.forEach(function (dispose) { return dispose(); }); this._disposables = []; this._hasListeners = false; }; CalloutContentBase.prototype._updateAsyncPosition = function () { var _this = this; this._async.requestAnimationFrame(function () { return _this._updatePosition(); }, this._calloutElement.current); }; CalloutContentBase.prototype._getBeakPosition = function () { var positions = this.state.positions; var beakPostionStyle = tslib_1.__assign({}, (positions && positions.beakPosition ? positions.beakPosition.elementPosition : null)); if (!beakPostionStyle.top && !beakPostionStyle.bottom && !beakPostionStyle.left && !beakPostionStyle.right) { beakPostionStyle.left = BEAK_ORIGIN_POSITION.left; beakPostionStyle.top = BEAK_ORIGIN_POSITION.top; } return beakPostionStyle; }; CalloutContentBase.prototype._updatePosition = function () { // Try to update the target, page might have changed this._setTargetWindowAndElement(this._getTarget()); var positions = this.state.positions; var hostElement = this._hostElement.current; var calloutElement = this._calloutElement.current; // If we expect a target element to position against, we need to wait until `this._target` is resolved. Otherwise // we can try to position. var expectsTarget = !!this.props.target; if (hostElement && calloutElement && (!expectsTarget || this._target)) { var currentProps = tslib_1.__assign({}, this.props); currentProps.bounds = this._getBounds(); currentProps.target = this._target; // If there is a finalHeight given then we assume that the user knows and will handle // additional positioning adjustments so we should call positionCard var newPositions = this.props.finalHeight ? positioning_1.positionCard(currentProps, hostElement, calloutElement, positions) : positioning_1.positionCallout(currentProps, hostElement, calloutElement, positions); // Set the new position only when the positions are not exists or one of the new callout positions are different. // The position should not change if the position is within 2 decimal places. if ((!positions && newPositions) || (positions && newPositions && !this._arePositionsEqual(positions, newPositions) && this._positionAttempts < 5)) { // We should not reposition the callout more than a few times, if it is then the content is likely resizing // and we should stop trying to reposition to prevent a stack overflow. this._positionAttempts++; this.setState({ positions: newPositions, }); } else if (this._positionAttempts > 0) { // Only call the onPositioned callback if the callout has been re-positioned at least once. this._positionAttempts = 0; if (this.props.onPositioned) { this.props.onPositioned(this.state.positions); } } } }; CalloutContentBase.prototype._getBounds = function () { if (!this._bounds) { var bounds = this.props.bounds; var currentBounds = typeof bounds === 'function' ? bounds(this.props.target, this._targetWindow) : bounds; if (!currentBounds) { currentBounds = positioning_1.getBoundsFromTargetWindow(this._target, this._targetWindow); currentBounds = { top: currentBounds.top + this.props.minPagePadding, left: currentBounds.left + this.props.minPagePadding, right: currentBounds.right - this.props.minPagePadding, bottom: currentBounds.bottom - this.props.minPagePadding, width: currentBounds.width - this.props.minPagePadding * 2, height: currentBounds.height - this.props.minPagePadding * 2, }; } this._bounds = currentBounds; } return this._bounds; }; // Max height should remain as synchronous as possible, which is why it is not done using set state. // It needs to be synchronous since it will impact the ultimate position of the callout. CalloutContentBase.prototype._getMaxHeight = function () { var _this = this; if (!this._maxHeight) { if (this.props.directionalHintFixed && this._target) { var beakWidth = this.props.isBeakVisible ? this.props.beakWidth : 0; var gapSpace = this.props.gapSpace ? this.props.gapSpace : 0; // Since the callout cannot measure it's border size it must be taken into account here. Otherwise it will // overlap with the target. var totalGap_1 = gapSpace + beakWidth; this._async.requestAnimationFrame(function () { if (_this._target) { _this._maxHeight = positioning_1.getMaxHeight(_this._target, _this.props.directionalHint, totalGap_1, _this._getBounds(), _this.props.coverTarget); _this._blockResetHeight = true; _this.forceUpdate(); } }, this._target); } else { this._maxHeight = this._getBounds().height; } } return this._maxHeight; }; CalloutContentBase.prototype._arePositionsEqual = function (positions, newPosition) { return (this._comparePositions(positions.elementPosition, newPosition.elementPosition) && this._comparePositions(positions.beakPosition.elementPosition, newPosition.beakPosition.elementPosition)); }; CalloutContentBase.prototype._comparePositions = function (oldPositions, newPositions) { for (var key in newPositions) { if (newPositions.hasOwnProperty(key)) { var oldPositionEdge = oldPositions[key]; var newPositionEdge = newPositions[key]; if (oldPositionEdge !== undefined && newPositionEdge !== undefined) { if (oldPositionEdge.toFixed(2) !== newPositionEdge.toFixed(2)) { return false; } } else { return false; } } } return true; }; CalloutContentBase.prototype._setTargetWindowAndElement = function (target) { var currentElement = this._calloutElement.current; if (target) { if (typeof target === 'string') { var currentDoc = Utilities_1.getDocument(currentElement); this._target = currentDoc ? currentDoc.querySelector(target) : null; this._targetWindow = Utilities_1.getWindow(currentElement); // Cast to any prevents error about stopPropagation always existing } else if (target.stopPropagation) { this._targetWindow = Utilities_1.getWindow(target.target); this._target = target; // Same reason here } else if (target.getBoundingClientRect) { var targetElement = target; this._targetWindow = Utilities_1.getWindow(targetElement); this._target = targetElement; } else if (target.current !== undefined) { this._target = target.current; this._targetWindow = Utilities_1.getWindow(this._target); // HTMLImgElements can have x and y values. The check for it being a point must go last. } else { this._targetWindow = Utilities_1.getWindow(currentElement); this._target = target; } } else { this._targetWindow = Utilities_1.getWindow(currentElement); } }; CalloutContentBase.prototype._setHeightOffsetEveryFrame = function () { var _this = this; if (this._calloutElement.current && this.props.finalHeight) { this._setHeightOffsetTimer = this._async.requestAnimationFrame(function () { var calloutMainElem = _this._calloutElement.current && _this._calloutElement.current.lastChild; if (!calloutMainElem) { return; } var cardScrollHeight = calloutMainElem.scrollHeight; var cardCurrHeight = calloutMainElem.offsetHeight; var scrollDiff = cardScrollHeight - cardCurrHeight; _this.setState({ heightOffset: _this.state.heightOffset + scrollDiff, }); if (calloutMainElem.offsetHeight < _this.props.finalHeight) { _this._setHeightOffsetEveryFrame(); } else { _this._async.cancelAnimationFrame(_this._setHeightOffsetTimer, _this._calloutElement.current); } }, this._calloutElement.current); } }; // Whether or not the current positions should be reset CalloutContentBase.prototype._didPositionPropsChange = function (newProps, oldProps) { return ((!newProps.hidden && newProps.hidden !== oldProps.hidden) || newProps.directionalHint !== oldProps.directionalHint || newProps.target !== oldProps.target); }; CalloutContentBase.prototype._getTarget = function (props) { if (props === void 0) { props = this.props; } var target = props.target; return target; }; CalloutContentBase.defaultProps = { preventDismissOnLostFocus: false, preventDismissOnScroll: false, preventDismissOnResize: false, isBeakVisible: true, beakWidth: 16, gapSpace: 0, minPagePadding: 8, directionalHint: DirectionalHint_1.DirectionalHint.bottomAutoEdge, }; return CalloutContentBase; }(React.Component)); exports.CalloutContentBase = CalloutContentBase; //# sourceMappingURL=CalloutContent.base.js.map