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
14 changes: 13 additions & 1 deletion apps/www/src/content/docs/components/container/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,16 @@ The `align` prop positions the container within its parent: left, center, or rig

<Demo data={alignDemo} />

### Custom render

Container renders a `div` by default. Use the `render` prop to render a semantic element such as `main`, `section`, or `article`.

```tsx
<Container size="medium" render={<main />}>
<Text>Page content</Text>
</Container>
```

## API Reference

Renders a centered content wrapper with max-width constraints.
Expand All @@ -50,7 +60,7 @@ Every rendered part carries a stable `data-slot` attribute for [styling and test

| Slot | Element |
|------|---------|
| `container` | The container `<div>` |
| `container` | The container `<div>` (or the element supplied via `render`) |

## Accessibility

Expand All @@ -61,3 +71,5 @@ The Container component is designed with accessibility in mind:
- Without a label, no role is applied, and Container stays a generic `<div>`
to avoid emitting unlabeled regions that clutter assistive-tech output.
- Pass `role` explicitly to override (e.g. `role="main"`).
- With `render`, Container applies no automatic role. The rendered element
keeps its implicit role, so `render={<main />}` stays a `main` landmark.
13 changes: 11 additions & 2 deletions apps/www/src/content/docs/components/container/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,21 @@ export interface ContainerProps {
*/
align?: 'left' | 'center' | 'right';

/**
* Renders the container as a different element.
*
* @remarks `ReactElement | function`
*/
render?:
| React.ReactElement
| ((props: React.HTMLAttributes<HTMLDivElement>) => React.ReactElement);

/** Additional CSS class names */
className?: string;

/** Accessible label for the container region */
ariaLabel?: string;
'aria-label'?: string;

/** ID of element that labels this container region */
ariaLabelledby?: string;
'aria-labelledby'?: string;
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { render, screen } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
import { describe, expect, it, vi } from 'vitest';
import { Container } from '../container';
import styles from '../container.module.css';

Expand All @@ -16,6 +16,23 @@ describe('Container', () => {
expect(element?.nodeName).toBe('DIV');
});

it('renders the element passed to render', () => {
const { container: root } = render(
<Container render={<section />} size='small'>
Content
</Container>
);
const container = root.firstChild as HTMLElement;
expect(container.nodeName).toBe('SECTION');
expect(container).toHaveClass(styles['container-small']);
});

it('forwards ref', () => {
const ref = vi.fn();
render(<Container ref={ref}>Content</Container>);
expect(ref).toHaveBeenCalledWith(expect.any(HTMLDivElement));
});

it('applies custom className', () => {
const { container: root } = render(
<Container className='custom-class'>Content</Container>
Expand Down Expand Up @@ -103,5 +120,14 @@ describe('Container', () => {
render(<Container aria-label='Labeled section'>Content</Container>);
expect(screen.getByRole('region')).toBeInTheDocument();
});

it('keeps the implicit role of a custom element', () => {
render(
<Container render={<main />} aria-label='Content'>
Content
</Container>
);
expect(screen.getByRole('main')).not.toHaveAttribute('role');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ describe('Container data-slot contract', () => {
expect(getSlot(container, 'container')?.tagName).toBe('DIV');
});

it('keeps the slot on the rendered element with a custom render', () => {
const { container } = render(<Container render={<section />} />);
expect(getSlot(container, 'container')?.tagName).toBe('SECTION');
});

it('lets callers override the slot name', () => {
const { container } = render(<Container data-slot='custom' />);
expect(getSlot(container, 'custom')).not.toBeNull();
Expand Down
38 changes: 19 additions & 19 deletions packages/raystack/components/container/container.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { mergeProps, useRender } from '@base-ui/react';
import { cva, VariantProps } from 'class-variance-authority';
import { ComponentProps } from 'react';

import styles from './container.module.css';

Expand All @@ -23,32 +23,32 @@ const container = cva(styles.container, {
}
});

type ContainerProps = VariantProps<typeof container> & ComponentProps<'div'>;
type ContainerProps = VariantProps<typeof container> &
useRender.ComponentProps<'div'>;

export function Container({
children,
size,
align,
className,
role,
'aria-label': ariaLabel,
'aria-labelledby': ariaLabelledBy,
render,
ref,
...props
}: ContainerProps) {
const hasLabel = !!(ariaLabel || ariaLabelledBy);
const resolvedRole = role ?? (hasLabel ? 'region' : undefined);
return (
<div
data-slot='container'
className={container({ size, align, className })}
role={resolvedRole}
aria-label={ariaLabel}
aria-labelledby={ariaLabelledBy}
{...props}
>
{children}
</div>
);
const hasLabel = !!(props['aria-label'] || props['aria-labelledby']);
const containerProps = {
'data-slot': 'container',
className: container({ size, align, className }),
// A custom element keeps its implicit role, for example `main`.
role: role ?? (hasLabel && !render ? 'region' : undefined)
Comment thread
rohanchkrabrty marked this conversation as resolved.
};

return useRender({
defaultTagName: 'div',
ref,
render,
props: mergeProps<'div'>(containerProps, props)
});
}

Container.displayName = 'Container';
Loading