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