243 |
244 | );
245 | };
246 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-resplit
2 |
3 | Resizable split pane layouts for React applications 🖖
4 |
5 | - Compound component API that works with any styling method
6 | - Built with modern CSS, a grid-based layout and custom properties
7 | - Works with any amount of panes in a vertical or horizontal layout
8 | - Built following the [Window Splitter](https://www.w3.org/WAI/ARIA/apg/patterns/windowsplitter/) pattern for accessibility and keyboard controls
9 |
10 | https://github.com/KenanYusuf/react-resplit/assets/9557798/d47ef278-bcb1-4c2b-99e6-7a9f99943f96
11 |
12 | _Example of a code editor built with `react-resplit`_
13 |
14 | ## Development
15 |
16 | Run the development server:
17 |
18 | ```
19 | yarn dev
20 | ```
21 |
22 | The files for the development app can be found in `src`, and the library files in `lib`.
23 |
24 | ---
25 |
26 | ## Usage
27 |
28 | Install the package using your package manager of choice.
29 |
30 | ```
31 | npm install react-resplit
32 | ```
33 |
34 | Import `Resplit` from `react-resplit` and render the Root, Pane(s) and Splitter(s).
35 |
36 | ```tsx
37 | import { Resplit } from 'react-resplit';
38 |
39 | function App() {
40 | return (
41 |
42 |
43 | Pane 1
44 |
45 |
46 |
47 | Pane 2
48 |
49 |
50 | );
51 | }
52 | ```
53 |
54 | ### Styling
55 |
56 | The Root, Splitter and Pane elements are all unstyled by default apart from a few styles that are necessary for the layout - this is intentional so that the library remains flexible.
57 |
58 | Resplit will apply the correct cursor based on the `direction` provided to the hook.
59 |
60 | As a basic example, you could provide a `className` prop to the Splitter elements and style them as a solid 10px divider.
61 |
62 | ```tsx
63 |
64 | ```
65 |
66 | ```css
67 | .splitter {
68 | width: 100%;
69 | height: 100%;
70 | background: #ccc;
71 | }
72 | ```
73 |
74 | ### Accessibility
75 |
76 | Resplit has been implemented using guidence from the [Window Splitter](https://www.w3.org/WAI/ARIA/apg/patterns/windowsplitter/) pattern.
77 |
78 | In addition to built-in accessibility considerations, you should also ensure that splitters are correctly labelled.
79 |
80 | If the primary pane has a visible label, the `aria-labelledby` attribute can be used.
81 |
82 | ```tsx
83 |
84 |
Pane 1
85 |
86 |
87 | ```
88 |
89 | Alternatively, if the pane does not have a visible label, the `aria-label` attribute can be used on the Splitter instead.
90 |
91 | ```tsx
92 |
93 | ```
94 |
95 | ## API
96 |
97 | All of the resplit components extend the `React.HTMLAttributes` interface, so you can pass any valid HTML attribute to them.
98 |
99 | ### Root `(ResplitRootProps)`
100 |
101 | The root component of a resplit layout. Provides context to all child components.
102 |
103 | | Prop | Type | Default | Description |
104 | | ----------- | ---------------------------- | -------------- | ------------------------------------- |
105 | | `direction` | `"horizontal" \| "vertical"` | `"horizontal"` | Direction of the panes |
106 | | `asChild` | `boolean` | `false` | Merges props onto the immediate child |
107 | | `children` | `ReactNode` | | Child elements |
108 | | `className` | `string` | | Class name |
109 | | `style` | `CSSProperties` | | Style object |
110 |
111 | ### Pane `(ResplitPaneProps)`
112 |
113 | A pane is a container that can be resized.
114 |
115 | | Prop | Type | Default | Description |
116 | | --------------- | ------------------------------ | ------------------------------------- | -------------------------------------------------------------------------- |
117 | | `order` | `number` | | Specifies the order of the resplit child (pane or splitter) in the DOM |
118 | | `initialSize` | `${number}fr` | `[available space]/[number of panes]` | Set the initial size of the pane as a fractional unit (fr) |
119 | | `minSize` | `${number}fr` \| `${number}px` | `"0fr"` | Set the minimum size of the pane as a fractional (fr) or pixel (px) unit |
120 | | `collapsible` | `boolean` | `false` | Whether the pane can be collapsed below its minimum size |
121 | | `collapsedSize` | `${number}fr` \| `${number}px` | `"0fr"` | Set the collapsed size of the pane as a fractional (fr) or pixel (px) unit |
122 | | `onResizeStart` | `() => void` | | Callback function that is called when the pane starts being resized. |
123 | | `onResize` | `(size: FrValue) => void` | | Callback function that is called when the pane is actively being resized. |
124 | | `onResizeEnd` | `(size: FrValue) => void` | | Callback function that is called when the pane is actively being resized. |
125 | | `asChild` | `boolean` | `false` | Merges props onto the immediate child |
126 | | `children` | `ReactNode` | | Child elements |
127 | | `className` | `string` | | Class name |
128 | | `style` | `CSSProperties` | | Style object |
129 |
130 | ### Splitter `(ResplitSplitterProps)`
131 |
132 | A splitter is a draggable element that can be used to resize panes.
133 |
134 | | Name | Type | Default | Description |
135 | | ----------- | --------------- | -------- | ---------------------------------------------------------------------- |
136 | | `order` | `number` | | Specifies the order of the resplit child (pane or splitter) in the DOM |
137 | | `size` | `${number}px` | `"10px"` | Set the size of the splitter as a pixel unit |
138 | | `asChild` | `boolean` | `false` | Merges props onto the immediate child |
139 | | `children` | `ReactNode` | | Child elements |
140 | | `className` | `string` | | Class name |
141 | | `style` | `CSSProperties` | | Style object |
142 |
143 | ### useResplitContext `() => ResplitContextValue`
144 |
145 | The `useResplitContext` hook provides access to the context of the nearest `Resplit.Root` component.
146 |
147 | See the methods below for more information on what is available.
148 |
149 | #### setPaneSizes `(paneSizes: FrValue[]) => void`
150 |
151 | Get the collapsed state of a pane.
152 |
153 | Specify the size of each pane as a fractional unit (fr). The number of values should match the number of panes.
154 |
155 | ```tsx
156 | setPaneSizes(['0.6fr', '0.4fr']);
157 | ```
158 |
159 | If your pane has an `onResize` callback, it will be called with the new size.
160 |
161 | #### isPaneCollapsed `(order: number) => boolean`
162 |
163 | Get the collapsed state of a pane.
164 |
165 | **Note**: The returned value will not update on every render and should be used in a callback e.g. used in combination with a pane's `onResize` callback.
166 |
167 | ```tsx
168 | import { Resplit, useResplitContext, ResplitPaneProps, FrValue } from 'react-resplit';
169 |
170 | function CustomPane(props: ResplitPaneProps) {
171 | const { isPaneCollapsed } = useResplitContext();
172 |
173 | const handleResize = (size: FrValue) => {
174 | if (isPaneCollapsed(props.order)) {
175 | // Do something
176 | }
177 | };
178 |
179 | return (
180 |
187 | );
188 | }
189 |
190 | function App() {
191 | return (
192 |
193 |
194 |
195 |
196 |
197 | );
198 | }
199 | ```
200 |
201 | #### isPaneMinSize `(order: number) => boolean`
202 |
203 | Get the min size state of a pane.
204 |
205 | **Note**: The returned value will not update on every render and should be used in a callback e.g. used in combination with a pane's `onResize` callback.
206 |
207 | ```tsx
208 | import { Resplit, useResplitContext, ResplitPaneProps, FrValue } from 'react-resplit';
209 |
210 | function CustomPane(props: ResplitPaneProps) {
211 | const { isPaneMinSize } = useResplitContext();
212 |
213 | const handleResize = (size: FrValue) => {
214 | if (isPaneMinSize(props.order)) {
215 | // Do something
216 | }
217 | };
218 |
219 | return ;
220 | }
221 |
222 | function App() {
223 | return (
224 |
225 |
226 |
227 |
228 |
229 | );
230 | }
231 | ```
232 |
--------------------------------------------------------------------------------
/lib/Root.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | MutableRefObject,
3 | HTMLAttributes,
4 | ReactNode,
5 | forwardRef,
6 | useId,
7 | useRef,
8 | useState,
9 | useCallback,
10 | useMemo,
11 | } from 'react';
12 | import { Slot } from '@radix-ui/react-slot';
13 |
14 | import { ResplitContext } from './ResplitContext';
15 | import { RootContext } from './RootContext';
16 | import { CURSOR_BY_DIRECTION, GRID_TEMPLATE_BY_DIRECTION } from './const';
17 | import {
18 | convertFrToNumber,
19 | convertPxToNumber,
20 | convertSizeToFr,
21 | isPx,
22 | mergeRefs,
23 | useIsomorphicLayoutEffect,
24 | } from './utils';
25 |
26 | import type { FrValue, Order, PxValue, Direction } from './types';
27 | import type { ResplitPaneOptions } from './Pane';
28 | import type { ResplitSplitterOptions } from './Splitter';
29 |
30 | /**
31 | * The state of an individual pane.
32 | *
33 | * @internal For internal use only.
34 | *
35 | * @see {@link PaneOptions} for the public API.
36 | */
37 | export interface PaneChild {
38 | type: 'pane';
39 | options: MutableRefObject<
40 | ResplitPaneOptions & {
41 | minSize: PxValue | FrValue;
42 | collapsedSize: PxValue | FrValue;
43 | }
44 | >;
45 | }
46 |
47 | /**
48 | * The state of an individual splitter.
49 | *
50 | * @internal For internal use only.
51 | *
52 | * @see {@link SplitterOptions} for the public API.
53 | */
54 | export interface SplitterChild {
55 | type: 'splitter';
56 | options: MutableRefObject<
57 | ResplitSplitterOptions & {
58 | size: PxValue;
59 | }
60 | >;
61 | }
62 |
63 | /**
64 | * An object containing panes and splitters. Indexed by order.
65 | *
66 | * @internal For internal use only.
67 | */
68 | export interface ChildrenState {
69 | [order: Order]: PaneChild | SplitterChild;
70 | }
71 |
72 | export interface ResplitOptions {
73 | /**
74 | * Direction of the panes.
75 | *
76 | * @defaultValue 'horizontal'
77 | *
78 | */
79 | direction?: Direction;
80 | }
81 |
82 | export type ResplitRootProps = ResplitOptions &
83 | HTMLAttributes & {
84 | /**
85 | * The children of the ResplitRoot component.
86 | */
87 | children: ReactNode;
88 | /**
89 | * Merges props onto the immediate child.
90 | *
91 | * @defaultValue false
92 | *
93 | * @example
94 | *
95 | * ```tsx
96 | *
97 | *
98 | * ...
99 | *
100 | *
101 | * ```
102 | */
103 | asChild?: boolean;
104 | };
105 |
106 | /**
107 | * The root component of a resplit layout. Provides context to all child components.
108 | *
109 | * @example
110 | * ```tsx
111 | *
112 | *
113 | *
114 | *
115 | *
116 | * ```
117 | */
118 | export const ResplitRoot = forwardRef(function Root(
119 | { direction = 'horizontal', children: reactChildren, style, asChild = false, ...rest },
120 | forwardedRef,
121 | ) {
122 | const id = useId();
123 | const Comp = asChild ? Slot : 'div';
124 | const activeSplitterOrder = useRef(null);
125 | const rootRef = useRef(null);
126 | const [children, setChildren] = useState({});
127 |
128 | const getChildElement = useCallback(
129 | (order: Order) => rootRef.current?.querySelector(`:scope > [data-resplit-order="${order}"]`),
130 | [],
131 | );
132 |
133 | const getChildSize = useCallback(
134 | (order: Order) => rootRef.current?.style.getPropertyValue(`--resplit-${order}`),
135 | [],
136 | );
137 |
138 | const getChildSizeAsNumber = useCallback(
139 | (order: Order) => {
140 | const childSize = getChildSize(order);
141 | if (!childSize) return 0;
142 | return isPx(childSize as PxValue | FrValue)
143 | ? convertPxToNumber(childSize as PxValue)
144 | : convertFrToNumber(childSize as FrValue);
145 | },
146 | [getChildSize],
147 | );
148 |
149 | const setChildSize = useCallback(
150 | (order: Order, size: FrValue | PxValue) => {
151 | rootRef.current?.style.setProperty(`--resplit-${order}`, size);
152 | const child = children[order];
153 |
154 | if (child.type === 'pane') {
155 | const paneSplitter = getChildElement(order + 1);
156 | paneSplitter?.setAttribute(
157 | 'aria-valuenow',
158 | String(convertFrToNumber(size as FrValue).toFixed(2)),
159 | );
160 | }
161 | },
162 | [children, getChildElement],
163 | );
164 |
165 | const isPaneMinSize = useCallback(
166 | (order: Order) => getChildElement(order)?.getAttribute('data-resplit-is-min') === 'true',
167 | [getChildElement],
168 | );
169 |
170 | const setIsPaneMinSize = useCallback(
171 | (order: Order, value: boolean) =>
172 | getChildElement(order)?.setAttribute('data-resplit-is-min', String(value)),
173 | [getChildElement],
174 | );
175 |
176 | const isPaneCollapsed = useCallback(
177 | (order: Order) => getChildElement(order)?.getAttribute('data-resplit-is-collapsed') === 'true',
178 | [getChildElement],
179 | );
180 |
181 | const setIsPaneCollapsed = useCallback(
182 | (order: Order, value: boolean) =>
183 | getChildElement(order)?.setAttribute('data-resplit-is-collapsed', String(value)),
184 | [getChildElement],
185 | );
186 |
187 | const getRootSize = useCallback(
188 | () =>
189 | (direction === 'horizontal' ? rootRef.current?.offsetWidth : rootRef.current?.offsetHeight) ||
190 | 0,
191 | [direction],
192 | );
193 |
194 | const findResizablePane = useCallback(
195 | (start: number, direction: number) => {
196 | let index = start;
197 | let pane: PaneChild | null = children[index] as PaneChild;
198 |
199 | while (index >= 0 && index < Object.values(children).length) {
200 | const child = children[index];
201 |
202 | if (
203 | child.type === 'splitter' ||
204 | (isPaneMinSize(index) && !child.options.current.collapsible) ||
205 | (isPaneMinSize(index) && child.options.current.collapsible && isPaneCollapsed(index))
206 | ) {
207 | index += direction;
208 | pane = null;
209 | } else {
210 | pane = child;
211 | break;
212 | }
213 | }
214 |
215 | return { index, pane };
216 | },
217 | [children, isPaneCollapsed, isPaneMinSize],
218 | );
219 |
220 | const resizeByDelta = useCallback(
221 | (splitterOrder: Order, delta: number) => {
222 | const isGrowing = delta > 0;
223 | const isShrinking = delta < 0;
224 |
225 | // Find the previous and next resizable panes
226 | const { index: prevPaneIndex, pane: prevPane } = isShrinking
227 | ? findResizablePane(splitterOrder - 1, -1)
228 | : { index: splitterOrder - 1, pane: children[splitterOrder - 1] as PaneChild };
229 |
230 | const { index: nextPaneIndex, pane: nextPane } = isGrowing
231 | ? findResizablePane(splitterOrder + 1, 1)
232 | : { index: splitterOrder + 1, pane: children[splitterOrder + 1] as PaneChild };
233 |
234 | // Return if no panes are resizable
235 | if (!prevPane || !nextPane) return;
236 |
237 | const rootSize = getRootSize();
238 |
239 | const prevPaneOptions = prevPane.options.current;
240 | let prevPaneSize = getChildSizeAsNumber(prevPaneIndex) + delta;
241 | const prevPaneMinSize = convertFrToNumber(convertSizeToFr(prevPaneOptions.minSize, rootSize));
242 | const prevPaneisPaneMinSize = prevPaneSize <= prevPaneMinSize;
243 | const prevPaneisPaneCollapsed =
244 | !!prevPaneOptions.collapsible && prevPaneSize <= prevPaneMinSize / 2;
245 |
246 | const nextPaneOptions = nextPane.options.current;
247 | let nextPaneSize = getChildSizeAsNumber(nextPaneIndex) - delta;
248 | const nextPaneMinSize = convertFrToNumber(convertSizeToFr(nextPaneOptions.minSize, rootSize));
249 | const nextPaneisPaneMinSize = nextPaneSize <= nextPaneMinSize;
250 | const nextPaneisPaneCollapsed =
251 | !!nextPaneOptions.collapsible && nextPaneSize <= nextPaneMinSize / 2;
252 |
253 | if (prevPaneisPaneCollapsed || nextPaneisPaneCollapsed) {
254 | if (prevPaneisPaneCollapsed) {
255 | const prevPaneCollapsedSize = convertFrToNumber(
256 | convertSizeToFr(prevPaneOptions.collapsedSize, rootSize),
257 | );
258 | nextPaneSize = nextPaneSize + prevPaneSize - prevPaneCollapsedSize;
259 | prevPaneSize = prevPaneCollapsedSize;
260 | }
261 |
262 | if (nextPaneisPaneCollapsed) {
263 | const nextPaneCollapsedSize = convertFrToNumber(
264 | convertSizeToFr(nextPaneOptions.collapsedSize, rootSize),
265 | );
266 | prevPaneSize = prevPaneSize + nextPaneSize - nextPaneCollapsedSize;
267 | nextPaneSize = nextPaneCollapsedSize;
268 | }
269 | } else {
270 | if (prevPaneisPaneMinSize) {
271 | nextPaneSize = nextPaneSize + (prevPaneSize - prevPaneMinSize);
272 | prevPaneSize = prevPaneMinSize;
273 | }
274 |
275 | if (nextPaneisPaneMinSize) {
276 | prevPaneSize = prevPaneSize + (nextPaneSize - nextPaneMinSize);
277 | nextPaneSize = nextPaneMinSize;
278 | }
279 | }
280 |
281 | setChildSize(prevPaneIndex, `${prevPaneSize}fr`);
282 | setIsPaneMinSize(prevPaneIndex, prevPaneisPaneMinSize);
283 | setIsPaneCollapsed(prevPaneIndex, prevPaneisPaneCollapsed);
284 | prevPaneOptions.onResize?.(`${prevPaneSize}fr`);
285 |
286 | setChildSize(nextPaneIndex, `${nextPaneSize}fr`);
287 | setIsPaneMinSize(nextPaneIndex, nextPaneisPaneMinSize);
288 | setIsPaneCollapsed(nextPaneIndex, nextPaneisPaneCollapsed);
289 | nextPaneOptions.onResize?.(`${nextPaneSize}fr`);
290 | },
291 | [
292 | children,
293 | findResizablePane,
294 | getChildSizeAsNumber,
295 | getRootSize,
296 | setChildSize,
297 | setIsPaneMinSize,
298 | setIsPaneCollapsed,
299 | ],
300 | );
301 |
302 | /**
303 | * Mouse move handler
304 | * - Fire when user is interacting with splitter
305 | * - Handle resizing of panes
306 | */
307 | const handleMouseMove = useCallback(
308 | (e: MouseEvent) => {
309 | // Return if no active splitter
310 | if (activeSplitterOrder.current === null) return;
311 |
312 | // Get the splitter element
313 | const splitter = getChildElement(activeSplitterOrder.current);
314 |
315 | // Return if no splitter element could be found
316 | if (!splitter) return;
317 |
318 | // Calculate available space
319 | const combinedSplitterSize = Object.entries(children).reduce(
320 | (total, [order, child]) =>
321 | total + (child.type === 'splitter' ? getChildSizeAsNumber(Number(order)) : 0),
322 | 0,
323 | );
324 |
325 | const availableSpace = getRootSize() - combinedSplitterSize;
326 |
327 | // Calculate delta
328 | const splitterRect = splitter.getBoundingClientRect();
329 | const movement =
330 | direction === 'horizontal' ? e.clientX - splitterRect.left : e.clientY - splitterRect.top;
331 | const delta = movement / availableSpace;
332 |
333 | // Return if no change in the direction of movement
334 | if (!delta) return;
335 |
336 | resizeByDelta(activeSplitterOrder.current, delta);
337 | },
338 | [children, direction, getChildElement, getChildSizeAsNumber, getRootSize, resizeByDelta],
339 | );
340 |
341 | /**
342 | * Mouse up handler
343 | * - Fire when user stops interacting with splitter
344 | */
345 | const handleMouseUp = useCallback(() => {
346 | const order = activeSplitterOrder.current;
347 |
348 | if (order === null) return;
349 |
350 | // Set data attributes
351 | rootRef.current?.setAttribute('data-resplit-resizing', 'false');
352 |
353 | if (order !== null) {
354 | getChildElement(order)?.setAttribute('data-resplit-active', 'false');
355 | }
356 |
357 | const prevPane = children[order - 1];
358 | if (prevPane.type === 'pane')
359 | prevPane.options.current.onResizeEnd?.(getChildSize(order - 1) as FrValue);
360 |
361 | const nextPane = children[order + 1];
362 | if (nextPane.type === 'pane')
363 | nextPane.options.current.onResizeEnd?.(getChildSize(order + 1) as FrValue);
364 |
365 | // Unset refs
366 | activeSplitterOrder.current = null;
367 |
368 | // Re-enable text selection and cursor
369 | document.documentElement.style.cursor = '';
370 | document.documentElement.style.pointerEvents = '';
371 | document.documentElement.style.userSelect = '';
372 |
373 | // Remove mouse event listeners
374 | window.removeEventListener('mouseup', handleMouseUp);
375 | window.removeEventListener('mousemove', handleMouseMove);
376 | }, [children, getChildElement, getChildSize, handleMouseMove]);
377 |
378 | /**
379 | * Mouse down handler
380 | * - Fire when user begins interacting with splitter
381 | * - Handle resizing of panes using cursor
382 | */
383 | const handleSplitterMouseDown = useCallback(
384 | (order: number) => () => {
385 | // Set active splitter
386 | activeSplitterOrder.current = order;
387 |
388 | // Set data attributes
389 | rootRef.current?.setAttribute('data-resplit-resizing', 'true');
390 |
391 | if (activeSplitterOrder.current !== null) {
392 | getChildElement(activeSplitterOrder.current)?.setAttribute('data-resplit-active', 'true');
393 | }
394 |
395 | const prevPane = children[order - 1];
396 | if (prevPane.type === 'pane') prevPane.options.current.onResizeStart?.();
397 |
398 | const nextPane = children[order + 1];
399 | if (nextPane.type === 'pane') nextPane.options.current.onResizeStart?.();
400 |
401 | // Disable text selection and cursor
402 | document.documentElement.style.cursor = CURSOR_BY_DIRECTION[direction];
403 | document.documentElement.style.pointerEvents = 'none';
404 | document.documentElement.style.userSelect = 'none';
405 |
406 | // Add mouse event listeners
407 | window.addEventListener('mouseup', handleMouseUp);
408 | window.addEventListener('mousemove', handleMouseMove);
409 | },
410 | [direction, children, getChildElement, handleMouseUp, handleMouseMove],
411 | );
412 |
413 | /**
414 | * Key down handler
415 | * - Fire when user presses a key whilst focused on a splitter
416 | * - Handle resizing of panes using keyboard
417 | * - Refer to: https://www.w3.org/WAI/ARIA/apg/patterns/windowsplitter/
418 | */
419 | const handleSplitterKeyDown = useCallback(
420 | (splitterOrder: number) => (e: React.KeyboardEvent) => {
421 | const isHorizontal = direction === 'horizontal';
422 | const isVertical = direction === 'vertical';
423 |
424 | if ((e.key === 'ArrowLeft' && isHorizontal) || (e.key === 'ArrowUp' && isVertical)) {
425 | resizeByDelta(splitterOrder, -0.01);
426 | } else if (
427 | (e.key === 'ArrowRight' && isHorizontal) ||
428 | (e.key === 'ArrowDown' && isVertical)
429 | ) {
430 | resizeByDelta(splitterOrder, 0.01);
431 | } else if (e.key === 'Home') {
432 | resizeByDelta(splitterOrder, -1);
433 | } else if (e.key === 'End') {
434 | resizeByDelta(splitterOrder, 1);
435 | } else if (e.key === 'Enter') {
436 | if (isPaneMinSize(splitterOrder - 1)) {
437 | const initialSize =
438 | (children[splitterOrder - 1] as PaneChild).options.current.initialSize || '1fr';
439 | resizeByDelta(splitterOrder, convertFrToNumber(initialSize));
440 | } else {
441 | resizeByDelta(splitterOrder, -1);
442 | }
443 | }
444 | },
445 | [direction, children, resizeByDelta, isPaneMinSize],
446 | );
447 |
448 | const registerPane = useCallback(
449 | (order: string, options: MutableRefObject) => {
450 | setChildren((children) => ({
451 | ...children,
452 | [order]: {
453 | type: 'pane',
454 | options,
455 | },
456 | }));
457 | },
458 | [],
459 | );
460 |
461 | const registerSplitter = useCallback(
462 | (order: string, options: MutableRefObject) => {
463 | setChildren((children) => ({
464 | ...children,
465 | [order]: {
466 | type: 'splitter',
467 | options,
468 | },
469 | }));
470 | },
471 | [],
472 | );
473 |
474 | const setPaneSizes = useCallback(
475 | (paneSizes: FrValue[]) => {
476 | paneSizes.forEach((paneSize, index) => {
477 | const order = index * 2;
478 | setChildSize(order, paneSize);
479 | setIsPaneMinSize(
480 | order,
481 | (children[order] as PaneChild).options.current.minSize === paneSize,
482 | );
483 | setIsPaneCollapsed(
484 | order,
485 | (children[order] as PaneChild).options.current.collapsedSize === paneSize,
486 | );
487 |
488 | const pane = children[order] as PaneChild;
489 | if (pane.type === 'pane') {
490 | pane.options.current.onResize?.(paneSize);
491 | }
492 | });
493 | },
494 | [children, setChildSize, setIsPaneMinSize, setIsPaneCollapsed],
495 | );
496 |
497 | /**
498 | * Recalculate pane sizes when children are added or removed
499 | */
500 | const childrenLength = Object.keys(children).length;
501 |
502 | useIsomorphicLayoutEffect(() => {
503 | const paneCount = Object.values(children).filter((child) => child.type === 'pane').length;
504 |
505 | Object.keys(children).forEach((key) => {
506 | const order = Number(key);
507 | const child = children[order];
508 |
509 | if (child.type === 'pane') {
510 | const paneSize = isPaneMinSize(order)
511 | ? '0fr'
512 | : child.options.current.initialSize || `${1 / paneCount}fr`;
513 | setChildSize(order, paneSize);
514 | } else {
515 | setChildSize(order, child.options.current.size);
516 | }
517 | });
518 | // eslint-disable-next-line react-hooks/exhaustive-deps
519 | }, [childrenLength]);
520 |
521 | const rootContextValue = useMemo(
522 | () => ({
523 | id,
524 | direction,
525 | registerPane,
526 | registerSplitter,
527 | handleSplitterMouseDown,
528 | handleSplitterKeyDown,
529 | }),
530 | [id, direction, registerPane, registerSplitter, handleSplitterMouseDown, handleSplitterKeyDown],
531 | );
532 |
533 | const resplitContextValue = useMemo(
534 | () => ({
535 | isPaneMinSize,
536 | isPaneCollapsed,
537 | setPaneSizes,
538 | }),
539 | [isPaneMinSize, isPaneCollapsed, setPaneSizes],
540 | );
541 |
542 | return (
543 |
544 |
545 | {
554 | const childVar = `minmax(0, var(--resplit-${order}))`;
555 | return value ? `${value} ${childVar}` : `${childVar}`;
556 | },
557 | '',
558 | ),
559 | ...style,
560 | }}
561 | {...rest}
562 | >
563 | {reactChildren}
564 |
565 |
566 |
567 | );
568 | });
569 |
--------------------------------------------------------------------------------