diff --git a/apps/www/src/content/docs/components/announcement-bar/index.mdx b/apps/www/src/content/docs/components/announcement-bar/index.mdx index 1ee72e7fc..966d6b6c2 100644 --- a/apps/www/src/content/docs/components/announcement-bar/index.mdx +++ b/apps/www/src/content/docs/components/announcement-bar/index.mdx @@ -34,6 +34,9 @@ Renders a dismissible banner for announcements and alerts. ## Accessibility -- Uses `role="banner"` for proper landmark identification -- Supports keyboard dismissal and focus management -- Provides `aria-label` support for screen readers +- The action (when `actionLabel` or `actionIcon` is provided) is rendered + as a real ` ) : null} ); diff --git a/packages/raystack/components/announcement-bar/index.tsx b/packages/raystack/components/announcement-bar/index.tsx index 95cbf88c9..8d1cbf37f 100644 --- a/packages/raystack/components/announcement-bar/index.tsx +++ b/packages/raystack/components/announcement-bar/index.tsx @@ -1 +1 @@ -export { AnnouncementBar } from "./announcement-bar"; +export { AnnouncementBar } from './announcement-bar'; diff --git a/packages/raystack/components/button/__tests__/button.test.tsx b/packages/raystack/components/button/__tests__/button.test.tsx index 375d80beb..dbafbd9ad 100644 --- a/packages/raystack/components/button/__tests__/button.test.tsx +++ b/packages/raystack/components/button/__tests__/button.test.tsx @@ -99,20 +99,26 @@ describe('Button', () => { }); it('handles loading state', () => { - render(); + const { container } = render(); const button = screen.getByRole('button'); expect(button).toHaveClass(styles['button-loading']); - expect(screen.getByRole('status', { hidden: true })).toBeInTheDocument(); + expect(button).toHaveAttribute('aria-busy', 'true'); + // Spinner is rendered as a decorative aria-hidden element inside the button + expect( + container.querySelector('[aria-hidden="true"]') + ).toBeInTheDocument(); }); it('shows loader text when loading', () => { - render( + const { container } = render( ); expect(screen.getByText('Please wait...')).toBeInTheDocument(); - expect(screen.getByRole('status', { hidden: true })).toBeInTheDocument(); + expect( + container.querySelector('[aria-hidden="true"]') + ).toBeInTheDocument(); }); it('does not show children when loading', () => { diff --git a/packages/raystack/components/button/button.module.css b/packages/raystack/components/button/button.module.css index ea8b7a14e..784c99610 100644 --- a/packages/raystack/components/button/button.module.css +++ b/packages/raystack/components/button/button.module.css @@ -558,4 +558,4 @@ .button-text-success:disabled:hover, .button-text-success.button-loading:hover { color: var(--rs-color-foreground-success-primary); -} \ No newline at end of file +} diff --git a/packages/raystack/components/button/button.tsx b/packages/raystack/components/button/button.tsx index d523842ca..b4571146e 100644 --- a/packages/raystack/components/button/button.tsx +++ b/packages/raystack/components/button/button.tsx @@ -167,11 +167,12 @@ export const Button = ({ render={render} nativeButton={!render} focusableWhenDisabled={loading} + aria-busy={loading || undefined} {...props} > {loading ? ( <> - + , ...props }: LinkProps) { + const userAriaLabel = props['aria-label']; + const textLabel = typeof children === 'string' ? children : undefined; + const externalProps = external ? { target: '_blank', rel: 'noopener noreferrer', - 'aria-label': `${children} (opens in new tab)` + 'aria-label': + userAriaLabel ?? + (textLabel ? `${textLabel} (opens in new tab)` : undefined) } : {}; const downloadProps = download ? { download: typeof download === 'string' ? download : true, - 'aria-label': `${children} (download)` + 'aria-label': + userAriaLabel ?? (textLabel ? `${textLabel} (download)` : undefined) } : {}; @@ -40,7 +46,6 @@ export function Link({ className={cx(styles.link, className)} variant={variant} size={size} - role='link' {...externalProps} {...downloadProps} {...props} diff --git a/packages/raystack/components/list/__tests__/list.test.tsx b/packages/raystack/components/list/__tests__/list.test.tsx index e7efd6e84..900e92bc4 100644 --- a/packages/raystack/components/list/__tests__/list.test.tsx +++ b/packages/raystack/components/list/__tests__/list.test.tsx @@ -45,10 +45,22 @@ describe('List', () => { expect(list).toHaveStyle({ maxWidth: '400px' }); }); - it('has default aria-label', () => { + it('does not set a generic default aria-label', () => { render(Content); const list = screen.getByRole('list'); - expect(list).toHaveAttribute('aria-label', 'List'); + expect(list).not.toHaveAttribute('aria-label'); + }); + + it('forwards user-provided aria-label', () => { + render(Content); + const list = screen.getByRole('list'); + expect(list).toHaveAttribute('aria-label', 'Recent activity'); + }); + + it('keeps explicit role="list" for Safari/VoiceOver', () => { + render(Content); + const list = screen.getByRole('list'); + expect(list).toHaveAttribute('role', 'list'); }); }); diff --git a/packages/raystack/components/list/index.tsx b/packages/raystack/components/list/index.tsx index fc80cd36e..53821c4bd 100644 --- a/packages/raystack/components/list/index.tsx +++ b/packages/raystack/components/list/index.tsx @@ -1 +1 @@ -export { List } from "./list"; \ No newline at end of file +export { List } from './list'; diff --git a/packages/raystack/components/list/list.tsx b/packages/raystack/components/list/list.tsx index d4fcd6961..86bab9661 100644 --- a/packages/raystack/components/list/list.tsx +++ b/packages/raystack/components/list/list.tsx @@ -32,6 +32,7 @@ interface ListValueProps extends ComponentProps<'span'> { interface ListHeaderProps extends ComponentProps<'div'> { children: ReactNode; + level?: 1 | 2 | 3 | 4 | 5 | 6; } const ListRoot = ({ @@ -45,8 +46,9 @@ const ListRoot = ({