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 })}
+
+ );
+};