UNPKG

azure-devops-ui

Version:

React components for building web UI in Azure DevOps

210 lines (209 loc) 12.5 kB
import "../../CommonImports"; import "../../Core/core.css"; import "./Sizer.css"; import * as React from "react"; import { ObservableLike } from '../../Core/Observable'; import { TimerManagement } from '../../Core/TimerManagement'; import { Observer } from '../../Observer'; import { css, KeyCode, Mouse } from '../../Util'; import { Orientation, Position } from "./Sizer.Props"; import { Tooltip } from '../../TooltipEx'; import * as Resources from '../../Resources.Widgets'; import { Button } from '../../Button'; import { Location } from '../../Utilities/Position'; import { Callout } from '../../Callout'; export class Sizer extends React.Component { constructor(props) { super(props); this.sizerRef = React.createRef(); this.sizerCalloutRef = React.createRef(); this.lastLocation = -1; this.timerManagement = new TimerManagement(); this.state = { showPortal: false, resizeInProgress: false, lastTouchLocation: 0, resizeButtonsUsed: false }; this.onDragStart = (event) => { // Prevent default to stop the ColumnDragDropBehavior from trying to handle this event. event.preventDefault(); this.setState({ showPortal: false }); Mouse.setCapture(this.onMouseCapture); }; this.onDragEnd = (event) => { Mouse.releaseCapture(this.onMouseCapture); }; this.onDragEquivalentButtonClick = (event, direction) => { event.preventDefault(); this.setState({ resizeButtonsUsed: true, resizeInProgress: true }); this.updateSize(event, direction, true); this.onSizeEnd(); }; this.onMouseCapture = (event) => { var _a, _b; const locationTouchDiff = this.getMouseLocation(event) - this.state.lastTouchLocation; // Recompute the size and update the lastLocation based on the amount changed. this.lastLocation += this.updateSize(event, this.getMouseLocation(event) - this.lastLocation); // Remove the portal now that we are no longer sizing. if (event.type === "mouseup") { event.preventDefault(); event.stopPropagation(); document.body.setAttribute('data-resize-active', 'false'); setTimeout(() => { document.body.removeAttribute('data-resize-active'); }, 0); if (locationTouchDiff !== 0) { this.setState({ resizeInProgress: false, showPortal: false }); let focuselt = (_a = this.sizerRef.current) === null || _a === void 0 ? void 0 : _a.parentElement; focuselt === null || focuselt === void 0 ? void 0 : focuselt.blur(); } else if (this.state.resizeInProgress) { this.setState({ showPortal: true }); let focuselt = (_b = this.sizerRef.current) === null || _b === void 0 ? void 0 : _b.parentElement; focuselt === null || focuselt === void 0 ? void 0 : focuselt.focus(); } else { this.setState({ showPortal: false }); } // If the user wanted to know when the sizing was complete, we will notify // the now that sizing is complete. this.onSizeEnd(); } }; this.onKeyDown = (event) => { var _a; if (!event.defaultPrevented) { const { orientation } = this.props; let sizeChange; this.setState({ resizeInProgress: true }); if (orientation === Orientation.row) { if (event.which === KeyCode.enter) { let focuselt = (_a = this.sizerRef.current) === null || _a === void 0 ? void 0 : _a.parentElement; focuselt === null || focuselt === void 0 ? void 0 : focuselt.focus(); } if (event.which === KeyCode.leftArrow) { sizeChange = this.updateSize(event.nativeEvent, -1, true); } else if (event.which === KeyCode.rightArrow) { sizeChange = this.updateSize(event.nativeEvent, 1, true); } } else { if (event.which === KeyCode.upArrow) { sizeChange = this.updateSize(event.nativeEvent, -1, true); } else if (event.which === KeyCode.downArrow) { sizeChange = this.updateSize(event.nativeEvent, 1, true); } } // If we changed the size, we will prevent the default and start a // timer to end the sizing operation. Since there is no gesture to // start and complete with the keyboard. if (sizeChange) { event.preventDefault(); this.debouncedEnd(); } } }; this.onSizeEnd = () => { if (this.props.onSizeEnd) { this.props.onSizeEnd(this.props.id); } document.body.removeAttribute("data-resize-active"); }; this.onMouseDown = (event) => { // Look for only main button clicks, not right click if (!event.defaultPrevented && event.button === 0 && event.currentTarget === this.sizerRef.current) { document.body.setAttribute('data-resize-active', 'true'); this.lastLocation = this.getMouseLocation(event.nativeEvent); if (!this.state.resizeInProgress) { this.setState({ lastTouchLocation: this.lastLocation, resizeInProgress: true }); } // Capture the mouse, this will let us size the column when dragging. Mouse.setCapture(this.onMouseCapture); // Show the portal that keeps our sizing actions for effecting the other elements. this.setState({ showPortal: true }); // Don't let the event set focus or start a mouse selection. event.preventDefault(); } }; this.debouncedEnd = this.timerManagement.debounce(this.onSizeEnd, 500, { trailing: true }); this.handleClickOutside = this.handleClickOutside.bind(this); } render() { const { divider } = this.props; return (React.createElement(Tooltip, { text: Resources.SizerAnnouncement, anchorOffset: { horizontal: 0, vertical: 30 }, anchorOrigin: { horizontal: Location.center, vertical: Location.start } }, React.createElement("div", { "aria-label": this.props.ariaLabel || Resources.SizerAnnouncement, "aria-valuetext": `${ObservableLike.getValue(this.props.size)} pixels wide`, "aria-valuenow": ObservableLike.getValue(this.props.size), className: css(this.props.className, "bolt-sizer", (this.state.resizeInProgress || this.state.showPortal) && 'bolt-sizer-active', this.props.orientation === Orientation.row ? "bolt-sizer-row flex-row" : "bolt-sizer-column flex-column", divider && "divider"), id: this.props.id, onKeyDown: this.onKeyDown, onMouseDown: this.onMouseDown, onDragStart: this.onDragStart, onDragEnd: this.onDragEnd, ref: this.sizerRef, role: "separator", tabIndex: this.props.tabIndex }, this.state.showPortal && (React.createElement(React.Fragment, null, React.createElement(Callout, { anchorElement: this.sizerRef.current, anchorOffset: { horizontal: 0, vertical: -50 }, anchorOrigin: { horizontal: Location.start, vertical: Location.start }, calloutOrigin: { horizontal: Location.center, vertical: Location.start }, contentShadow: true, viewportChangeDismiss: false }, React.createElement("div", { className: "flex-row bolt-sizer-callout-row", ref: this.sizerCalloutRef }, React.createElement(Button, { ariaLabel: Resources.ColumnButtonSizerLeftAriaLabel, className: "icon-only bolt-sizer-resize-button", iconProps: { iconName: "ChevronLeft" }, tooltipProps: { anchorOrigin: { horizontal: Location.center, vertical: Location.end }, anchorOffset: { horizontal: 0, vertical: -70 }, text: Resources.ColumnButtonSizerLeft }, onMouseDown: (e) => e.stopPropagation(), onClick: (event) => this.onDragEquivalentButtonClick(event.nativeEvent, -1) }), React.createElement(Button, { ariaLabel: Resources.ColumnButtonSizerRightAriaLabel, className: "icon-only bolt-sizer-resize-button", iconProps: { iconName: "ChevronRight" }, tooltipProps: { anchorOrigin: { horizontal: Location.center, vertical: Location.end }, anchorOffset: { horizontal: 0, vertical: -70 }, text: Resources.ColumnButtonSizerRight }, onMouseDown: (e) => e.stopPropagation(), onClick: (event) => this.onDragEquivalentButtonClick(event.nativeEvent, 1) })))))))); } componentDidMount() { document.addEventListener("mousedown", this.handleClickOutside); } componentWillUnmount() { Mouse.releaseCapture(this.onMouseCapture); document.removeEventListener("mousedown", this.handleClickOutside); document.body.removeAttribute('data-resize-active'); } getMouseLocation(event) { return this.props.orientation === Orientation.row ? event.pageX : event.pageY; } handleClickOutside(event) { var _a; const composedPathArray = Array.from(event.composedPath()); const isInComposedPath = composedPathArray.includes(this.sizerCalloutRef.current); if (this.state.resizeInProgress && this.getMouseLocation(event) !== this.state.lastTouchLocation && !isInComposedPath) { this.setState({ resizeInProgress: false, showPortal: false, resizeButtonsUsed: false, }); let focuselt = (_a = this.sizerRef.current) === null || _a === void 0 ? void 0 : _a.parentElement; focuselt === null || focuselt === void 0 ? void 0 : focuselt.blur(); this.onSizeEnd(); } } updateSize(event, sizeChange, applyKeyboardMultiplier = false) { // Compute the udpated size of the Sized element. const currentSize = ObservableLike.getValue(this.props.size); const multiplier = this.props.position === Position.far ? -1 : 1; const keyboardStepMultiplier = applyKeyboardMultiplier ? this.props.keyboardStepMultiplier || 1 : 1; const updatedSize = Math.floor(Math.min(this.props.maxSize, Math.max(this.props.minSize, currentSize + sizeChange * multiplier * keyboardStepMultiplier))); this.props.onSize(event, updatedSize, this.props.id); // console.log("currentSize = " + currentSize + " updateSize = " + updatedSize); return (updatedSize - currentSize) * multiplier; } } Sizer.defaultProps = { divider: true, maxSize: 10000, minSize: 100 }; /** * The Sized function is used to produce a div that has a fixed width or height * based on the orientation of the sized props. This is a basic component that * can be used with the Sizer to produce a basic splitter like UI. * * @param props properties to render the appropriate container element given the * props. */ export function Sized(props) { return (React.createElement(Observer, { height: props.height, width: props.width }, (observedProps) => { const style = {}; // Add any specific height that has been defined. if (observedProps.height !== undefined) { style.height = observedProps.height + "px"; } // Add any specific width that has been defined. if (observedProps.width !== undefined) { style.width = observedProps.width + "px"; } return (React.createElement("div", { className: css(props.className, "flex-noshrink"), style: style }, props.children)); })); }