feat: add an icon button that always has an accessible name

This commit is contained in:
Linus Rath
2026-10-28 01:11:21 +01:00
parent f8b061f07c
commit c0fa11e5ec
2 changed files with 64 additions and 0 deletions
@@ -0,0 +1,31 @@
import { createRef } from 'react';
import { fireEvent, render, screen } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import { IconButton } from '../icon-button';
import { X } from '@/components/icons';
describe('IconButton', () => {
it('is named and titled by its label', () => {
render(<IconButton label="Close"><X /></IconButton>);
const button = screen.getByRole('button', { name: 'Close' });
expect(button).toHaveAttribute('title', 'Close');
expect(button).toHaveAttribute('type', 'button');
});
it('defaults to ghost icon-sm and takes another size', () => {
const { rerender } = render(<IconButton label="More"><X /></IconButton>);
expect(screen.getByRole('button')).toHaveClass('h-8', 'w-8', 'hover:bg-accent');
rerender(<IconButton label="More" size="icon-lg"><X /></IconButton>);
expect(screen.getByRole('button')).toHaveClass('h-11', 'w-11');
});
it('forwards the ref and other props', () => {
const ref = createRef<HTMLButtonElement>();
const onClick = vi.fn();
render(<IconButton ref={ref} label="Delete" onClick={onClick} data-testid="del" title=""><X /></IconButton>);
expect(ref.current).toBe(screen.getByTestId('del'));
expect(ref.current).not.toHaveAttribute('title', 'Delete');
fireEvent.click(ref.current!);
expect(onClick).toHaveBeenCalledOnce();
});
});
+33
View File
@@ -0,0 +1,33 @@
import * as React from "react";
import { Button, type ButtonProps } from "@/components/ui/button";
export interface IconButtonProps extends Omit<ButtonProps, "aria-label"> {
/** What the button does. Becomes the accessible name and the tooltip. */
label: string;
/** The icon. It gets w-4 in icon-sm and w-5 in icon-lg unless it sets its own size. */
children: React.ReactNode;
}
/**
* An icon-only button (R1): ghost, 32px (`icon-sm`) unless told otherwise,
* always named. `title` defaults to the label; pass `title=""` to drop the
* tooltip where another one already shows.
*/
const IconButton = React.forwardRef<HTMLButtonElement, IconButtonProps>(
({ label, variant = "ghost", size = "icon-sm", type = "button", title, children, ...props }, ref) => (
<Button
ref={ref}
type={type}
variant={variant}
size={size}
aria-label={label}
title={title ?? label}
{...props}
>
{children}
</Button>
)
);
IconButton.displayName = "IconButton";
export { IconButton };