// Copyright (c) Meta Platforms, Inc. and affiliates. 'use client'; /** * @file MetadataList.tsx * @input Uses React, ReactNode, StyleXStyles, theme tokens, MetadataListContext, i18n (useTranslator) * @output Exports MetadataList component, MetadataListProps, MetadataListColumns types * @position Core implementation; consumed by index.ts, tested by MetadataList.test.tsx * * SYNC: When modified, update these files to stay in sync: * - /packages/core/src/MetadataList/MetadataList.doc.mjs * - /packages/core/src/MetadataList/MetadataList.test.tsx * - /packages/core/src/MetadataList/index.ts * - /apps/storybook/stories/MetadataList.stories.tsx * - /packages/cli/assets/templates/blocks/components/MetadataList/ (showcase blocks) */ import { Children, useId, useMemo, useState, type ReactNode } from 'react'; import * as stylex from "../style-runtime.js"; import "../theme/tokens.stylex.js"; import { spacingVars, colorVars, typeScaleVars, fontWeightVars } from "../theme/tokens.stylex.js"; import { MetadataListContext, type MetadataListLabelConfig } from "./MetadataListContext.js"; import type { BaseProps } from "../BaseProps.js"; import { mergeProps } from "../utils/index.js"; import { themeProps } from "../utils/themeProps.js"; import { useTranslator } from "../i18n/index.js"; // ============================================================================= // Types // ============================================================================= export type MetadataListColumns = 'multi' | 'single' | number; export interface MetadataListProps extends Omit, 'title'> { /** Ref forwarded to the root element */ ref?: React.Ref; /** * Metadata list items. Should be MetadataListItem components. */ children: ReactNode; /** * Column layout mode. * - 'single': Items in a single column * - 'multi': Auto-fill columns based on available width * - number: Fixed number of columns * @default 'single' */ columns?: MetadataListColumns; /** * Label display configuration. * - position: 'start' places labels to the left, 'top' stacks labels above content * - width: Custom label width (number in px or CSS string) * * Defaults to `{ position: 'top' }` for multi-column layouts and * `{ position: 'start' }` for single-column layouts. */ label?: MetadataListLabelConfig; /** * Maximum number of items to show before collapsing. * When set and items exceed this count, a "Show more" / "Show less" * toggle appears. */ maxNumOfItems?: number; /** * Layout orientation for metadata items. * - 'vertical': Items stack vertically (default) * - 'horizontal': Items flow horizontally with flex-wrap * * In horizontal mode, items display with labels stacked above content * and wrap to new lines as needed. The following props are ignored: * `columns`, `label`, `maxNumOfItems`. * @default 'vertical' */ orientation?: 'vertical' | 'horizontal'; /** * Optional title or heading rendered above the list. */ title?: ReactNode; /** * Test ID for testing frameworks. */ 'data-testid'?: string; } // ============================================================================= // Styles // ============================================================================= const styles = { root: { k1xSpc: "x78zum5", kXwgrk: "xdt5ytf", $$css: true as const }, dl: { kogj98: "x1ghz6dp", kmVPX3: "x1717udv", $$css: true as const }, gridSingle: { k1xSpc: "xrvj5dj", kumcoG: "x1pmbctz", kOIVth: "xpec5dj", kGNEyG: "x1pha0wt", $$css: true as const }, gridMulti: { k1xSpc: "xrvj5dj", kumcoG: "x189bvgu", kOIVth: "x18g69wz", $$css: true as const }, gridStackedSingle: { k1xSpc: "xrvj5dj", kumcoG: "x1y6fwsi", kOIVth: "xjcht0a", $$css: true as const }, gridStackedMulti: { k1xSpc: "xrvj5dj", kumcoG: "x189bvgu", kOIVth: "x18g69wz", $$css: true as const }, horizontal: { k1xSpc: "x78zum5", kXwgrk: "x1q0g3np", kwnvtZ: "x1a02dak", kOIVth: "x18g69wz", $$css: true as const } }; // A fixed numeric column count (and a custom label width) is only known at // runtime, so it comes from a dynamic style rather than a static rule. const dynamicStyles = { gridTemplate: (gridTemplateColumns: string) => [{ kumcoG: gridTemplateColumns != null ? "xqketvx" : gridTemplateColumns, $$css: true as const }, { "--x-gridTemplateColumns": gridTemplateColumns != null ? gridTemplateColumns : undefined }] }; // ============================================================================= // Component // ============================================================================= /** * A read-only labeled list for displaying key-value metadata. * * Renders semantic `
` / `
` / `
` elements. Supports multiple * column layouts, horizontal orientation, and show more/less collapsing. * * @example * ``` * * MetadataList * Active * * ``` */ const LABEL_START: MetadataListLabelConfig = { position: 'start' }; const LABEL_TOP: MetadataListLabelConfig = { position: 'top' }; export function MetadataList({ children, columns = 'single', label, maxNumOfItems, orientation = 'vertical', title, xstyle, className, style, 'data-testid': testId, ref }: MetadataListProps) { const isMultiColumn = columns === 'multi' || typeof columns === 'number' && columns > 1; // Default to 'top' for multi-column and horizontal layouts since side labels // don't work well when items are in separate grid cells. const labelConfig = label ?? (isMultiColumn ? LABEL_TOP : LABEL_START); const [isShowAll, setIsShowAll] = useState(false); const contentId = useId(); const t = useTranslator(); const contextValue = useMemo(() => ({ labelConfig: orientation === 'horizontal' ? LABEL_TOP : labelConfig, orientation }), [labelConfig, orientation]); const childrenArray = Children.toArray(children); const isHorizontal = orientation === 'horizontal'; // In horizontal mode, ignore maxNumOfItems const effectiveMax = isHorizontal ? undefined : maxNumOfItems; const isExceedMax = effectiveMax != null && childrenArray.length > effectiveMax; const visibleChildren = isExceedMax && !isShowAll ? childrenArray.slice(0, effectiveMax) : childrenArray; const titleContent = title != null ?
{title}
: null; // Determine grid style based on columns and label position const getGridStyle = () => { if (isHorizontal) { return styles.horizontal; } const isStacked = labelConfig.position === 'top'; if (isStacked) { return columns === 'single' || columns === 1 ? styles.gridStackedSingle : styles.gridStackedMulti; } // Labels on the left if (columns === 'single' || columns === 1) { return styles.gridSingle; } return styles.gridMulti; }; // The grid template for a fixed numeric column count, or for a custom label // width. Both are runtime values, so they resolve to a dynamic style below. // The track shape depends on the label position: stacked labels put a whole // item in one cell, while side labels split each item into a label track and // a value track. const getGridTemplateColumns = () => { if (isHorizontal) { return null; } const isStacked = labelConfig.position === 'top'; if (typeof columns === 'number' && columns > 1) { return isStacked ? `repeat(${columns}, 1fr)` : `repeat(${columns}, auto 1fr)`; } // A custom label width only applies to the label track of side labels. if (!isStacked && labelConfig.width != null) { const width = typeof labelConfig.width === 'number' ? `${labelConfig.width}px` : labelConfig.width; return `${width} 1fr`; } return null; }; const gridTemplateColumns = getGridTemplateColumns(); return
{titleContent}
{visibleChildren}
{isExceedMax && }
; } MetadataList.displayName = 'MetadataList';