From aceb88f2de9e454dfce2ccd35bc4e23c8d8e3cb7 Mon Sep 17 00:00:00 2001 From: John Walley Date: Fri, 3 Dec 2021 09:34:58 +0000 Subject: [PATCH] feat: add imperative reset method (#69) --- README.md | 24 +++ src/allotment.tsx | 326 ++++++++++++++++++---------------- stories/allotment.stories.tsx | 26 ++- 3 files changed, 220 insertions(+), 156 deletions(-) diff --git a/README.md b/README.md index 42159e79..c630de3c 100644 --- a/README.md +++ b/README.md @@ -134,6 +134,30 @@ You can customize the following default variables. } ``` +### Programmatic control + +You can use a ref to get access to the Allotment component instance and call its reset method manually: + +```jsx +const ref = React.useRef(ref); + +return ( +
+ + +
+
+ +
+); +``` + ## Frequently asked questions ### Next.js diff --git a/src/allotment.tsx b/src/allotment.tsx index 0c52166a..5507cc5f 100644 --- a/src/allotment.tsx +++ b/src/allotment.tsx @@ -2,6 +2,7 @@ import classNames from "classnames"; import React, { forwardRef, useEffect, + useImperativeHandle, useLayoutEffect, useMemo, useRef, @@ -41,6 +42,8 @@ export const Pane = forwardRef( Pane.displayName = "Allotment.Pane"; +export type AllotmentHandle = { reset: () => void }; + export type AllotmentProps = { children: React.ReactNode; /** @@ -58,182 +61,197 @@ export type AllotmentProps = { onChange?: (sizes: number[]) => void; } & CommonProps; -const Allotment = ({ - children, - maxSize = Infinity, - minSize = 30, - sizes, - defaultSizes = sizes, - snap = false, - vertical = false, - onChange, -}: AllotmentProps) => { - const containerRef = useRef(null!); - const previousKeys = useRef([]); - const splitViewPropsRef = useRef(new Map()); - const splitViewRef = useRef(null); - const splitViewViewRef = useRef(new Map()); - - if (process.env.NODE_ENV !== "production" && sizes) { - console.warn(`Prop sizes is deprecated. Please use defaultSizes instead.`); - } - - const childrenArray = useMemo( - () => React.Children.toArray(children).filter(React.isValidElement), - [children] - ); - - useLayoutEffect(() => { - let initializeSizes = true; - - if (defaultSizes && splitViewViewRef.current.size !== defaultSizes.length) { - initializeSizes = false; - +const Allotment = forwardRef( + ( + { + children, + maxSize = Infinity, + minSize = 30, + sizes, + defaultSizes = sizes, + snap = false, + vertical = false, + onChange, + }, + ref + ) => { + const containerRef = useRef(null!); + const previousKeys = useRef([]); + const splitViewPropsRef = useRef(new Map()); + const splitViewRef = useRef(null); + const splitViewViewRef = useRef(new Map()); + + if (process.env.NODE_ENV !== "production" && sizes) { console.warn( - `Expected ${defaultSizes.length} children based on sizes but found ${splitViewViewRef.current.size}` + `Prop sizes is deprecated. Please use defaultSizes instead.` ); } - if (initializeSizes && defaultSizes) { - previousKeys.current = childrenArray.map((child) => child.key as string); - } - - const options: SplitViewOptions = { - orientation: vertical ? Orientation.Vertical : Orientation.Horizontal, - ...(initializeSizes && - defaultSizes && { - descriptor: { - size: defaultSizes.reduce((a, b) => a + b, 0), - views: defaultSizes.map((size, index) => ({ - container: [...splitViewViewRef.current.values()][index], - size: size, - view: { - element: document.createElement("div"), - minimumSize: minSize, - maximumSize: maxSize, - snap: snap, - layout: () => {}, - }, - })), - }, - }), - }; - - splitViewRef.current = new SplitView( - containerRef.current, - options, - onChange + const childrenArray = useMemo( + () => React.Children.toArray(children).filter(React.isValidElement), + [children] ); - splitViewRef.current.on("sashreset", (_index: number) => { - splitViewRef.current?.distributeViewSizes(); - }); - - const that = splitViewRef.current; + useImperativeHandle(ref, () => ({ + reset: () => { + splitViewRef.current?.distributeViewSizes(); + }, + })); - return () => { - that.dispose(); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + useLayoutEffect(() => { + let initializeSizes = true; - /** - * Add or remove views as number of children changes - */ - useEffect(() => { - const keys = childrenArray.map((child) => child.key as string); + if (sizes && splitViewViewRef.current.size !== sizes.length) { + initializeSizes = false; - const enter = keys.filter((key) => !previousKeys.current.includes(key)); - const exit = previousKeys.current.map((key) => !keys.includes(key)); + console.warn( + `Expected ${sizes.length} children based on sizes but found ${splitViewViewRef.current.size}` + ); + } - exit.forEach((flag, index) => { - if (flag) { - splitViewRef.current?.removeView(index); + if (initializeSizes && sizes) { + previousKeys.current = childrenArray.map( + (child) => child.key as string + ); } - }); - for (const key of enter) { - const props = splitViewPropsRef.current.get(key); - - splitViewRef.current?.addView( - splitViewViewRef.current.get(key)!, - { - element: document.createElement("div"), - minimumSize: props?.minSize ?? minSize, - maximumSize: props?.maxSize ?? maxSize, - snap: props?.snap ?? snap, - layout: () => {}, - }, - Sizing.Distribute + const options: SplitViewOptions = { + orientation: vertical ? Orientation.Vertical : Orientation.Horizontal, + ...(initializeSizes && + sizes && { + descriptor: { + size: sizes.reduce((a, b) => a + b, 0), + views: sizes.map((size, index) => ({ + container: [...splitViewViewRef.current.values()][index], + size: size, + view: { + element: document.createElement("div"), + minimumSize: minSize, + maximumSize: maxSize, + snap: snap, + layout: () => {}, + }, + })), + }, + }), + }; + + splitViewRef.current = new SplitView( + containerRef.current, + options, + onChange ); - } - if (enter.length > 0 || exit.length > 0) { - previousKeys.current = keys; - } - }, [childrenArray, maxSize, minSize, snap]); + splitViewRef.current.on("sashreset", (_index: number) => { + splitViewRef.current?.distributeViewSizes(); + }); + + const that = splitViewRef.current; + + return () => { + that.dispose(); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + /** + * Add or remove views as number of children changes + */ + useEffect(() => { + const keys = childrenArray.map((child) => child.key as string); + + const enter = keys.filter((key) => !previousKeys.current.includes(key)); + const exit = previousKeys.current.map((key) => !keys.includes(key)); + + exit.forEach((flag, index) => { + if (flag) { + splitViewRef.current?.removeView(index); + } + }); + + for (const key of enter) { + const props = splitViewPropsRef.current.get(key); + + splitViewRef.current?.addView( + splitViewViewRef.current.get(key)!, + { + element: document.createElement("div"), + minimumSize: props?.minSize ?? minSize, + maximumSize: props?.maxSize ?? maxSize, + snap: props?.snap ?? snap, + layout: () => {}, + }, + Sizing.Distribute + ); + } - useResizeObserver({ - ref: containerRef, - onResize: ({ width, height }) => { - if (width && height) { - splitViewRef.current?.layout(vertical ? height : width); + if (enter.length > 0 || exit.length > 0) { + previousKeys.current = keys; } - }, - }); - - return ( -
-
- {React.Children.toArray(children).map((child, index) => { - if (!React.isValidElement(child)) { - return null; - } - - // toArray flattens and converts nulls to non-null keys - const key = child.key!; - - if (isPane(child)) { - splitViewPropsRef.current.set(key, child.props); - - return React.cloneElement(child, { - key: key, - ref: (el: HTMLElement | null) => { - if (el) { - splitViewViewRef.current.set(key, el); - } else { - splitViewViewRef.current.delete(key); - } - }, - }); - } else { - return ( - { + }, [childrenArray, maxSize, minSize, snap]); + + useResizeObserver({ + ref: containerRef, + onResize: ({ width, height }) => { + if (width && height) { + splitViewRef.current?.layout(vertical ? height : width); + } + }, + }); + + return ( +
+
+ {React.Children.toArray(children).map((child, index) => { + if (!React.isValidElement(child)) { + return null; + } + + // toArray flattens and converts nulls to non-null keys + const key = child.key!; + + if (isPane(child)) { + splitViewPropsRef.current.set(key, child.props); + + return React.cloneElement(child, { + key: key, + ref: (el: HTMLElement | null) => { if (el) { splitViewViewRef.current.set(key, el); } else { splitViewViewRef.current.delete(key); } - }} - > - {child} - - ); - } - })} + }, + }); + } else { + return ( + { + if (el) { + splitViewViewRef.current.set(key, el); + } else { + splitViewViewRef.current.delete(key); + } + }} + > + {child} + + ); + } + })} +
-
- ); -}; + ); + } +); Allotment.displayName = "Allotment"; diff --git a/stories/allotment.stories.tsx b/stories/allotment.stories.tsx index 150fc162..0aa15748 100644 --- a/stories/allotment.stories.tsx +++ b/stories/allotment.stories.tsx @@ -1,8 +1,8 @@ import { Meta, Story } from "@storybook/react"; import { debounce } from "lodash"; -import { useEffect, useMemo, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; -import Allotment, { AllotmentProps } from "../src/allotment"; +import Allotment, { AllotmentHandle, AllotmentProps } from "../src/allotment"; import { range } from "../src/helpers/range"; import styles from "./allotment.stories.module.css"; @@ -137,3 +137,25 @@ export const Nested: Story = () => { ); }; Nested.args = {}; + +export const Reset: Story = (args) => { + const ref = useRef(null!); + + return ( +
+ +
+ +
One
+
Two
+
+
+
+ ); +};