/*! Strand UI | MIT License | dillingerstaffing.com */ import type { JSX } from "preact"; import { useRef, useState } from "preact/hooks"; import { cx } from "../../internal/index.js"; export type StarRatingSize = "sm" | "md" | "lg"; export interface StarRatingProps extends Omit, "onChange" | "role" | "aria-label" | "size"> { /** 0 through count; 0 is unset. */ value: number; /** Called with the new value, 1 through count, or 0 when allowClear re-selects the current star. */ onChange?: (value: number) => void; /** Number of stars. */ count?: number; /** Selecting the current star again clears the rating. */ allowClear?: boolean; size?: StarRatingSize; /** Renders as an image named "{value} of {count} stars"; no controls. */ readOnly?: boolean; /** Accessible name for the group. */ ariaLabel: string; className?: string; } /** * Star rating control: each star is a radio; arrows, Home and End move the rating. * * @example * */ export function StarRating({ value, onChange, count = 5, allowClear = false, size = "md", readOnly = false, ariaLabel, className = "", ...rest }: StarRatingProps) { const [hover, setHover] = useState(0); const stars = useRef(new Map()); const values = Array.from({ length: count }, (_, i) => i + 1); const display = hover || value; const rootClass = cx("strand-star-rating", `strand-star-rating--${size}`, readOnly && "strand-star-rating--readonly", className); const glyph = () => ( ); if (readOnly) { return (
{values.map((n) => ( {glyph()} ))}
); } const select = (n: number) => onChange?.(allowClear && n === value ? 0 : n); const moveTo = (n: number) => { const next = Math.min(count, Math.max(1, n)); onChange?.(next); stars.current.get(next)?.focus(); }; const onKeyDown = (e: KeyboardEvent) => { const next: Record = { ArrowRight: value + 1, ArrowUp: value + 1, ArrowLeft: value - 1, ArrowDown: value - 1, Home: 1, End: count }; if (!(e.key in next)) return; e.preventDefault(); moveTo(next[e.key]); }; const focused = value || 1; return (
{values.map((n) => ( ))}
); }