|
import type { RefObject } from 'react'; |
|
import { useCallback, useLayoutEffect, useRef, useState } from 'react'; |
|
import type { |
|
ImperativePanelGroupHandle, |
|
ImperativePanelHandle, |
|
PanelOnCollapse, |
|
PanelOnExpand, |
|
PanelProps, |
|
PanelResizeHandleProps, |
|
} from 'react-resizable-panels'; |
|
import { getPanelGroupElement, getResizeHandleElementsForGroup } from 'react-resizable-panels'; |
|
|
|
type Direction = 'horizontal' | 'vertical'; |
|
|
|
const NO_SIZE = Symbol('NO_SIZE'); |
|
|
|
export type UsePanelOptions = { |
|
id: string; |
|
|
|
|
|
|
|
minSizePx: number; |
|
|
|
|
|
|
|
defaultSizePx?: number; |
|
|
|
|
|
|
|
|
|
panelGroupDirection: Direction; |
|
|
|
|
|
|
|
imperativePanelGroupRef: RefObject<ImperativePanelGroupHandle>; |
|
|
|
|
|
|
|
onCollapse?: (isCollapsed: boolean) => void; |
|
}; |
|
|
|
export type UsePanelReturn = { |
|
|
|
|
|
|
|
isCollapsed: boolean; |
|
|
|
|
|
|
|
reset: () => void; |
|
|
|
|
|
|
|
toggle: () => void; |
|
|
|
|
|
|
|
expand: () => void; |
|
|
|
|
|
|
|
collapse: () => void; |
|
|
|
|
|
|
|
resize: (sizePx: number) => void; |
|
|
|
|
|
|
|
panelProps: Partial<PanelProps & { ref: RefObject<ImperativePanelHandle> }>; |
|
|
|
|
|
|
|
resizeHandleProps: Partial<PanelResizeHandleProps>; |
|
}; |
|
|
|
export const usePanel = (arg: UsePanelOptions): UsePanelReturn => { |
|
const imperativePanelRef = useRef<ImperativePanelHandle>(null); |
|
const [_minSize, _setMinSize] = useState(1); |
|
const [_defaultSize, _setDefaultSize] = useState(1); |
|
|
|
const observerCallback = useCallback(() => { |
|
if (!imperativePanelRef?.current) { |
|
return; |
|
} |
|
|
|
const minSizePct = getSizeAsPercentage(arg.minSizePx, arg.imperativePanelGroupRef, arg.panelGroupDirection); |
|
const defaultSizePct = getSizeAsPercentage( |
|
arg.defaultSizePx ?? arg.minSizePx, |
|
arg.imperativePanelGroupRef, |
|
arg.panelGroupDirection |
|
); |
|
|
|
if (minSizePct === NO_SIZE || defaultSizePct === NO_SIZE) { |
|
|
|
return; |
|
} |
|
|
|
_setMinSize(minSizePct); |
|
|
|
if (defaultSizePct > minSizePct) { |
|
_setDefaultSize(defaultSizePct); |
|
} else { |
|
_setDefaultSize(minSizePct); |
|
} |
|
|
|
const currentSize = imperativePanelRef.current.getSize(); |
|
const isCollapsed = imperativePanelRef.current.isCollapsed(); |
|
|
|
if (!isCollapsed && currentSize < minSizePct && minSizePct > 0) { |
|
imperativePanelRef.current.resize(minSizePct); |
|
} |
|
}, [arg.defaultSizePx, arg.imperativePanelGroupRef, arg.minSizePx, arg.panelGroupDirection]); |
|
|
|
|
|
|
|
useLayoutEffect(() => { |
|
if (!arg.imperativePanelGroupRef.current) { |
|
return; |
|
} |
|
const id = arg.imperativePanelGroupRef.current.getId(); |
|
const panelGroupElement = getPanelGroupElement(id); |
|
const panelGroupHandleElements = getResizeHandleElementsForGroup(id); |
|
if (!panelGroupElement) { |
|
return; |
|
} |
|
const resizeObserver = new ResizeObserver(observerCallback); |
|
|
|
resizeObserver.observe(panelGroupElement); |
|
panelGroupHandleElements.forEach((el) => resizeObserver.observe(el)); |
|
|
|
observerCallback(); |
|
|
|
return () => { |
|
resizeObserver.disconnect(); |
|
}; |
|
}, [arg.imperativePanelGroupRef, observerCallback]); |
|
|
|
const [isCollapsed, setIsCollapsed] = useState(() => Boolean(imperativePanelRef.current?.isCollapsed())); |
|
|
|
const onCollapse = useCallback<PanelOnCollapse>(() => { |
|
setIsCollapsed(true); |
|
arg.onCollapse?.(true); |
|
}, [arg]); |
|
|
|
const onExpand = useCallback<PanelOnExpand>(() => { |
|
setIsCollapsed(false); |
|
arg.onCollapse?.(false); |
|
}, [arg]); |
|
|
|
const toggle = useCallback(() => { |
|
if (imperativePanelRef.current?.isCollapsed()) { |
|
imperativePanelRef.current?.expand(); |
|
} else { |
|
imperativePanelRef.current?.collapse(); |
|
} |
|
}, []); |
|
|
|
const expand = useCallback(() => { |
|
imperativePanelRef.current?.expand(); |
|
}, []); |
|
|
|
const collapse = useCallback(() => { |
|
imperativePanelRef.current?.collapse(); |
|
}, []); |
|
|
|
const resize = useCallback( |
|
(sizePx: number) => { |
|
|
|
const sizeAsPct = getSizeAsPercentage(sizePx, arg.imperativePanelGroupRef, arg.panelGroupDirection); |
|
if (sizeAsPct === NO_SIZE) { |
|
return; |
|
} |
|
imperativePanelRef.current?.resize(sizeAsPct); |
|
}, |
|
[arg] |
|
); |
|
|
|
const reset = useCallback(() => { |
|
imperativePanelRef.current?.resize(_minSize); |
|
}, [_minSize]); |
|
|
|
const cycleState = useCallback(() => { |
|
|
|
if (Math.abs((imperativePanelRef.current?.getSize() ?? 0) - _defaultSize) < 0.01) { |
|
collapse(); |
|
return; |
|
} |
|
|
|
|
|
imperativePanelRef.current?.resize(_defaultSize); |
|
}, [_defaultSize, collapse]); |
|
|
|
return { |
|
isCollapsed, |
|
reset, |
|
toggle, |
|
expand, |
|
collapse, |
|
resize, |
|
panelProps: { |
|
id: arg.id, |
|
defaultSize: _defaultSize, |
|
onCollapse, |
|
onExpand, |
|
ref: imperativePanelRef, |
|
minSize: _minSize, |
|
}, |
|
resizeHandleProps: { |
|
onDoubleClick: cycleState, |
|
}, |
|
}; |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const getSizeAsPercentage = ( |
|
sizeInPixels: number, |
|
panelGroupHandleRef: RefObject<ImperativePanelGroupHandle>, |
|
panelGroupDirection: Direction |
|
) => { |
|
if (!panelGroupHandleRef.current) { |
|
|
|
return NO_SIZE; |
|
} |
|
const id = panelGroupHandleRef.current.getId(); |
|
const panelGroupElement = getPanelGroupElement(id); |
|
if (!panelGroupElement) { |
|
|
|
return NO_SIZE; |
|
} |
|
|
|
|
|
let availableSpace = |
|
panelGroupDirection === 'horizontal' ? panelGroupElement.offsetWidth : panelGroupElement.offsetHeight; |
|
|
|
if (!availableSpace) { |
|
|
|
return NO_SIZE; |
|
} |
|
|
|
|
|
getResizeHandleElementsForGroup(id).forEach((el) => { |
|
availableSpace -= panelGroupDirection === 'horizontal' ? el.offsetWidth : el.offsetHeight; |
|
}); |
|
|
|
|
|
return Math.max(Math.min((sizeInPixels / availableSpace) * 100, 100), 1); |
|
}; |
|
|