mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 08:13:50 +00:00
feat: add an icon button that always has an accessible name
This commit is contained in:
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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 };
|
||||
Reference in New Issue
Block a user