// Copyright (c) Meta Platforms, Inc. and affiliates. 'use client'; /** * @file Banner.tsx * @input Uses React useState/useRef/useId, useCollapsible, Button, Icon (with registry string names), StyleX * @output Exports Banner component, BannerProps, BannerStatus, BannerContainer types * @position Core implementation; consumed by index.ts, tested by Banner.test.tsx * * Visual structure: * - Banner frame (themeProps 'banner-frame'): owns the resting elevation and, * for elevated card banners, the outer radius that shapes that shadow * - Header area (themeProps 'banner'): colored status background with icon, title, description, actions, dismiss * - Content area (themeProps 'banner-content'): card background for additional content (children) * - Status icon (themeProps 'banner-icon'): the target rides on the default * itself — the element that paints — so 'status:X' overrides reach * the glyph (#4166); for a custom `icon` node it stays on the layout wrapper * - Description (themeProps 'banner-description'): the supporting line owns its * own colour and type, and the space between it and the title * - The end area carries no target: it is a layout row (flex, wrap, edge * compensation) rather than a painted surface, and a theme that wants the * header to grow around its buttons sets `padding-block` on 'banner' * - No left border accent — color is expressed through the full header background * - Each visual area owns its own border-radius (no overflow:clip on the container) * - Children are collapsible by default: a toggle appears in the header end * area and the content starts closed. `collapsible={false}` opts out and * pins the content open with no toggle. The whole collapse axis (enabled / * default / controlled) lives on that one prop and is driven by the shared * `useCollapsible` hook rather than local state. * * A status added through `BannerStatusMap` augmentation has no entry in the * status lookups, so it renders with no status fill, no default glyph and the * polite `role="status"` rather than losing its ARIA role entirely. * * Title and description render as
(not

): they accept arbitrary * ReactNode content, and

cannot legally contain block-level children * (the HTML parser reparents them, desyncing SSR markup from the hydrated * DOM). Using

keeps these slots composable with any content. Their * StyleX styles set margin: 0 and explicit typography, so the rendered * appearance is identical to the previous

. * * SYNC: When modified, update these files to stay in sync: * - /packages/core/src/Banner/Banner.doc.mjs (props table, features, implementation notes) * - /packages/core/src/Banner/Banner.test.tsx (tests for new/changed behavior) * - /packages/core/src/Banner/index.ts (exports if types change) * - /apps/storybook/stories/Banner.stories.tsx (storybook stories) * - /packages/cli/assets/templates/blocks/components/Banner/ (showcase blocks) */ import { useId, useRef, useState, type ReactNode } from 'react'; import * as stylex from "../style-runtime.js"; import type { BaseProps } from "../BaseProps.js"; import { Button } from "../Button/index.js"; import { Icon } from "../Icon/index.js"; import type { IconName, IconColor } from "../Icon/index.js"; import "../theme/tokens.stylex.js"; import { colorVars, spacingVars, radiusVars, fontWeightVars, typeScaleVars, borderVars, durationVars, easeVars, shadowVars } from "../theme/tokens.stylex.js"; import { useCollapsible } from "../Collapsible/useCollapsible.js"; import type { CollapsibleConfig } from "../Collapsible/useCollapsible.js"; import { composeEventHandlers, isRenderable, mergeProps } from "../utils/index.js"; import type { Elevation } from "../utils/types.js"; import { edgeCompSlot } from "../Layout/edgeCompensation.stylex.js"; import { themeProps } from "../utils/themeProps.js"; import { useTranslator } from "../i18n/index.js"; import type { BannerStatusMap, BannerContainerMap } from "./index.js"; // ============================================================================= // Types // ============================================================================= /** * Status type controlling the banner's icon and color. * Extensible via module augmentation of BannerStatusMap. */ export type BannerStatus = keyof BannerStatusMap; /** * Container type of the banner. * - `card`: standalone card with border-radius and shadow * - `section`: full-width section banner (no border-radius) * * Extensible via module augmentation of BannerContainerMap. */ export type BannerContainer = keyof BannerContainerMap; export interface BannerProps extends BaseProps { /** Ref forwarded to the root element */ ref?: React.Ref; /** * Status type controlling the icon and color scheme. */ status: BannerStatus; /** * Title text or ReactNode displayed prominently in the header area. */ title: ReactNode; /** * Optional description text below the title in the header area. */ description?: ReactNode; /** * Override the default status icon. */ icon?: ReactNode; /** * Whether the banner can be dismissed. * When true, shows a close button and manages internal dismissed state * so the banner disappears even if `onDismiss` is not provided. * @default false */ isDismissable?: boolean; /** * Called when the dismiss button is clicked. * The banner will hide itself regardless of whether this callback is provided. */ onDismiss?: () => void; /** * Accessible name and visible tooltip for the dismiss button, replacing the default. * Pass an already-translated string. * * The default is "Dismiss {title}" when `title` is a string, so stacked * banners are told apart by a screen reader; a non-string `title` falls back * to "Dismiss" and should set this. */ dismissLabel?: string; /** * Action button rendered in the header area (end-aligned). * Typically an Button with a secondary or ghost variant. * * When the header is too narrow to hold the actions and a readable text * column, the whole end area wraps to its own row below the text. * * @example * ``` * endContent={

}
{/* Content area: collapsible card background — theme target ('banner-content') */} {showContent &&
{children}
} ; } Banner.displayName = 'Banner';