UNPKG

@sap-ux/ui-components

Version:

SAP UI Components Library

232 lines 8.8 kB
import React from 'react'; import './UISplitter.scss'; import { UiIcons } from '../Icons.js'; import { UIIcon } from '../UIIcon/index.js'; export var UISplitterType; (function (UISplitterType) { UISplitterType["Resize"] = "resize"; UISplitterType["Toggle"] = "toggle"; })(UISplitterType || (UISplitterType = {})); export var UISplitterLayoutType; (function (UISplitterLayoutType) { UISplitterLayoutType["Standard"] = "standard"; UISplitterLayoutType["Compact"] = "compact"; })(UISplitterLayoutType || (UISplitterLayoutType = {})); /** * */ export class UISplitter extends React.Component { /** * * @param props */ constructor(props) { super(props); this.mousedownPosition = { x: 0, y: 0 }; this.size = 8; this.compactSize = 4; this.toggleSize = 14; this.state = { active: false }; this.rootRef = React.createRef(); this.stopMousemoveResize = this.stopMousemoveResize.bind(this); this.doMousemoveResize = this.doMousemoveResize.bind(this); // Splitter overlay DOM for block dom during resize this.splitterOverlay = document.createElement('div'); this.splitterOverlay.setAttribute('class', `splitter__overlay ${props.vertical ? 'splitter__overlay--vertical' : 'splitter__overlay--horizontal'}`); } /** * Method called on mousedown over splitter. * Method would start resize session. * * @param {React.MouseEvent} event Mouse. */ startSplitterMove(event) { this.mousedownPosition.x = event.clientX; this.mousedownPosition.y = event.clientY; // Update DOM this.updateSplitterEvents(true); // Stop event bubbling event.stopPropagation(); } /** * Method called when resize session is in progress and user moves mouse. * * @param {React.MouseEvent} event Mouse event. */ doMousemoveResize(event) { const propertyName = this.props.vertical ? 'clientY' : 'clientX'; const coordinateNameName = this.props.vertical ? 'y' : 'x'; const newPosition = event[propertyName] - this.mousedownPosition[coordinateNameName]; this.doResize(newPosition); } /** * Method which receives new delta position of splitter and updates DOM with calling callback. * * @param {number} deltaPosition Delta position of splitter. */ doResize(deltaPosition) { const cssPropertyName = this.props.vertical ? 'top' : 'left'; if (this.animationFrame) { window.cancelAnimationFrame(this.animationFrame); } this.animationFrame = window.requestAnimationFrame(() => { // Call callback if (this.props.onResize(deltaPosition)) { // Update resizer DOM const dom = this.rootRef.current; if (dom) { dom.style[cssPropertyName] = `${deltaPosition - this.size / 2}px`; } } }); } /** * Method called on mouseup or mouseleave events. * Method would end resize session. */ stopMousemoveResize() { this.updateSplitterEvents(false); } /** * Method to update splitter DOM and events depending on if resize started or ended. * * @param {boolean} start Resize is started or ended. */ updateSplitterEvents(start) { if (start) { document.body.addEventListener('mouseup', this.stopMousemoveResize); document.body.addEventListener('mouseleave', this.stopMousemoveResize); document.body.addEventListener('mousemove', this.doMousemoveResize); document.body.appendChild(this.splitterOverlay); } else { document.body.removeEventListener('mouseup', this.stopMousemoveResize); document.body.removeEventListener('mouseleave', this.stopMousemoveResize); document.body.removeEventListener('mousemove', this.doMousemoveResize); this.splitterOverlay.remove(); } if (start) { if (this.props.onResizeStart) { this.props.onResizeStart(); } this.setState({ active: true }); } else { if (this.props.onResizeEnd) { this.props.onResizeEnd(); } requestAnimationFrame(() => { this.setState({ active: false }); }); } } /** * Method called when clicked over splitter and splitter type is 'Toggle'. */ toggleSplitter() { if (this.props.onToggle) { this.props.onToggle(); } } /** * Returns the icon component for the given splitter type. * * @param type - The splitter type. * @returns The corresponding icon element, or an empty fragment if no icon is defined. */ getIcon(type) { let icon; if (type === UISplitterType.Toggle) { icon = UiIcons.ArrowLeft; } return icon ? React.createElement(UIIcon, { iconName: icon }) : React.createElement(React.Fragment, null); } /** * Method called when keydown events fired while splitter is focused. * Method enables support for resize using keyboard. * * @param {React.KeyboardEvent<HTMLDivElement>} event KeyDown event. */ onKeyDown(event) { if (this.props.type === UISplitterType.Resize) { const stepSize = 10; let deltaPosition = 0; if (['ArrowLeft', 'ArrowUp'].includes(event.key)) { deltaPosition = -stepSize; } else if (['ArrowRight', 'ArrowDown'].includes(event.key)) { deltaPosition = stepSize; } if (deltaPosition === 0) { // There no resize with keyboard return; } // Do full resize cycle - start, move and end if (this.props.onResizeStart) { this.props.onResizeStart(); } this.doResize(deltaPosition); if (this.props.onResizeEnd) { this.props.onResizeEnd(); } } else if (this.props.type === UISplitterType.Toggle && event.key === 'Enter') { this.toggleSplitter(); } } /** * Method returns class names string depending on props and component state. * * @returns {number} Minimal size of section. */ getClassNames() { const { type, vertical, hidden, splitterLayoutType } = this.props; const { active } = this.state; let classNames = `splitter splitter--${type}`; // vertical or horizontal classNames += ` ${vertical ? 'splitter--vertical' : 'splitter--horizontal'}`; if (hidden && type === UISplitterType.Resize) { classNames += ' splitter--hidden'; } classNames += ` ${splitterLayoutType === UISplitterLayoutType.Standard ? 'splitter--standard' : 'splitter--compact'}`; if (active && type === UISplitterType.Resize) { classNames += ' splitter--active'; } return classNames; } /** * @returns {React.ReactElement} */ render() { const { type, vertical, hidden, title, splitterLayoutType } = this.props; const tabIndex = this.props.splitterTabIndex ?? 0; let size = splitterLayoutType === UISplitterLayoutType.Standard ? this.size : this.compactSize; if (type === UISplitterType.Toggle) { size = this.toggleSize; } const splitterOffset = -size / 2; const role = type === UISplitterType.Toggle ? 'button' : 'separator'; const orientation = vertical ? 'horizontal' : 'vertical'; let ariaPressed; if (type === UISplitterType.Toggle) { ariaPressed = !hidden; } return (React.createElement("div", { ref: this.rootRef, role: role, "aria-orientation": orientation, "aria-pressed": ariaPressed, title: title, tabIndex: tabIndex, onKeyDown: this.onKeyDown.bind(this), style: { ...(vertical && { height: size, top: splitterOffset }), ...(!vertical && { width: size, left: splitterOffset }) }, onMouseDown: type === UISplitterType.Resize ? (event) => this.startSplitterMove(event) : undefined, onClick: type === UISplitterType.Toggle ? () => this.toggleSplitter() : undefined, className: this.getClassNames() }, React.createElement("div", { className: "splitter__grip" }, this.getIcon(type)))); } } //# sourceMappingURL=UISplitter.js.map