import * as React from "react"; import type { AffixProp } from "../../props/components/layout.prop.js"; export type { AffixProp, AffixProp as AffixProps, AffixTargetProp, } from "../../props/components/layout.prop.js"; /** * Affix — Ant Design `Affix` (6.6.5): pin an element to its scrollport once the page scrolls past * it, and REPORT that it is pinned. * * ## Why this is not `position: sticky` * * `position: sticky` pins, and then says nothing. There is no state, no attribute and no callback, * so a sticky header cannot shrink on pin, cannot swap a wordmark for a monogram, cannot raise its * shadow, and cannot tell the page that the filters are now floating over the table. Both website * showcases in `docs/showcase/` wrote `position: sticky` and got exactly nothing out of it. This * component's product is the boolean: `data-affixed` for CSS, `onChange` for JavaScript. * * It also pins against a scroll box that need not be the nearest scrolling ancestor — antd's * `target`, which `FloatButton.BackTop` already spells the same way here — where `sticky` is * captive to whichever ancestor happens to scroll. * * ## The placeholder is as much the component as the pin is * * Taking an element out of flow removes its height from the page, so everything below jumps UP by * exactly that height at the instant of pinning and drops back on release. Every hand-rolled * sticky header has this bug and it is invisible until someone measures it. So the outer box stays * in flow and, the moment its content goes `position: fixed`, is given the content's MEASURED * block size. Measured, not declared: a bar re-wraps between widths, and a number written once in * CSS is wrong at every other width. * * ## One `IntersectionObserver`, and it is not this file's * * The pin is a threshold crossing — the one question `IntersectionObserver` exists to answer — and * this component asks it through `useInView` (`src/lib/hooks.ts`), the library's single observer * wrapper, which gained `rootMargin` and `assumeInView` for exactly these two needs. Nothing here * polls, and nothing here listens to `scroll` at all when the scroll box is the viewport: antd's * `Affix` re-measures on seven event types on every animation frame of every scroll, and all of * that is replaced by one observer entry per transition. * * The observed element is a zero-ish **sentinel** at the pinning edge, not the box itself. This is * the published `position: sticky` sentinel technique ("An event for position: sticky", * developers.google.com), and the separate node is arithmetic, not decoration: the box's own * intersection flips when its TRAILING edge crosses the line, a full bar-height after the moment * it should pin. A hairline node at the LEADING edge flips on the leading edge. It is * `aria-hidden`, out of flow, and contributes nothing to the measured height. * * Both directions reduce to the same expression — `affixed = !inView` — because the sentinel sits * at the block-start edge with the clipping box grown downwards, and at the block-end edge with it * grown upwards. * * ## The offset is a TOKEN that the prop overrides, and it is never a number in JavaScript * * `--affix-inset-block-start` / `--affix-inset-block-end` place the pin line, and they place it * ONCE: the sentinel is offset by `calc(-1 * …)`, which moves the crossing, and the pinned bar is * inset by the same `var()`, which moves the paint. One declaration drives both, so they cannot * drift. A service sets the resting offset once (a product whose app header is 64px tall writes * `--affix-inset-block-start: 64px` in its theme and every affixed bar clears it); `offsetBlockStart` * / `offsetBlockEnd` override it per instance by writing the same custom property inline. That is * cardinal rules #44/#45 in their literal form. * * It also means the offset is never parsed back out of `getComputedStyle` — a custom property * hands back its AUTHORED text, so a theme written in `rem` would have come back as `"4rem"` and * `parseFloat` would have read it as 4px. CSS resolves the unit; JavaScript never sees it. * * ## What a pinned bar owes the keyboard (C6) * * A bar pinned over the block-start edge covers whatever the browser scrolls to — the next focused * field, the `#section` a skip link jumps to — and the browser has no idea it is there. While * pinned there, this sets `scroll-padding-block-start` on the scroll box to the bar's measured * bottom edge, and puts back whatever was there on release. That is WCAG 2.4.11 (Focus Not * Obscured) bought with one declaration, and it is why C6 passes rather than being asserted. * * ## Motion * * `Affix` animates nothing, so it has no transition to suppress and satisfies * `prefers-reduced-motion: reduce` by construction — the pin is instantaneous in every mode. The * SHRINK is a composition: read `[data-affixed]` off `data-slot="affix-content"` and transition * whatever the brand condenses, on `--duration-fast` / `--ease-standard`, snapping under reduced * motion. It must condense; it must never fade or disappear. * * ## One inherited limitation, stated rather than hidden * * A `transform`, `filter` or `backdrop-filter` on an ancestor makes that ancestor the containing * block for `position: fixed`, so a pinned bar inside one is positioned against it instead of the * viewport. `position: sticky` and antd's `Affix` are both subject to it; there is no workaround * inside a component, only a call site that does not do that. */ export declare const Affix: React.ForwardRefExoticComponent>;