UNPKG

react-horizontal-scrolling-menu

Version:

Horizontal scrolling menu component for React with per-item visibility tracking. Supports mouse and touch devices, SSR-safe.

192 lines (154 loc) 12.3 kB
# publicApiType member reference — react-horizontal-scrolling-menu 8.2.3 The complete api object surface, verified against `src/createApi.ts`, `src/ItemsMap/ItemsMap.ts`, `src/helpers.tsx` and `src/types.ts`. How to obtain the api: - Inside items/arrows/Header/Footer: `const api = React.useContext<publicApiType>(VisibilityContext);` - Outside the menu: the `apiRef` prop (populated in an effect after mount — fire methods only, never read data values from it) - Callbacks: `onInit`/`onUpdate` receive it as their only argument, `onWheel`/`onScroll` as the first argument, and every mouse/touch prop is a factory receiving it: `(api) => (event) => void` ## Shared types (src/types.ts) ```ts type ItemId = string; interface IOItem { index: string; // render order as a numeric string key: ItemId; // the itemId entry: IntersectionObserverEntry; visible: boolean; } type Item = [itemId: ItemId, observerEntry: IOItem]; type visibleElements = ItemId[]; // plain array of ids type ItemOrElement = IOItem | Element | undefined; // note: never a bare string // 'auto' | 'instant' | 'smooth', or a custom function (polyfill mode only) type ScrollBehaviorArg = ScrollBehavior | CustomScrollBehavior; interface scrollToItemOptions { boundary?: HTMLElement | null; // polyfill mode only duration?: number; // ms, polyfill mode only behavior: ScrollBehaviorArg; // required by the type but ALWAYS overridden // by the positional behavior argument — do not rely on it } ``` ## Getters | Member | Signature | Notes | | ----------------------- | -------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | | `getItemById` | `(id: ItemId) => IOItem \| undefined` | `String()`-coerces id. Reads the ItemsMap, which lags children by one render — returns undefined for items added this render | | `getItemByIndex` | `(index: number \| string) => IOItem \| undefined` | Compares `String(index)` against `IOItem.index` | | `getItemElementById` | `(id: ItemId) => Element \| null` | `document.querySelector("[data-key='<id>']")` — DOM truth, stale-proof for just-added items | | `getItemElementByIndex` | `(index: ItemId) => Element \| null` | `document.querySelector("[data-index='<index>']")` | | `getNextElement` | `() => IOItem \| undefined` | The item after the last currently visible one; undefined when nothing is visible yet | | `getPrevElement` | `() => IOItem \| undefined` | The item before the first currently visible one; undefined when nothing is visible yet | | `isItemVisible` | `(id: ItemId) => boolean` | Checks membership in `items.getVisible()` | | `isLastItem` | `(id: ItemId) => boolean` | Whether id is the last item in the row | ## Scroll methods ```ts scrollToItem( target?: ItemOrElement, // api.getItemById(id) or api.getItemElementById(id) behavior?: ScrollBehaviorArg, // default 'smooth' (or the transitionBehavior prop) inline?: ScrollLogicalPosition, // default 'end' block?: ScrollLogicalPosition, // default 'nearest' options?: scrollToItemOptions, // { duration, boundary } — noPolyfill={false} only; ): void // options.behavior is ignored (positional arg wins) ``` A bare string target is a silent no-op — the JSDoc example showing `'itemId'` is wrong. Undefined target returns without scrolling. ```ts scrollPrev( behavior?: ScrollBehaviorArg, // default 'smooth' inline?: ScrollLogicalPosition, // default 'end' block?: ScrollLogicalPosition, // default 'nearest' options?: { duration?: number; boundary?: HTMLElement | null }, ): void scrollNext( behavior?: ScrollBehaviorArg, // default 'smooth' inline?: ScrollLogicalPosition, // default 'start' block?: ScrollLogicalPosition, // default 'nearest' options?: { duration?: number; boundary?: HTMLElement | null }, ): void ``` `scrollPrev` targets `getPrevElement()`, `scrollNext` targets `getNextElement()` — one visible-group per call. Both are no-ops while no item has been seen yet (e.g. menu below the fold). All three drag ancestor scroll containers when the menu is off screen — gate on `menuVisible.current`. ## Hooks Call only inside components rendered under ScrollMenu, following the rules of hooks. | Hook | Signature | Notes | | ---------------------- | -------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | | `useIsVisible` | `(itemId: ItemId \| 'first' \| 'last', defaultValue?: boolean) => boolean` | `defaultValue` defaults to `false` and is the SSR/first-paint value | | `useLeftArrowVisible` | `() => boolean` | True when the first item is visible (disable the left arrow); latched — updates only while `menuVisible.current` | | `useRightArrowVisible` | `() => boolean` | Same for the last item / right arrow | ## Frozen snapshots — never use for reactivity | Member | Type | | -------------------- | --------- | | `isFirstItemVisible` | `boolean` | | `isLastItemVisible` | `boolean` | Computed once when the api memo runs — while the ItemsMap can still be empty — and never recomputed (src/createApi.ts:91-92). They read as `false` and stay frozen. Use `useLeftArrowVisible`/`useRightArrowVisible` or `useIsVisible('first' | 'last')` instead; inside callbacks, read `api.items.getVisible()`. ## items: ItemsMap Extends `Map<ItemId, IOItem>` — inherited `get`, `has`, `size`, `keys`, `values`, `entries`, `forEach` all work — plus: | Method | Signature | Notes | | --------------- | --------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | | `subscribe` | `(key: ItemId \| 'first' \| 'last' \| 'onInit' \| 'onUpdate', fn: (item?: IOItem) => void) => void` | Visibility-change events per item or edge | | `unsubscribe` | `(key: same, fn: same) => void` | Requires the SAME fn instance passed to subscribe; pair in a useEffect cleanup | | `toArr` | `() => Item[]` | All items sorted by numeric index | | `toItems` | `() => ItemId[]` | All ids in render order | | `sort` | `(arr: Item[]) => Item[]` | Sorts an Item array by numeric index in place | | `first` | `() => IOItem \| undefined` | First item in the row | | `last` | `() => IOItem \| undefined` | Last item in the row | | `filter` | `(predicate: (value: Item, index: number, array: Item[]) => boolean) => Item[]` | | | `find` | `(predicate: (value: Item, index: number, obj: Item[]) => boolean) => Item \| undefined` | | | `findIndex` | `(predicate: (value: Item, index: number, obj: Item[]) => unknown) => number` | | | `getCurrentPos` | `(item: ItemId \| IOItem) => [Item[], number]` | Sorted array plus the item's position in it (-1 when absent) | | `prev` | `(item: ItemId \| IOItem) => IOItem \| undefined` | Item before the given one | | `next` | `(item: ItemId \| IOItem) => IOItem \| undefined` | Item after the given one | | `getVisible` | `() => Item[]` | Only currently-visible items — ids via `.map(([id]) => id)` | | `set` | `(key: ItemId, value: IOItem) => this` | Internal — the observer writes entries; do not call | | `setBatch` | `(entries: Item[]) => this` | Internal — first IntersectionObserver batch; do not call | The map lags children by one render: an item appended this render is not in it yet — use `getItemElementById`/`getItemElementByIndex` for those. ## scrollContainer `scrollContainer: React.RefObject<HTMLElement | null>` — the scrollable container div. Read `scrollContainer.current?.scrollLeft` to save position (from `onUpdate`, not `onScroll` — onScroll fires before the scroll settles); assign `scrollLeft` for instant teleports (infinite-loop seams, position restore). ## menuVisible `menuVisible: { current: boolean }` — true while the menu itself is on screen (plain ref object, not reactive). Gate every programmatic scroll on it: scrollIntoView reaches ancestor scroll containers, so scrolling an off-screen menu jumps the whole page to it. ## Standalone package exports ```ts import { ScrollMenu, VisibilityContext, slidingWindow, getItemsPos, constants, type publicApiType, type ItemId, } from 'react-horizontal-scrolling-menu'; ``` ```ts slidingWindow( allItems: ItemId[], // api.items.toItems() visibleElements: ItemId[], // api.items.getVisible().map(([id]) => id) ): { prev: () => ItemId[]; next: () => ItemId[] } // Returns the previous/next group of ids, same size as the visible set, // clamped at the row edges. getItemsPos(items: ItemId[]): { first: ItemId; center: ItemId; last: ItemId } // First, middle and last id of a group — feed center to // scrollToItem(getItemById(center), 'smooth', 'center'). ``` `constants` exposes the CSS class names (`react-horizontal-scrolling-menu--item`, `--scroll-container`, `--wrapper`, `--inner-wrapper`, `--header`, `--footer`, `--arrow-left`, `--arrow-right`), the `data-key`/`data-index` attribute names, and the event keys (`first`, `last`, `onInit`, `onUpdate`).