/** * Showing a popup, as opposed to placing one. * * `overlay.ts` decides *where* a popup goes and stays free of the DOM. This decides *how it is * shown*, which cannot be: putting an element in the top layer is a DOM operation and nothing else. * The two are separate files so that the geometry stays testable without a document. */ /** * Shows or hides a popup, and puts it in the top layer while it is open. * * One place decides how a popup is shown, the way one place already decides where it is placed. * This began in the framework-free renderer, whose fields used to assign `popup.hidden` themselves * six times over; it is here because the top layer is not a renderer's idea, it is what the * anchoring contract needs to be true. * * `anchorOverlay` measures against the viewport and every adapter writes those coordinates for a * `position: fixed` box — which holds only while no ancestor is a containing block for fixed * descendants. Anything with `contain: layout` becomes one, and `container-type` (which the * foundation needs so a row can ask how wide its *form* is, rather than how wide the window is) * applies exactly that. A popup in the top layer is laid out against the viewport whatever its * ancestors do, so the coordinates stay true. It also stops a popup being clipped by an * `overflow: hidden` ancestor, which was a standing bug in its own right. * * `manual` rather than `auto`: light-dismiss would close the popup before an adapter's own * outside-pointer handling ran, and two things closing one popup is how a click-through appears. */ /** * Shows or hides a popup, and puts the backdrop under it when the placement is modal. * * The backdrop is what makes a modal one: the page behind stays visible and stops taking the * pointer. `MDY_SHARED_UI_CLASSES` names it, a theme paints it, and every adapter draws it — which * is the part that had gone wrong. One renderer wrote its colour as a literal, one drew it under * every open popup including the dropdowns, and one drew none at all, so the same contract produced * three different modals. * * Here rather than in each renderer, because "a modal dims what is behind it" is not a rendering * decision any of them gets to make differently. */ export declare function setOverlayOpen(popup: HTMLElement, open: boolean, modal?: boolean): boolean; /** Marks the backdrop this module owns, so it is never confused with one a renderer drew itself. */ /** * The attribute marking the dimming veil a panel draws behind itself. * * Published because the veil sits inside the panel's portal, as its sibling, and every rule that * asks "did this happen inside the overlay" would otherwise say yes to a press on it. */ export declare const MDY_BACKDROP_ATTRIBUTE = "data-mdy-overlay-backdrop"; /** * One backdrop per popup, in the popup's own document, removed when it closes. * * Placed immediately before the popup so it sits under it in paint order without a z-index race, * and marked so a renderer that draws its own — a framework whose template owns the element — is * left alone. * * Exported because the placement is often known a moment after the popup is shown: a renderer that * measures, decides and then places calls this with the answer rather than opening twice. */ export declare function syncOverlayBackdrop(popup: HTMLElement, wanted: boolean): void; /** * Keeps a popup on its anchor while the page moves under it. * * Scroll and resize are the two ways an anchor's viewport position changes without the widget doing * anything, so a popup that does not listen for them drifts off its control. * * Two properties, and each was a defect before this existed in one place: * * **Passive.** A non-passive `scroll` listener tells the engine the handler may cancel the scroll, so * it cannot commit the frame until the handler returns — a listener that follows a scroll ends up * blocking it. Nothing here calls `preventDefault`. * * **One reposition per frame.** Scroll events fire far more often than frames, and each reposition * measures layout and then writes it. Left uncoalesced that is a read-write cycle several times per * frame, which is both the cost and the visible judder: the popup lands on a position the page has * already left. `requestAnimationFrame` collapses a burst into the single placement that will * actually be painted. * * Capture, because the scroll that moves an anchor is often on an ancestor pane rather than the * window, and a scroll event does not bubble from an element. * * It was written three times — once per renderer — and the three disagreed: one passed `passive`, * two did not, and none coalesced. A behaviour every adapter needs is one this package owes them. * * **Scrolling and resizing are not the same event.** A page that scrolls moves the anchor and * nothing else, so the popup follows while keeping the side and height it opened with — re-deciding * on every scroll frame is what makes a popup flip sides under the pointer. A viewport that changes * size changes what fits, so there the decision is taken again. Both renderers that place their own * popups had drawn that distinction and neither could use this function, which had one callback for * two questions. */ export interface MdyAnchoredOverlayTracking { /** The page moved: follow the anchor, keeping the placement the popup opened with. */ reposition(): void; /** The viewport changed size: what fits changed, so decide again. Defaults to `reposition`. */ reflow?(): void; isOpen(): boolean; /** * Whether to follow scrolling at all. A popup that covers the viewport rather than hanging off a * control has no anchor to follow, and binding a capture-phase scroll listener for it is cost * with no effect. */ followsScroll?(): boolean; } export declare function trackAnchoredOverlay(tracking: MdyAnchoredOverlayTracking): () => void; /** * Writes a placement onto an element, skipping every property that already says it. * * A renderer re-applies the placement on each pass while a popup is open, so most of these writes * set a property to the value it already holds. That is not free: a custom property write * invalidates style on the element and everything inheriting from it, which for a popup holding a * calendar is its whole subtree, on every pass. * * Stated once because three renderers each write these properties, and a rule about when *not* to * write is exactly the kind that gets applied in one of them and forgotten in the others. */ export declare function applyOverlayProperties(element: HTMLElement, properties: Readonly>): void; /** * Leaving an open overlay with Tab, in the order that makes it work. * * Tab is the one key that means the same thing everywhere: *go to the next control*. Inside a panel * the arrows choose and Enter commits and Escape cancels, and Tab still has to be the way out — a * panel that takes it to do something else has made a keyboard trap. * * **Move the focus first, close after.** The other order is not a choice anybody made: the panel * closes while the focused element is inside it, the browser is left with an active element that no * longer exists, and it puts focus on the body. From the body the next Tab starts again at the top of * the document, so the person has lost their place in the form and nothing told them why. That is * what two of three renderers did. * * The opener is **crossed, not stopped at**. Focus lands on it and the browser's own Tab carries on * from there to the next field, which is where the person asked to go — one press, one move. Leaving * focus on the opener costs a second press and puts them *back* on the control they were leaving, * which reads as the key not working. * * The default is not prevented: from the opener the browser knows what the next control is, and from * inside a panel drawn outside the field it does not. * * Tab does not choose. A highlighted option stays unchosen, because a shortcut that commits on the * way out removes the ability to leave without choosing. */ export declare function stepOutOfOverlay(opener: HTMLElement | null | undefined, close: () => void): void; /** * The three measurements an anchoring decision is made from, taken from the document. * * `anchorOverlay` declares what it needs — a viewport, a direction, a content size — and has never * said how to obtain them, so each renderer gathered them itself. The three gatherings were * character-for-character identical: one answer written where three people had to write it. * * They live here rather than inside `anchorOverlay` because that function is pure and is exercised * against rectangles no document ever held. What is shared is the *reading*, not the decision. */ /** The space a popup has to fit in: the layout viewport, not the visual one a pinch-zoom leaves. */ export declare function viewportSize(document: Document): { readonly width: number; readonly height: number; }; /** * The direction an element is actually laid out in. * * Read, never assumed. A widget declares which *inline* edge its popup hangs from, and only the live * direction says which physical edge that is. */ export declare function inlineDirectionOf(element: Element): "ltr" | "rtl"; /** * How much room a popup's content wants, or `null` when it is not laid out yet. * * The scroll size plus the border box: `scrollHeight` stops at the padding edge, so a popup with a * border reports a size its own outline does not fit in, and every decision made from it clamps a few * pixels short. * * `null` rather than zero for a popup with no layout. A guessed size is worse than none — a decision * made from zero looks like a decision, and the caller cannot tell it from one made on a measurement. * * A hidden popup answers `null` for the same reason, and so does a missing one: the three renderers * that wrote this each guarded a different subset of "there is nothing here to measure", and the * shared answer is the union of what they guarded rather than the smallest of them. */ export declare function measureOverlayContent(popup: HTMLElement | null | undefined): { readonly height: number; readonly width: number; } | null;