UNPKG

azure-devops-ui

Version:

React components for building web UI in Azure DevOps

198 lines (197 loc) 11.6 kB
import "../../CommonImports"; import "../../Core/core.css"; import "./Panel.css"; import * as React from "react"; import { ScreenContext, ScreenSize } from '../../Core/Util/Screen'; import { format } from '../../Core/Util/String'; import { Callout, ContentJustification, ContentLocation, ContentOrientation, ContentSize } from '../../Callout'; import { FocusZoneKeyStroke } from '../../FocusZone'; import { Observer } from '../../Observer'; import * as Resources from '../../Resources.Layer'; import { Spacing, Surface, SurfaceContext } from '../../Surface'; import { css, KeyCode, Mouse } from '../../Util'; /** * Default minimum panel width in pixels when resizeOptions.minWidth is not specified. * Must match the @default documented in IPanelResizeOptions.minWidth. */ const defaultMinWidth = 340; export class CustomPanel extends React.Component { constructor(props) { super(props); this.calloutContentRef = React.createRef(); this.resizeStartX = 0; this.resizeStartWidth = 0; this.onResizeMouseDown = (event) => { if (event.defaultPrevented || event.button !== 0) { return; } const contentEl = this.calloutContentRef.current; if (!contentEl) { return; } this.resizeStartX = event.pageX; this.resizeStartWidth = contentEl.getBoundingClientRect().width; // Add resize class directly to avoid a re-render. A setState here would // cause Callout to re-render with style={{ width: undefined }}, which // clears the manually-applied inline width and produces a visible flicker. contentEl.classList.add("bolt-panel-resizing"); document.body.setAttribute("data-resize-active", "true"); Mouse.setCapture(this.onResizeMouseCapture); event.preventDefault(); }; this.onResizeMouseCapture = (event) => { var _a, _b, _c; const { resizeOptions } = this.props; const minWidth = (_a = resizeOptions === null || resizeOptions === void 0 ? void 0 : resizeOptions.minWidth) !== null && _a !== void 0 ? _a : defaultMinWidth; const maxWidth = (_b = resizeOptions === null || resizeOptions === void 0 ? void 0 : resizeOptions.maxWidth) !== null && _b !== void 0 ? _b : window.innerWidth * 0.9; // Panel is on the right, so dragging left (negative deltaX) increases width. const deltaX = this.resizeStartX - event.pageX; const newWidth = Math.floor(Math.min(maxWidth, Math.max(minWidth, this.resizeStartWidth + deltaX))); const contentEl = this.calloutContentRef.current; if (contentEl) { contentEl.style.width = `${newWidth}px`; contentEl.style.maxWidth = `${newWidth}px`; } if (event.type === "mouseup") { Mouse.releaseCapture(this.onResizeMouseCapture); document.body.removeAttribute("data-resize-active"); contentEl === null || contentEl === void 0 ? void 0 : contentEl.classList.remove("bolt-panel-resizing"); this.setState({ panelWidth: newWidth }); (_c = resizeOptions === null || resizeOptions === void 0 ? void 0 : resizeOptions.onWidthChanged) === null || _c === void 0 ? void 0 : _c.call(resizeOptions, newWidth); } }; this.onResizeKeyDown = (event) => { var _a, _b, _c, _d; const { resizeOptions } = this.props; if (!(resizeOptions === null || resizeOptions === void 0 ? void 0 : resizeOptions.userResizable)) { return; } const minWidth = (_a = resizeOptions.minWidth) !== null && _a !== void 0 ? _a : defaultMinWidth; const maxWidth = Math.floor((_b = resizeOptions.maxWidth) !== null && _b !== void 0 ? _b : window.innerWidth * 0.9); const step = event.shiftKey ? 50 : 10; const contentEl = this.calloutContentRef.current; if (!contentEl) { return; } const currentWidth = (_c = this.state.panelWidth) !== null && _c !== void 0 ? _c : Math.floor(contentEl.getBoundingClientRect().width); let newWidth; switch (event.which) { case KeyCode.leftArrow: // Left arrow increases width (panel grows to the left). newWidth = Math.min(maxWidth, currentWidth + step); break; case KeyCode.rightArrow: // Right arrow decreases width. newWidth = Math.max(minWidth, currentWidth - step); break; case KeyCode.home: newWidth = maxWidth; break; case KeyCode.end: newWidth = minWidth; break; default: return; } event.preventDefault(); event.stopPropagation(); this.setState({ panelWidth: newWidth }); (_d = resizeOptions.onWidthChanged) === null || _d === void 0 ? void 0 : _d.call(resizeOptions, newWidth); }; this.defaultActiveElement = () => { // We don't ever want the Panel to set focus to the body, so if the defaultActiveElement // prop that is provided cannot be found, instead use the panel's default focus element. const { defaultActiveElement } = this.props; const selector = typeof defaultActiveElement === "function" ? defaultActiveElement() : defaultActiveElement; if (selector && this.calloutContentRef.current) { const matches = this.calloutContentRef.current.querySelectorAll(selector); if (matches && matches.length) { return selector; } } return ".bolt-panel-focus-element"; }; this.state = {}; } render() { const { ariaLabel, ariaLabelledBy, blurDismiss, calloutClassName, children, className, contentClassName, escDismiss, id, lightDismiss, modal, onDismiss, portalProps, resizeOptions, size = ContentSize.Medium } = this.props; const hasCustomWidth = this.state.panelWidth != null; return (React.createElement(Observer, { size: this.context.size }, (props) => { const fullscreen = props.size === ScreenSize.xsmall; return (React.createElement(Callout, { ariaLabel: ariaLabel, ariaLabelledBy: ariaLabelledBy, blurDismiss: blurDismiss, className: css("bolt-panel", calloutClassName), contentClassName: css(contentClassName, "bolt-panel-callout-content scroll-auto", fullscreen ? "bolt-panel-fullscreen absolute-fill" : "relative"), contentJustification: ContentJustification.Stretch, contentLocation: ContentLocation.End, contentOrientation: ContentOrientation.Column, contentRef: this.calloutContentRef, contentShadow: true, contentSize: fullscreen || hasCustomWidth ? undefined : size, escDismiss: escDismiss, id: id, focuszoneProps: { circularNavigation: true, defaultActiveElement: this.defaultActiveElement, focusOnMount: true, handleTabKey: true, includeDefaults: true, postprocessKeyStroke: function (event) { // We want to prevent moving outside the panel if there are no focusable elements in the panel. event.which === KeyCode.tab && event.preventDefault(); return FocusZoneKeyStroke.IgnoreParents; } }, lightDismiss: lightDismiss, modal: modal, onDismiss: onDismiss, portalProps: portalProps }, React.createElement(SurfaceContext.Consumer, null, surfaceContext => { var _a, _b, _c, _d, _e, _f, _g, _h; return (React.createElement(Surface, Object.assign({}, surfaceContext, { spacing: Spacing.default }), React.createElement("div", { className: css(className, "bolt-panel-root flex-column flex-grow scroll-auto", (resizeOptions === null || resizeOptions === void 0 ? void 0 : resizeOptions.userResizable) && !fullscreen && "bolt-panel-resizable") }, React.createElement("div", { className: "bolt-panel-focus-element no-outline", tabIndex: -1 }), children, (resizeOptions === null || resizeOptions === void 0 ? void 0 : resizeOptions.userResizable) && !fullscreen && (React.createElement("div", { className: "bolt-panel-resize-handle", onMouseDown: this.onResizeMouseDown, onKeyDown: this.onResizeKeyDown, tabIndex: 0, role: "separator", "aria-orientation": "vertical", "aria-label": Resources.PanelResizeLabel, "aria-valuenow": (_c = (_b = (_a = this.state.panelWidth) !== null && _a !== void 0 ? _a : this.getRenderedWidth()) !== null && _b !== void 0 ? _b : resizeOptions.minWidth) !== null && _c !== void 0 ? _c : defaultMinWidth, "aria-valuemin": (_d = resizeOptions.minWidth) !== null && _d !== void 0 ? _d : defaultMinWidth, "aria-valuemax": (_e = resizeOptions.maxWidth) !== null && _e !== void 0 ? _e : Math.floor(window.innerWidth * 0.9), "aria-valuetext": format(Resources.PanelResizeValueText, (_h = (_g = (_f = this.state.panelWidth) !== null && _f !== void 0 ? _f : this.getRenderedWidth()) !== null && _g !== void 0 ? _g : resizeOptions.minWidth) !== null && _h !== void 0 ? _h : defaultMinWidth) }))))); }))); })); } componentDidMount() { this.applyCustomWidth(); } componentDidUpdate(_prevProps, prevState) { if (prevState.panelWidth !== this.state.panelWidth) { this.applyCustomWidth(); } } componentWillUnmount() { var _a; Mouse.releaseCapture(this.onResizeMouseCapture); document.body.removeAttribute("data-resize-active"); (_a = this.calloutContentRef.current) === null || _a === void 0 ? void 0 : _a.classList.remove("bolt-panel-resizing"); } animateOut() { return Promise.resolve(); } getRenderedWidth() { const contentEl = this.calloutContentRef.current; if (contentEl) { const width = contentEl.getBoundingClientRect().width; if (width > 0) { return Math.floor(width); } } return undefined; } applyCustomWidth() { const contentEl = this.calloutContentRef.current; if (!contentEl) { return; } // Don't override width in fullscreen mode. if (contentEl.classList.contains("bolt-panel-fullscreen")) { contentEl.style.width = ""; contentEl.style.maxWidth = ""; return; } const width = this.state.panelWidth; if (width != null) { contentEl.style.width = `${width}px`; contentEl.style.maxWidth = `${width}px`; } else { contentEl.style.width = ""; contentEl.style.maxWidth = ""; } } } CustomPanel.defaultProps = { escDismiss: true, lightDismiss: true, modal: true }; CustomPanel.contextType = ScreenContext;