improvement: some improvement done for appointment and treatment shared components.
This commit is contained in:
96
frontend/src/lib/hooks/useAnchoredPanelPosition.ts
Normal file
96
frontend/src/lib/hooks/useAnchoredPanelPosition.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
'use client';
|
||||
|
||||
import { useLayoutEffect, useRef, useState, type CSSProperties, type RefObject } from 'react';
|
||||
|
||||
const VIEWPORT_PADDING = 12;
|
||||
const DEFAULT_GAP = 8;
|
||||
|
||||
export type AnchoredPanelAlign = 'start' | 'stretch';
|
||||
|
||||
type UseAnchoredPanelPositionArgs = {
|
||||
open: boolean;
|
||||
/** Element the panel should attach to (field / trigger shell). */
|
||||
anchorRef: RefObject<HTMLElement | null>;
|
||||
/** Prefer aligning the panel's inline-start with the anchor's inline-start. */
|
||||
align?: AnchoredPanelAlign;
|
||||
gap?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Positions a panel with `position: fixed`, clamped to the viewport.
|
||||
* Flips above the anchor when there is not enough room below.
|
||||
* Handles LTR/RTL via `document.documentElement.dir`.
|
||||
*/
|
||||
export function useAnchoredPanelPosition({
|
||||
open,
|
||||
anchorRef,
|
||||
align = 'start',
|
||||
gap = DEFAULT_GAP,
|
||||
}: UseAnchoredPanelPositionArgs) {
|
||||
const panelRef = useRef<HTMLDivElement>(null);
|
||||
const [style, setStyle] = useState<CSSProperties | undefined>(undefined);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!open) {
|
||||
setStyle(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
function update() {
|
||||
const anchor = anchorRef.current;
|
||||
const panel = panelRef.current;
|
||||
if (!anchor || !panel) return;
|
||||
|
||||
const anchorRect = anchor.getBoundingClientRect();
|
||||
const panelRect = panel.getBoundingClientRect();
|
||||
const vw = window.innerWidth;
|
||||
const vh = window.innerHeight;
|
||||
const isRtl = document.documentElement.dir === 'rtl';
|
||||
|
||||
let width =
|
||||
align === 'stretch'
|
||||
? Math.min(anchorRect.width, vw - VIEWPORT_PADDING * 2)
|
||||
: Math.min(panelRect.width || panel.offsetWidth, vw - VIEWPORT_PADDING * 2);
|
||||
|
||||
if (width < 1) width = Math.min(280, vw - VIEWPORT_PADDING * 2);
|
||||
|
||||
let left: number;
|
||||
if (align === 'stretch') {
|
||||
left = anchorRect.left;
|
||||
} else if (isRtl) {
|
||||
left = anchorRect.right - width;
|
||||
} else {
|
||||
left = anchorRect.left;
|
||||
}
|
||||
left = Math.max(VIEWPORT_PADDING, Math.min(left, vw - width - VIEWPORT_PADDING));
|
||||
|
||||
const height = panelRect.height || panel.offsetHeight;
|
||||
let top = anchorRect.bottom + gap;
|
||||
if (top + height > vh - VIEWPORT_PADDING) {
|
||||
const above = anchorRect.top - height - gap;
|
||||
top = above >= VIEWPORT_PADDING ? above : Math.max(VIEWPORT_PADDING, vh - height - VIEWPORT_PADDING);
|
||||
}
|
||||
|
||||
setStyle({
|
||||
position: 'fixed',
|
||||
top,
|
||||
left,
|
||||
width,
|
||||
zIndex: 50,
|
||||
});
|
||||
}
|
||||
|
||||
update();
|
||||
// Second pass after fonts/layout settle panel size.
|
||||
const raf = window.requestAnimationFrame(update);
|
||||
window.addEventListener('resize', update);
|
||||
window.addEventListener('scroll', update, true);
|
||||
return () => {
|
||||
window.cancelAnimationFrame(raf);
|
||||
window.removeEventListener('resize', update);
|
||||
window.removeEventListener('scroll', update, true);
|
||||
};
|
||||
}, [align, anchorRef, gap, open]);
|
||||
|
||||
return { panelRef, style };
|
||||
}
|
||||
Reference in New Issue
Block a user