WFDASHDRIVER-APP/lib/hooks.ts

46 lines
1.7 KiB
TypeScript
Raw Permalink Normal View History

feat: popover stays above footer, partial collections must be justified Two improvements to the collection flow: 1. Popover clipping under BottomNav (last item in list) - Root cause: popovers were z-[60] *inside* a parent stacking context at z-30, so their effective page z-index was 30 — below BottomNav's z-50. Even if the z-index were right, the popover extends below the trigger button and could fall below the viewport on the last item in a long list. - Add lib/hooks.ts with usePopoverPlacement: measures the trigger's bottom edge and flips popover direction to "up" when there isn't ~240px of room below (reserving 80px for BottomNav + safe-area inset). - Bump the article/section/li stacking context from z-30 to z-[80] when a popover is open so the popover paints above BottomNav (z-50). Covers both compact-mode SupplierGroup rows and detailed-mode CollectionItemCard via the existing onPopoverChange callback. - For SupplierGroup: hooks live at the top level (record keyed by item.id) to avoid the Rules of Hooks trap of declaring hooks inside items.map(). 2. Partial collections must be justified - In CollectModal, when received qty < expected, require EITHER a reason (dropdown: out of stock / wrong item / quality issue / damaged / wrong qty packed / other) OR a photo before Confirm is enabled. Prevents silent partial submissions with no audit trail. - The picked reason flows into the auto-generated problem note so the operator sees "[Reason] driver note" in PB. Co-Authored-By: Claude <noreply@anthropic.com> Co-authored-by: Orca <help@stably.ai>
2026-08-02 20:58:08 +00:00
'use client';
import { useCallback, useState } from 'react';
export type PopoverPlacement = 'down' | 'up';
interface PopoverPlacementApi {
placement: PopoverPlacement;
/** Measure the trigger button and decide placement. Call this in
* the button's onClick before toggling the popover open so the
* placement is correct for the current scroll position. */
measure: () => void;
}
/**
* Decide whether a popover anchored to a button should render below
* the button (default) or above it. The "down" direction is the
* common case; we flip to "up" when the button is near the bottom of
* the viewport and the popover would otherwise extend below the
* visible area and get hidden behind the BottomNav (z-50).
*
* Reserves ~80px of headroom for the BottomNav (64px) + safe-area
* inset so the popover doesn't push right up against the chrome.
*
* The caller must still ensure the popover's parent stacking context
* sits above the BottomNav in the page context (z >= 50). On its own
* the popover can flip, but if its parent section is z-30 the popover
* still paints behind the BottomNav.
*/
export function usePopoverPlacement(
buttonRef: React.RefObject<HTMLElement | null>,
estimatedHeight: number = 240,
reservedForChrome: number = 80,
): PopoverPlacementApi {
const [placement, setPlacement] = useState<PopoverPlacement>('down');
const measure = useCallback(() => {
const btn = buttonRef.current;
if (!btn || typeof window === 'undefined') return;
const rect = btn.getBoundingClientRect();
const available = window.innerHeight - rect.bottom - reservedForChrome;
setPlacement(available < estimatedHeight ? 'up' : 'down');
}, [buttonRef, estimatedHeight, reservedForChrome]);
return { placement, measure };
}