Files
siftlode/frontend/src/components/ui/IconButton.tsx
T

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>
);
});