UNPKG

azure-devops-ui

Version:

React components for building web UI in Azure DevOps

206 lines (205 loc) 12.3 kB
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"; export class Callout extends React.Component { constructor() { super(...arguments); this.calloutContent = React.createRef(); } render() { const { portalProps } = this.props; return (React.createElement(Portal, Object.assign({}, portalProps, { className: css(portalProps && portalProps.className, this.props.anchorElement && "bolt-layout-relative") }), React.createElement(CalloutContent, Object.assign({ ref: this.calloutContent }, this.props)))); } componentWillUnmount() { // 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(); } } updateLayout() { if (this.calloutContent.current) { this.calloutContent.current.updateLayout(); } } } Callout.defaultProps = { blurDismiss: false, viewportChangeDismiss: true }; class CalloutContent extends React.Component { constructor(props) { super(props); this.calloutElement = React.createRef(); this.relayoutTimer = new TimerManagement(); this.scrollListen = false; this.scrollEvent = null; this.initialScreenWidth = window.innerWidth; this.onBlur = () => { this.props.onDismiss && this.props.onDismiss(); }; this.onClick = (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 = (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 = () => { // 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(() => { this.updateLayout(); }, 200); } }; this.onScroll = (event) => { if (this.scrollListen) { this.scrollEvent = event.nativeEvent; } }; this.onScrollDocument = (event) => { if (this.scrollListen) { if (event === this.scrollEvent) { this.scrollEvent = null; } else { if (this.props.viewportChangeDismiss === true) { const { anchorElement } = this.props; // 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(() => { this.updateLayout(); }, 50); } } } }; // Track the element that had focus when we mounted. this.focusElement = document.activeElement; this.contentElement = props.contentRef || React.createRef(); } render() { const { blurDismiss, contentJustification, contentLocation, contentOrientation, focuszoneProps, lightDismiss, modal, onAnimationEnd, onMouseEnter, onMouseLeave, anchorElement } = this.props; let 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 }, (props) => React.createElement(FocusZone, Object.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, Object.assign({}, focuszoneProps), content); } } const 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))); } componentDidMount() { 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; } } componentDidUpdate() { if (this.props.updateLayout) { this.updateLayout(); } } componentWillUnmount() { if (this.scrollListen) { document.removeEventListener("scroll", this.onScrollDocument, true); window.removeEventListener("resize", this.onResize); } if (this.relayoutTimer) { this.relayoutTimer.clearAllTimers(); } } portalWillUnmount() { const contentElement = this.contentElement.current; const { focusElement } = this; // 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(() => { // 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); } } updateLayout() { 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); } } } } renderContent(onFocus, onBlur) { const { contentJustification, contentOrientation, contentSize, height, width } = this.props; 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, width } }, this.props.children)); } }