├── docs ├── CNAME ├── .nojekyll ├── _coverpage.md ├── icon.svg ├── index.html └── README.md ├── CNAME ├── src ├── types │ └── utilities.ts ├── index.tsx ├── hooks │ ├── useMergeClasses.ts │ └── useEventListener.ts └── components │ ├── SplitPane │ ├── hooks │ │ ├── memos │ │ │ ├── useIsLtr.ts │ │ │ ├── useCollapsedSize.ts │ │ │ ├── useCollapsedSizes.ts │ │ │ ├── useIsCollapseReversed.ts │ │ │ ├── useMinSizes.ts │ │ │ ├── useChildPanes.ts │ │ │ └── useCollapseOptions.tsx │ │ ├── callbacks │ │ │ ├── useGetIsCollapsed.ts │ │ │ ├── useGetCurrentPaneSizes.ts │ │ │ ├── useToggleCollapse.ts │ │ │ ├── useHandleDragFinished.ts │ │ │ ├── useHandleDragStart.ts │ │ │ ├── useUncollapseSize.ts │ │ │ ├── useGetMovedSizes.ts │ │ │ ├── useCollapseSize.ts │ │ │ ├── useUpdateCollapsedSizes.ts │ │ │ └── useRecalculateSizes.ts │ │ ├── effects │ │ │ └── useDragState.ts │ │ └── useSplitPaneResize.ts │ ├── tests │ │ └── helpers.test.ts │ ├── helpers.tsx │ └── index.tsx │ ├── Resizer │ ├── hooks │ │ └── useTransition.ts │ ├── helpers.tsx │ └── index.tsx │ ├── CollapseButton.tsx │ └── Pane │ └── index.tsx ├── .gitignore ├── jest.config.js ├── .storybook ├── preview.js ├── main.js └── manager.js ├── .eslintrc.js ├── .github ├── dependabot.yml └── workflows │ ├── on-push ( Deploy ).yml │ └── on-pr (Test & Lint ).yml ├── tsconfig.json ├── .all-contributorsrc ├── LICENSE ├── stories ├── Crazy.stories.tsx ├── InitialState.stories.tsx ├── rtl.stories.tsx └── Collapse.stories.tsx ├── test └── splitpane.test.tsx ├── package.json ├── CONTRIBUTING.md ├── README.md └── logo.svg /docs/CNAME: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | b-zurg.github.io -------------------------------------------------------------------------------- /src/types/utilities.ts: -------------------------------------------------------------------------------- 1 | export type Nullable = T | null; 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | .cache 5 | dist 6 | .idea 7 | *.tgz 8 | storybook-static -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transform: { 3 | '^.+\\.tsx?$': 'ts-jest' 4 | } 5 | } -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | export { 2 | SplitPane, 3 | SplitPaneProps, 4 | ResizerOptions, 5 | CollapseOptions, 6 | SplitPaneHooks, 7 | } from './components/SplitPane'; 8 | -------------------------------------------------------------------------------- /src/hooks/useMergeClasses.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | 3 | export const useMergeClasses = (classes: (string | undefined)[]): string => 4 | useMemo(() => classes.filter(c => c).join(' '), [classes]); 5 | -------------------------------------------------------------------------------- /.storybook/preview.js: -------------------------------------------------------------------------------- 1 | 2 | export const parameters = { 3 | actions: { argTypesRegex: "^on[A-Z].*" }, 4 | controls: { 5 | matchers: { 6 | color: /(background|color)$/i, 7 | date: /Date$/, 8 | }, 9 | }, 10 | } -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "extends": [ 3 | "react-app", 4 | "prettier/@typescript-eslint", 5 | "plugin:prettier/recommended" 6 | ], 7 | "settings": { 8 | "react": { 9 | "version": "detect" 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "stories": [ 3 | "../stories/**/*.stories.mdx", 4 | "../stories/**/*.stories.@(js|jsx|ts|tsx)" 5 | ], 6 | "addons": [ 7 | "@storybook/addon-links", 8 | "@storybook/addon-essentials" 9 | ] 10 | } -------------------------------------------------------------------------------- /src/components/SplitPane/hooks/memos/useIsLtr.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | import { Direction, SplitType } from '../../index'; 3 | 4 | export const useIsLtr = ({ split, dir }: { dir?: Direction; split: SplitType }) => 5 | useMemo(() => (split === 'vertical' ? dir !== 'rtl' : true), [split, dir]); 6 | -------------------------------------------------------------------------------- /src/components/SplitPane/hooks/callbacks/useGetIsCollapsed.ts: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | 3 | export const useGetIsPaneCollapsed = ({ collapsedIndices }: { collapsedIndices: number[] }) => 4 | useCallback( 5 | (paneIndex: number) => 6 | collapsedIndices.length > 0 ? collapsedIndices.includes(paneIndex) : false, 7 | [collapsedIndices] 8 | ); 9 | -------------------------------------------------------------------------------- /src/components/SplitPane/hooks/memos/useCollapsedSize.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | import { CollapseOptions } from '../../index'; 3 | export const DEFAULT_COLLAPSE_SIZE = 50; 4 | 5 | export const useCollapsedSize = ({ collapseOptions }: { collapseOptions?: CollapseOptions }) => 6 | useMemo(() => collapseOptions?.collapsedSize ?? DEFAULT_COLLAPSE_SIZE, [collapseOptions]); 7 | -------------------------------------------------------------------------------- /docs/_coverpage.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | # react-collapse-pane 1.3.6 6 | 7 | > The splittable, draggable and collapsible react layout library. 8 | 9 | - Simple and lightweight (~89kB gzipped) 10 | - Minimal Dependencies 11 | - Easy styling and configuration 12 | 13 | [GitHub](https://github.com/b-zurg/react-collapse-pane/) 14 | [Storybook](https://storybook-collapse-pane.netlify.app//) 15 | [Get Started](/?id=react-collapse-pane) 16 | 17 | ![color](#2E2E2E) 18 | -------------------------------------------------------------------------------- /src/hooks/useEventListener.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | 3 | export const useEventListener = ( 4 | type: K, 5 | listener?: (this: Window, ev: WindowEventMap[K]) => void 6 | ): void => 7 | useEffect(() => { 8 | const abortController = new AbortController(); 9 | if (!listener) return; 10 | window.addEventListener(type, listener); 11 | return (): void => { 12 | window.removeEventListener(type, listener); 13 | abortController.abort(); 14 | }; 15 | }, [type, listener]); 16 | -------------------------------------------------------------------------------- /src/components/SplitPane/hooks/callbacks/useGetCurrentPaneSizes.ts: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | import { getRefSize } from '../../helpers'; 3 | import { ChildPane } from '../useSplitPaneResize'; 4 | import { SplitType } from '../..'; 5 | 6 | export const useGetCurrentPaneSizes = ({ 7 | childPanes, 8 | split, 9 | }: { 10 | childPanes: Pick[]; 11 | split: SplitType; 12 | }) => 13 | useCallback(() => childPanes.map(({ ref }): number => getRefSize({ split, ref })), [ 14 | childPanes, 15 | split, 16 | ]); 17 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "daily" 12 | -------------------------------------------------------------------------------- /src/components/SplitPane/hooks/memos/useCollapsedSizes.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | import { SplitPaneProps } from '../../index'; 3 | 4 | export const useCollapsedSizes = ({ 5 | collapsedSizes, 6 | children, 7 | collapse, 8 | }: Pick) => 9 | useMemo( 10 | () => 11 | collapsedSizes?.length === children.length && !!collapse 12 | ? collapsedSizes 13 | : new Array(children.length).fill(null), 14 | [children.length, collapse, collapsedSizes] 15 | ); 16 | -------------------------------------------------------------------------------- /docs/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/components/SplitPane/hooks/callbacks/useToggleCollapse.ts: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react'; 2 | 3 | export const useToggleCollapse = ({ 4 | collapsedIndices, 5 | setCollapsed, 6 | }: { 7 | collapsedIndices: number[]; 8 | setCollapsed: React.Dispatch>; 9 | }) => 10 | useCallback( 11 | (index: number) => { 12 | collapsedIndices.includes(index) 13 | ? setCollapsed(collapsedIndices.filter(i => i !== index)) 14 | : setCollapsed([...collapsedIndices, index]); 15 | }, 16 | [collapsedIndices, setCollapsed] 17 | ); 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "src", 4 | "types" 5 | ], 6 | "compilerOptions": { 7 | "module": "esnext", 8 | "lib": [ 9 | "dom", 10 | "esnext" 11 | ], 12 | "importHelpers": true, 13 | "declaration": true, 14 | "sourceMap": true, 15 | "rootDir": "./src", 16 | "strict": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "noImplicitReturns": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "moduleResolution": "node", 22 | "baseUrl": "./", 23 | "jsx": "react", 24 | "esModuleInterop": true, 25 | "skipLibCheck": true 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/components/Resizer/hooks/useTransition.ts: -------------------------------------------------------------------------------- 1 | import { CollapseOptions, TransitionType } from '../../SplitPane'; 2 | import { useMemo } from 'react'; 3 | import { Fade, Grow, Zoom } from '@mui/material'; 4 | type TransitionComponent = typeof Fade | typeof Grow | typeof Zoom; 5 | const transitionComponentMap: { 6 | [key in TransitionType]: TransitionComponent; 7 | } = { 8 | fade: Fade, 9 | grow: Grow, 10 | zoom: Zoom, 11 | none: Fade, 12 | }; 13 | 14 | export const useTransition = (collapseOptions?: CollapseOptions) => 15 | useMemo(() => transitionComponentMap[collapseOptions?.buttonTransition ?? 'fade'], [ 16 | collapseOptions, 17 | ]); 18 | -------------------------------------------------------------------------------- /src/components/SplitPane/hooks/memos/useIsCollapseReversed.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | import { CollapseOptions } from '../../index'; 3 | 4 | export const isCollapseDirectionReversed = ( 5 | collapseOptions?: Partial | boolean 6 | ): boolean => { 7 | if (typeof collapseOptions === 'boolean') return false; 8 | return collapseOptions?.collapseDirection 9 | ? ['right', 'down'].includes(collapseOptions.collapseDirection) 10 | : false; 11 | }; 12 | 13 | export const useIsCollapseReversed = (collapseOptions?: Partial | boolean) => 14 | useMemo(() => isCollapseDirectionReversed(collapseOptions), [collapseOptions]); 15 | -------------------------------------------------------------------------------- /src/components/SplitPane/hooks/callbacks/useHandleDragFinished.ts: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react'; 2 | import { SplitPaneHooks } from '../..'; 3 | 4 | /** 5 | * called at the end of a drag, sets the final size as well as runs the callback hook 6 | */ 7 | export const useHandleDragFinished = ({ 8 | setSizes, 9 | hooks, 10 | movedSizes, 11 | }: { 12 | children: React.ReactChild[]; 13 | setSizes: React.Dispatch>; 14 | movedSizes: number[]; 15 | hooks?: SplitPaneHooks; 16 | }) => 17 | useCallback(() => { 18 | setSizes(movedSizes); 19 | hooks?.onSaveSizes?.(movedSizes); 20 | }, [movedSizes, hooks, setSizes]); 21 | -------------------------------------------------------------------------------- /.storybook/manager.js: -------------------------------------------------------------------------------- 1 | import { addons } from '@storybook/addons'; 2 | import { create } from '@storybook/theming/create'; 3 | import { configureActions } from '@storybook/addon-actions'; 4 | 5 | addons.setConfig({ 6 | theme: create({ 7 | base: "dark", 8 | 9 | appContentBg: 'white', 10 | appBorderColor: 'silver', 11 | barBg: 'white', 12 | appBg: "#3e3e3e", 13 | 14 | textColor: 'rgba(211,211,211,1.9)', 15 | textInverseColor: 'rgba(211,211,211,1.9)', 16 | 17 | brandTitle: 'react-collapse-pane', 18 | brandUrl: "https://github.com/b-zurg/react-collapse-pane", 19 | brandImage: "https://github.com/b-zurg/react-collapse-pane/raw/master/logo.svg?sanitize=true", 20 | }), 21 | }); 22 | 23 | 24 | configureActions({ 25 | depth: 5, 26 | limit: 5, 27 | }); 28 | -------------------------------------------------------------------------------- /src/components/SplitPane/hooks/callbacks/useHandleDragStart.ts: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | import { BeginDragCallback, ClientPosition } from '../effects/useDragState'; 3 | import { SplitPaneHooks } from '../..'; 4 | 5 | /** 6 | * Callback that starts the drag process and called at the beginning of the dragging. 7 | */ 8 | export const useHandleDragStart = ({ 9 | isReversed, 10 | hooks, 11 | beginDrag, 12 | }: { 13 | isReversed: boolean; 14 | hooks?: SplitPaneHooks; 15 | beginDrag: BeginDragCallback; 16 | }) => 17 | useCallback( 18 | ({ index, position }: { index: number; position: ClientPosition }): void => { 19 | hooks?.onDragStarted?.(); 20 | beginDrag({ position, index: isReversed ? index - 1 : index }); 21 | }, 22 | [beginDrag, hooks, isReversed] 23 | ); 24 | -------------------------------------------------------------------------------- /.github/workflows/on-push ( Deploy ).yml: -------------------------------------------------------------------------------- 1 | name: "Release" 2 | on: 3 | push: 4 | branches: 5 | - master 6 | - next 7 | 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | 14 | - uses: actions/setup-node@v1 15 | with: 16 | node-version: 12 17 | 18 | - name: Use cached node_modules 19 | uses: actions/cache@v1 20 | with: 21 | path: node_modules 22 | key: nodeModules-${{ hashFiles('**/package-lock.json') }} 23 | restore-keys: | 24 | nodeModules- 25 | 26 | - name: Install Dependencies 27 | run: | 28 | npm ci 29 | 30 | - name: Semantic Release 31 | id: semantic 32 | uses: cycjimmy/semantic-release-action@v2 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 35 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 36 | -------------------------------------------------------------------------------- /src/components/SplitPane/hooks/memos/useMinSizes.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | import { DEFAULT_MIN_SIZE, getMinSize } from '../../helpers'; 3 | import { CollapseOptions } from '../../index'; 4 | 5 | /** 6 | * Returns the current actual minimum size of the panel. This in some cases means the collapsed size. 7 | */ 8 | export const useMinSizes = ({ 9 | minSizes, 10 | numSizes, 11 | collapsedIndices, 12 | collapseOptions, 13 | }: { 14 | numSizes: number; 15 | minSizes?: number | number[]; 16 | collapsedIndices: number[]; 17 | collapseOptions?: CollapseOptions; 18 | }): number[] => 19 | useMemo( 20 | () => 21 | Array.from({ length: numSizes }).map((_child, idx) => 22 | collapsedIndices.includes(idx) 23 | ? collapseOptions?.collapsedSize ?? DEFAULT_MIN_SIZE 24 | : getMinSize(idx, minSizes) 25 | ), 26 | [numSizes, collapseOptions, collapsedIndices, minSizes] 27 | ); 28 | -------------------------------------------------------------------------------- /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "README.md" 4 | ], 5 | "imageSize": 100, 6 | "commit": false, 7 | "contributors": [ 8 | { 9 | "login": "b-zurg", 10 | "name": "Buzurg Arjmandi", 11 | "avatar_url": "https://avatars3.githubusercontent.com/u/57298613?v=4", 12 | "profile": "https://github.com/b-zurg", 13 | "contributions": [ 14 | "test", 15 | "doc", 16 | "code", 17 | "design", 18 | "example", 19 | "platform" 20 | ] 21 | }, 22 | { 23 | "login": "hst44", 24 | "name": "hst44", 25 | "avatar_url": "https://avatars1.githubusercontent.com/u/54194733?v=4", 26 | "profile": "https://github.com/hst44", 27 | "contributions": [ 28 | "bug" 29 | ] 30 | } 31 | ], 32 | "contributorsPerLine": 7, 33 | "projectName": "react-collapse-pane", 34 | "projectOwner": "b-zurg", 35 | "repoType": "github", 36 | "repoHost": "https://github.com", 37 | "skipCi": true 38 | } 39 | -------------------------------------------------------------------------------- /src/components/SplitPane/tests/helpers.test.ts: -------------------------------------------------------------------------------- 1 | import { moveSizes } from '../helpers'; 2 | 3 | test('Move right does not extend past default minimum (50)', () => { 4 | const sizes = [138, 138, 193, 83]; 5 | moveSizes({ 6 | sizes, 7 | index: 2, 8 | offset: 36, 9 | minSizes: [50, 50, 50, 50], 10 | collapsedSize: 50, 11 | collapsedIndices: [], 12 | }); 13 | expect(sizes).toEqual([138, 138, 226, 50]); 14 | moveSizes({ 15 | sizes, 16 | index: 0, 17 | offset: 1000, 18 | minSizes: [50, 50, 50, 50], 19 | collapsedSize: 50, 20 | collapsedIndices: [], 21 | }); 22 | expect(sizes).toEqual([402, 50, 50, 50]); 23 | }); 24 | test('Move left does not extend past default minimum (50)', () => { 25 | const sizes = [138, 138, 193, 83]; 26 | moveSizes({ 27 | sizes, 28 | index: 2, 29 | offset: -1000, 30 | minSizes: [50, 50, 50, 50], 31 | collapsedSize: 50, 32 | collapsedIndices: [], 33 | }); 34 | expect(sizes).toEqual([50, 50, 50, 402]); 35 | }); 36 | -------------------------------------------------------------------------------- /src/components/SplitPane/hooks/memos/useChildPanes.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | import * as React from 'react'; 3 | import { ChildPane } from '../useSplitPaneResize'; 4 | 5 | // converts all children nodes into 'childPane' objects that has its ref, key, but not the size yet 6 | export const useChildPanes = ({ 7 | paneRefs, 8 | children, 9 | minSizes, 10 | }: { 11 | paneRefs: React.MutableRefObject>>; 12 | children: React.ReactChild[]; 13 | minSizes: number[]; 14 | }) => { 15 | const childPanes: Omit[] = useMemo(() => { 16 | const prevPaneRefs = paneRefs.current; 17 | paneRefs.current = new Map>(); 18 | return children.map((node, index) => { 19 | const key = `index.${index}`; 20 | const ref = prevPaneRefs.get(key) || React.createRef(); 21 | paneRefs.current.set(key, ref); 22 | return { key, node, ref, minSize: minSizes[index] }; 23 | }); 24 | }, [children, minSizes, paneRefs]); 25 | return childPanes; 26 | }; 27 | -------------------------------------------------------------------------------- /.github/workflows/on-pr (Test & Lint ).yml: -------------------------------------------------------------------------------- 1 | name: Test & Lint 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Begin CI... 14 | uses: actions/checkout@v2 15 | 16 | - name: Use Node 12 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: 12.x 20 | 21 | - name: Use cached node_modules 22 | uses: actions/cache@v1 23 | with: 24 | path: node_modules 25 | key: nodeModules-${{ hashFiles('**/package-lock.json') }} 26 | restore-keys: | 27 | nodeModules- 28 | 29 | - name: Install dependencies 30 | run: npm ci 31 | env: 32 | CI: true 33 | 34 | - name: Lint 35 | run: npm run lint 36 | env: 37 | CI: true 38 | 39 | - name: Test 40 | run: npm run test --ci --coverage --maxWorkers=2 41 | env: 42 | CI: true 43 | 44 | - name: Build 45 | run: npm run build 46 | env: 47 | CI: true 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Buzurgmehr Arjmandi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src/components/SplitPane/hooks/callbacks/useUncollapseSize.ts: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import { moveSizes } from '../../helpers'; 4 | 5 | export const useUncollapseSize = ({ 6 | isReversed, 7 | movedSizes, 8 | minSizes, 9 | setMovedSizes, 10 | setSizes, 11 | collapsedSize, 12 | collapsedIndices, 13 | }: { 14 | isReversed: boolean; 15 | movedSizes: number[]; 16 | minSizes: number[]; 17 | setSizes: React.Dispatch>; 18 | setMovedSizes: React.Dispatch>; 19 | collapsedSize: number; 20 | collapsedIndices: number[]; 21 | }) => 22 | useCallback( 23 | ({ size, idx }: { size: number; idx: number }) => { 24 | const offset = isReversed ? -(size - 50) : size - 50; 25 | const index = isReversed ? idx - 1 : idx; 26 | const newSizes = [...movedSizes]; 27 | moveSizes({ sizes: newSizes, index, offset, minSizes, collapsedSize, collapsedIndices }); 28 | ReactDOM.unstable_batchedUpdates(() => { 29 | setMovedSizes(newSizes); 30 | setSizes(newSizes); 31 | }); 32 | }, 33 | [collapsedIndices, collapsedSize, isReversed, minSizes, movedSizes, setMovedSizes, setSizes] 34 | ); 35 | -------------------------------------------------------------------------------- /src/components/SplitPane/hooks/callbacks/useGetMovedSizes.ts: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | import { DragState } from '../effects/useDragState'; 3 | import { moveCollapsedSiblings, moveSizes } from '../../helpers'; 4 | 5 | export const useGetMovedSizes = ({ 6 | sizes: originalSizes, 7 | isLtr, 8 | minSizes, 9 | collapsedIndices, 10 | isReversed, 11 | collapsedSize, 12 | }: { 13 | sizes: number[]; 14 | isLtr: boolean; 15 | minSizes: number[]; 16 | collapsedIndices: number[]; 17 | isReversed: boolean; 18 | collapsedSize: number; 19 | }) => 20 | useCallback( 21 | (dragState: DragState): number[] => { 22 | const sizes = [...originalSizes]; 23 | const index = dragState.index; 24 | const offset = isLtr ? dragState.offset : -dragState.offset; 25 | moveSizes({ 26 | sizes, 27 | index, 28 | offset, 29 | minSizes, 30 | collapsedIndices, 31 | collapsedSize, 32 | }); 33 | moveCollapsedSiblings({ 34 | collapsedSize, 35 | sizes, 36 | minSizes, 37 | collapsedIndices, 38 | isReversed, 39 | index, 40 | offset, 41 | }); 42 | 43 | return sizes; 44 | }, 45 | [collapsedIndices, collapsedSize, isLtr, isReversed, minSizes, originalSizes] 46 | ); 47 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | React Collapse Pane 7 | 8 | 9 | 10 | 11 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/components/SplitPane/hooks/memos/useCollapseOptions.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { CollapseOptions } from '../../index'; 3 | import { CollapseButton } from '../../../CollapseButton'; 4 | 5 | const getDefault = (props: { 6 | isVertical: boolean; 7 | isLtr: boolean; 8 | isReversed: boolean; 9 | }): CollapseOptions => ({ 10 | beforeToggleButton: , 11 | afterToggleButton: , 12 | collapseDirection: props.isVertical ? 'left' : 'up', 13 | overlayCss: { backgroundColor: 'rgba(0, 0, 0, 0.4)' }, 14 | buttonTransitionTimeout: 200, 15 | buttonTransition: 'grow', 16 | collapsedSize: 50, 17 | collapseTransitionTimeout: 500, 18 | buttonPositionOffset: 0, 19 | }); 20 | 21 | /** 22 | * function that returns a set of valid collapseOptions from uncertain input. 23 | */ 24 | export const useCollapseOptions = ({ 25 | originalValue, 26 | ...orientationDetails 27 | }: { 28 | originalValue: Partial | undefined | boolean; 29 | isVertical: boolean; 30 | isLtr: boolean; 31 | isReversed: boolean; 32 | }): Required | undefined => { 33 | if (originalValue === undefined || originalValue === false) return undefined; 34 | if (originalValue === true) return getDefault(orientationDetails); 35 | return { ...getDefault(orientationDetails), ...originalValue }; 36 | }; 37 | -------------------------------------------------------------------------------- /src/components/SplitPane/hooks/callbacks/useCollapseSize.ts: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react'; 2 | import { moveCollapsedSiblings, moveSizes } from '../../helpers'; 3 | import * as ReactDOM from 'react-dom'; 4 | 5 | export const useCollapseSize = ({ 6 | isReversed, 7 | movedSizes, 8 | minSizes, 9 | collapsedIndices, 10 | setSizes, 11 | setMovedSizes, 12 | collapsedSize, 13 | }: { 14 | isReversed: boolean; 15 | movedSizes: number[]; 16 | minSizes: number[]; 17 | collapsedIndices: number[]; 18 | setSizes: React.Dispatch>; 19 | setMovedSizes: React.Dispatch>; 20 | collapsedSize: number; 21 | }) => 22 | useCallback( 23 | ({ size, idx }: { idx: number; size: number }) => { 24 | const offset = isReversed ? -(collapsedSize - size) : collapsedSize - size; 25 | const index = isReversed ? idx - 1 : idx; 26 | const sizes = [...movedSizes]; 27 | moveSizes({ sizes, index, offset, minSizes, collapsedIndices, collapsedSize }); 28 | moveCollapsedSiblings({ 29 | offset, 30 | index, 31 | isReversed, 32 | collapsedIndices, 33 | minSizes, 34 | sizes, 35 | collapsedSize, 36 | }); 37 | ReactDOM.unstable_batchedUpdates(() => { 38 | setMovedSizes(sizes); 39 | setSizes(sizes); 40 | }); 41 | }, 42 | [isReversed, collapsedSize, movedSizes, minSizes, collapsedIndices, setMovedSizes, setSizes] 43 | ); 44 | -------------------------------------------------------------------------------- /stories/Crazy.stories.tsx: -------------------------------------------------------------------------------- 1 | import { SplitPane, SplitPaneProps } from '../src'; 2 | import { storiesOf } from '@storybook/react'; 3 | import { configureActions } from '@storybook/addon-actions'; 4 | import React from 'react'; 5 | 6 | configureActions({ 7 | depth: 5, 8 | limit: 5, 9 | }); 10 | storiesOf('Crazy Combo!', module).add('Do the Splits!', () => { 11 | const shouldCollapse = true; 12 | const VerticalSplitPane = (props: Omit) => ( 13 | 14 | {props.children} 15 | 16 | ); 17 | const HorizontalSplitpane = (props: Omit) => ( 18 | 19 | {props.children} 20 | 21 | ); 22 | 23 | return ( 24 | 25 | I'm at the top level! 26 | 27 |
This is a div
28 |
This is a second div
29 |
30 | 31 |
Horizontal 1
32 | 33 |
I'm within the horizontal but vertical!
34 |
I'm the same but again!
35 |
36 |
37 |
38 |
This is a fourth div
39 |
40 |
41 | ); 42 | }); 43 | -------------------------------------------------------------------------------- /src/components/SplitPane/hooks/callbacks/useUpdateCollapsedSizes.ts: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react'; 2 | import { useCollapseSize } from './useCollapseSize'; 3 | import { useUncollapseSize } from './useUncollapseSize'; 4 | import { SplitPaneHooks } from '../..'; 5 | import { Nullable } from '../../../../types/utilities'; 6 | 7 | export const useUpdateCollapsedSizes = ({ 8 | movedSizes, 9 | setCollapsedSizes, 10 | collapsedSizes, 11 | collapseSize, 12 | sizes, 13 | hooks, 14 | unCollapseSize, 15 | }: { 16 | movedSizes: number[]; 17 | collapsedSizes: Nullable[]; 18 | sizes: number[]; 19 | collapseSize: ReturnType; 20 | unCollapseSize: ReturnType; 21 | setCollapsedSizes: React.Dispatch[]>>; 22 | hooks?: SplitPaneHooks; 23 | }) => 24 | useCallback( 25 | (indices: number[]) => { 26 | setCollapsedSizes( 27 | collapsedSizes.map((size, idx) => { 28 | const isCollapsed = indices.includes(idx); 29 | if (isCollapsed && size === null) { 30 | collapseSize({ size: sizes[idx], idx }); 31 | hooks?.onChange?.(sizes); 32 | return movedSizes[idx]; // when collapsed store current size 33 | } 34 | if (!isCollapsed && size !== null) { 35 | unCollapseSize({ idx, size }); // when un-collapsed clear size 36 | hooks?.onChange?.(sizes); 37 | return null; 38 | } 39 | return size; 40 | }) 41 | ); 42 | }, 43 | [collapseSize, collapsedSizes, hooks, movedSizes, setCollapsedSizes, sizes, unCollapseSize] 44 | ); 45 | -------------------------------------------------------------------------------- /src/components/SplitPane/hooks/callbacks/useRecalculateSizes.ts: -------------------------------------------------------------------------------- 1 | import { addArray } from '../../helpers'; 2 | import React, { useCallback } from 'react'; 3 | 4 | export const useRecalculateSizes = ({ 5 | getCurrentPaneSizes, 6 | collapsedSize, 7 | collapsedIndices, 8 | setMovedSizes, 9 | setSizes, 10 | }: { 11 | getCurrentPaneSizes: () => number[]; 12 | collapsedIndices: number[]; 13 | collapsedSize: number; 14 | originalMinSizes: number | number[] | undefined; 15 | minSizes: number[]; 16 | setMovedSizes: React.Dispatch>; 17 | setSizes: React.Dispatch>; 18 | }) => 19 | useCallback( 20 | (initialSizes?: number[]) => { 21 | const curSizes = getCurrentPaneSizes(); 22 | const ratio = 23 | initialSizes && initialSizes.length > 0 ? addArray(curSizes) / addArray(initialSizes) : 1; 24 | const initialRatioSizes = initialSizes ? initialSizes.map(size => size * ratio) : curSizes; 25 | const adjustedSizes = initialRatioSizes.map((size, idx) => { 26 | if (collapsedIndices.includes(idx)) { 27 | return collapsedSize; 28 | } 29 | if (collapsedIndices.includes(idx - 1)) { 30 | const totalPrevSizeToAdd = addArray( 31 | collapsedIndices 32 | .filter((_collapsedIdx, index) => index <= idx) 33 | .map((_i, index) => initialRatioSizes[index] - collapsedSize) 34 | ); 35 | return size + totalPrevSizeToAdd; 36 | } 37 | return size; 38 | }); 39 | setMovedSizes(adjustedSizes); 40 | setSizes(adjustedSizes); 41 | }, 42 | [collapsedIndices, collapsedSize, getCurrentPaneSizes, setMovedSizes, setSizes] 43 | ); 44 | -------------------------------------------------------------------------------- /src/components/Resizer/helpers.tsx: -------------------------------------------------------------------------------- 1 | import styled, { css } from 'styled-components'; 2 | 3 | type OrientationProps = { 4 | $isVertical: boolean; 5 | }; 6 | export const topBottomCss = css` 7 | top: 0; 8 | bottom: 0; 9 | `; 10 | const leftRightCss = css` 11 | right: 0; 12 | left: 0; 13 | `; 14 | 15 | export const ButtonWrapper = styled.div` 16 | cursor: pointer; 17 | position: absolute; 18 | `; 19 | 20 | interface ButtonContainerProps extends OrientationProps { 21 | $grabberSize: string | null; 22 | $isLtr: boolean; 23 | } 24 | export const ButtonContainer = styled.div` 25 | z-index: 3; 26 | position: absolute; 27 | overflow: initial; 28 | display: flex; 29 | flex-direction: column; 30 | align-items: center; 31 | justify-content: center; 32 | 33 | ${props => `${props.$isVertical ? 'width' : 'height'}: ${props.$grabberSize}`}; 34 | ${props => (props.$isVertical ? topBottomCss : leftRightCss)} 35 | `; 36 | 37 | interface GrabberProps extends OrientationProps { 38 | $isCollapsed: boolean; 39 | $isLtr: boolean; 40 | } 41 | export const ResizeGrabber = styled.div` 42 | position: absolute; 43 | z-index: 3; 44 | transform: ${props => 45 | props.$isVertical ? `translateX(${props.$isLtr ? '-' : ''}50%)` : 'translateY(-50%)'}; 46 | cursor: ${props => !props.$isCollapsed && (props.$isVertical ? 'col-resize' : 'row-resize')}; 47 | ${props => (props.$isVertical ? topBottomCss : leftRightCss)} 48 | `; 49 | 50 | export const ResizePresentation = styled.div` 51 | z-index: 2; 52 | position: absolute; 53 | ${props => (props.$isVertical ? topBottomCss : leftRightCss)} 54 | `; 55 | 56 | export const getSizeWithUnit = (size: string | number): string => 57 | isNaN(size as number) ? size.toString() : `${size}px`; 58 | -------------------------------------------------------------------------------- /src/components/CollapseButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | const Button = styled.div` 5 | width: 1.8rem; 6 | height: 1.8rem; 7 | border-radius: 300px; 8 | background: #0092d1; 9 | cursor: pointer; 10 | user-select: none; 11 | text-align: center; 12 | color: white; 13 | border: 2px rgba(200, 200, 200, 0.5) solid; 14 | display: flex; 15 | align-items: center; 16 | justify-content: center; 17 | svg { 18 | width: 100%; 19 | height: 100%; 20 | } 21 | `; 22 | 23 | enum Direction { 24 | left, 25 | right, 26 | up, 27 | down, 28 | } 29 | 30 | const paths: { [key in Direction]: string } = { 31 | [Direction.left]: 'M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z', 32 | [Direction.right]: 'M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z', 33 | [Direction.up]: 'M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z', 34 | [Direction.down]: 'M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z', 35 | }; 36 | 37 | interface IconProps extends React.SVGProps { 38 | dir: Direction; 39 | } 40 | 41 | const Icon: React.FC = props => ( 42 | 45 | ); 46 | 47 | interface CollapseButtonProps { 48 | isLtr: boolean; 49 | isVertical: boolean; 50 | isBefore: boolean; 51 | isReversed: boolean; 52 | } 53 | export const CollapseButton: React.FC = props => { 54 | const dirs: Direction[] = props.isVertical 55 | ? [Direction.left, Direction.right] 56 | : [Direction.up, Direction.down]; 57 | const [a, b] = props.isReversed ? dirs.reverse() : dirs; 58 | const dir: Direction = props.isBefore ? (props.isLtr ? a : b) : props.isLtr ? b : a; 59 | return ( 60 | 63 | ); 64 | }; 65 | -------------------------------------------------------------------------------- /stories/InitialState.stories.tsx: -------------------------------------------------------------------------------- 1 | import { SplitPane, SplitPaneProps } from '../src'; 2 | import { storiesOf } from '@storybook/react'; 3 | import { configureActions } from '@storybook/addon-actions'; 4 | import React from 'react'; 5 | 6 | configureActions({ 7 | depth: 5, 8 | limit: 5, 9 | }); 10 | storiesOf('Initial States', module) 11 | .add('Ltr, First Pane Collapsed', () => { 12 | const shouldCollapse = true; 13 | const VerticalSplitPane = (props: Omit) => ( 14 | 15 | {props.children} 16 | 17 | ); 18 | 19 | return ( 20 | 21 |
This is a div
22 |
This is a second div
23 |
This is a third div
24 |
25 | ); 26 | }) 27 | .add('Ltr, First Two Panes Collapsed', () => { 28 | const shouldCollapse = true; 29 | const VerticalSplitPane = (props: Omit) => ( 30 | 31 | {props.children} 32 | 33 | ); 34 | 35 | return ( 36 | 37 |
This is a div
38 |
This is a second div
39 |
This is a third div
40 |
41 | ); 42 | }) 43 | .add('Initial Sizes as Flex-Basis proportions', () => { 44 | return ( 45 | 46 |
This is a div
47 |
This is a second div
48 |
This is a third div
49 |
50 | ); 51 | }) 52 | .add('Only one child skips splitpane layout', () => { 53 | return ( 54 | //@ts-ignore 55 | 56 |
This is only one div!
57 |
58 | ); 59 | }); 60 | -------------------------------------------------------------------------------- /test/splitpane.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import { SplitPaneProps, SplitPane } from '../src'; 4 | 5 | const SplitPaneBase: React.FC = props => ( 6 | 13 | {props.children} 14 | 15 | ); 16 | 17 | describe('Vertical', () => { 18 | it('Split with Divs renders without crashing', () => { 19 | const div = document.createElement('div'); 20 | ReactDOM.render( 21 | 22 |
This is a div
23 |
This is a second div
24 |
This is a third div
25 | This is not a fourth div 26 |
, 27 | div 28 | ); 29 | ReactDOM.unmountComponentAtNode(div); 30 | }); 31 | it('Split with only one child renders without crashing', () => { 32 | console.error = jest.fn(); 33 | const div = document.createElement('div'); 34 | ReactDOM.render( 35 | // @ts-ignore ignore type error to help out javascript users as they won't get it. 36 | 37 |
This is not a fourth div
38 |
, 39 | div 40 | ); 41 | ReactDOM.unmountComponentAtNode(div); 42 | expect(console.error).toHaveBeenCalledTimes(3); 43 | expect(console.error).toHaveBeenCalledWith( 44 | '[react-collapse-pane] - You must have more than one non-null child inside the SplitPane component. Even though SplitPane does not crash, you should resolve this error.' 45 | ); 46 | }); 47 | }); 48 | 49 | describe('Horizontal', () => { 50 | it('Split with Divs, nulls, and text renders without crashing', () => { 51 | const div = document.createElement('div'); 52 | ReactDOM.render( 53 | 54 |
This is a div
55 |
This is a second div
56 |
This is a third div
57 | This is not a fourth div 58 |
, 59 | div 60 | ); 61 | ReactDOM.unmountComponentAtNode(div); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1.0", 3 | "license": "MIT", 4 | "name": "react-collapse-pane", 5 | "repository": "https://github.com/b-zurg/react-collapse-pane", 6 | "homepage": "https://b-zurg.github.io/react-collapse-pane/", 7 | "bugs": "https://github.com/b-zurg/react-collapse-pane/issues", 8 | "description": "The splittable, draggable and collapsible react layout library.", 9 | "author": "Buzurgmehr Arjmandi", 10 | "module": "dist/react-collapse-pane.esm.js", 11 | "main": "dist/index.js", 12 | "typings": "dist/index.d.ts", 13 | "files": [ 14 | "dist", 15 | "src" 16 | ], 17 | "keywords": [ 18 | "react", 19 | "react-component", 20 | "split", 21 | "pane", 22 | "layout", 23 | "collapse", 24 | "typescript", 25 | "splitter", 26 | "splittable", 27 | "collapsible", 28 | "drag", 29 | "draggable", 30 | "dragging", 31 | "panel", 32 | "component" 33 | ], 34 | "engines": { 35 | "node": ">=10" 36 | }, 37 | "scripts": { 38 | "start": "tsdx watch", 39 | "build": "tsdx build", 40 | "test": "tsdx test --passWithNoTests", 41 | "lint": "tsdx lint", 42 | "lint:fix": "tsdx lint --fix", 43 | "prepare": "tsdx build", 44 | "storybook": "start-storybook -p 6006", 45 | "build-storybook": "build-storybook" 46 | }, 47 | "husky": { 48 | "hooks": { 49 | "pre-commit": "tsdx lint" 50 | } 51 | }, 52 | "prettier": { 53 | "printWidth": 100, 54 | "semi": true, 55 | "singleQuote": true, 56 | "trailingComma": "es5" 57 | }, 58 | "dependencies": {}, 59 | "devDependencies": { 60 | "@babel/core": "^7.18.0", 61 | "@mui/material": "^5.8.0", 62 | "@storybook/addon-actions": "^6.5.3", 63 | "@storybook/addon-essentials": "^6.5.3", 64 | "@storybook/addon-links": "^6.5.3", 65 | "@storybook/react": "^6.5.3", 66 | "@types/react": "^18.0.9", 67 | "@types/react-dom": "^18.0.4", 68 | "@types/styled-components": "^5.1.25", 69 | "babel-loader": "^8.2.5", 70 | "html-webpack-plugin": "^5.5.0", 71 | "husky": "^6.0.0", 72 | "react": "^18.1.0", 73 | "react-docgen-typescript-loader": "^3.7.2", 74 | "react-dom": "^18.1.0", 75 | "react-is": "^18.1.0", 76 | "styled-components": "^5.3.5", 77 | "ts-loader": "^9.3.0", 78 | "tsdx": "^0.14.1", 79 | "tslib": "^2.4.0", 80 | "@emotion/styled": "^11.8.1", 81 | "@emotion/react": "^11.9.0", 82 | "typescript": "^4.6.4" 83 | }, 84 | "peerDependencies": { 85 | "react": ">=16", 86 | "@mui/material": "^5.8.0" 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /stories/rtl.stories.tsx: -------------------------------------------------------------------------------- 1 | import { SplitPane } from '../src'; 2 | import { storiesOf } from '@storybook/react'; 3 | import { action, configureActions } from '@storybook/addon-actions'; 4 | import React from 'react'; 5 | 6 | configureActions({ 7 | depth: 5, 8 | limit: 5, 9 | }); 10 | 11 | storiesOf('Right to Left Support', module) 12 | .add('Vertical Split', () => { 13 | const collapseDirection = 'left'; 14 | const resizerCss = { 15 | width: '1px', 16 | background: 'rgba(0, 0, 0, 0.1)', 17 | }; 18 | const resizerHoverCss = { 19 | width: '10px', 20 | marginLeft: '-10px', 21 | backgroundImage: 22 | 'radial-gradient(at center center,rgba(0,0,0,0.2) 0%,transparent 70%,transparent 100%)', 23 | backgroundSize: '50px 100%', 24 | backgroundPosition: '0 50%', 25 | backgroundRepeat: 'no-repeat', 26 | borderRight: '1px solid rgba(0, 0, 0, 0.1)', 27 | }; 28 | return ( 29 |
30 | 47 |
اللوحة الأولى
48 |
اللوحة الثانية
49 |
اللوحة الثالثة
50 |
اللوحة الرابعة
51 |
52 |
53 | ); 54 | }) 55 | .add('Horizontal Split', () => { 56 | const collapseDirection = 'up'; 57 | const resizerCss = { 58 | height: '1px', 59 | background: 'rgba(0, 0, 0, 0.1)', 60 | }; 61 | const resizerHoverCss = { 62 | height: '10px', 63 | marginTop: '-10px', 64 | backgroundImage: 65 | 'radial-gradient(at center center,rgba(0,0,0,0.2) 0%,transparent 70%,transparent 100%)', 66 | backgroundSize: '100% 50px', 67 | backgroundPosition: '50% 0', 68 | backgroundRepeat: 'no-repeat', 69 | borderRight: '1px solid rgba(0, 0, 0, 0.1)', 70 | }; 71 | return ( 72 |
73 | 91 |
اللوحة الأولى
92 |
اللوحة الثانية
93 |
اللوحة الثالثة
94 |
اللوحة الرابعة
95 |
96 |
97 | ); 98 | }); 99 | -------------------------------------------------------------------------------- /stories/Collapse.stories.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { SplitPane } from '../src'; 3 | import { storiesOf } from '@storybook/react'; 4 | import { action, configureActions } from '@storybook/addon-actions'; 5 | import React from 'react'; 6 | // @ts-ignore 7 | import logo from '../docs/icon.svg'; 8 | 9 | configureActions({ 10 | depth: 5, 11 | limit: 5, 12 | }); 13 | 14 | const Logo = styled.img` 15 | @keyframes App-logo-spin { 16 | from { 17 | transform: rotate(0deg); 18 | } 19 | to { 20 | transform: rotate(360deg); 21 | } 22 | } 23 | height: 40vmin; 24 | pointer-events: none; 25 | animation: App-logo-spin infinite 20s linear; 26 | `; 27 | const Header = styled.div` 28 | background-color: #282c34; 29 | min-height: 100vh; 30 | display: flex; 31 | flex-direction: column; 32 | align-items: center; 33 | justify-content: center; 34 | font-size: calc(10px + 2vmin); 35 | color: white; 36 | text-align: center; 37 | `; 38 | const Link = styled.a` 39 | color: #61dafb; 40 | padding-top: 2rem; 41 | `; 42 | 43 | storiesOf('Collapsable Panes', module) 44 | .add('Vertical Split', () => { 45 | const buttonPositionOffset = 0; 46 | const collapseDirection = 'left'; 47 | const minSizes = [50, 50, 50, 50]; 48 | const collapseTransition = 500; 49 | const grabberSize = 10; 50 | const buttonTransition = 'grow'; 51 | 52 | return ( 53 |
54 | 72 | 73 |

You can collapse and resize these panes!

74 | 75 |

Check out the Docs

76 | 77 |
78 |
79 | ); 80 | }) 81 | .add('Horizontal Split', () => { 82 | const collapseDirection = 'up'; 83 | const resizerCss = { 84 | height: '1px', 85 | background: 'rgba(0, 0, 0, 0.1)', 86 | }; 87 | const resizerHoverCss = { 88 | height: '10px', 89 | marginTop: '-10px', 90 | backgroundImage: 91 | 'radial-gradient(at center center,rgba(0,0,0,0.2) 0%,transparent 70%,transparent 100%)', 92 | backgroundSize: '100% 50px', 93 | backgroundPosition: '50% 0', 94 | backgroundRepeat: 'no-repeat', 95 | borderRight: '1px solid rgba(0, 0, 0, 0.1)', 96 | }; 97 | const minSizes = [50, 50, 50, 50]; 98 | return ( 99 | 117 |
This is a div
118 |
This is a second div
119 |
This is a third div
120 |
This is a fourth div
121 |
122 | ); 123 | }); 124 | -------------------------------------------------------------------------------- /src/components/Pane/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { useMergeClasses } from '../../hooks/useMergeClasses'; 3 | import { useEffect, useMemo, useRef, useState } from 'react'; 4 | import styled, { css } from 'styled-components'; 5 | import { SplitType } from '../SplitPane'; 6 | 7 | const DEFAULT_COLLAPSE_TRANSITION_TIMEOUT = 500; 8 | const verticalCss = css` 9 | width: 0; 10 | height: 100%; 11 | `; 12 | const horizontalCss = css` 13 | width: 100%; 14 | height: 0; 15 | `; 16 | const coverCss = css` 17 | position: absolute; 18 | top: 0; 19 | bottom: 0; 20 | left: 0; 21 | right: 0; 22 | `; 23 | 24 | interface PaneRootProps { 25 | $isVertical: boolean; 26 | $shouldAnimate: boolean; 27 | $timeout: number; 28 | } 29 | const PaneRoot = styled.div` 30 | position: relative; 31 | outline: none; 32 | border: 0; 33 | overflow: hidden; 34 | display: flex; 35 | flex-grow: 1; 36 | flex-shrink: 1; 37 | ${props => (props.$isVertical ? verticalCss : horizontalCss)} 38 | ${props => props.$shouldAnimate && `transition: flex-basis ${props.$timeout}ms ease-in-out`} 39 | `; 40 | const WidthPreserver = styled.div<{ $isCollapsed: boolean }>` 41 | ${coverCss} 42 | ${props => 43 | props.$isCollapsed && 44 | css` 45 | * { 46 | z-index: 0; 47 | } 48 | z-index: 0; 49 | `} 50 | `; 51 | 52 | const CollapseOverlay = styled.div<{ $timeout: number; $isCollapsed: boolean }>` 53 | ${props => props.$isCollapsed && coverCss} 54 | ${props => 55 | props.$isCollapsed && 56 | css` 57 | z-index: 1; 58 | `}; 59 | opacity: ${props => (props.$isCollapsed ? 1 : 0)}; 60 | transition: opacity ${props => props.$timeout}ms ease-in-out; 61 | `; 62 | 63 | export interface PaneProps { 64 | size: number; 65 | minSize: number; 66 | isVertical: boolean; 67 | split: SplitType; 68 | className?: string; 69 | isCollapsed: boolean; 70 | forwardRef: React.Ref; 71 | collapseOverlayCss?: React.CSSProperties; 72 | collapsedIndices: number[]; 73 | children: React.ReactNode; 74 | transitionTimeout: number | undefined; 75 | } 76 | const UnMemoizedPane = ({ 77 | size, 78 | minSize, 79 | isCollapsed, 80 | collapseOverlayCss = { background: 'rgba(220,220,220, 0.1)' }, 81 | isVertical, 82 | split, 83 | className, 84 | children, 85 | forwardRef, 86 | collapsedIndices, 87 | transitionTimeout, 88 | }: PaneProps) => { 89 | const classes = useMergeClasses(['Pane', split, className]); 90 | const timeout = useMemo(() => transitionTimeout ?? DEFAULT_COLLAPSE_TRANSITION_TIMEOUT, [ 91 | transitionTimeout, 92 | ]); 93 | const [shouldAnimate, setShouldAnimate] = useState(false); 94 | 95 | const didMount = useRef(false); 96 | 97 | useEffect(() => { 98 | if (didMount.current) { 99 | if (timeout !== 0) { 100 | setShouldAnimate(true); 101 | setTimeout(() => setShouldAnimate(false), 500); 102 | } 103 | } else { 104 | didMount.current = true; 105 | } 106 | }, [setShouldAnimate, collapsedIndices, timeout]); 107 | 108 | const minStyle = useMemo(() => (isVertical ? { minWidth: minSize } : { minHeight: minSize }), [ 109 | minSize, 110 | isVertical, 111 | ]); 112 | const widthPreserverStyle: React.CSSProperties = isCollapsed 113 | ? { ...minStyle, userSelect: 'none' } 114 | : minStyle; 115 | return ( 116 | 124 | 125 | 126 | {children} 127 | 128 | 129 | ); 130 | }; 131 | 132 | UnMemoizedPane.displayName = 'Pane'; 133 | export const Pane = React.memo(UnMemoizedPane); 134 | -------------------------------------------------------------------------------- /src/components/SplitPane/hooks/effects/useDragState.ts: -------------------------------------------------------------------------------- 1 | import * as ReactDOM from 'react-dom'; 2 | import { useEventListener } from '../../../../hooks/useEventListener'; 3 | import { useCallback, useMemo, useState } from 'react'; 4 | 5 | export interface ClientPosition { 6 | clientX: number; 7 | clientY: number; 8 | } 9 | 10 | export interface DragState { 11 | offset: number; 12 | index: number; 13 | } 14 | export type BeginDragCallback = (props: { position: ClientPosition; index: number }) => void; 15 | interface DragStateHandlers { 16 | beginDrag: BeginDragCallback; 17 | dragState: DragState | null; 18 | onMouseMove?: (event: ClientPosition) => void; 19 | onTouchMove?: (event: TouchEvent) => void; 20 | onMouseUp?: () => void; 21 | onMouseEnter?: (event: MouseEvent) => void; 22 | } 23 | 24 | const useDragStateHandlers = ( 25 | isVertical: boolean, 26 | onDragFinished: (dragState: DragState) => void 27 | ): DragStateHandlers => { 28 | const [isDragging, setIsDragging] = useState(false); 29 | const [dragStartPos, setDragStartPos] = useState(null); 30 | const [currentPos, setCurrentPos] = useState(null); 31 | const [draggingIndex, setDraggingIndex] = useState(null); 32 | 33 | const beginDrag: BeginDragCallback = useCallback( 34 | ({ position, index }: { position: ClientPosition; index: number }): void => { 35 | const pos = isVertical ? position.clientX : position.clientY; 36 | ReactDOM.unstable_batchedUpdates(() => { 37 | setDraggingIndex(index); 38 | setIsDragging(true); 39 | setDragStartPos(pos); 40 | setCurrentPos(pos); 41 | }); 42 | }, 43 | [isVertical] 44 | ); 45 | 46 | const dragState: DragState | null = useMemo(() => { 47 | if (isDragging && currentPos !== null && dragStartPos !== null && draggingIndex !== null) { 48 | const offset = currentPos - dragStartPos; 49 | return { offset, index: draggingIndex }; 50 | } else return null; 51 | }, [currentPos, dragStartPos, draggingIndex, isDragging]); 52 | 53 | const onMouseUp = useCallback((): void => { 54 | if (isDragging && dragState) { 55 | ReactDOM.unstable_batchedUpdates(() => { 56 | setIsDragging(false); 57 | onDragFinished(dragState); 58 | }); 59 | } 60 | }, [isDragging, dragState, onDragFinished]); 61 | 62 | const onMouseMove = useCallback( 63 | (event: ClientPosition): void => { 64 | if (isDragging) { 65 | const pos = isVertical ? event.clientX : event.clientY; 66 | setCurrentPos(pos); 67 | } else setCurrentPos(null); 68 | }, 69 | [isDragging, isVertical] 70 | ); 71 | 72 | const onTouchMove = useCallback( 73 | (event: TouchEvent): void => { 74 | if (isDragging) { 75 | onMouseMove(event.touches[0]); 76 | } 77 | }, 78 | [isDragging, onMouseMove] 79 | ); 80 | const onMouseEnter = useCallback( 81 | (event: MouseEvent): void => { 82 | if (isDragging) { 83 | const isPrimaryPressed = (event.buttons & 1) === 1; 84 | if (!isPrimaryPressed) { 85 | onMouseUp(); 86 | } 87 | } 88 | }, 89 | [isDragging, onMouseUp] 90 | ); 91 | 92 | return { beginDrag, dragState, onMouseMove, onTouchMove, onMouseUp, onMouseEnter }; 93 | }; 94 | 95 | interface UseDragStateReturn { 96 | dragState: DragState | null; 97 | beginDrag: BeginDragCallback; 98 | } 99 | export const useDragState = ( 100 | isVertical: boolean, 101 | onDragFinished: (dragState: DragState) => void 102 | ): UseDragStateReturn => { 103 | const { 104 | beginDrag, 105 | dragState, 106 | onMouseMove, 107 | onTouchMove, 108 | onMouseUp, 109 | onMouseEnter, 110 | } = useDragStateHandlers(isVertical, onDragFinished); 111 | 112 | useEventListener('mousemove', onMouseMove); 113 | useEventListener('touchmove', onTouchMove); 114 | useEventListener('mouseup', onMouseUp); 115 | useEventListener('mouseenter', onMouseEnter); 116 | 117 | return { dragState, beginDrag }; 118 | }; 119 | -------------------------------------------------------------------------------- /src/components/SplitPane/helpers.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled, { css } from 'styled-components'; 3 | import { SplitType } from '.'; 4 | import { Nullable } from '../../types/utilities'; 5 | 6 | export const DEFAULT_MIN_SIZE = 50; 7 | 8 | export const getMinSize = (index: number, minSizes?: number | number[]): number => { 9 | if (typeof minSizes === 'number') { 10 | if (minSizes > 0) { 11 | return minSizes; 12 | } 13 | } else if (minSizes instanceof Array) { 14 | const value = minSizes[index]; 15 | if (value > 0) { 16 | return value; 17 | } 18 | } 19 | return DEFAULT_MIN_SIZE; 20 | }; 21 | 22 | export const getRefSize = ({ 23 | ref, 24 | split, 25 | }: { 26 | split: SplitType; 27 | ref: React.RefObject; 28 | }) => { 29 | const sizeAttr = split === 'vertical' ? 'width' : 'height'; 30 | return ref.current?.getBoundingClientRect()[sizeAttr] ?? 0; 31 | }; 32 | 33 | export type MoveDetails = { 34 | sizes: number[]; 35 | index: number; 36 | offset: number; 37 | minSizes: number[]; 38 | collapsedIndices: number[]; 39 | collapsedSize: number; 40 | }; 41 | /** 42 | * Mutates the original array in a recursive fashion, identifying the current sizes, whether they need to be changed, and whether they need to push the next or previous pane. 43 | */ 44 | export const moveSizes = ({ 45 | index, 46 | minSizes, 47 | offset, 48 | sizes, 49 | collapsedIndices, 50 | collapsedSize, 51 | }: MoveDetails): number => { 52 | //recursion break points 53 | if (!offset || index < 0 || index + 1 >= sizes.length) { 54 | return 0; 55 | } 56 | const isCollapsed = (idx: number) => collapsedIndices.includes(idx); 57 | const firstMinSize = isCollapsed(index) ? collapsedSize : getMinSize(index, minSizes); 58 | const secondMinSize = isCollapsed(index + 1) ? collapsedSize : getMinSize(index + 1, minSizes); 59 | const firstSize = sizes[index] + offset; 60 | const secondSize = sizes[index + 1] - offset; 61 | 62 | if (offset < 0 && firstSize < firstMinSize) { 63 | const missing = firstSize - firstMinSize; 64 | const pushedOffset = moveSizes({ 65 | sizes, 66 | index: index - 1, 67 | offset: missing, 68 | minSizes, 69 | collapsedIndices, 70 | collapsedSize, 71 | }); 72 | 73 | offset -= missing - pushedOffset; 74 | } else if (offset > 0 && secondSize < secondMinSize) { 75 | const missing = secondMinSize - secondSize; 76 | const pushedOffset = moveSizes({ 77 | sizes, 78 | index: index + 1, 79 | offset: missing, 80 | minSizes, 81 | collapsedIndices, 82 | collapsedSize, 83 | }); 84 | 85 | offset -= missing - pushedOffset; 86 | } 87 | sizes[index] += offset; 88 | sizes[index + 1] -= offset; 89 | 90 | return offset; 91 | }; 92 | 93 | interface MoveCollapsedDetails { 94 | offset: number; 95 | isReversed: boolean; 96 | index: number; 97 | sizes: number[]; 98 | collapsedIndices: number[]; 99 | minSizes: number[]; 100 | collapsedSize: number; 101 | } 102 | /** 103 | * This is only used when a collapse action is invoked. It's meant to move any collapsed siblings along with the move. 104 | */ 105 | export const moveCollapsedSiblings = ({ 106 | offset, 107 | isReversed, 108 | collapsedIndices, 109 | minSizes, 110 | sizes, 111 | index, 112 | collapsedSize, 113 | }: MoveCollapsedDetails) => { 114 | if (isReversed ? offset > 0 : offset < 0) { 115 | for ( 116 | let i = isReversed ? index : index + 1; 117 | isReversed ? i > 0 : i < sizes.length - 1; 118 | isReversed ? i-- : i++ 119 | ) { 120 | if (collapsedIndices.includes(i)) { 121 | moveSizes({ 122 | sizes, 123 | index: isReversed ? i - 1 : i, 124 | offset, 125 | minSizes, 126 | collapsedIndices, 127 | collapsedSize, 128 | }); 129 | } 130 | } 131 | } 132 | }; 133 | 134 | const verticalCss = css` 135 | left: 0; 136 | right: 0; 137 | flex-direction: row; 138 | `; 139 | const horizontalCss = css` 140 | bottom: 0; 141 | top: 0; 142 | flex-direction: column; 143 | min-height: 100%; 144 | width: 100%; 145 | `; 146 | export const Wrapper = styled.div<{ split: SplitType }>` 147 | display: flex; 148 | flex: 1; 149 | height: 100%; 150 | position: absolute; 151 | outline: none; 152 | overflow: hidden; 153 | ${props => (props.split === 'vertical' ? verticalCss : horizontalCss)} 154 | `; 155 | 156 | /** 157 | * Infers the indices of the collapsed panels from an array of nullable collapse sizes. If the index is null then it's not collapsed. 158 | */ 159 | export const convertCollapseSizesToIndices = (sizes?: Nullable[]) => 160 | sizes?.reduce((prev, cur, idx) => (cur !== null ? [...prev, idx] : [...prev]), [] as number[]) ?? 161 | []; 162 | 163 | export const addArray = (arr: number[]) => arr.reduce((prev, cur) => prev + cur, 0); 164 | 165 | /** 166 | * Returns a debounced version of a function. Similar to lodash's _.debounce 167 | * @param func the function to be debounced 168 | * @param waitFor the amount of time that must elapse before the debounce expires and the callback is called. 169 | */ 170 | export const debounce = any>(func: F, waitFor: number) => { 171 | let timeout: ReturnType | null = null; 172 | 173 | const debounced = (...args: Parameters) => { 174 | if (timeout !== null) { 175 | clearTimeout(timeout); 176 | timeout = null; 177 | } 178 | timeout = setTimeout(() => func(...args), waitFor); 179 | }; 180 | 181 | return debounced as (...args: Parameters) => ReturnType; 182 | }; 183 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Thanks! 2 | 3 | It's great that you want to contribute to this library and make things better. 4 | 5 | Below you'll find a general outline of how to use TSDX, which handles the boilerplate around managing a react component lib. 6 | 7 | # Updating Documentation 8 | 9 | All documentation exists in the `./docs` subfolder and is powered by [docsify](https://docsify.js.org/#/quickstart). 10 | 11 | Currently there is only one `README.md` file in the `docs` folder, but this can be expanded into more files if necessary (unlikely). 12 | 13 | To run the documentation site install docsify globally: 14 | ```sh 15 | npm i docsify-cli -g 16 | ``` 17 | 18 | Then you can run the following command and open the URL locally for the documentation site. 19 | ```sh 20 | docsify serve docs 21 | ``` 22 | 23 | 24 | # Developer User Guide 25 | 26 | Let’s get you oriented with what’s here and how to use it. 27 | 28 | > If you’re new to TypeScript and React, checkout [this handy cheatsheet](https://github.com/sw-yx/react-typescript-cheatsheet/) 29 | 30 | ## Commands 31 | 32 | [TSDX](https://github.com/jaredpalmer/tsdx) scaffolds the library inside `/src`, and also sets up a [Parcel-based](https://parceljs.org) playground for it inside `/example`. 33 | 34 | The recommended workflow is to run [TSDX](https://github.com/jaredpalmer/tsdx) in one terminal: 35 | 36 | ``` 37 | npm start # or yarn start 38 | ``` 39 | 40 | This builds to `/dist` and runs the project in watch mode so any edits you save inside `src` causes a rebuild to `/dist`. 41 | 42 | Then run either example playground or storybook: 43 | 44 | ### [Storybook](https://storybook.js.org/) 45 | 46 | Run inside another terminal: 47 | 48 | ``` 49 | npm run storybook 50 | ``` 51 | 52 | This loads the stories from `./stories`. 53 | 54 | ### Example 55 | 56 | Then run the example inside another: 57 | 58 | ``` 59 | cd example 60 | npm i 61 | npm start 62 | ``` 63 | 64 | The default example imports and live reloads whatever is in `/dist`, so if you are seeing an out of date component, make sure TSDX is running in watch mode like we recommend above. **No symlinking required**, [we use Parcel's aliasing](https://github.com/palmerhq/tsdx/pull/88/files). 65 | 66 | To do a one-off build, use `npm run build. 67 | 68 | To run tests, use `npm test`. 69 | 70 | ## Configuration 71 | 72 | Static code checking is [set up ](https://github.com/palmerhq/tsdx/pull/45/files) with `prettier`, `eslint`, `husky`, and `lint-staged`. Adjust the respective fields in `package.json` accordingly. 73 | 74 | ### [Jest](https://jestjs.io/) Testing 75 | 76 | Jest tests are set up to run with `npm test`. This runs the test watcher (Jest) in an interactive mode. By default, runs tests related to files changed since the last commit. 77 | 78 | ### Setup Files 79 | 80 | This is the folder structure: 81 | 82 | ``` 83 | /example 84 | index.html 85 | index.tsx # test the component here in a demo app 86 | package.json 87 | tsconfig.json 88 | /src 89 | components/ # the source files for the SplitPane and supporting components 90 | hooks/ # hooks used across components 91 | types/ # global and other general type definitions 92 | index.tsx # the export point for the SplitPane component 93 | /test 94 | splitpane.test.tsx # High level tests for the exported copmonent 95 | .gitignore 96 | package.json 97 | README.md 98 | tsconfig.json 99 | ``` 100 | 101 | ### Rollup 102 | 103 | TSDX uses [Rollup v1.x](https://rollupjs.org) as a bundler and generates multiple rollup configs for various module formats and build settings. See [Optimizations](#optimizations) for details. 104 | 105 | ### TypeScript 106 | 107 | `tsconfig.json` is set up to interpret `dom` and `esnext` types, as well as `react` for `jsx`. Adjust according to your needs. 108 | 109 | ## Continuous Integration 110 | 111 | ### Github Actions 112 | A github actions workflow is set up to automatically release changes based on [semantic-release](https://github.com/semantic-release/semantic-release). This is only something to note, but releases and publishes to npm are fully automated based on commmit messages. 113 | 114 | ### Conventional Commits 115 | 116 | This repo follows the [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/#summary) specification. 117 | 118 | Please use it in your commit messages. 119 | 120 | 121 | ## Optimizations 122 | 123 | Please see the main `tsdx` [optimizations docs](https://github.com/palmerhq/tsdx#optimizations). In particular, know that you can take advantage of development-only optimizations. 124 | 125 | You can also choose to install and use [invariant](https://github.com/palmerhq/tsdx#invariant) and [warning](https://github.com/palmerhq/tsdx#warning) functions. 126 | 127 | ## Module Formats 128 | 129 | CJS, ESModules, and UMD module formats are supported. 130 | 131 | The appropriate paths are configured in `package.json` and `dist/index.js` accordingly. Please report if any issues are found. 132 | 133 | ## Using the Playground 134 | 135 | ``` 136 | cd example 137 | npm i # or yarn to install dependencies 138 | npm start # or yarn start 139 | ``` 140 | 141 | The default example imports and live reloads whatever is in `/dist`, so if you are seeing an out of date component, make sure TSDX is running in watch mode like we recommend above. **No symlinking required**! 142 | 143 | ## Named Exports 144 | 145 | [always use named exports.](https://github.com/palmerhq/typescript#exports) Code split inside your React app instead of your React library. 146 | 147 | ## Including Styles 148 | 149 | For styling the components we're using a mixture of react's inline styles (for rapidly changing styles) and [styled-components](https://styled-components.com/) -------------------------------------------------------------------------------- /src/components/SplitPane/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Pane } from '../Pane'; 3 | import { Resizer } from '../Resizer'; 4 | import { useSplitPaneResize } from './hooks/useSplitPaneResize'; 5 | import { convertCollapseSizesToIndices, getMinSize, Wrapper } from './helpers'; 6 | import { useMergeClasses } from '../../hooks/useMergeClasses'; 7 | import { useIsCollapseReversed } from './hooks/memos/useIsCollapseReversed'; 8 | import { useToggleCollapse } from './hooks/callbacks/useToggleCollapse'; 9 | import { useGetIsPaneCollapsed } from './hooks/callbacks/useGetIsCollapsed'; 10 | import { useIsLtr } from './hooks/memos/useIsLtr'; 11 | import { useCollapsedSizes } from './hooks/memos/useCollapsedSizes'; 12 | import { Nullable } from '../../types/utilities'; 13 | import { useCollapseOptions } from './hooks/memos/useCollapseOptions'; 14 | 15 | // String Unions 16 | export type SplitType = 'horizontal' | 'vertical'; 17 | export type Direction = 'ltr' | 'rtl'; 18 | export type TransitionType = 'fade' | 'grow' | 'zoom' | 'none'; 19 | export type CollapseDirection = 'left' | 'right' | 'up' | 'down'; 20 | 21 | export type SplitPaneHooks = { 22 | onDragStarted?: () => void; 23 | onChange?: (sizes: number[]) => void; 24 | onSaveSizes?: (sizes: number[]) => void; 25 | onCollapse?: (collapsedSizes: Nullable[]) => void; 26 | }; 27 | export interface CollapseOptions { 28 | beforeToggleButton: React.ReactElement; 29 | afterToggleButton: React.ReactElement; 30 | buttonTransition: TransitionType; 31 | buttonTransitionTimeout: number; 32 | buttonPositionOffset: number; 33 | collapseDirection: CollapseDirection; 34 | collapseTransitionTimeout: number; 35 | collapsedSize: number; 36 | overlayCss: React.CSSProperties; 37 | } 38 | export interface ResizerOptions { 39 | css?: React.CSSProperties; 40 | hoverCss?: React.CSSProperties; 41 | grabberSize?: number | string; 42 | } 43 | 44 | export interface SplitPaneProps { 45 | split: SplitType; 46 | collapse?: boolean | Partial; 47 | 48 | dir?: Direction; 49 | className?: string; 50 | 51 | initialSizes?: number[]; 52 | minSizes?: number | number[]; 53 | collapsedSizes?: Nullable[]; 54 | 55 | hooks?: SplitPaneHooks; 56 | resizerOptions?: ResizerOptions; 57 | 58 | children: React.ReactChild[]; 59 | } 60 | 61 | export const SplitPane: React.FC = props => { 62 | const collapsedSizes = useCollapsedSizes(props); 63 | const isLtr = useIsLtr(props); 64 | const isVertical = props.split === 'vertical'; 65 | const isReversed = useIsCollapseReversed(props.collapse); 66 | 67 | const collapseOptions = useCollapseOptions({ 68 | isVertical, 69 | isLtr, 70 | originalValue: props.collapse, 71 | isReversed, 72 | }); 73 | 74 | const [collapsedIndices, setCollapsed] = useState( 75 | convertCollapseSizesToIndices(collapsedSizes) 76 | ); 77 | 78 | const { childPanes, handleDragStart, resizingIndex } = useSplitPaneResize({ 79 | ...props, 80 | isLtr, 81 | isVertical, 82 | collapsedIndices, 83 | collapsedSizes, 84 | collapseOptions, 85 | }); 86 | 87 | const splitPaneClass = useMergeClasses(['SplitPane', props.split, props.className]); 88 | const resizingClass = useMergeClasses(['Resizing', props.className]); 89 | 90 | const toggleCollapse = useToggleCollapse({ setCollapsed, collapsedIndices }); 91 | const getIsPaneCollapsed = useGetIsPaneCollapsed({ collapsedIndices }); 92 | 93 | if (childPanes.length <= 1) { 94 | if (process.env.NODE_ENV !== 'production') { 95 | console.error( 96 | '[react-collapse-pane] - You must have more than one non-null child inside the SplitPane component. Even though SplitPane does not crash, you should resolve this error.' 97 | ); 98 | } 99 | return <>{props.children}; 100 | } 101 | 102 | // stacks the children and places a resizer in between each of them. Each resizer has the same index as the pane that it controls. 103 | const entries = childPanes.map((pane, paneIndex) => { 104 | const resizerPaneIndex = isReversed ? paneIndex : paneIndex - 1; 105 | return ( 106 | 107 | {paneIndex - 1 >= 0 ? ( 108 | 121 | ) : null} 122 | 135 | {pane.node} 136 | 137 | 138 | ); 139 | }); 140 | 141 | return ( 142 | 143 | {entries} 144 | 145 | ); 146 | }; 147 | SplitPane.displayName = 'SplitPane'; 148 | -------------------------------------------------------------------------------- /src/components/Resizer/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useMemo, useState } from 'react'; 2 | import { Fade } from '@mui/material'; 3 | import { BeginDragCallback } from '../SplitPane/hooks/effects/useDragState'; 4 | import { 5 | ButtonContainer, 6 | ButtonWrapper, 7 | getSizeWithUnit, 8 | ResizeGrabber, 9 | ResizePresentation, 10 | } from './helpers'; 11 | import { useMergeClasses } from '../../hooks/useMergeClasses'; 12 | import { CollapseOptions, ResizerOptions } from '../SplitPane'; 13 | import { useTransition } from './hooks/useTransition'; 14 | import { SplitType } from '../SplitPane'; 15 | import { debounce } from '../SplitPane/helpers'; 16 | 17 | const defaultResizerOptions: Required = { 18 | grabberSize: '1rem', 19 | css: { backgroundColor: 'rgba(120, 120, 120, 0.3)' }, 20 | hoverCss: { backgroundColor: 'rgba(120, 120, 120, 0.6)' }, 21 | }; 22 | 23 | export interface ResizerProps { 24 | isVertical: boolean; 25 | isLtr: boolean; 26 | split: SplitType; 27 | className?: string; 28 | paneIndex: number; 29 | collapseOptions?: CollapseOptions; 30 | resizerOptions?: Partial; 31 | onDragStarted: BeginDragCallback; 32 | onCollapseToggle: (paneIndex: number) => void; 33 | isCollapsed: boolean; 34 | } 35 | export const Resizer = ({ 36 | isVertical, 37 | split, 38 | className, 39 | paneIndex, 40 | onDragStarted, 41 | resizerOptions, 42 | collapseOptions, 43 | onCollapseToggle, 44 | isLtr, 45 | isCollapsed, 46 | }: ResizerProps) => { 47 | const { grabberSize, css, hoverCss } = { ...defaultResizerOptions, ...resizerOptions }; 48 | 49 | const classes = useMergeClasses(['Resizer', split, className]); 50 | const grabberSizeWithUnit = useMemo(() => getSizeWithUnit(grabberSize), [grabberSize]); 51 | const Transition = useTransition(collapseOptions); 52 | 53 | const [isHovered, setIsHovered] = useState(false); 54 | 55 | const handleMouseDown = useCallback( 56 | (event: React.MouseEvent) => { 57 | event.preventDefault(); 58 | if (!isCollapsed) { 59 | onDragStarted({ index: paneIndex, position: event }); 60 | } 61 | }, 62 | [paneIndex, isCollapsed, onDragStarted] 63 | ); 64 | const handleTouchStart = useCallback( 65 | (event: React.TouchEvent) => { 66 | event.preventDefault(); 67 | if (!isCollapsed) { 68 | onDragStarted({ index: paneIndex, position: event.touches[0] }); 69 | } 70 | }, 71 | [paneIndex, isCollapsed, onDragStarted] 72 | ); 73 | const handleButtonClick = useCallback( 74 | (event: React.MouseEvent) => { 75 | event.stopPropagation(); 76 | onCollapseToggle(paneIndex); 77 | }, 78 | [paneIndex, onCollapseToggle] 79 | ); 80 | const handleButtonMousedown = useCallback((event: React.MouseEvent) => { 81 | event.stopPropagation(); 82 | }, []); 83 | 84 | const debouncedSetHovered = useCallback( 85 | debounce(() => setIsHovered(true), 50), 86 | [setIsHovered] 87 | ); 88 | const handleMouseEnterGrabber = useCallback(() => { 89 | debouncedSetHovered(); 90 | }, [debouncedSetHovered]); 91 | 92 | const debouncedSetNotHovered = useCallback( 93 | debounce(() => setIsHovered(false), 100), 94 | [setIsHovered] 95 | ); 96 | const handleMouseLeaveGrabber = useCallback(() => debouncedSetNotHovered(), [ 97 | debouncedSetNotHovered, 98 | ]); 99 | 100 | const getWidthOrHeight = useCallback( 101 | (size: string | number) => (isVertical ? { width: size } : { height: size }), 102 | [isVertical] 103 | ); 104 | const preButtonFlex = useMemo( 105 | () => Math.max(100 - (collapseOptions?.buttonPositionOffset ?? 0), 0), 106 | [collapseOptions] 107 | ); 108 | const postButtonFlex = useMemo( 109 | () => Math.max(100 + (collapseOptions?.buttonPositionOffset ?? 0), 0), 110 | [collapseOptions] 111 | ); 112 | const isTransition = collapseOptions?.buttonTransition !== 'none'; 113 | const collapseButton = collapseOptions ? ( 114 | 115 |
116 | 121 | 126 | {isCollapsed ? collapseOptions.afterToggleButton : collapseOptions.beforeToggleButton} 127 | 128 | 129 |
130 | 131 | ) : null; 132 | 133 | return ( 134 |
135 | 148 | {collapseButton} 149 | 150 | 151 | 152 | 153 | 154 | 158 | 159 |
160 | ); 161 | }; 162 | Resizer.displayName = 'Resizer'; 163 | -------------------------------------------------------------------------------- /src/components/SplitPane/hooks/useSplitPaneResize.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; 3 | import { SplitPaneProps, CollapseOptions } from '..'; 4 | import { useDragState, BeginDragCallback } from './effects/useDragState'; 5 | import { useMinSizes } from './memos/useMinSizes'; 6 | import { useGetMovedSizes } from './callbacks/useGetMovedSizes'; 7 | import { useIsCollapseReversed } from './memos/useIsCollapseReversed'; 8 | import { useHandleDragFinished } from './callbacks/useHandleDragFinished'; 9 | import { useHandleDragStart } from './callbacks/useHandleDragStart'; 10 | import { useChildPanes } from './memos/useChildPanes'; 11 | import { useGetCurrentPaneSizes } from './callbacks/useGetCurrentPaneSizes'; 12 | import { useCollapseSize } from './callbacks/useCollapseSize'; 13 | import { useUncollapseSize } from './callbacks/useUncollapseSize'; 14 | import { useUpdateCollapsedSizes } from './callbacks/useUpdateCollapsedSizes'; 15 | import { useCollapsedSize } from './memos/useCollapsedSize'; 16 | import { debounce } from '../helpers'; 17 | import { useRecalculateSizes } from './callbacks/useRecalculateSizes'; 18 | import { useEventListener } from '../../../hooks/useEventListener'; 19 | import { Nullable } from '../../../types/utilities'; 20 | 21 | export interface ChildPane { 22 | node: React.ReactChild; 23 | ref: React.RefObject; 24 | key: string; 25 | size: number; 26 | } 27 | interface SplitPaneResizeReturns { 28 | childPanes: ChildPane[]; 29 | resizingIndex: Nullable; 30 | handleDragStart: BeginDragCallback; 31 | } 32 | 33 | interface SplitPaneResizeOptions 34 | extends Pick { 35 | collapsedIndices: number[]; 36 | isLtr: boolean; 37 | collapseOptions?: CollapseOptions; 38 | children: React.ReactChild[]; 39 | isVertical: boolean; 40 | } 41 | 42 | /** 43 | * Manages the dragging, size calculation, collapse calculation, and general state management of the panes. It propogates the results of its complex calculations into the `childPanes` which are used by the rest of the "dumb" react components that just take all of them and render them 44 | */ 45 | export const useSplitPaneResize = (options: SplitPaneResizeOptions): SplitPaneResizeReturns => { 46 | const { 47 | children: originalChildren, 48 | split, 49 | initialSizes: originalDefaults, 50 | minSizes: originalMinSizes, 51 | hooks, 52 | collapsedIndices, 53 | collapsedSizes: originalCollapsedSizes, 54 | collapseOptions, 55 | isVertical, 56 | isLtr, 57 | } = options; 58 | 59 | const children = !Array.isArray(originalChildren) ? [originalChildren] : originalChildren; 60 | // VALUES: const values used throughout the different logic 61 | const paneRefs = useRef(new Map>()); 62 | 63 | const minSizes = useMinSizes({ 64 | minSizes: originalMinSizes, 65 | numSizes: children.length, 66 | collapseOptions, 67 | collapsedIndices, 68 | }); 69 | const collapsedSize = useCollapsedSize({ collapseOptions }); 70 | const childPanes = useChildPanes({ minSizes, children, paneRefs }); 71 | const isReversed = useIsCollapseReversed(collapseOptions); 72 | const initialSizes = useMemo(() => children.map((_c, idx) => originalDefaults?.[idx] ?? 1), [ 73 | children, 74 | originalDefaults, 75 | ]); 76 | 77 | // STATE: a map keeping track of all of the pane sizes 78 | const [sizes, setSizes] = useState(initialSizes); 79 | const [movedSizes, setMovedSizes] = useState(sizes); 80 | const [collapsedSizes, setCollapsedSizes] = useState[]>( 81 | originalCollapsedSizes ?? new Array(children.length).fill(null) 82 | ); 83 | // CALLBACKS callback functions used throughout. all functions are memoized by useCallback 84 | const getMovedSizes = useGetMovedSizes({ 85 | minSizes, 86 | sizes, 87 | isLtr, 88 | collapsedSize, 89 | collapsedIndices, 90 | isReversed, 91 | }); 92 | const getCurrentPaneSizes = useGetCurrentPaneSizes({ childPanes, split }); 93 | const handleDragFinished = useHandleDragFinished({ movedSizes, children, hooks, setSizes }); 94 | const recalculateSizes = useRecalculateSizes({ 95 | setMovedSizes, 96 | minSizes, 97 | collapsedIndices, 98 | collapsedSize, 99 | getCurrentPaneSizes, 100 | setSizes, 101 | originalMinSizes, 102 | }); 103 | 104 | // STATE: if dragging, contains which pane is dragging and what the offset is. If not dragging then null 105 | const { dragState, beginDrag } = useDragState(isVertical, handleDragFinished); 106 | 107 | const collapseSize = useCollapseSize({ 108 | setMovedSizes, 109 | setSizes, 110 | minSizes, 111 | movedSizes, 112 | isReversed, 113 | collapsedIndices, 114 | collapsedSize, 115 | }); 116 | const unCollapseSize = useUncollapseSize({ 117 | isReversed, 118 | movedSizes, 119 | minSizes, 120 | setMovedSizes, 121 | setSizes, 122 | collapsedSize, 123 | collapsedIndices, 124 | }); 125 | const updateCollapsedSizes = useUpdateCollapsedSizes({ 126 | sizes, 127 | collapsedSizes, 128 | setCollapsedSizes, 129 | movedSizes, 130 | collapseSize, 131 | unCollapseSize, 132 | hooks, 133 | }); 134 | 135 | // EFFECTS: manage updates and calculations based on dependency changes for states that are interacted with by multiple functions 136 | useEffect(() => { 137 | if (dragState !== null) setMovedSizes(getMovedSizes(dragState)); 138 | // eslint-disable-next-line react-hooks/exhaustive-deps 139 | }, [dragState]); 140 | useEffect(() => { 141 | if (dragState !== null) hooks?.onChange?.(movedSizes); 142 | }, [dragState, movedSizes, hooks]); 143 | useEffect(() => { 144 | hooks?.onCollapse?.(collapsedSizes); 145 | }, [collapsedSizes, hooks]); 146 | useEffect(() => { 147 | updateCollapsedSizes(collapsedIndices); 148 | // eslint-disable-next-line react-hooks/exhaustive-deps 149 | }, [collapsedIndices]); 150 | // recalculate initial sizes on window size change to maintain min sizes 151 | 152 | const resetSizes = useCallback( 153 | debounce(() => recalculateSizes(), 100), 154 | [recalculateSizes] 155 | ); 156 | useEventListener('resize', resetSizes); 157 | useEffect( 158 | () => recalculateSizes(initialSizes), 159 | // eslint-disable-next-line react-hooks/exhaustive-deps 160 | [] 161 | ); 162 | //populates the sizes of all the initially populated childPanes, adjust sizes based on collapsed state 163 | const childPanesWithSizes: ChildPane[] = useMemo( 164 | () => 165 | childPanes.map((child, index) => { 166 | return { ...child, size: movedSizes[index] }; 167 | }), 168 | [childPanes, movedSizes] 169 | ); 170 | 171 | const handleDragStart = useHandleDragStart({ isReversed, hooks, beginDrag }); 172 | return { 173 | childPanes: childPanesWithSizes, 174 | resizingIndex: dragState?.index ?? null, 175 | handleDragStart, 176 | }; 177 | }; 178 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-collapse-pane 2 | 3 | This is intended to be **the** simple, reliable, configurable, and elegant solution to having splittable, draggable and collapsible panes in your React application. 4 | 5 | logo 6 |

7 | 8 | prs welcome 9 | 10 | Release 11 | 12 | All Contributors 13 | 14 | 15 | styled with prettier 16 | 17 | 18 | semantic-release 19 | 20 | 21 | npm latest version 22 | 23 | 24 | npm next version 25 | 26 | 27 | npm downloads 28 | 29 | 30 | storybook 31 | 32 | 33 | storybook 34 | 35 | 36 |

37 | 38 | ## [[click for storybook demo]](https://storybook-collapse-pane.netlify.app/) 39 | ## [[click for documentation site]](https://b-zurg.github.io/react-collapse-pane/) 40 | 41 | # Getting Started :rocket: 42 | 43 | Install react-collapse-pane: 44 | ```bash 45 | npm i react-collapse-pane 46 | 47 | # or for yarn 48 | 49 | yarn add react-collapse-pane 50 | ``` 51 | 52 | Once installed you can import the `SplitPane` component in your code. 53 | 54 | ```ts 55 | import { SplitPane } from "react-collapse-pane"; 56 | ``` 57 | 58 | If you're using Typescript the `SplitPaneProps`, as well as a few other helper types type is also available. 59 | 60 | NOTE: Since the upgrade to MUI v5 you need to install a peer dependency style engine. Since there is a decision between styled components and emotion I did not make this an explicit dependency. 61 | 62 | If you want to simply use the default then follow the install guide here https://mui.com/material-ui/getting-started/installation/ 63 | If you want to use styled components then follow the configuration guide here https://mui.com/material-ui/guides/styled-engine/ 64 | 65 | In the future this dependency will be removed, apologies for the hassle while that gets sorted out. The next version will be much leaner. 66 | ```ts 67 | import { SplitPane, SplitPaneProps, ResizerOptions, CollapseOptions, SplitPaneHooks } from "react-collapse-pane"; 68 | ``` 69 | # Quick Start Usage :fire: 70 | 71 | The only component you must interact with is `SplitPane`. This serves as a wrapper for all of the children you wish to lay out. 72 | 73 | All you're required to give is a `split` prop which can be either `"horizontal"` or `"vertical"`. This identifies what the orientation of the split panel will be. 74 | 75 | ```tsx 76 | 77 |
This is the first div
78 |
This is the second div
79 |
This is the third div
80 | This is the fourth but not a div! 81 |
82 | ``` 83 | 84 | What you just did is make a split collapsible panel layout! 85 | 86 | ## Congrats! That was easy! :grin: 87 | 88 | This basically splits the children vertically (i.e. full-height split). The children can be any valid React child. If a child is `null` it will be excluded from being split or displayed. 89 | 90 | By default there is a 1px divider with a grabbable surface of 1rem width or height (depending on the split). If you hover over the divider a button will appear that you can use to collapse the panel. 91 | 92 | There is no limit to the number of elements you have as children. The `SplitPane` will split them all accordingly. 93 | 94 | ## But what about *styling* the resizer, the buttons, controlling the animations, or *RTL* support? :sob: 95 | 96 | This library supports all of these things and more! 97 | 98 | For more details check out [the documentation](https://b-zurg.github.io/react-collapse-pane/) 99 | 100 | # Documentation 101 | 102 | Documentation can be found at https://b-zurg.github.io/react-collapse-pane/ 103 | 104 | If you notice an issue then please make an issue or a PR! All docs are generated from the `docs` folder in the master branch. 105 | 106 | # Contributing and PRs :sparkling_heart: 107 | 108 | If you would like to contribute please check out the [contributor guide](/CONTRIBUTING.md) 109 | 110 | All contributions are welcome! All issues and feature requests are welcome! 111 | 112 | # Credit and Attribution :pray: 113 | 114 | This project did not start off from scratch. The foundation of the project was the excellently written [react-multi-split-pane](https://github.com/neoraider/react-multi-split-pane) library which is itself a typescript rewrite of the [react-split-pane](https://github.com/tomkp/react-split-pane) library. 115 | 116 | Much gratitude to their authors, [@NeoRaider](https://github.com/NeoRaider) and [@tomkp](https://github.com/tomkp) 117 | 118 | ## Contributors ✨ 119 | 120 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 |

Buzurg Arjmandi

⚠️ 📖 💻 🎨 💡 📦

hst44

🐛
131 | 132 | 133 | 134 | 135 | 136 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! 137 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # react-collapse-pane 2 | 3 | This is intended to be **the** simple, reliable, configurable, and elegant solution to having splittable, draggable and collapsible panes in your React application. 4 | 5 | # Getting Started 🚀 6 | 7 | Install react-collapse-pane: 8 | ```sh 9 | npm i react-collapse-pane 10 | 11 | # or for yarn 12 | 13 | yarn add react-collapse-pane 14 | 15 | ``` 16 | 17 | Once installed you can import the `SplitPane` component. 18 | 19 | ```ts 20 | import { SplitPane } from "react-collapse-pane"; 21 | ``` 22 | 23 | If you're using Typescript the `SplitPaneProps`, and a few other helper types are also available. 24 | ```ts 25 | import { SplitPane, SplitPaneProps, ResizerOptions, CollapseOptions, SplitPaneHooks } from "react-collapse-pane"; 26 | ``` 27 | # Usage 👓 28 | 29 | ## The Basics - Laying Things Out 📘 30 | 31 | The only component you use is `SplitPane`. This is a wrapper for the children you wish to lay out in a panel form. 32 | 33 | There is only 1 required prop, `split`, which can be either `"horizontal"` or `"vertical"`. This identifies what the orientation of the split panel will be. 34 | 35 | Here's a basic example: 36 | ```tsx 37 | 38 |
This is the first div
39 |
This is the second div
40 |
This is the third div
41 | This is the fourth but not a div! 42 |
43 | ``` 44 | 45 | ?> **Note** There is no limit to the number of divs you have as children. The `SplitPane` will split them all accordingly. 46 | 47 | !> The children can be any valid React child, but if a child is `null` it will be excluded from being split or displayed. 48 | 49 | !> You must have **MORE** than one non-null child in the split-pane otherwise it will log an error and simply render the single child. 50 | 51 | ## Styling the Resizer 💅 52 | 53 | By default there is a 1px divider with some basic CSS. 54 | 55 | This is replaceable with the `css` and `hoverCss` options. No need to worry about pseudo selectors, transitions, animations or anything. You just have to indicate what the divider should look like **before** and **after**. This is accomplished by having two separate divs, one of which fades out and the other which fades in. 56 | 57 | !> **Note!** The css props must be valid `React.CSSProperties` objects. 58 | 59 | The sizer also has a grabbable surface that spans the height (or length) of the split and has a default grabbable surface of `1rem`. This is changeable by the `grabberSize` option which can be set to any valid CSS size value for `width` or `height`. 60 | 61 | ?> As per default React CSS, a number will be interpreted as a `px` value. 62 | 63 | 64 | Here's an example: 65 | 66 | ```tsx 67 | 81 |
This is the first div
82 |
This is the second div
83 |
84 | ``` 85 | 86 | 87 | ## Using a Collapse Button 🤹‍♀️ 88 | 89 | !> This is the killer feature of this library :eyes: 90 | 91 | It's a common need to want to collapse the left or initial panel to give more room for another part of a site or app. This is easily accomplished by including several `CollapseOptions` as a prop to the `SplitPane`. 92 | 93 | * `beforeToggleButton` - the element displayed as the collapse button **before** the panel is collapsed. This is an purely aesthetic component. 94 | * `afterToggleButton` - the element displayed as the collapse button **after** the panel is collapsed. This is an purely aesthetic component. 95 | * `buttonTransition` - the animation applied to the button appear/disappear. Possible options are `zoom`, `grow`, `fade`, or `none`. You can try them out in the storybook. `none` indicates to keep the button always visible. 96 | * `buttonTransitionTimeout` - the time (in millisecons) that the animation for the appear/disappear of the button will take place 97 | * `buttonPositionOffset` - a positive or negative number that will either add or subtract the flex-basis (starting at 100) of an invisible div before or after the button. e.g. 50 would make the "before" 150 and the "after" 50 98 | * `collapseDirection` - `'left' | 'right' | 'up' | 'down'` - this is used to indicate the direction that it should collapse. By default collapsing happens left and up for the vertical and horizontal splits respectively. Valid values for a vertical split are `left` or `right` and valid values for a horizontal split are `up` or `down` 99 | * `collapseSize` - the size of the collapsed panel after it has been collapsed 100 | * `collapseTransitionTimeout` - the duration within the collapse animation will take place 101 | * `overlayCss` - the css applied to a div positioned on top of the content. The overlay div has an initial opacity of zero which transitions to 1 over the course of the collapse. 102 | 103 | Here's an example using a `Button` element imported from elsewhere. 104 | 105 | ```tsx 106 | ⬅, 110 | afterToggleButton: , 111 | overlayCss: { backgroundColor: "black" }, 112 | buttonTransition: "zoom", 113 | buttonPositionOffset: -20, 114 | collapsedSize: 50, 115 | collapseTransitionTimeout: 350, 116 | }} 117 | > 118 |
This is a div
119 |
This is a second div
120 |
This is a third div
121 |
This is a fourth div
122 |
123 | ``` 124 | 125 | ?> **Note!** When collapsing a panel, the `minSize` value is used to freeze the width of the collapsed panel to its minimum size and hides the rest of the content. This allows for a smooth collapse animation and is something to keep in mind. Until the animation reaches the min size it will shrink the panel as normal. Try it out for yourself! 126 | 127 | 128 | ## Hooks and Saving State ⚡ 129 | 130 | The component manages its own state while resizing however also allows an initial state as well as callbacks to save state changes. 131 | 132 | These callbacks are in the `hooks` prop: 133 | ```ts 134 | onDragStarted?: () => void; 135 | onChange?: (sizes: number[]) => void; 136 | onSaveSizes?: (sizes: number[]) => void; 137 | onCollapse?: (collapsedSizes: Nullable[]) => void; 138 | ``` 139 | * `onDragStarted` fires as soon as you click down on a resizer and begin moving 140 | * `onSaveSizes` fires when the movement of a resizer is finished and the mouse lifts **OR** when a panel is collapsed - as both of these trigger size changes. 141 | * `onChange` fires on every size change, which can be **quite** often 142 | * `onCollapse` fires whenever a panel is collapsed, and keeps track of the previously collapsed panes 143 | 144 | The initial state is passed in with these three props: 145 | 146 | ```ts 147 | initialSizes?: number[]; 148 | minSizes?: number | number[]; 149 | collapsedSizes?: Nullable[]; 150 | ``` 151 | * `initialSizes` is the default flex-basis that's given to the panes. This can be a simple ratio if it's the first time the render will happen and there's no saved sizes. e.g. `[1, 2, 1]` would make the second panel twice as big as its siblings. If you're saving state this should be the saved size value on a second render. 152 | * `minSizes` is either (1) a minimum size that's given to **all** the panes, or (2) an array of minimum sizes that's given to each pane in order. Any missing sizes in the array will be assumed default. 153 | * `collapsedSizes` an array of nullable numbers. This keeps track of a pane's size before it was collapsed. If not collapsed it's null. This will determine which panels are collapsed and what to do when they're uncollapsed. 154 | 155 | Typically if this is a controlled component you would have state variables for `initialSizes` and `collapsedSizes` and have callbacks on `onSaveSizes` and `onCollapse` that would save the two data points and pass them back into the `SplitPane` on a remount. The `minSizes` would typically never change. 156 | 157 | 158 | ## RTL Support ( Arabic, Hebrew, Farsi ) 🕋 159 | 160 | This library easily supports RTL languages by providing a `direction` prop. This is only necessary if you're using RTL. 161 | 162 | **Note!** 🚨 the `direction` is _only_ applicable if the split is `vertical` 163 | 164 | ```tsx 165 |
166 | 170 |
اللوحة الأولى
171 |
اللوحة الثانية
172 |
اللوحة الثالثة
173 |
اللوحة الرابعة
174 |
175 |
176 | ``` 177 | 178 | # Note about Polyfills 179 | * If you require IE11 support then you will need to add a pollyfill for `AbortController` 180 | * If you are using the ionic framework then you will need a polyfill for `resize-observer-polyfill` otherwise the pane size calculation will not work. -------------------------------------------------------------------------------- /logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | --------------------------------------------------------------------------------