import { type Debounced } from '../../internal/debounce'; import type { ComboboxOption } from './combobox'; /** Matches maker-ds's default page size. */ export declare const DEFAULT_PAGE_SIZE = 100; /** Matches maker-ds: load the next page once 80% of the list has been scrolled past. */ export declare const DEFAULT_SCROLL_THRESHOLD = 0.8; /** Matches maker-ds's scroll throttle. */ export declare const DEFAULT_SCROLL_THROTTLE_MS = 300; /** * What the loader is asked for. * * `offset` and `pageToken` are both sent on every call so the loader can use whichever * its backend speaks, which is upstream's design and worth keeping — an offset API and a * cursor API can share one component without a mode flag. */ export interface InfiniteOptionsPageParams { /** The requested page size. A loader is allowed to return fewer. */ limit: number; /** * Number of items already held. Advanced by the length of each returned page rather * than by `limit`, so a short page does not skip rows — upstream gets this right and * it is the reason to keep counting items instead of pages. */ offset: number; /** `nextPageToken` from the previous page, or `undefined` for the first page. */ pageToken?: string; searchTerm: string; } export interface InfiniteOptionsPage { items: TOption[]; /** The only thing that terminates paging. A loader that always returns `true` pages forever. */ hasMore: boolean; /** Cursor for the following page, echoed back as `pageToken`. */ nextPageToken?: string; /** Optional grand total, for a "showing N of M" affordance. */ totalNumberOfItems?: number; } /** * A paged option loader. Receives an `AbortSignal` as a second argument, matching * {@link AsyncOptionsLoader}'s ergonomics rather than folding the signal into the params * object, so the two loaders read the same way at a call site. * * The signal is aborted as soon as this request is superseded — by a newer first-page * load, by `reset()`, or by unmount. Upstream has no signal at all, so every abandoned * page request runs to completion. Ignoring it still works. */ export type InfiniteOptionsLoader = (params: InfiniteOptionsPageParams, signal: AbortSignal) => Promise> | InfiniteOptionsPage; export interface UseInfiniteOptionsParams { /** Identifies the combobox in the error it logs when a page fails. */ id: string; /** Whether infinite mode is active. `false` makes every member of the result inert. */ enabled: boolean; /** * Omitting this yields a loader that reports no items and no more pages, matching * upstream's `NOOP_CALLBACK`. It exists so a caller can pass `undefined` while not in * infinite mode without the hook throwing; `enabled: false` is the better way to say that. */ fetchPage?: InfiniteOptionsLoader; /** @default 100 */ pageSize?: number; /** Scrolled fraction at which the next page loads. @default 0.8 */ scrollThreshold?: number; /** Debounce for search-term-driven first-page loads. @default 250 */ debounceMs?: number; /** Throttle for the scroll listener. @default 300 */ throttleMs?: number; } export interface UseInfiniteOptionsResult { options: TOption[]; /** True while any page request is in flight. */ isLoading: boolean; /** True only while the very first page of the current search is in flight. */ isInitializing: boolean; /** False once a page says so. Paging stops. */ hasMore: boolean; totalNumberOfItems: number | undefined; /** * The last failure, or `undefined`. Upstream only `console.error`s, so a consumer * cannot render an error UI — the list simply stops growing and looks like the end of * the data. Exposed here for the same reason {@link useAsyncOptions} exposes it. */ error: unknown; /** Discards everything loaded and loads page one for `searchTerm`. */ loadFirstPage: (searchTerm: string) => Promise; debouncedLoadFirstPage: Debounced<[string]>; /** Appends the next page. A no-op while a request is in flight or when `hasMore` is false. */ loadNextPage: () => Promise; /** Ref-callback for the scroll container. Pass `null` to detach. */ attachScrollListener: (element: HTMLElement | null) => void; /** Returns to the pre-load state without fetching. Aborts anything in flight. */ reset: () => void; } /** * Drives a paged `options` loader for `Combobox`. * * Ported from maker-ds `form-inputs/combobox-shared/use-infinite-scroll.ts`. The * mechanism is upstream's: offset advanced by `items.length`, both offset and cursor sent * every call, `hasMore` as the only terminator, a throttled scroll listener firing at a * scrolled fraction, and a synchronous `isLoadingRef` mutex so two scroll events in the * same tick cannot both start a fetch — a `useState` flag cannot do that job, because the * second event reads the pre-render value. * * `attachScrollListener` stores the element in state rather than a ref, also upstream's * choice and a deliberate one: the popup mounts after the hook, so the listener effect has * to re-run when the element appears. A ref would not re-render and the listener would * never attach. * * Four upstream defects are fixed here: * * 1. **First-page loads could interleave with next-page loads.** Upstream's * `loadFirstPage` has neither the `isLoadingRef` guard nor any stale check, so a * debounced search landing while a scroll-triggered page is in flight lets that older * page `setItems((prev) => [...prev, ...])` on top of the *new* search's results. Fixed * with a monotonic request id: a page whose id is no longer current cannot append, set * `hasMore`, advance the offset, or clear the loading flags. A first-page load * deliberately does not respect the mutex — it supersedes, bumping the id and aborting * the in-flight request, because the newest search is always the one the user wants. * 2. **An empty result refetched forever.** Upstream's mount effect lists `items.length` * in its dependencies and bails when it is non-zero, so a backend that legitimately * returns nothing leaves the condition true and the effect re-runs on every render, * fetching in a loop. Fixed with an `initializedRef` latch, the same shape * {@link useAsyncOptions} uses for StrictMode's double effect, which this also covers. * 3. **No cancellation.** See {@link InfiniteOptionsLoader}. * 4. **Failures were invisible to the consumer.** See `error` above. * * Deliberately not ported: * * - **The generic `` element type.** Upstream pages arbitrary objects; here the pages * are combobox options, so `TOption extends ComboboxOption` and values are strings. * - **Filtering the accumulated list.** Search is the loader's job in this mode — that is * what `searchTerm` is for — and re-filtering client-side would hide rows the server * deliberately returned. * * `scrollThreshold`, `throttleMs`, and `reset` are kept even though upstream's combobox * never passes or calls them. They are legitimate configuration rather than dead * capability, and unlike upstream they are covered by tests here. */ export declare function useInfiniteOptions({ id, enabled, fetchPage, pageSize, scrollThreshold, debounceMs, throttleMs, }: UseInfiniteOptionsParams): UseInfiniteOptionsResult; //# sourceMappingURL=use-infinite-options.d.ts.map