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
276 changes: 276 additions & 0 deletions apps/www/src/app/examples/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ const Page = () => {
const [selectValue1, setSelectValue1] = useState('');
const [selectValue2, setSelectValue2] = useState('');
const [inputValue, setInputValue] = useState('');
const [calloutDismissed, setCalloutDismissed] = useState(false);
const [rangeValue, setRangeValue] = useState({
from: dayjs('2027-11-15').toDate(),
to: dayjs('2027-12-10').toDate()
Expand Down Expand Up @@ -2764,6 +2765,281 @@ const Page = () => {
</Flex>
</Flex>

<Text
size='large'
weight='medium'
style={{ marginTop: '32px', marginBottom: '16px' }}
>
Callout
</Text>

<Flex direction='column' gap={6} style={{ maxWidth: '600px' }}>
{/* Types */}
<Flex direction='column' gap={3}>
<Text size='small' variant='secondary'>
Types
</Text>
<Flex direction='column' gap={3}>
<Callout width='100%' type='grey'>
Grey callout (default)
</Callout>
<Callout width='100%' type='success'>
Success callout
</Callout>
<Callout width='100%' type='alert'>
Alert callout
</Callout>
<Callout width='100%' type='gradient'>
Gradient callout
</Callout>
<Callout width='100%' type='accent'>
Accent callout
</Callout>
<Callout width='100%' type='attention'>
Attention callout
</Callout>
<Callout width='100%' type='normal'>
Normal callout
</Callout>
</Flex>
</Flex>

{/* Outline */}
<Flex direction='column' gap={3}>
<Text size='small' variant='secondary'>
Outline
</Text>
<Callout width='100%' type='success'>
Without outline
</Callout>
<Callout width='100%' type='success' outline>
With outline
</Callout>
</Flex>

{/* High contrast */}
<Flex direction='column' gap={3}>
<Text size='small' variant='secondary'>
High contrast
</Text>
<Callout width='100%' type='alert'>
Normal contrast
</Callout>
<Callout width='100%' type='alert' highContrast>
High contrast
</Callout>
<Callout width='100%' type='accent' outline highContrast>
Outline + high contrast
</Callout>
</Flex>

{/* Custom icon */}
<Flex direction='column' gap={3}>
<Text size='small' variant='secondary'>
Custom icon
</Text>
<Callout width='100%' type='attention' icon={<RadixBellIcon />}>
Callout with a custom bell icon
</Callout>
<Callout width='100%' type='grey' icon={null}>
Callout with no icon
</Callout>
</Flex>

{/* With action */}
<Flex direction='column' gap={3}>
<Text size='small' variant='secondary'>
With action
</Text>
<Callout
width='100%'
type='accent'
action={
<Button size='small' variant='outline'>
Upgrade
</Button>
}
>
You&apos;re on the free plan
</Callout>
</Flex>

{/* Dismissible (controlled) */}
<Flex direction='column' gap={3}>
<Text size='small' variant='secondary'>
Dismissible (controlled — consumer removes it in onDismiss)
</Text>
{calloutDismissed ? (
<Button
size='small'
onClick={() => setCalloutDismissed(false)}
>
Restore callout
</Button>
) : (
<Callout
width='100%'
type='success'
dismissible
onDismiss={() => setCalloutDismissed(true)}
>
Dismiss me
</Callout>
)}
</Flex>

{/* Custom width */}
<Flex direction='column' gap={3}>
<Text size='small' variant='secondary'>
Custom width
</Text>
<Callout type='gradient' width={240}>
width = 240
</Callout>
<Callout type='gradient' width={480}>
width = 480
</Callout>
</Flex>

{/*Figma replicas*/}
<Flex direction='column' gap={3}>
<Text size='small' variant='secondary'>
Figma replicas
</Text>
<Callout type='normal' outline>
A short message to attract user’s attention
</Callout>
<Callout type='success' outline>
A short message to attract user’s attention
</Callout>
<Callout type='gradient'>
A short message to attract user’s attention
</Callout>
<Callout type='accent'>
A short message to attract user’s attention
</Callout>
<Callout
type='grey'
action={
<Button variant='outline' color='neutral' size='small'>
Button
</Button>
}
>
A short message to attract user’s attention
</Callout>
<Callout type='grey' dismissible>
A short message to attract user’s attention
</Callout>
<Callout
type='grey'
action={
<Button variant='outline' color='neutral' size='small'>
Button
</Button>
}
>
A short message to attract user’s attention
</Callout>
<Callout type='gradient'>
A short message to attract user’s attention
</Callout>
<Callout type='gradient' outline>
A short message to attract user’s attention
</Callout>
<Callout type='gradient' highContrast>
A short message to attract user’s attention
</Callout>
<Callout type='gradient' outline highContrast>
A short message to attract user’s attention
</Callout>
<Callout type='normal'>
A short message to attract user’s attention
</Callout>
<Callout type='normal' outline>
A short message to attract user’s attention
</Callout>
<Callout type='normal' highContrast>
A short message to attract user’s attention
</Callout>
<Callout type='normal' outline highContrast>
A short message to attract user’s attention
</Callout>
<Callout type='grey'>
A short message to attract user’s attention
</Callout>
<Callout type='grey' outline>
A short message to attract user’s attention
</Callout>
<Callout type='grey' highContrast>
A short message to attract user’s attention
</Callout>
<Callout type='grey' outline highContrast>
A short message to attract user’s attention
</Callout>
<Callout type='accent'>
A short message to attract user’s attention
</Callout>
<Callout type='accent' outline>
A short message to attract user’s attention
</Callout>
<Callout type='accent' highContrast>
A short message to attract user’s attention
</Callout>
<Callout type='accent' outline highContrast>
A short message to attract user’s attention
</Callout>
<Callout type='alert'>
A short message to attract user’s attention
</Callout>
<Callout type='alert' outline>
A short message to attract user’s attention
</Callout>
<Callout type='alert' highContrast>
A short message to attract user’s attention
</Callout>
<Callout type='alert' outline highContrast>
A short message to attract user’s attention
</Callout>
<Callout type='success'>
A short message to attract user’s attention
</Callout>
<Callout type='success' outline>
A short message to attract user’s attention
</Callout>
<Callout type='success' highContrast>
A short message to attract user’s attention
</Callout>
<Callout type='success' outline highContrast>
A short message to attract user’s attention
</Callout>
<Callout type='attention'>
A short message to attract user’s attention
</Callout>
<Callout type='attention' outline>
A short message to attract user’s attention
</Callout>
<Callout type='attention' highContrast>
A short message to attract user’s attention
</Callout>
<Callout type='attention' outline highContrast>
A short message to attract user’s attention
</Callout>
<Callout type='gradient'>
A short message to attract user’s attention
</Callout>
<Callout type='gradient' outline>
A short message to attract user’s attention
</Callout>
<Callout type='gradient' highContrast>
A short message to attract user’s attention
</Callout>
<Callout type='gradient' outline highContrast>
A short message to attract user’s attention
</Callout>
</Flex>
</Flex>

<Flex justify='center' style={{ marginTop: 40 }}>
<Button type='submit'>Submit button</Button>
</Flex>
Expand Down
5 changes: 4 additions & 1 deletion apps/www/src/content/docs/components/callout/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,9 @@ The Callout component supports high contrast mode for better visibility:

### Dismissible

The Callout component can be made dismissible:
The Callout component can be made dismissible. When `onDismiss` is provided, the
consumer controls removal (the callout stays mounted); when it is omitted, the
callout hides itself.

<Demo data={dismissibleDemo} />

Expand All @@ -77,3 +79,4 @@ The Callout component includes appropriate ARIA attributes for accessibility:
- Uses semantic HTML elements for proper structure
- Dismiss button includes `aria-label` for screen readers
- Interactive elements are keyboard accessible
- Dismiss button shows a visible focus ring on keyboard focus
10 changes: 8 additions & 2 deletions apps/www/src/content/docs/components/callout/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,19 @@ export interface CalloutProps {
*/
icon?: React.ReactNode;

/** Callback function when dismiss button is clicked */
/**
* Called when the dismiss button is clicked. When provided, the consumer owns
* removal (the callout stays mounted). When omitted, the callout hides itself.
*/
onDismiss?: () => void;

/** Text content of the callout */
children?: React.ReactNode;

/** Custom width for the callout */
/**
* Custom width for the callout
* @defaultValue 400
*/
width?: string | number;

/** Additional CSS class names */
Expand Down
47 changes: 45 additions & 2 deletions packages/raystack/components/callout/__tests__/callout.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,31 @@ describe('Callout', () => {
expect(onDismiss).toHaveBeenCalledTimes(1);
});

it('stays mounted after dismiss when onDismiss is provided (controlled)', () => {
const onDismiss = vi.fn();
render(
<Callout dismissible onDismiss={onDismiss}>
Controlled message
</Callout>
);

fireEvent.click(screen.getByRole('button', { name: 'Dismiss message' }));

// Consumer owns removal — the callout must not hide itself.
expect(screen.getByText('Controlled message')).toBeInTheDocument();
});

it('hides itself after dismiss when onDismiss is omitted (uncontrolled)', () => {
render(<Callout dismissible>Uncontrolled message</Callout>);

fireEvent.click(screen.getByRole('button', { name: 'Dismiss message' }));

expect(
screen.queryByText('Uncontrolled message')
).not.toBeInTheDocument();
expect(screen.queryByRole('status')).not.toBeInTheDocument();
});

it('renders both action and dismissible button', () => {
const onDismiss = vi.fn();
render(
Expand Down Expand Up @@ -159,6 +184,22 @@ describe('Callout', () => {
const callout = screen.getByRole('status');
expect(callout).toHaveStyle({ width: '50%' });
});

it('applies width={0} instead of dropping it', () => {
render(<Callout width={0}>Zero width message</Callout>);

const callout = screen.getByRole('status');
expect(callout).toHaveStyle({ width: '0px' });
});

it('falls back to style.width when no width prop is given', () => {
render(
<Callout style={{ width: '200px' }}>Styled width message</Callout>
);

const callout = screen.getByRole('status');
expect(callout).toHaveStyle({ width: '200px' });
});
});

describe('Accessibility', () => {
Expand Down Expand Up @@ -189,9 +230,11 @@ describe('Callout', () => {
expect(dismissButton).toHaveAttribute('type', 'button');
expect(dismissButton).toHaveAttribute('aria-label', 'Dismiss message');

// The icon is decorative: IconButton wraps it in an aria-hidden element,
// so the button is announced only by its aria-label.
const svg = dismissButton.querySelector('svg');
expect(svg).toHaveAttribute('aria-hidden', 'true');
expect(svg).toHaveAttribute('role', 'presentation');
expect(svg).toBeInTheDocument();
expect(svg?.closest('[aria-hidden="true"]')).toBeInTheDocument();
});
});
});
Loading
Loading