office-ui-fabric-react
Version:
Reusable React components for building experiences for Microsoft 365.
427 lines • 21.4 kB
JavaScript
import { __assign, __extends } from "tslib";
// Utilities
import * as React from 'react';
import { classNamesFunction, elementContains, focusFirstChild, getDocument, KeyCodes, shallowCompare, getRTL, warnDeprecations, EventGroup, Async, initializeComponentRef, } from '../../Utilities';
import { RectangleEdge, getOppositeEdge } from '../../utilities/positioning';
// Component Dependencies
import { PositioningContainer } from './PositioningContainer/index';
import { Beak, BEAK_HEIGHT, BEAK_WIDTH } from './Beak/Beak';
import { DirectionalHint } from '../../common/DirectionalHint';
import { COACHMARK_HEIGHT, COACHMARK_WIDTH } from './Coachmark.styles';
import { FocusTrapZone } from '../../FocusTrapZone';
var getClassNames = classNamesFunction();
export var COACHMARK_ATTRIBUTE_NAME = 'data-coachmarkid';
var COMPONENT_NAME = 'Coachmark';
var CoachmarkBase = /** @class */ (function (_super) {
__extends(CoachmarkBase, _super);
function CoachmarkBase(props) {
var _this = _super.call(this, props) || this;
/**
* The cached HTMLElement reference to the Entity Inner Host
* element.
*/
_this._entityHost = React.createRef();
_this._entityInnerHostElement = React.createRef();
_this._translateAnimationContainer = React.createRef();
_this._ariaAlertContainer = React.createRef();
_this._childrenContainer = React.createRef();
_this._positioningContainer = React.createRef();
_this.dismiss = function (ev) {
var onDismiss = _this.props.onDismiss;
if (onDismiss) {
onDismiss(ev);
}
};
_this._onKeyDown = function (e) {
// Open coachmark if user presses ALT + C (arbitrary keypress for now)
if ((e.altKey && e.which === KeyCodes.c) ||
(e.which === KeyCodes.enter &&
_this._translateAnimationContainer.current &&
_this._translateAnimationContainer.current.contains(e.target))) {
_this._onFocusHandler();
}
};
_this._onFocusHandler = function () {
if (_this.state.isCollapsed) {
_this._openCoachmark();
}
};
_this._onPositioned = function (positionData) {
_this._async.requestAnimationFrame(function () {
_this.setState({
targetAlignment: positionData.alignmentEdge,
targetPosition: positionData.targetEdge,
});
});
};
_this._setBeakPosition = function () {
var beakLeft;
var beakTop;
var beakRight;
var beakBottom;
var transformOriginX;
var transformOriginY;
var targetAlignment = _this.state.targetAlignment;
var distanceAdjustment = '3px'; // Adjustment distance for Beak to shift towards Coachmark bubble.
switch (_this._beakDirection) {
// If Beak is pointing Up or Down
case RectangleEdge.top:
case RectangleEdge.bottom:
// If there is no target alignment, then beak is X-axis centered in callout
if (!targetAlignment) {
beakLeft = "calc(50% - " + BEAK_WIDTH / 2 + "px)";
transformOriginX = 'center';
}
else {
// Beak is aligned to the left of target
if (targetAlignment === RectangleEdge.left) {
beakLeft = COACHMARK_WIDTH / 2 - BEAK_WIDTH / 2 + "px";
transformOriginX = 'left';
}
else {
// Beak is aligned to the right of target
beakRight = COACHMARK_WIDTH / 2 - BEAK_WIDTH / 2 + "px";
transformOriginX = 'right';
}
}
if (_this._beakDirection === RectangleEdge.top) {
beakTop = distanceAdjustment;
transformOriginY = 'top';
}
else {
beakBottom = distanceAdjustment;
transformOriginY = 'bottom';
}
break;
// If Beak is pointing Left or Right
case RectangleEdge.left:
case RectangleEdge.right:
// If there is no target alignment, then beak is Y-axis centered in callout
if (!targetAlignment) {
beakTop = "calc(50% - " + BEAK_WIDTH / 2 + "px)";
transformOriginY = "center";
}
else {
// Beak is aligned to the top of target
if (targetAlignment === RectangleEdge.top) {
beakTop = COACHMARK_WIDTH / 2 - BEAK_WIDTH / 2 + "px";
transformOriginY = "top";
}
else {
// Beak is aligned to the bottom of target
beakBottom = COACHMARK_WIDTH / 2 - BEAK_WIDTH / 2 + "px";
transformOriginY = "bottom";
}
}
if (_this._beakDirection === RectangleEdge.left) {
if (getRTL(_this.props.theme)) {
beakRight = distanceAdjustment;
}
else {
beakLeft = distanceAdjustment;
}
transformOriginX = 'left';
}
else {
if (getRTL(_this.props.theme)) {
beakLeft = distanceAdjustment;
}
else {
beakRight = distanceAdjustment;
}
transformOriginX = 'right';
}
break;
}
_this.setState({
beakLeft: beakLeft,
beakRight: beakRight,
beakBottom: beakBottom,
beakTop: beakTop,
transformOrigin: transformOriginX + " " + transformOriginY,
});
};
_this._openCoachmark = function () {
_this.setState({
isCollapsed: false,
});
if (_this.props.onAnimationOpenStart) {
_this.props.onAnimationOpenStart();
}
_this._entityInnerHostElement.current &&
_this._entityInnerHostElement.current.addEventListener('transitionend', function () {
// Need setTimeout to trigger narrator
_this._async.setTimeout(function () {
if (_this._entityInnerHostElement.current) {
focusFirstChild(_this._entityInnerHostElement.current);
}
}, 1000);
if (_this.props.onAnimationOpenEnd) {
_this.props.onAnimationOpenEnd();
}
});
};
_this._async = new Async(_this);
_this._events = new EventGroup(_this);
initializeComponentRef(_this);
warnDeprecations(COMPONENT_NAME, props, {
teachingBubbleRef: undefined,
collapsed: 'isCollapsed',
beakWidth: undefined,
beakHeight: undefined,
width: undefined,
height: undefined,
});
// Set defaults for state
_this.state = {
isCollapsed: props.isCollapsed,
isBeaconAnimating: true,
isMeasuring: true,
entityInnerHostRect: {
width: 0,
height: 0,
},
isMouseInProximity: false,
isMeasured: false,
};
return _this;
}
Object.defineProperty(CoachmarkBase.prototype, "_beakDirection", {
get: function () {
var targetPosition = this.state.targetPosition;
if (targetPosition === undefined) {
return RectangleEdge.bottom;
}
return getOppositeEdge(targetPosition);
},
enumerable: true,
configurable: true
});
CoachmarkBase.prototype.render = function () {
var _a = this.props, beaconColorOne = _a.beaconColorOne, beaconColorTwo = _a.beaconColorTwo, children = _a.children, target = _a.target, color = _a.color, positioningContainerProps = _a.positioningContainerProps, ariaDescribedBy = _a.ariaDescribedBy, ariaDescribedByText = _a.ariaDescribedByText, ariaLabelledBy = _a.ariaLabelledBy, ariaLabelledByText = _a.ariaLabelledByText, ariaAlertText = _a.ariaAlertText, delayBeforeCoachmarkAnimation = _a.delayBeforeCoachmarkAnimation, styles = _a.styles, theme = _a.theme, className = _a.className, persistentBeak = _a.persistentBeak;
var _b = this.state, beakLeft = _b.beakLeft, beakTop = _b.beakTop, beakRight = _b.beakRight, beakBottom = _b.beakBottom, isCollapsed = _b.isCollapsed, isBeaconAnimating = _b.isBeaconAnimating, isMeasuring = _b.isMeasuring, entityInnerHostRect = _b.entityInnerHostRect, transformOrigin = _b.transformOrigin, alertText = _b.alertText, isMeasured = _b.isMeasured;
// Defaulting the main background before passing it to the styles because it is used for `Beak` too.
var defaultColor = color;
if (!defaultColor && theme) {
defaultColor = theme.semanticColors.primaryButtonBackground;
}
var classNames = getClassNames(styles, {
theme: theme,
beaconColorOne: beaconColorOne,
beaconColorTwo: beaconColorTwo,
className: className,
isCollapsed: isCollapsed,
isBeaconAnimating: isBeaconAnimating,
isMeasuring: isMeasuring,
color: defaultColor,
transformOrigin: transformOrigin,
isMeasured: isMeasured,
entityHostHeight: entityInnerHostRect.height + "px",
entityHostWidth: entityInnerHostRect.width + "px",
width: COACHMARK_WIDTH + "px",
height: COACHMARK_HEIGHT + "px",
delayBeforeCoachmarkAnimation: delayBeforeCoachmarkAnimation + "ms",
});
var finalHeight = isCollapsed ? COACHMARK_HEIGHT : entityInnerHostRect.height;
return (React.createElement(PositioningContainer, __assign({ target: target, offsetFromTarget: BEAK_HEIGHT, componentRef: this._positioningContainer, finalHeight: finalHeight, onPositioned: this._onPositioned, bounds: this._getBounds() }, positioningContainerProps),
React.createElement("div", { className: classNames.root },
ariaAlertText && (React.createElement("div", { className: classNames.ariaContainer, role: "alert", ref: this._ariaAlertContainer, "aria-hidden": !isCollapsed }, alertText)),
React.createElement("div", { className: classNames.pulsingBeacon }),
React.createElement("div", { className: classNames.translateAnimationContainer, ref: this._translateAnimationContainer },
React.createElement("div", { className: classNames.scaleAnimationLayer },
React.createElement("div", { className: classNames.rotateAnimationLayer },
this._positioningContainer.current && (isCollapsed || persistentBeak) && (React.createElement(Beak, { left: beakLeft, top: beakTop, right: beakRight, bottom: beakBottom, direction: this._beakDirection, color: defaultColor })),
React.createElement("div", { className: classNames.entityHost, ref: this._entityHost, tabIndex: -1, "data-is-focusable": true, role: "dialog", "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy },
isCollapsed && [
ariaLabelledBy && (React.createElement("p", { id: ariaLabelledBy, key: 0, className: classNames.ariaContainer }, ariaLabelledByText)),
ariaDescribedBy && (React.createElement("p", { id: ariaDescribedBy, key: 1, className: classNames.ariaContainer }, ariaDescribedByText)),
],
React.createElement(FocusTrapZone, { isClickableOutsideFocusTrap: true, forceFocusInsideTrap: false },
React.createElement("div", { className: classNames.entityInnerHost, ref: this._entityInnerHostElement },
React.createElement("div", { className: classNames.childrenContainer, ref: this._childrenContainer, "aria-hidden": isCollapsed }, children))))))))));
};
CoachmarkBase.prototype.UNSAFE_componentWillReceiveProps = function (newProps) {
if (this.props.isCollapsed && !newProps.isCollapsed) {
// The coachmark is about to open
this._openCoachmark();
}
};
CoachmarkBase.prototype.shouldComponentUpdate = function (newProps, newState) {
return !shallowCompare(newProps, this.props) || !shallowCompare(newState, this.state);
};
CoachmarkBase.prototype.componentDidUpdate = function (prevProps, prevState) {
if (prevState.targetAlignment !== this.state.targetAlignment ||
prevState.targetPosition !== this.state.targetPosition) {
this._setBeakPosition();
}
if (prevProps.preventDismissOnLostFocus !== this.props.preventDismissOnLostFocus) {
this._addListeners();
}
};
CoachmarkBase.prototype.componentDidMount = function () {
var _this = this;
this._async.requestAnimationFrame(function () {
if (_this._entityInnerHostElement.current &&
_this.state.entityInnerHostRect.width + _this.state.entityInnerHostRect.width === 0) {
_this.setState({
isMeasuring: false,
entityInnerHostRect: {
width: _this._entityInnerHostElement.current.offsetWidth,
height: _this._entityInnerHostElement.current.offsetHeight,
},
isMeasured: true,
});
_this._setBeakPosition();
_this.forceUpdate();
}
_this._addListeners();
// We don't want to the user to immediately trigger the Coachmark when it's opened
_this._async.setTimeout(function () {
_this._addProximityHandler(_this.props.mouseProximityOffset);
}, _this.props.delayBeforeMouseOpen);
// Need to add setTimeout to have narrator read change in alert container
if (_this.props.ariaAlertText) {
_this._async.setTimeout(function () {
if (_this.props.ariaAlertText && _this._ariaAlertContainer.current) {
_this.setState({
alertText: _this.props.ariaAlertText,
});
}
}, 0);
}
if (!_this.props.preventFocusOnMount) {
_this._async.setTimeout(function () {
if (_this._entityHost.current) {
_this._entityHost.current.focus();
}
}, 1000);
}
});
};
CoachmarkBase.prototype.componentWillUnmount = function () {
this._async.dispose();
this._events.dispose();
};
CoachmarkBase.prototype._addListeners = function () {
var preventDismissOnLostFocus = this.props.preventDismissOnLostFocus;
var currentDoc = getDocument();
this._events.off();
if (currentDoc) {
this._events.on(currentDoc, 'keydown', this._onKeyDown, true);
if (!preventDismissOnLostFocus) {
this._events.on(currentDoc, 'click', this._dismissOnLostFocus, true);
this._events.on(currentDoc, 'focus', this._dismissOnLostFocus, true);
}
}
};
CoachmarkBase.prototype._dismissOnLostFocus = function (ev) {
var clickTarget = ev.target;
var clickedOutsideCallout = this._translateAnimationContainer.current &&
!elementContains(this._translateAnimationContainer.current, clickTarget);
var target = this.props.target;
if (clickedOutsideCallout && clickTarget !== target && !elementContains(target, clickTarget)) {
this.dismiss(ev);
}
};
CoachmarkBase.prototype._getBounds = function () {
var _a = this.props, isPositionForced = _a.isPositionForced, positioningContainerProps = _a.positioningContainerProps;
if (isPositionForced) {
// If directionalHint direction is the top or bottom auto edge, then we want to set the left/right bounds
// to the window x-axis to have auto positioning work correctly.
if (positioningContainerProps &&
(positioningContainerProps.directionalHint === DirectionalHint.topAutoEdge ||
positioningContainerProps.directionalHint === DirectionalHint.bottomAutoEdge)) {
return {
left: 0,
top: -Infinity,
bottom: Infinity,
right: window.innerWidth,
width: window.innerWidth,
height: Infinity,
};
}
else {
return {
left: -Infinity,
top: -Infinity,
bottom: Infinity,
right: Infinity,
width: Infinity,
height: Infinity,
};
}
}
else {
return undefined;
}
};
CoachmarkBase.prototype._addProximityHandler = function (mouseProximityOffset) {
var _this = this;
if (mouseProximityOffset === void 0) { mouseProximityOffset = 0; }
/**
* An array of cached ids returned when setTimeout runs during
* the window resize event trigger.
*/
var timeoutIds = [];
// Take the initial measure out of the initial render to prevent
// an unnecessary render.
this._async.setTimeout(function () {
_this._setTargetElementRect();
// When the window resizes we want to async
// get the bounding client rectangle.
// Every time the event is triggered we want to
// setTimeout and then clear any previous instances
// of setTimeout.
_this._events.on(window, 'resize', function () {
timeoutIds.forEach(function (value) {
clearInterval(value);
});
timeoutIds.push(_this._async.setTimeout(function () {
_this._setTargetElementRect();
}, 100));
});
}, 10);
// Every time the document's mouse move is triggered
// we want to check if inside of an element and
// set the state with the result.
this._events.on(document, 'mousemove', function (e) {
if (_this.state.isCollapsed) {
var mouseY = e.clientY;
var mouseX = e.clientX;
_this._setTargetElementRect();
var isMouseInProximity = _this._isInsideElement(mouseX, mouseY, mouseProximityOffset);
if (isMouseInProximity !== _this.state.isMouseInProximity) {
_this._openCoachmark();
}
}
if (_this.props.onMouseMove) {
_this.props.onMouseMove(e);
}
});
};
CoachmarkBase.prototype._setTargetElementRect = function () {
if (this._translateAnimationContainer && this._translateAnimationContainer.current) {
this._targetElementRect = this._translateAnimationContainer.current.getBoundingClientRect();
}
};
CoachmarkBase.prototype._isInsideElement = function (mouseX, mouseY, mouseProximityOffset) {
if (mouseProximityOffset === void 0) { mouseProximityOffset = 0; }
return (mouseX > this._targetElementRect.left - mouseProximityOffset &&
mouseX < this._targetElementRect.left + this._targetElementRect.width + mouseProximityOffset &&
mouseY > this._targetElementRect.top - mouseProximityOffset &&
mouseY < this._targetElementRect.top + this._targetElementRect.height + mouseProximityOffset);
};
CoachmarkBase.defaultProps = {
isCollapsed: true,
mouseProximityOffset: 10,
delayBeforeMouseOpen: 3600,
delayBeforeCoachmarkAnimation: 0,
isPositionForced: true,
positioningContainerProps: {
directionalHint: DirectionalHint.bottomAutoEdge,
},
};
return CoachmarkBase;
}(React.Component));
export { CoachmarkBase };
//# sourceMappingURL=Coachmark.base.js.map