azure-devops-ui
Version:
React components for building web UI in Azure DevOps
215 lines (214 loc) • 13.5 kB
JavaScript
import { __assign, __extends } from "tslib";
import "../../CommonImports";
import "../../Core/core.css";
import "./Callout.css";
import * as React from "react";
import { FocusWithin } from '../../FocusWithin';
import { FocusZone } from '../../FocusZone';
import { Portal } from '../../Portal';
import { css, getSafeId, KeyCode } from '../../Util';
import { Location, position, updateLayout } from '../../Utilities/Position';
import { TimerManagement } from '../../Core/TimerManagement';
import { SurfaceBackground, SurfaceContext } from '../../Surface';
import { ContentJustification, ContentLocation, ContentOrientation, ContentSize } from "./Callout.Props";
var Callout = /** @class */ (function (_super) {
__extends(Callout, _super);
function Callout() {
var _this = _super !== null && _super.apply(this, arguments) || this;
_this.calloutContent = React.createRef();
return _this;
}
Callout.prototype.render = function () {
var portalProps = this.props.portalProps;
return (React.createElement(Portal, __assign({}, portalProps, { className: css(portalProps && portalProps.className, this.props.anchorElement && "bolt-layout-relative") }),
React.createElement(CalloutContent, __assign({ ref: this.calloutContent }, this.props))));
};
Callout.prototype.componentWillUnmount = function () {
// We need to let the content handle the WillUnmount before the Portal, this
// will ensure the the callout can deal with unmounting content that still has
// focus. Otherwise the root will be detached from the document and focus will
// have moved to the body.
if (this.calloutContent.current) {
this.calloutContent.current.portalWillUnmount();
}
};
Callout.prototype.updateLayout = function () {
if (this.calloutContent.current) {
this.calloutContent.current.updateLayout();
}
};
Callout.defaultProps = {
blurDismiss: false,
viewportChangeDismiss: true
};
return Callout;
}(React.Component));
export { Callout };
var CalloutContent = /** @class */ (function (_super) {
__extends(CalloutContent, _super);
function CalloutContent(props) {
var _this = _super.call(this, props) || this;
_this.calloutElement = React.createRef();
_this.relayoutTimer = new TimerManagement();
_this.scrollListen = false;
_this.scrollEvent = null;
_this.initialScreenWidth = window.innerWidth;
_this.onBlur = function () {
_this.props.onDismiss && _this.props.onDismiss();
};
_this.onClick = function (event) {
// If we click on the light dismiss div we will dismiss it.
if (_this.props.lightDismiss && !event.defaultPrevented) {
if (_this.props.onDismiss) {
_this.props.onDismiss();
}
event.preventDefault();
}
};
_this.onKeyDown = function (event) {
var _a, _b;
// If we press escape from within the callout this will dismiss it.
if (_this.props.escDismiss && event.which === KeyCode.escape && !event.defaultPrevented) {
if (_this.props.onDismiss) {
_this.props.onDismiss();
}
event.preventDefault();
}
(_b = (_a = _this.props).onKeyDown) === null || _b === void 0 ? void 0 : _b.call(_a, event);
};
_this.onResize = function () {
// Fix for issue where the soft keyboard on android closes callouts.
if (_this.props.viewportChangeDismiss === true &&
(_this.initialScreenWidth !== window.innerWidth ||
!document.activeElement ||
(document.activeElement.tagName !== "INPUT" && document.activeElement.tagName !== "TEXTAREA"))) {
_this.props.onDismiss && _this.props.onDismiss();
}
else if (_this.props.updateLayout) {
_this.relayoutTimer.clearAllTimers();
_this.relayoutTimer.setTimeout(function () {
_this.updateLayout();
}, 200);
}
};
_this.onScroll = function (event) {
if (_this.scrollListen) {
_this.scrollEvent = event.nativeEvent;
}
};
_this.onScrollDocument = function (event) {
if (_this.scrollListen) {
if (event === _this.scrollEvent) {
_this.scrollEvent = null;
}
else {
if (_this.props.viewportChangeDismiss === true) {
var anchorElement = _this.props.anchorElement;
// If the element containing the anchor is scrolled dismiss the callout.
if (event.target && anchorElement && event.target.contains(anchorElement)) {
_this.props.onDismiss && _this.props.onDismiss();
}
}
else if (_this.props.updateLayout) {
_this.relayoutTimer.setTimeout(function () {
_this.updateLayout();
}, 50);
}
}
}
};
// Track the element that had focus when we mounted.
_this.focusElement = document.activeElement;
_this.contentElement = props.contentRef || React.createRef();
return _this;
}
CalloutContent.prototype.render = function () {
var _this = this;
var _a = this.props, blurDismiss = _a.blurDismiss, contentJustification = _a.contentJustification, contentLocation = _a.contentLocation, contentOrientation = _a.contentOrientation, focuszoneProps = _a.focuszoneProps, lightDismiss = _a.lightDismiss, modal = _a.modal, onAnimationEnd = _a.onAnimationEnd, onMouseEnter = _a.onMouseEnter, onMouseLeave = _a.onMouseLeave, anchorElement = _a.anchorElement;
var content;
// If we have both a FocusWithin and a FocusZone we need to use the functional version
// of the FocusWithin to allow the FocusZone to contain the content directly.
if (blurDismiss && focuszoneProps) {
content = (React.createElement(FocusWithin, { onBlur: this.onBlur, updateStateOnFocusChange: false }, function (props) { return React.createElement(FocusZone, __assign({}, focuszoneProps), _this.renderContent(props.onFocus, props.onBlur)); }));
}
else {
content = this.renderContent();
// Add the focus tracker to dismiss the callout if we are dismissing on blur.
if (blurDismiss) {
content = (React.createElement(FocusWithin, { onBlur: this.onBlur, updateStateOnFocusChange: false }, content));
}
// Add focus zone if focuszoneProperties are specified
if (focuszoneProps) {
content = React.createElement(FocusZone, __assign({}, focuszoneProps), content);
}
}
var lightDismissDiv = lightDismiss ? (React.createElement("div", { className: css("absolute-fill bolt-light-dismiss", modal && "bolt-callout-modal"), onClick: this.onClick })) : null;
// The callout is wrapped in a floating element in the portal.
// If lightDismiss is enabled we will create an absolute-fill div to capture onClick events.
return (React.createElement(SurfaceContext.Provider, { value: { background: SurfaceBackground.callout } },
React.createElement("div", { className: "flex-row flex-grow" },
React.createElement("div", { className: css(this.props.className, "bolt-callout absolute", contentLocation !== undefined && "absolute-fill", contentJustification === ContentJustification.Start && "justify-start", contentJustification === ContentJustification.Center && "justify-center", contentJustification === ContentJustification.End && "justify-end", contentLocation === ContentLocation.Start && "flex-start", contentLocation === ContentLocation.Center && "flex-center", contentLocation === ContentLocation.End && "flex-end", contentOrientation === ContentOrientation.Column && "flex-column", contentOrientation !== ContentOrientation.Column && "flex-row", modal && !lightDismiss && "bolt-callout-modal"), id: getSafeId(this.props.id), onAnimationEnd: onAnimationEnd, onMouseEnter: onMouseEnter, onMouseLeave: onMouseLeave, onKeyDown: this.onKeyDown, ref: this.calloutElement, role: this.props.role, tabIndex: -1 },
!anchorElement && lightDismissDiv,
content),
!!anchorElement && lightDismissDiv)));
};
CalloutContent.prototype.componentDidMount = function () {
this.updateLayout();
// If this is an element relative layout we need to listen for scroll events
// on the document and dismiss the callout if the scroll event didnt pass
// through the callout.
if (this.props.anchorElement) {
window.addEventListener("resize", this.onResize);
document.addEventListener("scroll", this.onScrollDocument, true);
this.scrollListen = true;
}
};
CalloutContent.prototype.componentDidUpdate = function () {
if (this.props.updateLayout) {
this.updateLayout();
}
};
CalloutContent.prototype.componentWillUnmount = function () {
if (this.scrollListen) {
document.removeEventListener("scroll", this.onScrollDocument, true);
window.removeEventListener("resize", this.onResize);
}
if (this.relayoutTimer) {
this.relayoutTimer.clearAllTimers();
}
};
CalloutContent.prototype.portalWillUnmount = function () {
var contentElement = this.contentElement.current;
var focusElement = this.focusElement;
// If the callout has focus when unmounted we need to set focus back to the last element with focus.
// Need to wait for next tick otherwise focus/blur events are not fired.
if (focusElement && contentElement && contentElement.contains(document.activeElement)) {
window.setTimeout(function () {
// We need to make sure the active element is portal after the timeout.
// It may have moved through other means before the timeout expires.
// Set focus to the focusElement if our element contains focus, or if the focus has gone back to the document body
if (contentElement.contains(document.activeElement) || document.activeElement === document.body || document.activeElement === null) {
focusElement.focus();
}
}, 0);
}
};
CalloutContent.prototype.updateLayout = function () {
if (this.props.contentLocation === undefined) {
if (this.calloutElement.current) {
// Position the element based on the initial layout parameters.
position(this.calloutElement.current, this.props.calloutOrigin || { horizontal: Location.start, vertical: Location.start }, this.props.anchorOffset, this.props.anchorElement, this.props.anchorOrigin, this.props.anchorPoint, this.props.anchorElement ? 5000 : 0);
// Now that the component is placed at the requested location, update
// the layout if the caller didnt request a fixed layout.
if (!this.props.fixedLayout) {
updateLayout(this.calloutElement.current, this.props.calloutOrigin || { horizontal: Location.start, vertical: Location.start }, this.props.anchorOffset, this.props.anchorElement, this.props.anchorOrigin, this.props.anchorPoint, this.props.anchorElement ? 5000 : 0);
}
}
}
};
CalloutContent.prototype.renderContent = function (onFocus, onBlur) {
var _a = this.props, contentJustification = _a.contentJustification, contentOrientation = _a.contentOrientation, contentSize = _a.contentSize, height = _a.height, width = _a.width;
return (React.createElement("div", { "aria-describedby": getSafeId(this.props.ariaDescribedBy), "aria-label": this.props.ariaLabel, "aria-labelledby": getSafeId(this.props.ariaLabelledBy), "aria-modal": this.props.modal, className: css(this.props.contentClassName, "bolt-callout-content", this.props.contentShadow && "bolt-callout-shadow", contentJustification === ContentJustification.Stretch && "flex-grow", contentOrientation === ContentOrientation.Column && "flex-column", contentOrientation === ContentOrientation.Row && "flex-row", contentSize === ContentSize.Small && "bolt-callout-small", contentSize === ContentSize.Medium && "bolt-callout-medium", contentSize === ContentSize.Large && "bolt-callout-large", contentSize === ContentSize.ExtraLarge && "bolt-callout-extra-large", contentSize === ContentSize.Auto && "bolt-callout-auto"), onBlur: onBlur, onFocus: onFocus, onScroll: this.onScroll, ref: this.contentElement, role: this.props.role || "dialog", style: { height: height, width: width } }, this.props.children));
};
return CalloutContent;
}(React.Component));