azure-devops-ui
Version:
React components for building web UI in Azure DevOps
198 lines (197 loc) • 11.6 kB
JavaScript
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;