feat(popover): fullscreen-aware portal + migrate the hover surfaces (R8 S4a)
The platform's FloatingPortal targeted <body>, which is painted UNDERNEATH whatever holds the fullscreen lock. That is why the player's in-bar menus were either non-portaled (and clipped) or, for add-to-playlist / download, disabled outright in fullscreen. PopoverSurface now portals into `document.fullscreenElement` while one exists, via a new reactive `useFullscreenElement()` on the existing WebKit-aware fullscreen lib. Two floating-ui details this had to work around: a null `root` means "the container does not exist yet" (no portal node is created at all), so the no-fullscreen case must pass undefined; and the portal node is built once and ignores a later root change, so the portal is keyed on the fullscreen state to remount on a transition. Migrated the three hand-rolled hover engines onto the platform: - Tooltip: kept as a passive caption via a new `passthrough` prop on PopoverSurface, so it cannot swallow a click aimed at a control it overlaps during the close delay. Gains aria-describedby wiring it never had. - AboutCell: the 160 ms hide-timer hover bridge is now safePolygon; the close-on-page- scroll is now hide-and-follow, so the card reappears when the row scrolls back. - TagManager: same bridge removal; the scroll-fade moves to an inner scroller so the mask rides the scrolling box rather than the glass panel. Verified in-browser: tooltip flip + centering + pointer-transparency, About card flip- above with the measured height, tag popover right-start anchoring, and the portal round-trip body -> fullscreen element -> body on a live fullscreenchange.
This commit is contained in:
@@ -35,6 +35,7 @@ import {
|
||||
type UseRoleProps,
|
||||
} from "@floating-ui/react";
|
||||
import { useLayer } from "../lib/layerStack";
|
||||
import { useFullscreenElement } from "../lib/fullscreen";
|
||||
|
||||
// R8 popover platform. ONE anchored-popover primitive over @floating-ui/react, replacing the ~9
|
||||
// hand-rolled positioning engines (measured flip / width clamp / rAF scroll-follow / scroller-relative
|
||||
@@ -271,6 +272,7 @@ export function PopoverSurface({
|
||||
children,
|
||||
focusManaged,
|
||||
initialFocus,
|
||||
passthrough,
|
||||
}: {
|
||||
popover: AnchoredPopover;
|
||||
className?: string;
|
||||
@@ -278,6 +280,10 @@ export function PopoverSurface({
|
||||
children: ReactNode;
|
||||
focusManaged?: boolean;
|
||||
initialFocus?: number | React.MutableRefObject<HTMLElement | null>;
|
||||
/** The surface never receives pointer events — for a passive caption that must not swallow a click
|
||||
* aimed at whatever it happens to cover during its close delay. Interactive hover cards (scrollable
|
||||
* content, links) must NOT set this. */
|
||||
passthrough?: boolean;
|
||||
}) {
|
||||
const {
|
||||
context,
|
||||
@@ -295,6 +301,11 @@ export function PopoverSurface({
|
||||
elementsRef,
|
||||
labelsRef,
|
||||
} = popover;
|
||||
// A node portaled to <body> is painted UNDERNEATH whatever holds the fullscreen lock, so a popover
|
||||
// opened from a player's control bar in fullscreen would be invisible. Portal into the fullscreen
|
||||
// element instead while one exists — this is what let the player's in-bar menus stop being either
|
||||
// non-portaled (and clipped) or outright disabled in fullscreen.
|
||||
const fsRoot = useFullscreenElement();
|
||||
const { style: floatingPropStyle, ...floatingProps } = getFloatingProps() as {
|
||||
style?: CSSProperties;
|
||||
} & Record<string, unknown>;
|
||||
@@ -317,8 +328,9 @@ export function PopoverSurface({
|
||||
// Hide until the first position is measured (else it flashes at 0,0 before the transform
|
||||
// lands) and while the trigger is scrolled out of its clipping scroller (hide-and-follow).
|
||||
...(!isPositioned || referenceHidden
|
||||
? { visibility: "hidden", pointerEvents: "none" }
|
||||
? { visibility: "hidden" as const, pointerEvents: "none" as const }
|
||||
: null),
|
||||
...(passthrough ? { pointerEvents: "none" as const } : null),
|
||||
}}
|
||||
>
|
||||
{/* Inner wrapper carries the visual styling + pop-in animation. Keeping the ANIMATION off the
|
||||
@@ -345,7 +357,11 @@ export function PopoverSurface({
|
||||
|
||||
return (
|
||||
<FloatingNode id={nodeId}>
|
||||
<FloatingPortal>
|
||||
{/* `root` must be undefined (NOT null) to mean "default to <body>" — floating-ui reads a null
|
||||
root as "the container doesn't exist yet" and creates no portal node at all. And it only
|
||||
builds that node once, so a root that changes mid-open is ignored; the key remounts the
|
||||
portal on a fullscreen transition, which is exactly when the popover must move. */}
|
||||
<FloatingPortal key={fsRoot ? "fullscreen" : "body"} root={fsRoot ?? undefined}>
|
||||
{managed ? (
|
||||
<FloatingFocusManager
|
||||
context={context}
|
||||
|
||||
Reference in New Issue
Block a user