Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 4 additions & 17 deletions apps/www/src/content/docs/components/scroll-area/demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -100,9 +100,9 @@ export const typeDemo = {
type: 'code',
tabs: [
{
name: 'Auto (default)',
name: 'Hover (default)',
code: `
<ScrollArea style={{ height: '200px', width: '300px' }} type="auto">
<ScrollArea style={{ height: '200px', width: '300px' }} type="hover">
<Flex direction="column" gap={2}>
{Array.from({ length: 20 }, (_, i) => (
<Text key={i} size="small">
Expand All @@ -123,19 +123,6 @@ export const typeDemo = {
</Text>
))}
</Flex>
</ScrollArea>`
},
{
name: 'Hover',
code: `
<ScrollArea style={{ height: '200px', width: '300px' }} type="hover">
<Flex direction="column" gap={2}>
{Array.from({ length: 20 }, (_, i) => (
<Text key={i} size="small">
Item {i + 1}
</Text>
))}
</Flex>
</ScrollArea>`
},
{
Expand Down
12 changes: 1 addition & 11 deletions apps/www/src/content/docs/components/scroll-area/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<auto-type-table path="./props.ts" name="ScrollAreaRootProps" />
<auto-type-table path="./props.ts" name="ScrollAreaProps" />

## Examples

Expand Down Expand Up @@ -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

21 changes: 15 additions & 6 deletions apps/www/src/content/docs/components/scroll-area/props.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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<HTMLElement>) => React.ReactElement);
}
Original file line number Diff line number Diff line change
@@ -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) => (
<ScrollArea type={type} data-testid={TEST_ID} {...props}>
{children}
</ScrollArea>
Expand Down Expand Up @@ -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(
Expand All @@ -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(
<ScrollArea data-testid={TEST_ID}>
<div>Content</div>
</ScrollArea>
);

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(
<BasicScrollArea
type='always'
Expand All @@ -132,7 +135,7 @@ describe('ScrollArea', () => {
expect(scrollbar).toBeInTheDocument();
});

it('renders horizontal scrollbar automatically', () => {
it('renders horizontal scrollbar', () => {
const { container } = render(
<BasicScrollArea
type='always'
Expand Down Expand Up @@ -172,20 +175,8 @@ describe('ScrollArea', () => {
</BasicScrollArea>
);

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();
});
});

Expand Down
3 changes: 2 additions & 1 deletion packages/raystack/components/scroll-area/index.ts
Original file line number Diff line number Diff line change
@@ -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';
42 changes: 0 additions & 42 deletions packages/raystack/components/scroll-area/scroll-area-root.tsx

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,30 +1,30 @@
'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<typeof ScrollAreaPrimitive.Scrollbar> {
orientation?: 'vertical' | 'horizontal';
className?: string;
extends ScrollAreaPrimitive.Scrollbar.Props {
type?: ScrollAreaType;
}

export const ScrollAreaScrollbar = forwardRef<
ComponentRef<typeof ScrollAreaPrimitive.Scrollbar>,
HTMLDivElement,
ScrollAreaScrollbarProps
>(({ className, orientation = 'vertical', ...props }, ref) => {
>(({ className, orientation = 'vertical', type = 'hover', ...props }, ref) => {
return (
<ScrollAreaPrimitive.Scrollbar
ref={ref}
orientation={orientation}
className={cx(styles.scrollbar, className)}
className={cx(styles.scrollbar, styles[`scrollbar-${type}`], className)}
{...props}
>
<ScrollAreaPrimitive.Thumb className={styles.thumb} />
</ScrollAreaPrimitive.Scrollbar>
);
});

ScrollAreaScrollbar.displayName = ScrollAreaPrimitive.Scrollbar.displayName;
ScrollAreaScrollbar.displayName = 'ScrollAreaScrollbar';
19 changes: 16 additions & 3 deletions packages/raystack/components/scroll-area/scroll-area.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -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"] {
Expand All @@ -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;
Expand All @@ -58,4 +60,15 @@

.corner {
background: transparent;
}
}
.scrollbar:hover,
.scrollbar-always,
.scrollbar-hover[data-hovering],
.scrollbar-scroll[data-scrolling] {
opacity: 1;
}

.scrollbar-hover,
.scrollbar-scroll {
opacity: 0;
}
35 changes: 33 additions & 2 deletions packages/raystack/components/scroll-area/scroll-area.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLDivElement, ScrollAreaProps>(
({ className, type = 'hover', children, ...props }, ref) => {
return (
<ScrollAreaPrimitive.Root
ref={ref}
className={cx(styles.root, className)}
{...props}
>
<ScrollAreaPrimitive.Viewport className={styles.viewport}>
<ScrollAreaPrimitive.Content>{children}</ScrollAreaPrimitive.Content>
</ScrollAreaPrimitive.Viewport>
<ScrollAreaScrollbar orientation='vertical' type={type} />
<ScrollAreaScrollbar orientation='horizontal' type={type} />
<ScrollAreaPrimitive.Corner className={styles.corner} />
</ScrollAreaPrimitive.Root>
);
}
);

ScrollArea.displayName = 'ScrollArea';
2 changes: 1 addition & 1 deletion packages/raystack/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down