Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 | 24x 24x 24x 211x 211x 211x 211x 211x 211x 1055x 1055x 1055x 1055x | import { Box } from "@mui/material";
import { StarIcon } from "@phosphor-icons/react";
import { useAppTheme } from "@theme/v2/Provider";
import { GiveStarRatingProps } from "./GiveStarRating.types";
const MAX_STARS = 5;
const STAR_GAP = 4;
/**
* The star row used by every review surface — the side panel's summary and
* cards, and the Reviews tab's summary, filter and rows.
*
* Fractional values matter: the summary shows an average, so 4.3 has to draw as
* four stars and a third of a fifth rather than snapping to a whole one. Each
* star is an outline glyph with a filled copy clipped to its own share of the
* value on top, which keeps the two glyphs pixel-aligned at any size.
*/
const GiveStarRating = ({
value,
size = 16,
onChange,
ariaLabel,
sx,
}: GiveStarRatingProps) => {
const { palette } = useAppTheme();
const filled = palette.primitive?.blue?.["70"];
const empty = palette.primitive?.neutral?.["40"];
const isInteractive = Boolean(onChange);
// Clamp before drawing: a value outside 0-5 would otherwise produce a star
// with a negative or >100% clip, and the API is not the only caller.
const safeValue = Math.min(Math.max(value, 0), MAX_STARS);
return (
<Box
role={isInteractive ? "radiogroup" : "img"}
aria-label={ariaLabel ?? `${safeValue} out of ${MAX_STARS} stars`}
sx={{
display: "inline-flex",
alignItems: "center",
gap: `${STAR_GAP}px`,
...sx,
}}
>
{Array.from({ length: MAX_STARS }, (_, index) => {
// How much of THIS star is filled: 1 for a whole star below the value,
// 0 above it, and the remainder for the one the value falls inside.
const fillRatio = Math.min(Math.max(safeValue - index, 0), 1);
const starValue = index + 1;
const star = (
<Box
key={starValue}
sx={{
position: "relative",
width: size,
height: size,
lineHeight: 0,
flexShrink: 0,
}}
>
<StarIcon size={size} weight="regular" color={empty} />
{fillRatio > 0 && (
<Box
aria-hidden
sx={{
position: "absolute",
inset: 0,
overflow: "hidden",
width: `${fillRatio * 100}%`,
lineHeight: 0,
}}
>
<StarIcon size={size} weight="fill" color={filled} />
</Box>
)}
</Box>
);
Eif (!isInteractive) return star;
return (
<Box
key={starValue}
component="button"
type="button"
role="radio"
aria-checked={safeValue === starValue}
aria-label={`${starValue} ${starValue === 1 ? "star" : "stars"}`}
onClick={() => onChange?.(starValue)}
sx={{
padding: 0,
border: "none",
background: "none",
cursor: "pointer",
lineHeight: 0,
}}
>
{star}
</Box>
);
})}
</Box>
);
};
export default GiveStarRating;
|