'use client'; import React, { forwardRef } from 'react'; import { cn, Surface, useEffectiveSurface, surfaceClasses } from '../common'; type ScrollType = 'auto' | 'always' | 'scroll' | 'hover'; export interface PixelScrollAreaProps extends Omit, 'type'> { maxHeight?: string | number; /** Canonical structural variant (scrollbar visibility mode). */ variant?: ScrollType; /** * @deprecated Use `variant` instead. Retained as alias for one minor. */ type?: ScrollType; offsetScrollbars?: boolean; scrollbarSize?: number; surface?: Surface; /** Render with surface-aware border + radius chrome. Defaults to false (no chrome). */ bordered?: boolean; /** * Accessible name for the scrollable region. Required for keyboard users to * understand what they've landed on. Provide either `aria-label` or * `aria-labelledby`. In dev, a missing label logs a warning. */ 'aria-label'?: string; 'aria-labelledby'?: string; children: React.ReactNode; } const typeClass: Record = { auto: 'pxl-scroll-auto overflow-auto', always: 'pxl-scroll-always overflow-scroll', scroll: 'pxl-scroll-scroll overflow-scroll', hover: 'pxl-scroll-hover overflow-auto', }; /** * Surface-aware scroll container with styled scrollbar (CSS `scrollbar-width` * + `::-webkit-scrollbar`). Use `type` to control scrollbar visibility, * `maxHeight` to cap height before content scrolls. * * The styled scrollbar palette is owned by `styles.css` `.pxl-scroll-*` * classes; this component only wires the variant + dimensions. */ export const PixelScrollArea = forwardRef(function PixelScrollArea( { maxHeight, variant, type, offsetScrollbars = false, scrollbarSize, surface: surfaceProp, bordered = false, className, children, style, ...rest }, ref, ) { const surface = useEffectiveSurface(surfaceProp); const s = surfaceClasses(surface); const resolvedVariant: ScrollType = variant ?? type ?? 'auto'; const inlineStyle: React.CSSProperties = { ...style, ...(maxHeight !== undefined ? { maxHeight: typeof maxHeight === 'number' ? `${maxHeight}px` : maxHeight } : null), ...(scrollbarSize !== undefined ? ({ ['--pxl-scrollbar-size' as string]: `${scrollbarSize}px` } as React.CSSProperties) : null), ...(offsetScrollbars ? { scrollbarGutter: 'stable' as React.CSSProperties['scrollbarGutter'] } : null), }; const ariaLabel = (rest as { 'aria-label'?: string })['aria-label']; const ariaLabelledBy = (rest as { 'aria-labelledby'?: string })['aria-labelledby']; const explicitTabIndex = (rest as { tabIndex?: number }).tabIndex; const explicitRole = (rest as { role?: string }).role; if ( process.env.NODE_ENV !== 'production' && !ariaLabel && !ariaLabelledBy && explicitTabIndex === undefined ) { // eslint-disable-next-line no-console console.warn( '[PixelScrollArea] missing aria-label / aria-labelledby on a focusable scroll region. ' + 'Provide one so keyboard + screen-reader users know what they\'re scrolling.', ); } return (
{children}
); }); PixelScrollArea.displayName = 'PixelScrollArea';