// Copyright (c) Meta Platforms, Inc. and affiliates. 'use client'; /** * @file List.tsx * @input Uses React, ReactNode, StyleXStyles, theme tokens, ListContext * @output Exports List component, ListProps, ListDensity, ListStyle types * @position Core implementation; consumed by index.ts, tested by List.test.tsx * * SYNC: When modified, update these files to stay in sync: * - /packages/core/src/List/List.doc.mjs * - /packages/core/src/List/List.test.tsx * - /packages/core/src/List/index.ts * - /apps/storybook/stories/List.stories.tsx * - /packages/cli/assets/templates/blocks/components/List/ (showcase blocks) */ import { useId, useMemo, type ReactNode } from 'react'; import * as stylex from "../style-runtime.js"; import "../theme/tokens.stylex.js"; import { spacingVars } from "../theme/tokens.stylex.js"; import type { BaseProps } from "../BaseProps.js"; import { ListContext, type ListDensity, type ListMarkerStyle } from "./ListContext.js"; import { mergeProps } from "../utils/index.js"; import { themeProps } from "../utils/themeProps.js"; export { type ListDensity, type ListMarkerStyle as ListStyle } from "./ListContext.js"; export interface ListProps extends BaseProps { /** Ref forwarded to the root element */ ref?: React.Ref; /** * List items. Should be ListItem components. */ children: ReactNode; /** * Spacing density for list items. * - 'compact': Tighter spacing for dense UIs * - 'balanced': Standard spacing * - 'spacious': Extra spacing for readability * @default 'balanced' */ density?: ListDensity; /** * Whether to show dividers between list items. * @default false */ hasDividers?: boolean; /** * Header content rendered above the list. * Semantically associated via aria-labelledby. */ header?: ReactNode; /** * List marker style. * When 'decimal', renders an `
    `. Otherwise renders a `
      `. * @default 'none' */ listStyle?: ListMarkerStyle; /** * Starting number for ordered lists (listStyle='decimal'). * Sets the CSS counter to begin at this value. * @default 1 */ start?: number; /** * Test ID for testing frameworks. */ 'data-testid'?: string; } // ============================================================================= // Styles // ============================================================================= const styles = { list: { kogj98: "x1ghz6dp", kZCmMZ: "x1c1uobl", kH6xsr: "x3ct3a4", k1xSpc: "x78zum5", kXwgrk: "xdt5ytf", kOIVth: "x1lsbc85", $$css: true as const }, withDividers: { kOIVth: "xxhr3t", $$css: true as const }, withCounter: { kt6KFK: "xif0320", $$css: true as const } }; const _temp = { kt6KFK: "x1khind5", "$$css": true }; const dynamicStyles = { counterStart: (value: number) => [_temp, { "--x-counterReset": `astryx-list ${value}` != null ? `astryx-list ${value}` : undefined }] }; // ============================================================================= // Component // ============================================================================= /** * A vertical list component for rendering collections of items. * * Renders semantic `
        ` or `
          ` elements with configurable density, * dividers, marker styles, and an optional header. * * @example * ``` * * * * * * * * * ``` */ export function List({ children, density = 'balanced', hasDividers = false, header, listStyle = 'none', start, xstyle, className, style, 'data-testid': testId, ref, ...props }: ListProps) { const headerId = useId(); const isOrdered = listStyle === 'decimal'; const Tag = isOrdered ? 'ol' : 'ul'; const contextValue = useMemo(() => ({ density, hasDividers, listStyle }), [density, hasDividers, listStyle]); const listElement = } // Consumer props first: everything the component sets for itself below // is part of its contract and wins on conflict. {...props} data-testid={testId} // Only when this component renders the header — writing `undefined` // unconditionally would erase a label a consumer pointed at their own // heading. {...header != null ? { 'aria-labelledby': headerId } : null} {...isOrdered && start != null && start !== 1 ? { start } : {}} // The base list style always sets list-style-type: none (markers are // custom-rendered by ListItem), and Safari/VoiceOver drops implicit // list semantics for lists styled with list-style: none. The explicit // role restores "list, N items" announcements for every listStyle // variant. role="list" {...mergeProps(themeProps('list', { density, listStyle }), stylex.props(styles.list, hasDividers && styles.withDividers, listStyle !== 'none' && (start != null && start !== 1 ? dynamicStyles.counterStart(start - 1) : styles.withCounter), xstyle), className, style)}> {children} ; if (header == null) { return {listElement}; } return
          {header}
          {listElement}
          ; } List.displayName = 'List';