diff --git a/apps/www/src/content/docs/components/scroll-area/demo.ts b/apps/www/src/content/docs/components/scroll-area/demo.ts index b797f6c04..b1128e296 100644 --- a/apps/www/src/content/docs/components/scroll-area/demo.ts +++ b/apps/www/src/content/docs/components/scroll-area/demo.ts @@ -21,8 +21,8 @@ export const playground = { controls: { type: { type: 'select', - options: ['auto', 'always', 'scroll', 'hover'], - defaultValue: 'auto' + options: ['always', 'hover', 'scroll'], + defaultValue: 'hover' } }, getCode @@ -100,9 +100,9 @@ export const typeDemo = { type: 'code', tabs: [ { - name: 'Auto (default)', + name: 'Hover (default)', code: ` - + {Array.from({ length: 20 }, (_, i) => ( @@ -123,19 +123,6 @@ export const typeDemo = { ))} -` - }, - { - name: 'Hover', - code: ` - - - {Array.from({ length: 20 }, (_, i) => ( - - Item {i + 1} - - ))} - ` }, { diff --git a/apps/www/src/content/docs/components/scroll-area/index.mdx b/apps/www/src/content/docs/components/scroll-area/index.mdx index a8cd98d93..c2a3ce592 100644 --- a/apps/www/src/content/docs/components/scroll-area/index.mdx +++ b/apps/www/src/content/docs/components/scroll-area/index.mdx @@ -25,7 +25,7 @@ import { ScrollArea } from "@raystack/apsara"; The Scroll Area component extends standard HTML div attributes, so you can use props like `style`, `id`, `onClick`, and other standard HTML attributes in addition to the props listed below. - + ## Examples @@ -61,13 +61,3 @@ Control when the scrollbar appears using the `type` prop. - **Auto Corner**: Corner element is automatically added when both scrollbars are visible - **Scroll Chaining**: Scroll continues to parent page when reaching container boundaries - **Customizable Visibility**: Control when scrollbars appear using the `type` prop - -## Accessibility - -The Scroll Area component is built on Radix UI primitives and provides: - -- Keyboard navigation support -- Screen reader compatibility -- Proper ARIA attributes -- Focus management - diff --git a/apps/www/src/content/docs/components/scroll-area/props.ts b/apps/www/src/content/docs/components/scroll-area/props.ts index d9347fc97..06b6d02d6 100644 --- a/apps/www/src/content/docs/components/scroll-area/props.ts +++ b/apps/www/src/content/docs/components/scroll-area/props.ts @@ -1,15 +1,14 @@ import type React from 'react'; -export interface ScrollAreaRootProps { +export interface ScrollAreaProps { /** * Controls when the scrollbar appears. - * - `auto`: Scrollbar appears only when content overflows (default) * - `always`: Scrollbar is always visible + * - `hover`: Scrollbar appears on hover (default) * - `scroll`: Scrollbar appears during scrolling - * - `hover`: Scrollbar appears on hover - * @default 'auto' + * @default 'hover' */ - type?: 'auto' | 'always' | 'scroll' | 'hover'; + type?: 'always' | 'hover' | 'scroll'; /** * Custom className for the root element. @@ -22,7 +21,17 @@ export interface ScrollAreaRootProps { style?: React.CSSProperties; /** - * The content to be scrolled. Both vertical and horizontal scrollbars are automatically rendered and shown when content overflows. + * The content to be scrolled. Both vertical and horizontal scrollbars are automatically rendered. */ children?: React.ReactNode; + + /** + * Allows you to replace the component's HTML element with a different tag, or compose it with another component. + * Accepts a `ReactElement` or a function that returns the element to render. + * + * @remarks `ReactElement | function` + */ + render?: + | React.ReactElement + | ((props: React.HTMLAttributes) => React.ReactElement); } diff --git a/packages/raystack/components/scroll-area/__tests__/scroll-area.test.tsx b/packages/raystack/components/scroll-area/__tests__/scroll-area.test.tsx index 368481641..7fd59a877 100644 --- a/packages/raystack/components/scroll-area/__tests__/scroll-area.test.tsx +++ b/packages/raystack/components/scroll-area/__tests__/scroll-area.test.tsx @@ -1,17 +1,16 @@ import { render, screen } from '@testing-library/react'; import { describe, expect, it, vi } from 'vitest'; -import { ScrollArea } from '../scroll-area'; -import { ScrollAreaRootProps } from '../scroll-area-root'; +import { ScrollArea, ScrollAreaProps } from '../scroll-area'; import styles from '../scroll-area.module.css'; const CONTENT_TEXT = 'Scrollable content'; const TEST_ID = 'test-scroll-area'; const BasicScrollArea = ({ - type = 'auto', + type = 'hover', children, ...props -}: ScrollAreaRootProps) => ( +}: ScrollAreaProps) => ( {children} @@ -90,7 +89,7 @@ describe('ScrollArea', () => { }); describe('Type Prop', () => { - const types = ['auto', 'always', 'scroll', 'hover'] as const; + const types = ['always', 'hover', 'scroll'] as const; it.each(types)('renders with type %s', type => { const { container } = render( @@ -101,22 +100,26 @@ describe('ScrollArea', () => { const root = container.querySelector(`[data-testid="${TEST_ID}"]`); expect(root).toBeInTheDocument(); + + // Check scrollbar has the correct type class + const scrollbar = container.querySelector(`.${styles.scrollbar}`); + expect(scrollbar).toHaveClass(styles[`scrollbar-${type}`]); }); - it('defaults to auto type', () => { + it('defaults to hover type', () => { const { container } = render(
Content
); - const root = container.querySelector(`[data-testid="${TEST_ID}"]`); - expect(root).toBeInTheDocument(); + const scrollbar = container.querySelector(`.${styles.scrollbar}`); + expect(scrollbar).toHaveClass(styles['scrollbar-hover']); }); }); describe('Scrollbars', () => { - it('renders vertical scrollbar automatically', () => { + it('renders vertical scrollbar', () => { const { container } = render( { expect(scrollbar).toBeInTheDocument(); }); - it('renders horizontal scrollbar automatically', () => { + it('renders horizontal scrollbar', () => { const { container } = render( { ); - const scrollbar = container.querySelector( - `[data-orientation="vertical"]` - ); - expect(scrollbar).toBeInTheDocument(); - // Thumb is a child of scrollbar (Radix UI controls its rendering based on scroll position) - const thumb = scrollbar?.querySelector(`.${styles.thumb}`); - // If thumb exists, verify it has the correct class - if (thumb) { - expect(thumb).toHaveClass(styles.thumb); - } else { - // Thumb may not render if Radix UI determines no scroll is needed - // This is expected behavior - we verify the scrollbar structure is correct - expect(scrollbar).toBeInTheDocument(); - } + const thumb = container.querySelector(`.${styles.thumb}`); + expect(thumb).toBeInTheDocument(); }); }); diff --git a/packages/raystack/components/scroll-area/index.ts b/packages/raystack/components/scroll-area/index.ts index 1dab8413d..553a4c31d 100644 --- a/packages/raystack/components/scroll-area/index.ts +++ b/packages/raystack/components/scroll-area/index.ts @@ -1,3 +1,4 @@ +export type { ScrollAreaProps, ScrollAreaType } from './scroll-area'; export { ScrollArea } from './scroll-area'; -export type { ScrollAreaRootProps } from './scroll-area-root'; export type { ScrollAreaScrollbarProps } from './scroll-area-scrollbar'; +export { ScrollAreaScrollbar } from './scroll-area-scrollbar'; diff --git a/packages/raystack/components/scroll-area/scroll-area-root.tsx b/packages/raystack/components/scroll-area/scroll-area-root.tsx deleted file mode 100644 index a64ed95ec..000000000 --- a/packages/raystack/components/scroll-area/scroll-area-root.tsx +++ /dev/null @@ -1,42 +0,0 @@ -'use client'; - -import { cx } from 'class-variance-authority'; -import { ScrollArea as ScrollAreaPrimitive } from 'radix-ui'; -import { - ComponentPropsWithoutRef, - ComponentRef, - ReactNode, - forwardRef -} from 'react'; -import { ScrollAreaScrollbar } from './scroll-area-scrollbar'; -import styles from './scroll-area.module.css'; - -export interface ScrollAreaRootProps - extends ComponentPropsWithoutRef { - type?: 'auto' | 'always' | 'scroll' | 'hover'; - className?: string; - children?: ReactNode; -} - -export const ScrollAreaRoot = forwardRef< - ComponentRef, - ScrollAreaRootProps ->(({ className, type = 'auto', children, ...props }, ref) => { - return ( - - - {children} - - - - - - ); -}); - -ScrollAreaRoot.displayName = ScrollAreaPrimitive.Root.displayName; diff --git a/packages/raystack/components/scroll-area/scroll-area-scrollbar.tsx b/packages/raystack/components/scroll-area/scroll-area-scrollbar.tsx index b915a2fb7..bcb3ca4ce 100644 --- a/packages/raystack/components/scroll-area/scroll-area-scrollbar.tsx +++ b/packages/raystack/components/scroll-area/scroll-area-scrollbar.tsx @@ -1,25 +1,25 @@ 'use client'; +import { ScrollArea as ScrollAreaPrimitive } from '@base-ui/react/scroll-area'; import { cx } from 'class-variance-authority'; -import { ScrollArea as ScrollAreaPrimitive } from 'radix-ui'; -import { ComponentPropsWithoutRef, ComponentRef, forwardRef } from 'react'; +import { forwardRef } from 'react'; +import type { ScrollAreaType } from './scroll-area'; import styles from './scroll-area.module.css'; export interface ScrollAreaScrollbarProps - extends ComponentPropsWithoutRef { - orientation?: 'vertical' | 'horizontal'; - className?: string; + extends ScrollAreaPrimitive.Scrollbar.Props { + type?: ScrollAreaType; } export const ScrollAreaScrollbar = forwardRef< - ComponentRef, + HTMLDivElement, ScrollAreaScrollbarProps ->(({ className, orientation = 'vertical', ...props }, ref) => { +>(({ className, orientation = 'vertical', type = 'hover', ...props }, ref) => { return ( @@ -27,4 +27,4 @@ export const ScrollAreaScrollbar = forwardRef< ); }); -ScrollAreaScrollbar.displayName = ScrollAreaPrimitive.Scrollbar.displayName; +ScrollAreaScrollbar.displayName = 'ScrollAreaScrollbar'; diff --git a/packages/raystack/components/scroll-area/scroll-area.module.css b/packages/raystack/components/scroll-area/scroll-area.module.css index 86c424a97..39be70a82 100644 --- a/packages/raystack/components/scroll-area/scroll-area.module.css +++ b/packages/raystack/components/scroll-area/scroll-area.module.css @@ -25,7 +25,10 @@ background: transparent; pointer-events: auto; position: relative; - transition: width 150ms ease-out, height 150ms ease-out; + transition: + width 150ms ease-out, + height 150ms ease-out, + opacity 150ms ease-out; } .scrollbar[data-orientation="vertical"] { @@ -48,7 +51,6 @@ .thumb { flex: 1; background: var(--rs-color-border-base-primary); - /* TODO: Change to appropriate background var after the correct var is introduced */ border-radius: var(--rs-radius-2); transition: opacity 150ms ease-out; opacity: 1; @@ -58,4 +60,15 @@ .corner { background: transparent; -} \ No newline at end of file +} +.scrollbar:hover, +.scrollbar-always, +.scrollbar-hover[data-hovering], +.scrollbar-scroll[data-scrolling] { + opacity: 1; +} + +.scrollbar-hover, +.scrollbar-scroll { + opacity: 0; +} diff --git a/packages/raystack/components/scroll-area/scroll-area.tsx b/packages/raystack/components/scroll-area/scroll-area.tsx index bd45a551f..a6e0c767f 100644 --- a/packages/raystack/components/scroll-area/scroll-area.tsx +++ b/packages/raystack/components/scroll-area/scroll-area.tsx @@ -1,3 +1,34 @@ -import { ScrollAreaRoot } from './scroll-area-root'; +'use client'; -export const ScrollArea = ScrollAreaRoot; +import { ScrollArea as ScrollAreaPrimitive } from '@base-ui/react/scroll-area'; +import { cx } from 'class-variance-authority'; +import { forwardRef } from 'react'; +import styles from './scroll-area.module.css'; +import { ScrollAreaScrollbar } from './scroll-area-scrollbar'; + +export type ScrollAreaType = 'always' | 'hover' | 'scroll'; + +export interface ScrollAreaProps extends ScrollAreaPrimitive.Root.Props { + type?: ScrollAreaType; +} + +export const ScrollArea = forwardRef( + ({ className, type = 'hover', children, ...props }, ref) => { + return ( + + + {children} + + + + + + ); + } +); + +ScrollArea.displayName = 'ScrollArea'; diff --git a/packages/raystack/index.tsx b/packages/raystack/index.tsx index f7eed35ef..24d5d72e9 100644 --- a/packages/raystack/index.tsx +++ b/packages/raystack/index.tsx @@ -44,8 +44,8 @@ export { List } from './components/list'; export { Navbar } from './components/navbar'; export { Popover } from './components/popover'; export { Radio } from './components/radio'; -export { Search } from './components/search'; export { ScrollArea } from './components/scroll-area'; +export { Search } from './components/search'; export { Select } from './components/select'; export { Separator } from './components/separator'; export { Sheet } from './components/sheet';