// Copyright (c) Meta Platforms, Inc. and affiliates. 'use client'; /** * @file OverflowList.tsx * @input Uses React, StyleX, useOverflow hook * @output Exports OverflowList component and OverflowListProps type * @position Core implementation; consumed by index.ts * * Renders a horizontal list of items, hiding those that don't fit in the * available width and optionally showing an overflow indicator. Supports an * optional item cap (`maxVisibleItems`) and bounded multi-row wrapping * (`maxRows`). Uses a hidden measurement container to avoid flickering. * * SYNC: When modified, update these files to stay in sync: * - /packages/core/src/OverflowList/index.ts (exports if types change) * - /packages/cli/assets/templates/blocks/components/OverflowList/ (showcase blocks) */ import { type ReactNode, type ReactElement, Children, useCallback, useRef, useState } from 'react'; import type { BaseProps } from "../BaseProps.js"; import type { SpacingStep } from "../utils/types.js"; import * as stylex from "../style-runtime.js"; import { mergeProps } from "../utils/index.js"; import { useOverflow } from "../hooks/useOverflow.js"; import { useIsomorphicLayoutEffect } from "../hooks/useIsomorphicLayoutEffect.js"; import "../theme/tokens.stylex.js"; import { spacingVars } from "../theme/tokens.stylex.js"; import { themeProps } from "../utils/themeProps.js"; import { useMergedRefs } from "../hooks/useMergedRefs.js"; const styles = { container: { k1xSpc: "x78zum5", kGNEyG: "x6s0dn4", kVQacm: "xb3r6kr", khDVqt: "xuxw1ft", k7Eaqz: "xeuugli", $$css: true as const }, containerMultiRow: { k1xSpc: "x78zum5", kwnvtZ: "x1a02dak", kfiyM8: "x8gbvx8", kVQacm: "xb3r6kr", khDVqt: "xeaf4i8", k7Eaqz: "xeuugli", $$css: true as const }, fillParent: { kzqmXN: "xh8yej3", $$css: true as const }, measureContainer: { kVAEAm: "x10l6tqk", k33iCy: "xlshs6z", kZKoxP: "xqtp20y", kVQacm: "xb3r6kr", k1xSpc: "x78zum5", kGNEyG: "x6s0dn4", khDVqt: "xuxw1ft", kfzvcC: "x47corl", $$css: true as const } }; const _temp = { kskxy: "x1jols5v", "$$css": true }; const multiRowHeight = { height: (maxRows: number, rowHeight: number, gapPx: number) => [_temp, { "--x-maxHeight": (val => typeof val === "number" ? val + "px" : val != null ? val : undefined)(`calc(${rowHeight}px * ${maxRows} + ${gapPx}px * ${maxRows - 1})`) }] }; const gapStyles = { "0": { kOIVth: "xsn7fz1", $$css: true as const }, "1": { kOIVth: "xzye2dw", $$css: true as const }, "2": { kOIVth: "x1txdalj", $$css: true as const }, "3": { kOIVth: "xjcht0a", $$css: true as const }, "4": { kOIVth: "x18g69wz", $$css: true as const }, "5": { kOIVth: "x9mgr7n", $$css: true as const }, "6": { kOIVth: "x1qh66ti", $$css: true as const }, "8": { kOIVth: "x4t41sb", $$css: true as const }, "10": { kOIVth: "x3hoi3v", $$css: true as const }, "0.5": { kOIVth: "x1lsbc85", $$css: true as const }, "1.5": { kOIVth: "x1s4dlld", $$css: true as const } }; /** * Maps spacing token steps to pixel values for overflow calculations. */ const spacingToPx: Record = { 0: 0, 0.5: 2, 1: 4, 1.5: 6, 2: 8, 3: 12, 4: 16, 5: 20, 6: 24, 8: 32, 10: 40 }; export interface OverflowItem { /** The React element for this item */ child: ReactElement; /** The index of this item in the original children list */ index: number; } export interface OverflowListProps extends BaseProps { /** Ref forwarded to the visible container element */ ref?: React.Ref; /** * The items to render. Each child should be a single element. */ children: ReactNode; /** * Gap between items as a spacing token step. * Accepts: 0, 0.5, 1, 1.5, 2, 3, 4, 5, 6, 8, 10 * @default 2 */ gap?: SpacingStep; /** * Minimum number of items to always show. * @default 0 */ minVisibleItems?: number; /** * Maximum number of items to ever show, even when they all fit. The ceiling * partner to `minVisibleItems`; extra items collapse into the overflow * indicator. Leave undefined for no cap. If it is less than * `minVisibleItems`, the floor wins (and a dev-only warning is logged). * @default undefined */ maxVisibleItems?: number; /** * Wrap items across up to this many rows before collapsing the remainder * into the overflow indicator. Leave undefined (or set `1`) for the default * single-line behavior. A number, not a boolean — unbounded wrapping is a * plain flex-wrap layout, not overflow collapse. Assumes uniform row height. * @default undefined */ maxRows?: number; /** * Which end to collapse items from. * @default 'end' */ collapseFrom?: 'start' | 'end'; /** * Which element to observe for overflow calculations. * - `'observeSelf'`: uses the container's own width (default) * - `'observeParent'`: observes the parent element's content width * for overflow calculations. This keeps the overflow list * content-sized while still detecting available space for * grow-back. Siblings that don't fit can wrap and be clipped by * the parent's overflow. * @default 'observeSelf' */ behavior?: 'observeParent' | 'observeSelf'; /** * Render function for the overflow indicator. Receives the list of * items that are not visible, each with its original index. Only called * when there are overflowing items. * * The indicator is automatically measured in a hidden container to * reserve the correct amount of space. * * @example * ``` * const labels = ['Save', 'Edit', 'Share']; * ( * ({ label: labels[index] }))} * /> * )}> * {labels.map(l =>