Skip to content

Commit

Permalink
Merge pull request #110 from perimetre/10.5.0
Browse files Browse the repository at this point in the history
10.5.0
  • Loading branch information
AssisrMatheus authored Sep 18, 2023
2 parents 75be814 + 7bf1189 commit 58729fc
Show file tree
Hide file tree
Showing 6 changed files with 297 additions and 9 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -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: () => (
<div className="h-full w-[2px] bg-transparent transition-colors duration-100 group-hover:bg-pui-secondary" />
)
};
23 changes: 16 additions & 7 deletions src/components/HorizontalResizeablePanel/index.tsx
Original file line number Diff line number Diff line change
@@ -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<React.HTMLAttributes<HTMLDivElement>, 'children'> & {
/**
* Turn on or off the resizing behavior on the left border
Expand Down Expand Up @@ -35,6 +34,10 @@ export type HorizontalResizeablePanelProps = Omit<React.HTMLAttributes<HTMLDivEl
* 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;
};
Expand All @@ -51,6 +54,7 @@ export type HorizontalResizeablePanelProps = Omit<React.HTMLAttributes<HTMLDivEl
* @param props.maxRightSize The maximum width allowed when resizing from the right 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 HorizontalResizeablePanel: React.FC<HorizontalResizeablePanelProps> = ({
Expand All @@ -62,6 +66,7 @@ export const HorizontalResizeablePanel: React.FC<HorizontalResizeablePanelProps>
maxRightSize,
onResize,
onResizeChange,
renderDragContent,
children,
...props
}) => {
Expand Down Expand Up @@ -148,18 +153,22 @@ export const HorizontalResizeablePanel: React.FC<HorizontalResizeablePanelProps>
{resizeLeft && (
<button
type="button"
className="absolute h-full inset-y-0 w-2 cursor-col-resize"
style={{ left: '-4px' }}
className="absolute h-full inset-y-0 w-2 cursor-col-resize group"
style={{ left: '-5px' }}
onMouseDown={(e) => onMove(e, 'left')}
/>
>
{renderDragContent && renderDragContent({ isResizing })}
</button>
)}
{resizeRight && (
<button
type="button"
className="absolute h-full inset-y-0 w-2 cursor-col-resize"
style={{ right: '-4px' }}
className="absolute h-full inset-y-0 w-2 cursor-col-resize group"
style={{ right: '-5px' }}
onMouseDown={(e) => onMove(e, 'right')}
/>
>
{renderDragContent && renderDragContent({ isResizing })}
</button>
)}
{children && children({ isResizing })}
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<div>
<VerticalResizeablePanel
{...props}
className={classnames(
backgroundColorClassnameMap[backgroundColor || 'transparent'],
{
[heightClassnameMap[height || 'auto']]: height && height.length > 0,
[widthClassnameMap[width || 'auto']]: width && width.length > 0
},
className
)}
>
{({ isResizing }) => <span>isResizing: {`${isResizing}`}</span>}
</VerticalResizeablePanel>
</div>
);
};

export const Default = Template.bind({});

export const RenderDragContent = Template.bind({});
RenderDragContent.args = {
/**
* Renders content inside the drag area
*/
renderDragContent: () => (
<div className="w-full h-[2px] bg-transparent transition-colors duration-100 group-hover:bg-pui-secondary" />
)
};
176 changes: 176 additions & 0 deletions src/components/VerticalResizeablePanel/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import classNames from 'classnames';
import React, { useCallback, useEffect, useRef, useState } from 'react';

export type VerticalResizeablePanelProps = Omit<React.HTMLAttributes<HTMLDivElement>, '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<VerticalResizeablePanelProps> = ({
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<HTMLDivElement>(null);
const didInitialize = useRef<boolean | null>(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<HTMLButtonElement, MouseEvent>, resizeSide: 'top' | 'bottom') => {
dragRef.current = { isResizing: true, lastDownX: e.clientX, resizeSide };
setIsResizing(true);
}, []);

return (
<div {...props} className={classNames('relative', props.className)} ref={ref}>
{resizeTop && (
<button
type="button"
className="absolute w-full inset-x-0 h-2 cursor-row-resize group"
style={{ top: '-5px' }}
onMouseDown={(e) => onMove(e, 'top')}
>
{renderDragContent && renderDragContent({ isResizing })}
</button>
)}
{resizeBottom && (
<button
type="button"
className="absolute w-full inset-x-0 h-2 cursor-row-resize group"
style={{ bottom: '-5px' }}
onMouseDown={(e) => onMove(e, 'bottom')}
>
{renderDragContent && renderDragContent({ isResizing })}
</button>
)}
{children && children({ isResizing })}
</div>
);
};

0 comments on commit 58729fc

Please sign in to comment.