office-ui-fabric-react
Version:
Reusable React components for building experiences for Office 365.
398 lines • 20.1 kB
JavaScript
import * as tslib_1 from "tslib";
// Utilities
import * as React from 'react';
import { BaseComponent, classNamesFunction, createRef, elementContains, focusFirstChild, getDocument, shallowCompare } from '../../Utilities';
import { DefaultPalette } from '../../Styling';
import { RectangleEdge, getOppositeEdge } from '../../utilities/positioning';
// Component Dependencies
import { PositioningContainer } from './PositioningContainer/index';
import { Beak, BEAK_HEIGHT, BEAK_WIDTH } from './Beak/Beak';
import { COACHMARK_HEIGHT, COACHMARK_WIDTH, getStyles } from './Coachmark.styles';
import { FocusTrapZone } from '../../FocusTrapZone';
var getClassNames = classNamesFunction();
export var COACHMARK_ATTRIBUTE_NAME = 'data-coachmarkid';
var Coachmark = /** @class */ (function (_super) {
tslib_1.__extends(Coachmark, _super);
function Coachmark(props) {
var _this = _super.call(this, props) || this;
/**
* The cached HTMLElement reference to the Entity Inner Host
* element.
*/
_this._entityHost = createRef();
_this._entityInnerHostElement = createRef();
_this._translateAnimationContainer = createRef();
_this._ariaAlertContainer = createRef();
_this._childrenContainer = createRef();
_this._positioningContainer = 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 === 67 /* c */) ||
(e.which === 13 /* 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) {
beakLeft = distanceAdjustment;
transformOriginX = 'left';
}
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._warnDeprecations({
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(Coachmark.prototype, "_beakDirection", {
get: function () {
var targetPosition = this.state.targetPosition;
if (targetPosition === undefined) {
return RectangleEdge.bottom;
}
return getOppositeEdge(targetPosition);
},
enumerable: true,
configurable: true
});
Coachmark.prototype.render = function () {
var _a = this.props, 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;
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;
var classNames = getClassNames(getStyles, {
isCollapsed: isCollapsed,
isBeaconAnimating: isBeaconAnimating,
isMeasuring: isMeasuring,
entityHostHeight: entityInnerHostRect.height + "px",
entityHostWidth: entityInnerHostRect.width + "px",
width: COACHMARK_WIDTH + "px",
height: COACHMARK_HEIGHT + "px",
color: color,
transformOrigin: transformOrigin,
isMeasured: isMeasured
});
var finalHeight = isCollapsed ? COACHMARK_HEIGHT : entityInnerHostRect.height;
return (React.createElement(PositioningContainer, tslib_1.__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 && (React.createElement(Beak, { left: beakLeft, top: beakTop, right: beakRight, bottom: beakBottom, direction: this._beakDirection, color: color })),
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))))))))));
};
Coachmark.prototype.componentWillReceiveProps = function (newProps) {
if (this.props.isCollapsed && !newProps.isCollapsed) {
// The coachmark is about to open
this._openCoachmark();
}
};
Coachmark.prototype.shouldComponentUpdate = function (newProps, newState) {
return !shallowCompare(newProps, this.props) || !shallowCompare(newState, this.state);
};
Coachmark.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();
}
};
Coachmark.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);
}
_this._async.setTimeout(function () {
if (_this._entityHost.current) {
_this._entityHost.current.focus();
}
}, 1000);
});
};
Coachmark.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);
}
}
};
Coachmark.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);
}
};
Coachmark.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 === 3 /* topAutoEdge */ ||
positioningContainerProps.directionalHint === 7 /* 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;
}
};
Coachmark.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.pageY;
var mouseX = e.pageX;
_this._setTargetElementRect();
var isMouseInProximity = _this._isInsideElement(mouseX, mouseY, mouseProximityOffset);
if (isMouseInProximity !== _this.state.isMouseInProximity) {
_this._openCoachmark();
}
}
if (_this.props.onMouseMove) {
_this.props.onMouseMove(e);
}
});
};
Coachmark.prototype._setTargetElementRect = function () {
if (this._translateAnimationContainer && this._translateAnimationContainer.current) {
this._targetElementRect = this._translateAnimationContainer.current.getBoundingClientRect();
}
};
Coachmark.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);
};
Coachmark.defaultProps = {
isCollapsed: true,
mouseProximityOffset: 10,
delayBeforeMouseOpen: 3600,
color: DefaultPalette.themePrimary,
isPositionForced: true,
positioningContainerProps: {
directionalHint: 7 /* bottomAutoEdge */
}
};
return Coachmark;
}(BaseComponent));
export { Coachmark };
//# sourceMappingURL=Coachmark.js.map