Skip to content

Commit 1404779

Browse files
committed
feat(SplitterLayout): introduceonResize event
1 parent 0518709 commit 1404779

File tree

5 files changed

+81
-6
lines changed

5 files changed

+81
-6
lines changed

packages/main/src/components/Splitter/index.tsx

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,19 @@ import horizontalGripIcon from '@ui5/webcomponents-icons/dist/horizontal-grip.js
55
import verticalGripIcon from '@ui5/webcomponents-icons/dist/vertical-grip.js';
66
import { useCurrentTheme, useI18nBundle, useIsRTL, useSyncRef, useStylesheet } from '@ui5/webcomponents-react-base';
77
import { forwardRef, useEffect, useRef, useState } from 'react';
8+
import type { KeyboardEventHandler, PointerEventHandler } from 'react';
89
import { PRESS_ARROW_KEYS_TO_MOVE } from '../../i18n/i18n-defaults.js';
910
import type { CommonProps } from '../../types/index.js';
10-
import { Button, Icon } from '../../webComponents/index.js';
11+
import { Button } from '../../webComponents/Button/index.js';
12+
import { Icon } from '../../webComponents/Icon/index.js';
13+
import type { SplitterLayoutPropTypes } from '../SplitterLayout/types.js';
1114
import { classNames, styleData } from './Splitter.module.css.js';
1215

1316
export interface SplitterPropTypes extends CommonProps {
1417
height: string | number;
1518
width: string | number;
1619
vertical: boolean;
20+
onResize: SplitterLayoutPropTypes['onResize'] | undefined;
1721
}
1822

1923
const verticalPositionInfo = {
@@ -39,7 +43,7 @@ const horizontalPositionInfo = {
3943
};
4044

4145
const Splitter = forwardRef<HTMLDivElement, SplitterPropTypes>((props, ref) => {
42-
const { vertical } = props;
46+
const { vertical, onResize } = props;
4347
const i18nBundle = useI18nBundle('@ui5/webcomponents-react');
4448
const [componentRef, localRef] = useSyncRef<HTMLDivElement>(ref);
4549
const isRtl = useIsRTL(localRef);
@@ -58,6 +62,38 @@ const Splitter = forwardRef<HTMLDivElement, SplitterPropTypes>((props, ref) => {
5862
const [isDragging, setIsDragging] = useState<boolean | string>(false);
5963
const [isSiblings, setIsSiblings] = useState(['previousSibling', 'nextSibling']);
6064

65+
const animationFrameIdRef = useRef(null);
66+
function fireOnResize(prevSibling: HTMLElement, nextSibling: HTMLElement) {
67+
if (animationFrameIdRef.current) {
68+
cancelAnimationFrame(animationFrameIdRef.current);
69+
}
70+
if (typeof onResize !== 'function') {
71+
return;
72+
}
73+
animationFrameIdRef.current = requestAnimationFrame(() => {
74+
const logicalPrevSibling = isRtl ? nextSibling : prevSibling;
75+
const logicalNextSibling = isRtl ? prevSibling : nextSibling;
76+
const splitterWidth = localRef.current.getBoundingClientRect()[positionKeys.size];
77+
onResize({
78+
areas: [
79+
{
80+
size: logicalPrevSibling.getBoundingClientRect()?.[positionKeys.size] + splitterWidth,
81+
area: logicalPrevSibling,
82+
},
83+
{
84+
// last element doesn't have splitter
85+
size:
86+
logicalNextSibling.getBoundingClientRect()?.[positionKeys.size] +
87+
(logicalNextSibling.nextElementSibling !== null ? splitterWidth : 0),
88+
area: logicalNextSibling,
89+
},
90+
],
91+
splitter: localRef.current,
92+
});
93+
animationFrameIdRef.current = null;
94+
});
95+
}
96+
6197
const handleSplitterMove = (e) => {
6298
const offset = resizerClickOffset.current;
6399
const previousSibling = localRef.current[isSiblings[0]] as HTMLDivElement;
@@ -71,10 +107,10 @@ const Splitter = forwardRef<HTMLDivElement, SplitterPropTypes>((props, ref) => {
71107

72108
const move = () => {
73109
previousSibling.style.flex = `0 0 ${previousSiblingSize.current + sizeDiv}px`;
74-
75110
if (nextSibling.nextSibling && previousSiblingSize.current + sizeDiv > 0) {
76111
nextSibling.style.flex = `0 0 ${nextSiblingSize.current - sizeDiv}px`;
77112
}
113+
fireOnResize(previousSibling, nextSibling);
78114
};
79115

80116
if (
@@ -126,6 +162,7 @@ const Splitter = forwardRef<HTMLDivElement, SplitterPropTypes>((props, ref) => {
126162
(nextSiblingRect?.[positionKeys.size] as number) + prevSiblingRect?.[positionKeys.size]
127163
}px`;
128164
}
165+
fireOnResize(prevSibling, nextSibling);
129166
}
130167

131168
// right
@@ -142,10 +179,12 @@ const Splitter = forwardRef<HTMLDivElement, SplitterPropTypes>((props, ref) => {
142179
(prevSiblingRect?.[positionKeys.size] as number) + nextSiblingRect?.[positionKeys.size]
143180
}px`;
144181
}
182+
183+
fireOnResize(prevSibling, nextSibling);
145184
}
146185
};
147186

148-
const handleMoveSplitterStart = (e) => {
187+
const handleMoveSplitterStart: PointerEventHandler<HTMLDivElement> = (e) => {
149188
if (e.type === 'pointerdown' && e.pointerType !== 'touch') {
150189
return;
151190
}
@@ -175,7 +214,7 @@ const Splitter = forwardRef<HTMLDivElement, SplitterPropTypes>((props, ref) => {
175214
start.current = e[`client${positionKeys.position}`];
176215
};
177216

178-
const onHandleKeyDown = (e) => {
217+
const onHandleKeyDown: KeyboardEventHandler<HTMLDivElement> = (e) => {
179218
const keyEventProperties = e.code ?? e.key;
180219
if (
181220
keyEventProperties === 'ArrowRight' ||
@@ -203,6 +242,12 @@ const Splitter = forwardRef<HTMLDivElement, SplitterPropTypes>((props, ref) => {
203242
const secondSiblingSize = secondSibling.getBoundingClientRect()?.[positionKeys.size] as number;
204243
secondSibling.style.flex = `0 0 ${secondSiblingSize - tickSize}px`;
205244
firstSibling.style.flex = `0 0 ${firstSiblingSize + tickSize}px`;
245+
246+
if (keyEventProperties === 'ArrowLeft' || keyEventProperties === 'ArrowUp') {
247+
fireOnResize(secondSibling, firstSibling);
248+
} else {
249+
fireOnResize(firstSibling, secondSibling);
250+
}
206251
}
207252
}
208253
};

packages/main/src/components/SplitterElement/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ export interface SplitterElementPropTypes extends CommonProps {
2020
/**
2121
* Defines the initial size of the `SplitterElement`.
2222
*
23+
* __Note:__ In order to preserve the intended design, at least one `SplitterElement` should have a dynamic `size`.
24+
*
2325
* @default `"auto"`
2426
*/
2527
size?: CSSProperties['width'] | CSSProperties['height'];

packages/main/src/components/SplitterLayout/index.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@ import { useConcatSplitterElements } from './useConcatSplitterElements.js';
2020
* can be set.
2121
* The splitter bars are focusable to enable resizing of the content areas via keyboard. The size of the content areas
2222
* can be manipulated when the splitter bar is focused and Left/Down/Right/Up are pressed.
23+
*
24+
* __Note:__ In order to preserve the intended design, at least one `SplitterElement` should have a dynamic `size`.
2325
*/
2426
const SplitterLayout = forwardRef<HTMLDivElement, SplitterLayoutPropTypes>((props, ref) => {
25-
const { vertical, children, title, style, className, options, ...rest } = props;
27+
const { vertical, children, title, style, className, options, onResize, ...rest } = props;
2628
const [componentRef, sLRef] = useSyncRef(ref);
2729
const [reset, setReset] = useState(undefined);
2830
const prevSize = useRef({ width: undefined, height: undefined });
@@ -34,6 +36,7 @@ const SplitterLayout = forwardRef<HTMLDivElement, SplitterLayoutPropTypes>((prop
3436
width: style?.width,
3537
height: style?.height,
3638
vertical,
39+
onResize,
3740
});
3841

3942
useStylesheet(styleData, SplitterLayout.displayName);

packages/main/src/components/SplitterLayout/types.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,21 @@ interface SplitterLayoutOptions {
2121

2222
type SplitterLayoutChild = ReactElement<SplitterElementPropTypes> | undefined | false | null;
2323

24+
interface ResizeArea {
25+
size: number;
26+
area: HTMLElement;
27+
}
28+
interface OnResizeParam {
29+
/**
30+
* The `SplitterElement`s that are being resized.
31+
* The first element is the previous sibling of the splitter bar, the second element is the next sibling.
32+
*
33+
* __Note:__ The array reflects the logical position of the `SplitterElement`s.
34+
*/
35+
areas: [ResizeArea, ResizeArea];
36+
splitter: HTMLElement;
37+
}
38+
2439
export interface SplitterLayoutPropTypes extends CommonProps {
2540
/**
2641
* Controls if a vertical or horizontal `SplitterLayout` is rendered.
@@ -34,4 +49,12 @@ export interface SplitterLayoutPropTypes extends CommonProps {
3449
* Defines options to customize the behavior of the SplitterLayout.
3550
*/
3651
options?: SplitterLayoutOptions;
52+
/**
53+
* Fired when contents are resized.
54+
*
55+
* __Note:__
56+
* - Resize events can fire many times in quick succession, it’s therefore strongly recommended to debounce your handler if you’re updating React state or causing other expensive operations.
57+
* - The `areas` array reflects the logical position of the `SplitterElement`s relative to the "Splitter".
58+
*/
59+
onResize?: (e: OnResizeParam) => void;
3760
}

packages/main/src/components/SplitterLayout/useConcatSplitterElements.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ interface ConcatSplitterElements {
99
width: CSSProperties['width'];
1010
height: CSSProperties['height'];
1111
vertical: boolean;
12+
onResize: SplitterLayoutPropTypes['onResize'] | undefined;
1213
}
1314

1415
export const useConcatSplitterElements = (concatSplitterElements: ConcatSplitterElements) => {
@@ -42,6 +43,7 @@ export const useConcatSplitterElements = (concatSplitterElements: ConcatSplitter
4243
height={concatSplitterElements?.height}
4344
width={concatSplitterElements?.width}
4445
vertical={concatSplitterElements?.vertical}
46+
onResize={concatSplitterElements?.onResize}
4547
/>,
4648
);
4749
// -1 => prev element

0 commit comments

Comments
 (0)