From 7bf1189f94176dc9c444caca154781c5d51fb740 Mon Sep 17 00:00:00 2001 From: AssisrMatheus Date: Mon, 18 Sep 2023 15:04:38 -0400 Subject: [PATCH] feat: adds vertical resizeable panel --- CHANGELOG.md | 7 + package.json | 2 +- .../HorizontalResizeablePanel.stories.tsx | 12 +- .../HorizontalResizeablePanel/index.tsx | 23 ++- .../VerticalResizeablePanel.stories.tsx | 86 +++++++++ .../VerticalResizeablePanel/index.tsx | 176 ++++++++++++++++++ 6 files changed, 297 insertions(+), 9 deletions(-) create mode 100644 src/components/VerticalResizeablePanel/VerticalResizeablePanel.stories.tsx create mode 100644 src/components/VerticalResizeablePanel/index.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 800cd7b..3c25a9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +## [10.5.0] 2023-09-24 + +### Changes + +- Added `VerticalResizeablePanel` component +- Added `renderDragContent` property to `HorizontalResizeablePanel` that gives control over handle content when dragging + ## [10.4.0] 2023-04-24 ### Changes diff --git a/package.json b/package.json index e04241c..b552518 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@perimetre/ui", "description": "A component library made by @perimetre", - "version": "10.4.0", + "version": "10.5.0", "repository": { "type": "git", "url": "git+https://github.com/perimetre/ui.git" diff --git a/src/components/HorizontalResizeablePanel/HorizontalResizeablePanel.stories.tsx b/src/components/HorizontalResizeablePanel/HorizontalResizeablePanel.stories.tsx index 84f8419..49a6c8f 100644 --- a/src/components/HorizontalResizeablePanel/HorizontalResizeablePanel.stories.tsx +++ b/src/components/HorizontalResizeablePanel/HorizontalResizeablePanel.stories.tsx @@ -7,7 +7,7 @@ import { colorOptions, widthHeightOptions } from '../../prebuiltTailwindTheme'; import { backgroundColorClassnameMap, heightClassnameMap, widthClassnameMap } from '../../storybookMappers'; export default { - title: 'Components/HorizontalResizeablePanel', + title: 'Components/ResizeablePanel/Horizontal', component: HorizontalResizeablePanel, argTypes: { resizeRight: { @@ -74,3 +74,13 @@ const Template: Story = ({ width, height, backgroundColor, className, ...props } }; export const Default = Template.bind({}); + +export const RenderDragContent = Template.bind({}); +RenderDragContent.args = { + /** + * Renders content inside the drag area + */ + renderDragContent: () => ( +
+ ) +}; diff --git a/src/components/HorizontalResizeablePanel/index.tsx b/src/components/HorizontalResizeablePanel/index.tsx index e6bdbab..9016a82 100644 --- a/src/components/HorizontalResizeablePanel/index.tsx +++ b/src/components/HorizontalResizeablePanel/index.tsx @@ -1,7 +1,6 @@ import classNames from 'classnames'; import React, { useCallback, useEffect, useRef, useState } from 'react'; -// eslint-disable-next-line @typescript-eslint/ban-types export type HorizontalResizeablePanelProps = Omit, 'children'> & { /** * Turn on or off the resizing behavior on the left border @@ -35,6 +34,10 @@ export type HorizontalResizeablePanelProps = Omit void; + /** + * The content to render when the user is dragging the panel + */ + renderDragContent?: (props: { isResizing: boolean }) => React.ReactNode; children?: (props: { isResizing: boolean }) => React.ReactNode; }; @@ -51,6 +54,7 @@ export type HorizontalResizeablePanelProps = Omit = ({ @@ -62,6 +66,7 @@ export const HorizontalResizeablePanel: React.FC maxRightSize, onResize, onResizeChange, + renderDragContent, children, ...props }) => { @@ -148,18 +153,22 @@ export const HorizontalResizeablePanel: React.FC {resizeLeft && ( )} {resizeRight && ( )} {children && children({ isResizing })}
diff --git a/src/components/VerticalResizeablePanel/VerticalResizeablePanel.stories.tsx b/src/components/VerticalResizeablePanel/VerticalResizeablePanel.stories.tsx new file mode 100644 index 0000000..8d66257 --- /dev/null +++ b/src/components/VerticalResizeablePanel/VerticalResizeablePanel.stories.tsx @@ -0,0 +1,86 @@ +// also exported from '@storybook/react' if you can deal with breaking changes in 6.1 +import { Meta, Story } from '@storybook/react/types-6-0'; +import classnames from 'classnames'; +import React from 'react'; +import { VerticalResizeablePanel } from '.'; +import { colorOptions, widthHeightOptions } from '../../prebuiltTailwindTheme'; +import { backgroundColorClassnameMap, heightClassnameMap, widthClassnameMap } from '../../storybookMappers'; + +export default { + title: 'Components/ResizeablePanel/Vertical', + component: VerticalResizeablePanel, + argTypes: { + resizeBottom: { + defaultValue: true + }, + width: { + defaultValue: '1/4', + control: { + type: 'select', + options: widthHeightOptions + } + }, + height: { + defaultValue: '1/2', + control: { + type: 'select', + options: widthHeightOptions + } + }, + backgroundColor: { + defaultValue: 'pui-primary', + control: { + type: 'select', + options: colorOptions + } + }, + className: { + control: { + type: 'text' + } + }, + onResize: { action: 'onResize' }, + onResizeChange: { action: 'onResizeChange' } + } +} as Meta; + +/** + * A story that displays a horizontal resizeable example + * + * @param props The story props + * @param props.width The example width size + * @param props.height The example height size + * @param props.backgroundColor the example background color + * @param props.className the classname to pass down if any + */ +const Template: Story = ({ width, height, backgroundColor, className, ...props }) => { + return ( +
+ 0, + [widthClassnameMap[width || 'auto']]: width && width.length > 0 + }, + className + )} + > + {({ isResizing }) => isResizing: {`${isResizing}`}} + +
+ ); +}; + +export const Default = Template.bind({}); + +export const RenderDragContent = Template.bind({}); +RenderDragContent.args = { + /** + * Renders content inside the drag area + */ + renderDragContent: () => ( +
+ ) +}; diff --git a/src/components/VerticalResizeablePanel/index.tsx b/src/components/VerticalResizeablePanel/index.tsx new file mode 100644 index 0000000..30c1144 --- /dev/null +++ b/src/components/VerticalResizeablePanel/index.tsx @@ -0,0 +1,176 @@ +import classNames from 'classnames'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; + +export type VerticalResizeablePanelProps = Omit, 'children'> & { + /** + * Turn on or off the resizing behavior on the top border + */ + resizeTop?: boolean; + /** + * Turn on or off the resizing behavior on the bottom border + */ + resizeBottom?: boolean; + /** + * The minimum width allowed when resizing from the top border + */ + minTopSize?: number; + /** + * The maximum width allowed when resizing from the top border + */ + maxTopSize?: number; + /** + * The minimum width allowed when resizing from the bottom border + */ + minBottomSize?: number; + /** + * The maximum width allowed when resizing from the bottom border + */ + maxBottomSize?: number; + /** + * Callback that is called every time the panel is being resized + */ + onResize?: () => void; + /** + * Callback for when the isResizing state changes + */ + onResizeChange?: (isResizing: boolean) => void; + /** + * The content to render when the user is dragging the panel + */ + renderDragContent?: (props: { isResizing: boolean }) => React.ReactNode; + + children?: (props: { isResizing: boolean }) => React.ReactNode; +}; + +/** + * A panel with resize abilities on its top and bottom borders + * + * @param props The component props + * @param props.resizeTop Turn on or off the resizing behavior on the top border + * @param props.resizeBottom Turn on or off the resizing behavior on the bottom border + * @param props.minTopSize The minimum width allowed when resizing from the top border + * @param props.maxTopSize The maximum width allowed when resizing from the top border + * @param props.minBottomSize The minimum width allowed when resizing from the bottom border + * @param props.maxBottomSize The maximum width allowed when resizing from the bottom border + * @param props.onResize Callback that is called every time the panel is being resized + * @param props.onResizeChange Callback for when the isResizing state changes + * @param props.renderDragContent The content to render when the user is dragging the panel + * @param props.children The element children components + */ +export const VerticalResizeablePanel: React.FC = ({ + resizeTop, + resizeBottom, + minTopSize, + maxTopSize, + minBottomSize, + maxBottomSize, + onResize, + onResizeChange, + renderDragContent, + children, + ...props +}) => { + const [isResizing, setIsResizing] = useState(false); + + const dragRef = useRef<{ isResizing: boolean; lastDownX: number; resizeSide: 'top' | 'bottom' } | null>(null); + const ref = useRef(null); + const didInitialize = useRef(null); + + useEffect(() => { + // If the method exists, and has the component initialized + if (onResizeChange && didInitialize.current) { + onResizeChange(isResizing); + } + + if (!didInitialize.current) { + didInitialize.current = true; + } + }, [isResizing, onResizeChange]); + + useEffect(() => { + /** + * Handler for when the user is moving the mouse + * + * @param e The mouse event + */ + const onMouseMove = (e: MouseEvent) => { + // we don't want to do anything if we aren't resizing. + if (!ref.current || !dragRef.current?.isResizing) return; + + if (onResize) onResize(); + + if (dragRef.current.resizeSide === 'top') { + let height = ref.current.clientHeight + (e.clientY - ref.current.offsetTop) * -1; + + if (minTopSize && height < minTopSize) { + height = minTopSize; + } + + if (maxTopSize && height > maxTopSize) { + height = maxTopSize; + } + + ref.current.style.height = `${height}px`; + } else if (dragRef.current.resizeSide === 'bottom') { + let height = e.clientY - 16; + + if (minBottomSize && height < minBottomSize) { + height = minBottomSize; + } + + if (maxBottomSize && height > maxBottomSize) { + height = maxBottomSize; + } + + ref.current.style.height = `${height}px`; + } + }; + + /** + * Handler for when the user stops dragging the mouse + */ + const onMouseUp = () => { + // Resets the values + dragRef.current = { isResizing: false, lastDownX: 0, resizeSide: 'top' }; + setIsResizing(false); + }; + + window.addEventListener('mousemove', onMouseMove, false); + window.addEventListener('mouseup', onMouseUp, false); + return () => { + window.removeEventListener('mousemove', onMouseMove); + window.removeEventListener('mouseup', onMouseUp); + }; + }, [maxTopSize, maxBottomSize, minTopSize, minBottomSize, onResize]); + + const onMove = useCallback((e: React.MouseEvent, resizeSide: 'top' | 'bottom') => { + dragRef.current = { isResizing: true, lastDownX: e.clientX, resizeSide }; + setIsResizing(true); + }, []); + + return ( +
+ {resizeTop && ( + + )} + {resizeBottom && ( + + )} + {children && children({ isResizing })} +
+ ); +};