// Copyright (c) Meta Platforms, Inc. and affiliates. 'use client'; /** * @file ScrollableArea.tsx * @input Logical scroll intent, accessible name, overscroll policy, viewport sizing, content padding, and content * @output Native scroll viewport with an observed real content box * @position Reference composition over useScrollableArea * * SYNC: When modified, update: * - /packages/core/src/ScrollableArea/ScrollableArea.test.tsx * - /packages/core/src/ScrollableArea/ScrollableArea.doc.mjs * - /apps/storybook/stories/ScrollableArea.stories.tsx */ import type { ReactNode } from 'react'; import * as stylex from "../style-runtime.js"; import type { BaseProps } from "../BaseProps.js"; import { paddingStyles, paddingInlineStyles, paddingInlineStartStyles, paddingInlineEndStyles, paddingBlockStyles, paddingBlockStartStyles, paddingBlockEndStyles, containerPaddingInlineVarStyles, containerPaddingInlineStartVarStyles, containerPaddingInlineEndVarStyles, containerPaddingBlockStartVarStyles, containerPaddingBlockEndVarStyles } from "../Layout/padding.stylex.js"; import type { SizeValue, SpacingStep } from "../utils/types.js"; import { useScrollableArea, type ScrollAxis, type ScrollOverscroll, type ScrollStickyContainment } from "../hooks/useScrollableArea.js"; import "../theme/tokens.stylex.js"; import { colorVars } from "../theme/tokens.stylex.js"; import { focusOutlineStyles } from "../utils/focusOutline.stylex.js"; import { mergeProps } from "../utils/mergeProps.js"; import { themeProps } from "../utils/themeProps.js"; const styles = { viewport: { kB7OPa: "x9f619", ks0D6T: "x193iq5w", kskxy: "xmz0i5r", kuHK5b: "x5lxg6s", k6w4Gs: "x19jbwzc xrih18v", k5wCbM: "xosa0jh", kZ7BSC: "x24f09q", k9g6sI: "xafyuhj", $$css: true as const }, fullBleed: { keTefX: "xojxgvx", k71WvV: "x1fcf3bl", ks0D6T: "x1rfgw7b", keoZOQ: "xkibk3", k1K539: "xlayyun", $$css: true as const }, content: { kB7OPa: "x9f619", k7Eaqz: "xgqtt45", kAzted: "x1us19tq", $$css: true as const }, contentWithInlineOverflow: { kzqmXN: "xezivpi", $$css: true as const } }; const dynamicStyles = { sizing: (width: SizeValue | null, height: SizeValue | null, maxWidth: SizeValue | null, minHeight: SizeValue | null) => [{ kzqmXN: width != null ? "x5lhr3w" : width, kZKoxP: height != null ? "x16ye13r" : height, ks0D6T: maxWidth != null ? "xf68679" : maxWidth, kAzted: minHeight != null ? "x82snj4" : minHeight, $$css: true as const }, { "--x-width": (val => typeof val === "number" ? val + "px" : val != null ? val : undefined)(width), "--x-height": (val => typeof val === "number" ? val + "px" : val != null ? val : undefined)(height), "--x-maxWidth": (val => typeof val === "number" ? val + "px" : val != null ? val : undefined)(maxWidth), "--x-minHeight": (val => typeof val === "number" ? val + "px" : val != null ? val : undefined)(minHeight) }] }; export type ScrollableAreaStickyContainment = ScrollStickyContainment; export interface ScrollableAreaProps extends Omit, 'aria-label' | 'children' | 'role' | 'tabIndex'> { /** Content rendered inside the observed content box. */ children?: ReactNode; /** Logical axis or axes where scrolling is allowed. @default 'block' */ axis?: ScrollAxis; /** Accessible name used when an effective viewport enters the tab order. */ label: string; /** Semantics for the named scroll viewport. @default 'group' */ role?: 'group' | 'region'; /** Whether effective axes pass scroll gestures to ancestors at an edge. @default 'allow' */ overscroll?: ScrollOverscroll; /** Width of the viewport; numbers are interpreted as pixels. */ width?: SizeValue; /** Height of the viewport; numbers are interpreted as pixels. */ height?: SizeValue; /** Maximum width of the viewport. */ maxWidth?: SizeValue; /** Minimum height of the viewport. */ minHeight?: SizeValue; /** Whether a fitting viewport deliberately remains a Sticky containing boundary. @default 'whenScrollable' */ stickyContainment?: ScrollableAreaStickyContainment; /** Content padding using the shared spacing scale. @default 0 */ padding?: SpacingStep; /** Logical inline-axis content padding; overrides `padding` on that axis. */ paddingInline?: SpacingStep; /** Logical inline-start content padding; overrides broader padding values. */ paddingInlineStart?: SpacingStep; /** Logical inline-end content padding; overrides broader padding values. */ paddingInlineEnd?: SpacingStep; /** Logical block-axis content padding; overrides `padding` on that axis. */ paddingBlock?: SpacingStep; /** Logical block-start content padding; overrides broader padding values. */ paddingBlockStart?: SpacingStep; /** Logical block-end content padding; overrides broader padding values. */ paddingBlockEnd?: SpacingStep; /** Let the viewport escape inherited container padding. @default false */ isFullBleed?: boolean; /** Ref connected to the native scroll viewport. */ ref?: React.Ref; } /** * A native scroll viewport with axis-aware accessibility and chaining. * * The root is the viewport. Its single child is a normal block content box with * a 100% minimum size; children participate in that box rather than directly in * viewport flex/grid layout. Inline scrolling gives the box max-content inline * sizing, and its minimum block size deliberately does not create a definite * percentage-height basis. Use the hook on owned structure when those formatting * semantics must stay unchanged. Native scrolling and platform scrollbars remain * authoritative. * * @example * ``` * * * * ``` */ export function ScrollableArea({ children, axis = 'block', label, role = 'group', overscroll = 'allow', width, height, maxWidth, minHeight, stickyContainment = 'whenScrollable', padding = 0, paddingInline, paddingInlineStart, paddingInlineEnd, paddingBlock, paddingBlockStart, paddingBlockEnd, isFullBleed = false, ref, xstyle, className, style, ...props }: ScrollableAreaProps) { const { getViewportProps, getContentProps } = useScrollableArea({ axis, keyboardAccess: { owner: 'viewport', label, role }, overscroll, stickyContainment }); const mergedViewportProps = mergeProps(themeProps('scrollable-area', { axis }), stylex.props(styles.viewport, isFullBleed && styles.fullBleed, focusOutlineStyles.focusVisible, dynamicStyles.sizing(width ?? null, height ?? null, maxWidth ?? null, minHeight ?? null), xstyle), className, style); const viewportProps = getViewportProps({ ...props, ref, ...mergedViewportProps }); return
({ ...stylex.props(styles.content, axis !== 'block' && styles.contentWithInlineOverflow, paddingStyles[padding], containerPaddingInlineVarStyles[padding], containerPaddingBlockStartVarStyles[padding], containerPaddingBlockEndVarStyles[padding], paddingInline != null && paddingInlineStyles[paddingInline], paddingInline != null && containerPaddingInlineVarStyles[paddingInline], paddingBlock != null && paddingBlockStyles[paddingBlock], paddingBlock != null && containerPaddingBlockStartVarStyles[paddingBlock], paddingBlock != null && containerPaddingBlockEndVarStyles[paddingBlock], paddingInlineStart != null && paddingInlineStartStyles[paddingInlineStart], paddingInlineStart != null && containerPaddingInlineStartVarStyles[paddingInlineStart], paddingInlineEnd != null && paddingInlineEndStyles[paddingInlineEnd], paddingInlineEnd != null && containerPaddingInlineEndVarStyles[paddingInlineEnd], paddingBlockStart != null && paddingBlockStartStyles[paddingBlockStart], paddingBlockStart != null && containerPaddingBlockStartVarStyles[paddingBlockStart], paddingBlockEnd != null && paddingBlockEndStyles[paddingBlockEnd], paddingBlockEnd != null && containerPaddingBlockEndVarStyles[paddingBlockEnd]) })}> {children}
; } ScrollableArea.displayName = 'ScrollableArea';