office-ui-fabric-react
Version:
Reusable React components for building experiences for Office 365.
361 lines • 19.2 kB
JavaScript
import * as tslib_1 from "tslib";
import * as React from 'react';
import { BaseComponent, assign, elementContains, focusFirstChild, getWindow, getDocument, css, createRef, getNativeProps, divProperties } from '../../Utilities';
import { positionCallout, getMaxHeight, RectangleEdge } from '../../utilities/positioning';
import { Popup } from '../../Popup';
import { classNamesFunction } from '../../Utilities';
import { AnimationClassNames } from '../../Styling';
var ANIMATIONS = (_a = {},
_a[RectangleEdge.top] = AnimationClassNames.slideUpIn10,
_a[RectangleEdge.bottom] = AnimationClassNames.slideDownIn10,
_a[RectangleEdge.left] = AnimationClassNames.slideLeftIn10,
_a[RectangleEdge.right] = AnimationClassNames.slideRightIn10,
_a);
var getClassNames = classNamesFunction();
var BORDER_WIDTH = 1;
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.
var OFF_SCREEN_STYLE = { opacity: 0, filter: 'opacity(0)' };
var CalloutContentBase = /** @class */ (function (_super) {
tslib_1.__extends(CalloutContentBase, _super);
function CalloutContentBase(props) {
var _this = _super.call(this, props) || this;
_this._hostElement = createRef();
_this._calloutElement = createRef();
_this._hasListeners = false;
_this.dismiss = function (ev) {
var onDismiss = _this.props.onDismiss;
if (onDismiss) {
onDismiss(ev);
}
};
_this._setInitialFocus = function () {
if (_this.props.setInitialFocus &&
!_this._didSetInitialFocus &&
_this.state.positions &&
_this._calloutElement.current) {
_this._didSetInitialFocus = true;
_this._async.requestAnimationFrame(function () { return focusFirstChild(_this._calloutElement.current); });
}
};
_this._onComponentDidMount = function () {
_this._addListeners();
if (_this.props.onLayerMounted) {
_this.props.onLayerMounted();
}
_this._updateAsyncPosition();
_this._setHeightOffsetEveryFrame();
};
_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 () {
this._setInitialFocus();
if (!this.props.hidden) {
if (!this._hasListeners) {
this._addListeners();
}
this._updateAsyncPosition();
}
else {
if (this._hasListeners) {
this._removeListeners();
}
}
};
CalloutContentBase.prototype.componentWillMount = function () {
this._setTargetWindowAndElement(this._getTarget());
};
CalloutContentBase.prototype.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._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();
}
// if the callout becomes hidden, then remove any positions that were placed on it.
if (newProps.hidden && newProps.hidden !== this.props.hidden) {
this.setState({
positions: undefined
});
}
};
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, role = _a.role, 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, finalHeight = _a.finalHeight, _b = _a.hideOverflow, hideOverflow = _b === void 0 ? !!finalHeight : _b, backgroundColor = _a.backgroundColor, calloutMaxHeight = _a.calloutMaxHeight, onScroll = _a.onScroll;
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,
calloutMaxWidth: calloutMaxWidth
});
var overflowStyle = 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({}, getNativeProps(this.props, divProperties), { className: css(this._classNames.root, positions && positions.targetEdge && ANIMATIONS[positions.targetEdge]), style: positions ? positions.elementPosition : OFF_SCREEN_STYLE, tabIndex: -1,
// See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#Clicking_and_focus
ref: this._calloutElement }),
beakVisible && React.createElement("div", { className: this._classNames.beak, style: this._getBeakPosition() }),
beakVisible && React.createElement("div", { className: this._classNames.beakCurtain }),
!this.props.hidden && (React.createElement(Popup, { role: role, ariaLabel: ariaLabel, ariaDescribedBy: ariaDescribedBy, ariaLabelledBy: ariaLabelledBy, className: this._classNames.calloutMain, onDismiss: this.dismiss, onScroll: onScroll, shouldRestoreFocus: true, style: overflowStyle }, children)))));
return content;
};
CalloutContentBase.prototype._dismissOnScroll = function (ev) {
var preventDismissOnScroll = this.props.preventDismissOnScroll;
if (this.state.positions && !preventDismissOnScroll) {
this._dismissOnLostFocus(ev);
}
};
CalloutContentBase.prototype._dismissOnLostFocus = function (ev) {
var target = ev.target;
var clickedOutsideCallout = this._hostElement.current && !elementContains(this._hostElement.current, target);
var preventDismissOnLostFocus = this.props.preventDismissOnLostFocus;
if (!preventDismissOnLostFocus &&
((!this._target && clickedOutsideCallout) ||
(ev.target !== this._targetWindow &&
clickedOutsideCallout &&
(this._target.stopPropagation ||
(!this._target || (target !== this._target && !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._events.on(_this._targetWindow, 'scroll', _this._dismissOnScroll, true);
_this._events.on(_this._targetWindow, 'resize', _this.dismiss, true);
_this._events.on(_this._targetWindow.document.documentElement, 'focus', _this._dismissOnLostFocus, true);
_this._events.on(_this._targetWindow.document.documentElement, 'click', _this._dismissOnLostFocus, true);
_this._hasListeners = true;
}, 0);
};
CalloutContentBase.prototype._removeListeners = function () {
this._events.off(this._targetWindow, 'scroll', this._dismissOnScroll, true);
this._events.off(this._targetWindow, 'resize', this.dismiss, true);
this._events.off(this._targetWindow.document.documentElement, 'focus', this._dismissOnLostFocus, true);
this._events.off(this._targetWindow.document.documentElement, 'click', this._dismissOnLostFocus, true);
this._hasListeners = false;
};
CalloutContentBase.prototype._updateAsyncPosition = function () {
var _this = this;
this._async.requestAnimationFrame(function () { return _this._updatePosition(); });
};
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 = void 0;
currentProps = assign(currentProps, this.props);
currentProps.bounds = this._getBounds();
currentProps.target = this._target;
var newPositions = 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 {
this._positionAttempts = 0;
if (this.props.onPositioned) {
this.props.onPositioned(this.state.positions);
}
}
}
};
CalloutContentBase.prototype._getBounds = function () {
if (!this._bounds) {
var currentBounds = this.props.bounds;
if (!currentBounds) {
currentBounds = {
top: 0 + this.props.minPagePadding,
left: 0 + this.props.minPagePadding,
right: this._targetWindow.innerWidth - this.props.minPagePadding,
bottom: this._targetWindow.innerHeight - this.props.minPagePadding,
width: this._targetWindow.innerWidth - this.props.minPagePadding * 2,
height: this._targetWindow.innerHeight - 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 + BORDER_WIDTH * 2;
this._async.requestAnimationFrame(function () {
if (_this._target) {
_this._maxHeight = getMaxHeight(_this._target, _this.props.directionalHint, totalGap_1, _this._getBounds(), _this.props.coverTarget);
_this.forceUpdate();
}
});
}
else {
this._maxHeight = this._getBounds().height - BORDER_WIDTH * 2;
}
}
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) {
// This needs to be checked here and below because there is a linting error if for in does not immediately have an if statement
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) {
if (target) {
if (typeof target === 'string') {
var currentDoc = getDocument();
this._target = currentDoc ? currentDoc.querySelector(target) : null;
this._targetWindow = getWindow();
}
else if (target.stopPropagation) {
this._targetWindow = getWindow(target.toElement);
this._target = target;
}
else if (target.getBoundingClientRect) {
var targetElement = target;
this._targetWindow = getWindow(targetElement);
this._target = target;
// HTMLImgElements can have x and y values. The check for it being a point must go last.
}
else {
this._targetWindow = getWindow();
this._target = target;
}
}
else {
this._targetWindow = getWindow();
}
};
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);
}
});
}
};
CalloutContentBase.prototype._getTarget = function (props) {
if (props === void 0) { props = this.props; }
var target = props.target;
return target;
};
CalloutContentBase.defaultProps = {
preventDismissOnLostFocus: false,
preventDismissOnScroll: false,
isBeakVisible: true,
beakWidth: 16,
gapSpace: 0,
minPagePadding: 8,
directionalHint: 7 /* bottomAutoEdge */
};
return CalloutContentBase;
}(BaseComponent));
export { CalloutContentBase };
var _a;
//# sourceMappingURL=CalloutContent.base.js.map