77 lines
2.7 KiB
TypeScript
77 lines
2.7 KiB
TypeScript
import { useEffect, useRef } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
|
|
interface Props {
|
|
/** drag 中に呼ばれる。upperPct (0..100) を渡す。 */
|
|
onResize: (upperPct: number) => void;
|
|
/** drag 終了時に 1 度だけ呼ばれる。localStorage 保存用。 */
|
|
onResizeEnd: (upperPct: number) => void;
|
|
/** double-click でリセット。 */
|
|
onReset?: () => void;
|
|
/** 上下のパネルを含む親要素を識別する data-* selector。 */
|
|
parentSelector: string;
|
|
minUpperPct?: number;
|
|
minLowerPct?: number;
|
|
}
|
|
|
|
export function VerticalResizeHandle({
|
|
onResize, onResizeEnd, onReset, parentSelector,
|
|
minUpperPct = 20, minLowerPct = 15,
|
|
}: Props) {
|
|
const { t } = useTranslation('layout');
|
|
const onResizeRef = useRef(onResize);
|
|
const onResizeEndRef = useRef(onResizeEnd);
|
|
onResizeRef.current = onResize;
|
|
onResizeEndRef.current = onResizeEnd;
|
|
|
|
const draggingRef = useRef(false);
|
|
const lastPctRef = useRef<number | null>(null);
|
|
|
|
useEffect(() => {
|
|
const handleMove = (e: PointerEvent) => {
|
|
if (!draggingRef.current) return;
|
|
const parent = document.querySelector<HTMLElement>(parentSelector);
|
|
if (!parent) return;
|
|
const rect = parent.getBoundingClientRect();
|
|
const raw = ((e.clientY - rect.top) / rect.height) * 100;
|
|
const clamped = Math.max(minUpperPct, Math.min(100 - minLowerPct, raw));
|
|
lastPctRef.current = clamped;
|
|
onResizeRef.current(clamped);
|
|
};
|
|
const handleUp = () => {
|
|
if (!draggingRef.current) return;
|
|
draggingRef.current = false;
|
|
document.body.style.cursor = '';
|
|
document.body.style.userSelect = '';
|
|
if (lastPctRef.current !== null) onResizeEndRef.current(lastPctRef.current);
|
|
};
|
|
window.addEventListener('pointermove', handleMove);
|
|
window.addEventListener('pointerup', handleUp);
|
|
window.addEventListener('pointercancel', handleUp);
|
|
return () => {
|
|
window.removeEventListener('pointermove', handleMove);
|
|
window.removeEventListener('pointerup', handleUp);
|
|
window.removeEventListener('pointercancel', handleUp);
|
|
};
|
|
}, [parentSelector, minUpperPct, minLowerPct]);
|
|
|
|
return (
|
|
<div
|
|
role="separator"
|
|
aria-orientation="horizontal"
|
|
aria-label={t('resize.listPanel')}
|
|
onPointerDown={(e) => {
|
|
e.preventDefault();
|
|
draggingRef.current = true;
|
|
document.body.style.cursor = 'row-resize';
|
|
document.body.style.userSelect = 'none';
|
|
}}
|
|
onDoubleClick={onReset}
|
|
className="cursor-row-resize bg-transparent hover:bg-slate-300/60 transition-colors flex justify-center group"
|
|
style={{ height: 6, touchAction: 'none' }}
|
|
>
|
|
<div className="h-px self-center bg-hairline w-full group-hover:bg-slate-500/40" />
|
|
</div>
|
|
);
|
|
}
|