66 lines
2.4 KiB
TypeScript
66 lines
2.4 KiB
TypeScript
import { forwardRef } from "react";
|
|
import type { ButtonHTMLAttributes, ReactNode } from "react";
|
|
import clsx from "clsx";
|
|
import { Loader2 } from "lucide-react";
|
|
import { FocusSafeButton } from "./FocusSafeButton";
|
|
import { iconButtonClass, type IconButtonSize, type IconButtonVariant } from "./buttonStyles";
|
|
|
|
// A button that is only a glyph (docs/DESIGN.md A2): two sizes, r8, and a name it cannot ship without
|
|
// (`label` is required: it is the accessible name and the tooltip). On a picture it is round and dark
|
|
// translucent (`media`), readable on any thumbnail; round means "floats over something" (media, or a
|
|
// pill field it sits inside — `round`), and nothing else. The glyph
|
|
// is sized here, so a row of them never mixes 14 and 16px icons.
|
|
|
|
// `pressed`, never a bare `aria-pressed`: the prop is what tints a toggled glyph (see Button).
|
|
export interface IconButtonProps
|
|
extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, "children" | "aria-pressed"> {
|
|
/** The accessible name and the tooltip. Required: the button renders no text. */
|
|
label: string;
|
|
/** The glyph (a lucide element); the primitive sizes it. */
|
|
children: ReactNode;
|
|
variant?: IconButtonVariant;
|
|
size?: IconButtonSize;
|
|
/** A toggle: sets aria-pressed and tints the glyph with the accent. */
|
|
pressed?: boolean;
|
|
/** Not accepted: a toggle is `pressed` (JSX lets any undeclared `aria-*` through, so it is `never`). */
|
|
"aria-pressed"?: never;
|
|
/** Sits on a picture or inside a pill field: round (A2). Implied by the `media` variant. */
|
|
round?: boolean;
|
|
/** Busy: a spinner replaces the glyph, and the button is unavailable without losing the focus. */
|
|
loading?: boolean;
|
|
/** Leave the tooltip to a richer one the caller renders (a `Tooltip` around the button). */
|
|
noTitle?: boolean;
|
|
}
|
|
|
|
export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(function IconButton(
|
|
{
|
|
label,
|
|
children,
|
|
variant = "ghost",
|
|
size = "sm",
|
|
pressed,
|
|
round = false,
|
|
loading = false,
|
|
noTitle = false,
|
|
type,
|
|
className,
|
|
...rest
|
|
},
|
|
ref,
|
|
) {
|
|
return (
|
|
<FocusSafeButton
|
|
ref={ref}
|
|
type={type ?? "button"}
|
|
busy={loading}
|
|
aria-label={label}
|
|
aria-pressed={pressed}
|
|
title={noTitle ? undefined : label}
|
|
className={clsx(iconButtonClass({ variant, size, round, pressed }), className)}
|
|
{...rest}
|
|
>
|
|
{loading ? <Loader2 className="animate-spin" /> : children}
|
|
</FocusSafeButton>
|
|
);
|
|
});
|