@ui5/webcomponents-react
Version:
React Wrapper for UI5 Web Components and additional components
249 lines (245 loc) • 11.4 kB
JavaScript
'use client';
import ButtonDesign from '@ui5/webcomponents/dist/types/ButtonDesign.js';
import horizontalGripIcon from '@ui5/webcomponents-icons/dist/horizontal-grip.js';
import verticalGripIcon from '@ui5/webcomponents-icons/dist/vertical-grip.js';
import { useCurrentTheme, useI18nBundle, useIsRTL, useSyncRef, useStylesheet } from '@ui5/webcomponents-react-base';
import { forwardRef, useEffect, useRef, useState } from 'react';
import { PRESS_ARROW_KEYS_TO_MOVE } from '../../i18n/i18n-defaults.js';
import { Button } from '../../webComponents/Button/index.js';
import { Icon } from '../../webComponents/Icon/index.js';
import { classNames, styleData } from './Splitter.module.css.js';
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
const verticalPositionInfo = {
start: 'top',
startUppercase: 'Top',
end: 'bottom',
position: 'Y',
positionRect: 'y',
size: 'height',
min: 'minHeight',
offset: 'offsetY'
};
const horizontalPositionInfo = {
start: 'left',
startUppercase: 'Left',
end: 'right',
position: 'X',
positionRect: 'x',
size: 'width',
min: 'minWidth',
offset: 'offsetX'
};
const Splitter = /*#__PURE__*/forwardRef((props, ref) => {
const {
vertical,
onResize
} = props;
const i18nBundle = useI18nBundle('@ui5/webcomponents-react');
const [componentRef, localRef] = useSyncRef(ref);
const isRtl = useIsRTL(localRef);
const start = useRef(null);
useStylesheet(styleData, Splitter.displayName);
const previousSiblingSize = useRef(null);
const nextSiblingSize = useRef(null);
const previousElementEnd = useRef(null);
const nextElementStart = useRef(null);
const resizerClickOffset = useRef(0);
const positionKeys = vertical ? verticalPositionInfo : horizontalPositionInfo;
const [isDragging, setIsDragging] = useState(false);
const isSiblings = isRtl && !vertical ? ['nextSibling', 'previousSibling'] : ['previousSibling', 'nextSibling'];
const animationFrameIdRef = useRef(null);
const fireOnResize = (prevSibling, nextSibling) => {
if (animationFrameIdRef.current) {
cancelAnimationFrame(animationFrameIdRef.current);
}
if (typeof onResize !== 'function') {
return;
}
animationFrameIdRef.current = requestAnimationFrame(() => {
const logicalPrevSibling = isRtl ? nextSibling : prevSibling;
const logicalNextSibling = isRtl ? prevSibling : nextSibling;
const splitterWidth = localRef.current.getBoundingClientRect()[positionKeys.size];
onResize({
areas: [{
size: logicalPrevSibling.getBoundingClientRect()?.[positionKeys.size] + splitterWidth,
area: logicalPrevSibling
}, {
// last element doesn't have splitter
size: logicalNextSibling.getBoundingClientRect()?.[positionKeys.size] + (logicalNextSibling.nextElementSibling !== null ? splitterWidth : 0),
area: logicalNextSibling
}],
splitter: localRef.current
});
animationFrameIdRef.current = null;
});
};
const handleSplitterMove = e => {
const offset = resizerClickOffset.current;
const previousSibling = localRef.current[isSiblings[0]];
const nextSibling = localRef.current[isSiblings[1]];
const currentPosition = isDragging === 'touch' ? e.touches[0][`client${positionKeys.position}`] : e[`client${positionKeys.position}`];
const sizeDiv = currentPosition - start.current;
const splitterWidth = localRef.current.getBoundingClientRect()[positionKeys.size];
const moveLeft = sizeDiv < 0;
const move = () => {
previousSibling.style.flex = `0 0 ${previousSiblingSize.current + sizeDiv}px`;
if (nextSibling.nextSibling && previousSiblingSize.current + sizeDiv > 0) {
nextSibling.style.flex = `0 0 ${nextSiblingSize.current - sizeDiv}px`;
}
fireOnResize(previousSibling, nextSibling);
};
if (previousSiblingSize.current + sizeDiv > 0 && currentPosition + (splitterWidth - offset) <= nextElementStart.current) {
if (parseInt(previousSibling.dataset.minSize, 10) <= previousSiblingSize.current + sizeDiv && moveLeft) {
move();
}
if (nextSiblingSize.current - sizeDiv >= parseInt(nextSibling.dataset.minSize, 10) && !moveLeft) {
move();
}
}
if (!nextSibling[isSiblings[1]] && !isRtl) {
nextSibling.style.flex = '1 0 0px';
}
};
/**
* If the cursor is dragged outside the splitter (into another SplitterElement or outside the SplitterLayout), SplitterElements should increase/decrease their size to max/min.
*/
const handleFallback = (e, touchEvent) => {
if (!localRef.current) {
return;
}
const prevSibling = localRef.current[isSiblings[0]];
const nextSibling = localRef.current[isSiblings[1]];
const prevSiblingRect = localRef.current[isSiblings[0]].getBoundingClientRect();
const nextSiblingRect = localRef.current[isSiblings[1]].getBoundingClientRect();
const currentPos = touchEvent ? Math.round(e.changedTouches[0][`client${positionKeys.position}`]) : e[`client${positionKeys.position}`];
// left
if (!localRef.current.contains(e.target) && currentPos - localRef.current[`offset${positionKeys.startUppercase}`] + 1 /* border */ < 0) {
prevSibling.style.flex = '0 0 0px';
// Check if minSize is set on previous sibling
if (prevSibling.style?.[positionKeys.min]) {
nextSibling.style.flex = `0 0 ${nextSiblingRect?.[positionKeys.size] + (prevSiblingRect?.[positionKeys.size] - prevSibling.style?.[positionKeys.min].replace('px', ''))}px`;
} else {
nextSibling.style.flex = `0 0 ${nextSiblingRect?.[positionKeys.size] + prevSiblingRect?.[positionKeys.size]}px`;
}
fireOnResize(prevSibling, nextSibling);
}
// right
if (nextElementStart.current < currentPos) {
nextSibling.style.flex = '0 0 0px';
// Check if minSize is set on next sibling
if (nextSibling.style?.[positionKeys.min]) {
prevSibling.style.flex = `0 0 ${prevSiblingRect?.[positionKeys.size] + (nextSiblingRect?.[positionKeys.size] - nextSibling.style?.[positionKeys.min].replace('px', ''))}px`;
} else {
prevSibling.style.flex = `0 0 ${prevSiblingRect?.[positionKeys.size] + nextSiblingRect?.[positionKeys.size]}px`;
}
fireOnResize(prevSibling, nextSibling);
}
};
const handleMoveSplitterStart = e => {
if (e.type === 'pointerdown' && e.pointerType !== 'touch') {
return;
}
e.currentTarget.focus();
e.preventDefault();
setIsDragging(e.pointerType ?? 'mouse');
resizerClickOffset.current = e.nativeEvent[positionKeys.offset];
previousElementEnd.current = localRef.current[isSiblings[0]].getBoundingClientRect()?.[positionKeys.end];
if (localRef.current[isSiblings[1]][isSiblings[1]]) {
nextElementStart.current = localRef.current[isSiblings[1]][isSiblings[1]].getBoundingClientRect()?.[positionKeys.start];
} else {
nextElementStart.current = localRef.current.parentElement.getBoundingClientRect()[positionKeys.end];
}
previousSiblingSize.current = localRef.current[isSiblings[0]].getBoundingClientRect()?.[positionKeys.size];
nextSiblingSize.current = localRef.current[isSiblings[1]].getBoundingClientRect()?.[positionKeys.size];
start.current = e[`client${positionKeys.position}`];
};
const onHandleKeyDown = e => {
const keyEventProperties = e.code ?? e.key;
if (keyEventProperties === 'ArrowRight' || keyEventProperties === 'ArrowLeft' || keyEventProperties === 'ArrowUp' || keyEventProperties === 'ArrowDown') {
e.preventDefault();
let firstSibling = localRef.current[isSiblings[0]];
let secondSibling = localRef.current[isSiblings[1]];
if (keyEventProperties === 'ArrowLeft' || keyEventProperties === 'ArrowUp') {
secondSibling = localRef.current[isSiblings[0]];
firstSibling = localRef.current[isSiblings[1]];
}
const remainingSize = secondSibling.style[positionKeys.min] ? secondSibling.getBoundingClientRect()?.[positionKeys.size] - Number(secondSibling.style[positionKeys.min].replace('px', '')) : secondSibling.getBoundingClientRect()?.[positionKeys.size];
if (document.activeElement === localRef.current) {
const tickSize = remainingSize >= 20 ? 20 : remainingSize;
const firstSiblingSize = firstSibling.getBoundingClientRect()?.[positionKeys.size];
const secondSiblingSize = secondSibling.getBoundingClientRect()?.[positionKeys.size];
secondSibling.style.flex = `0 0 ${secondSiblingSize - tickSize}px`;
firstSibling.style.flex = `0 0 ${firstSiblingSize + tickSize}px`;
if (keyEventProperties === 'ArrowLeft' || keyEventProperties === 'ArrowUp') {
fireOnResize(secondSibling, firstSibling);
} else {
fireOnResize(firstSibling, secondSibling);
}
}
}
};
const end = e => {
handleFallback(e, isDragging === 'touch');
setIsDragging(false);
};
useEffect(() => {
const removeEventListeners = () => {
if (isDragging === 'touch') {
document.removeEventListener('touchmove', handleSplitterMove);
document.removeEventListener('touchend', end);
} else {
document.removeEventListener('mouseup', end);
document.removeEventListener('mousemove', handleSplitterMove);
}
};
if (isDragging) {
if (isDragging === 'touch') {
document.addEventListener('touchmove', handleSplitterMove);
document.addEventListener('touchend', end);
} else {
document.addEventListener('mousemove', handleSplitterMove);
document.addEventListener('mouseup', end);
}
} else {
removeEventListeners();
}
return () => {
removeEventListeners();
};
}, [isDragging]);
const currentTheme = useCurrentTheme();
const isHighContrast = currentTheme === 'sap_fiori_3_hcb' || currentTheme === 'sap_fiori_3_hcw' || currentTheme === 'sap_horizon_hcb' || currentTheme === 'sap_horizon_hcw' || currentTheme === 'sap_horizon_hc_auto';
return /*#__PURE__*/_jsxs("div", {
className: classNames.splitter,
tabIndex: 0,
onKeyDown: onHandleKeyDown,
onPointerDown: handleMoveSplitterStart,
onMouseDown: handleMoveSplitterStart,
ref: componentRef,
role: "separator",
"data-splitter-vertical": vertical ? 'vertical' : 'horizontal',
title: i18nBundle.getText(PRESS_ARROW_KEYS_TO_MOVE),
"aria-orientation": vertical ? 'vertical' : 'horizontal',
"aria-label": i18nBundle.getText(PRESS_ARROW_KEYS_TO_MOVE),
children: [/*#__PURE__*/_jsx("div", {
className: classNames.lineBefore
}), /*#__PURE__*/_jsx("div", {
className: classNames.gripContainer,
children: isHighContrast ? /*#__PURE__*/_jsx(Button, {
className: classNames.gripButton,
tabIndex: -1,
icon: vertical ? horizontalGripIcon : verticalGripIcon,
design: ButtonDesign.Transparent,
"data-component-name": "SplitterLayoutSplitterGrip"
}) : /*#__PURE__*/_jsx(Icon, {
className: classNames.icon,
name: vertical ? horizontalGripIcon : verticalGripIcon,
"data-component-name": "SplitterLayoutSplitterGrip"
})
}), /*#__PURE__*/_jsx("div", {
className: classNames.lineAfter
})]
});
});
Splitter.displayName = 'Splitter';
export { Splitter };