import { assignInlineVars } from '@vanilla-extract/dynamic'; import clsx from 'clsx'; import { forwardRef, useCallback, useLayoutEffect, useRef } from 'react'; import { useTransitionState } from 'react-transition-state'; import { useDropTarget } from '../../ui/dnd'; import { Tooltip, type TooltipProps } from '../../ui/tooltip'; import * as styles from './resize-panel.css'; export interface ResizeHandleProps extends React.HtmlHTMLAttributes { resizing: boolean; open: boolean; minWidth: number; maxWidth: number; resizeHandlePos: 'left' | 'right'; resizeHandleOffset?: number; resizeHandleVerticalPadding?: number; onOpen: (open: boolean) => void; onResizing: (resizing: boolean) => void; onWidthChange: (width: number) => void; tooltip?: TooltipProps['content']; tooltipShortcut?: TooltipProps['shortcut']; tooltipOptions?: Partial>; tooltipShortcutClassName?: string; dropTargetOptions?: Parameters[0]; } export interface ResizePanelProps extends React.HtmlHTMLAttributes { resizing: boolean; open: boolean; floating?: boolean; minWidth: number; maxWidth: number; resizeHandlePos: 'left' | 'right'; resizeHandleOffset?: number; resizeHandleVerticalPadding?: number; resizeHandleTooltip?: TooltipProps['content']; resizeHandleTooltipShortcut?: TooltipProps['shortcut']; resizeHandleTooltipShortcutClassName?: string; resizeHandleTooltipOptions?: Partial< Omit >; resizeHandleDropTargetOptions?: Parameters[0]; enableAnimation?: boolean; width: number; unmountOnExit?: boolean; onOpen: (open: boolean) => void; onResizing: (resizing: boolean) => void; onWidthChange: (width: number) => void; } const ResizeHandle = ({ className, resizing, minWidth, maxWidth, resizeHandlePos, resizeHandleOffset, resizeHandleVerticalPadding, dropTargetOptions, open, onOpen, onResizing, onWidthChange, tooltip, tooltipShortcut, tooltipOptions, tooltipShortcutClassName, ...rest }: ResizeHandleProps) => { const ref = useRef(null); const onResizeStart = useCallback( (event: React.MouseEvent) => { event.preventDefault(); let resized = false; const panelContainer = ref.current?.parentElement; if (!panelContainer) return; // add cursor style to body document.body.style.cursor = 'col-resize'; const { left: anchorLeft, right: anchorRight } = panelContainer.getBoundingClientRect(); function onMouseMove(e: MouseEvent) { e.preventDefault(); if (!panelContainer) return; const newWidth = Math.min( maxWidth, Math.max( resizeHandlePos === 'right' ? e.clientX - anchorLeft : anchorRight - e.clientX, minWidth ) ); onWidthChange(newWidth); onResizing(true); resized = true; } document.addEventListener('mousemove', onMouseMove); document.addEventListener( 'mouseup', () => { // if not resized, toggle sidebar if (!resized) { onOpen(false); } onResizing(false); document.removeEventListener('mousemove', onMouseMove); document.body.style.cursor = ''; }, { once: true } ); }, [maxWidth, resizeHandlePos, minWidth, onWidthChange, onResizing, onOpen] ); const { dropTargetRef } = useDropTarget(dropTargetOptions, [ dropTargetOptions, ]); return (
{ ref.current = node; dropTargetRef.current = node; }} style={assignInlineVars({ [styles.resizeHandleOffsetVar]: `${resizeHandleOffset ?? 0}px`, [styles.resizeHandleVerticalPadding]: `${ resizeHandleVerticalPadding ?? 0 }px`, })} className={clsx(styles.resizeHandleContainer, className)} data-handle-position={resizeHandlePos} data-resizing={resizing} data-open={open} onMouseDown={onResizeStart} >
); }; const animationTimeout = 300; export const ResizePanel = forwardRef( function ResizePanel( { children, className, resizing, minWidth, maxWidth, width, floating, enableAnimation = true, open, unmountOnExit, onOpen, onResizing, onWidthChange, resizeHandlePos, resizeHandleOffset, resizeHandleVerticalPadding, resizeHandleTooltip, resizeHandleTooltipShortcut, resizeHandleTooltipShortcutClassName, resizeHandleTooltipOptions, resizeHandleDropTargetOptions, ...rest }, ref ) { const safeWidth = Math.min(maxWidth, Math.max(minWidth, width)); const [{ status }, toggle] = useTransitionState({ timeout: animationTimeout, }); useLayoutEffect(() => { toggle(open); }, [open, toggle]); return (
{!(status === 'exited' && unmountOnExit !== false) && children}
); } );