Rating
별 한 줄로 점수를 매기는 컨트롤입니다. 고를 수 있게 두면 라디오 그룹이고, readOnly로 두면 평균 별점을 보여 주는 그림 하나가 됩니다.
import { Rating } from 'neba';
<Rating defaultValue={4} locale="ko" />;Props
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| value | number | — | 지금 점수. onValueChange와 함께 제어할 때 씁니다 |
| defaultValue | number | 0 | 제어하지 않을 때의 시작 점수 |
| onValueChange | (value: number) => void | — | 새 점수와 함께 호출됩니다. 지워진 Rating은 0을 보고합니다 |
| count | number | 5 | 별의 개수이자 만점 |
| precision | number | 1 | 고를 수 있는 최소 단위. 0.5면 반 개씩입니다. 고르는 범위만 정할 뿐, 4.3 같은 평균값은 언제나 그대로 그려집니다 |
| icon | ReactNode | — | 채워진 별을 그리는 글리프 |
| emptyIcon | ReactNode | — | 빈 별을 그리는 글리프. 채워진 쪽과 같은 모양이어야 합니다 |
| clearable | boolean | true | 이미 고른 점수를 다시 고르면 0으로 지웁니다 |
| readOnly | boolean | false | 바꿀 수 없는 점수, 평균 별점, 남이 남긴 평가. input이 사라지고 role="img" 하나만 남으며, 라이브러리에서 유일하게 채도를 빼지 않는 readOnly입니다 |
| disabled | boolean | false | 사용 불가. 색 계열을 버리고 중립 회색이 됩니다 |
| name | string | — | 폼 전송 시 값을 식별합니다 |
| required | boolean | false | 별을 고르기 전에는 폼이 제출되지 않습니다 |
| size공통 | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'md' | 높이와 타입 스케일 |
| color공통 | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' | 'warning' | 의미론적 색 역할. 기본값이 warning인 유일한 컴포넌트입니다. 별에 기대되는 호박색이기 때문입니다 |
| locale | string | 'en' | 접근성 이름의 언어. BCP 47 태그(ko, pt-BR, zh-Hant). 모르는 태그는 영어로 돌아갑니다 |
| label | string | — | 컨트롤 전체의 이름. 기본값은 locale이 정합니다 |
| valueLabel | (value: number, count: number) => string | — | 별 하나와, readOnly일 때 컨트롤 전체를 뭐라고 부를지. 기본값은 locale의 "5점 만점에 3점"입니다 |
나머지 <div> 속성은 모두 루트로 전달됩니다. 예외는 onChange로, 여기서 들을 만한 변화는 onValueChange입니다.
공통 축(size color)의 의미는 Prop 규약에 있습니다.
예시
count, precision
count는 별의 개수이자 만점이고, precision은 고를 수 있는 최소 단위입니다. 0.5면 별 하나가 두 개의 hit area로 나뉘어 반 개씩 고를 수 있습니다.
precision은 고르는 범위만 정합니다. value가 4.3이면 어떤 precision에서도 별 네 개와 3분의 1로 그려집니다. 평균은 선택이 아니고, 그것을 반올림하는 것은 받은 것과 다른 수를 보고하는 일이기 때문입니다.
import { useState } from 'react';
import { Rating, Typography } from 'neba';
export default function RatingPrecision() {
const [score, setScore] = useState(3.5);
return (
<div className="flex flex-col items-center gap-3">
<Rating value={score} onValueChange={setScore} precision={0.5} size="lg" />
<Typography level="caption" className="text-(--neba-muted-fg)">
{score || 'nothing'} chosen
</Typography>
<Rating defaultValue={7} count={10} precision={0.5} size="sm" label="Out of ten" />
</div>
);
}readOnly
readOnly는 컨트롤이 아니라 그림입니다. input이 하나도 남지 않고 role="img" 하나가 점수를 문장으로 들고 있으므로, 스무 개의 tab 정지점이 숫자 하나를 보고하는 일이 생기지 않습니다.
라이브러리에서 채도를 빼지 않는 유일한 readOnly이기도 합니다. 붙잡아 둔 컨트롤이 아니라 값 자체를 그린 것이고, 회색 별은 점수를 쓸 수 없다는 말이 되기 때문입니다.
import { Rating, Typography } from 'neba';
const reviews = [
{ name: 'Cold brew concentrate', score: 4.6, count: 1284 },
{ name: 'Ceramic pour-over cone', score: 3.2, count: 96 },
{ name: 'Paper filters, 100 pack', score: 5, count: 12 }
];
export default function RatingReadonly() {
return (
<div className="flex w-full max-w-sm flex-col gap-3">
{reviews.map((review) => (
<div key={review.name} className="flex items-center justify-between gap-4">
<Typography level="body">{review.name}</Typography>
<div className="flex shrink-0 items-center gap-2">
<Rating value={review.score} readOnly size="sm" />
<Typography level="caption" className="text-(--neba-muted-fg)">
{review.score} ({review.count})
</Typography>
</div>
</div>
))}
</div>
);
}size, color
size는 별 하나의 높이를 독립 글리프 사다리에서 가져옵니다. color는 라이브러리에서 유일하게 warning이 기본값인 자리입니다. 별에 기대되는 호박색이기 때문입니다.
import { Rating } from 'neba';
export default function RatingAppearance() {
return (
<div className="flex flex-col items-center gap-4">
<div className="flex flex-col items-center gap-2">
{(['xs', 'sm', 'md', 'lg', 'xl'] as const).map((size) => (
<Rating key={size} size={size} defaultValue={3} label={`Rating, ${size}`} />
))}
</div>
<div className="flex flex-wrap items-center justify-center gap-4">
{(['primary', 'success', 'danger', 'info'] as const).map((color) => (
<Rating key={color} color={color} defaultValue={4} label={`Rating, ${color}`} />
))}
</div>
</div>
);
}icon, emptyIcon
두 글리프를 직접 넘길 수 있습니다. 채워진 쪽은 빈 쪽 위에 겹쳐 폭으로 잘리므로, 같은 모양이어야 반 개가 아래 윤곽선에 정확히 맞습니다.
import { Rating } from 'neba';
function HeartIcon() {
return (
<svg viewBox="0 0 16 16" fill="currentColor">
<path d="M8 14S1.5 10 1.5 5.75A3.25 3.25 0 0 1 8 4.4a3.25 3.25 0 0 1 6.5 1.35C14.5 10 8 14 8 14Z" />
</svg>
);
}
function HeartOutlineIcon() {
return (
<svg viewBox="0 0 16 16" fill="none">
<path
d="M8 14S1.5 10 1.5 5.75A3.25 3.25 0 0 1 8 4.4a3.25 3.25 0 0 1 6.5 1.35C14.5 10 8 14 8 14Z"
stroke="currentColor"
strokeWidth="1.25"
strokeLinejoin="round"
/>
</svg>
);
}
export default function RatingIcons() {
return (
<Rating
icon={<HeartIcon />}
emptyIcon={<HeartOutlineIcon />}
color="danger"
size="lg"
defaultValue={3}
label="How much did you like it?"
/>
);
}clearable, disabled
이미 고른 별을 다시 누르면 점수가 0으로 지워집니다. clearable={false}면 지워지지 않습니다.
import { Rating, Typography } from 'neba';
export default function RatingStates() {
return (
<div className="flex flex-col items-center gap-4">
<div className="flex flex-col items-center gap-1">
<Rating defaultValue={3} clearable={false} />
<Typography level="caption" className="text-(--neba-muted-fg)">
clearable={'{false}'} — a score, once given, stays given
</Typography>
</div>
<div className="flex flex-col items-center gap-1">
<Rating defaultValue={2} disabled />
<Typography level="caption" className="text-(--neba-muted-fg)">
disabled
</Typography>
</div>
</div>
);
}locale, label, valueLabel
이 컴포넌트가 스스로 지어내는 말은 접근성 이름뿐이고, locale이 그 언어를 정합니다. ko, pt-BR, zh-Hant 같은 BCP 47 태그를 받으며 번역이 없는 태그는 영어로 돌아갑니다. label은 그룹의 이름을, valueLabel은 별 하나와 readOnly일 때의 문장을 직접 씁니다.
<Rating locale="ko" />
<Rating label="이번 주문은 어떠셨나요?" valueLabel={(value, count) => `${count}개 중 ${value}개`} />폼에 넣기
name을 주면 라디오들이 그 이름으로 제출됩니다. required는 별을 고르기 전까지 폼을 막습니다.
<form>
<Rating name="score" required locale="ko" />
</form>접근성
- 고를 수 있는 Rating은 진짜
<input type="radio">로 만들어진role="radiogroup"입니다. 줄 전체가 tab 정지점 하나이고, 그 안에서 방향키가 움직이며, 고른 것에aria-checked가 붙고, 폼 전송에 값이 실립니다. - 별 하나하나가 "5점 만점에 3점"처럼 읽힙니다. 개수가 아니라 분수로 말하는 이유는 별의 개수가 대부분의 언어에서 복수형이기 때문입니다.
readOnly는 input을 모두 없애고role="img"하나만 남깁니다.- 페이지의 언어로 읽히도록
locale을 지정하거나,label과valueLabel에 직접 쓰세요.