Skip to content

rate — Rate

A rate turns “scoring” into discrete star selection: the user lights up stars instead of typing a number. core/rate handles the per-star fill tier (full/half/empty), half-star stepping and floating-point accumulation, and APG slider semantics — at the accessibility level, a rate control is a slider, not a radio group.

  • Each port implements LhRate: product/service reviews, difficulty marking, satisfaction surveys.
  • Scenarios where the value domain is integers from 1 to N (or 0.5 stepping when half stars are allowed).
  • Should not use it: when precise decimals or continuous values in an arbitrary range are needed (use slider or input-number); when it is read-only display with no interaction needed, rate can still be used but should be switched to the readonly state.
import {
LH_RATE_MAX_DEFAULT,
clampRate,
stepRate,
getStarFill,
getRateAriaText,
getRateClassNames,
getRateAria,
} from '@lh-ui/share/core/rate'
import type { LhRateState } from '@lh-ui/share/core/rate'

stepRate does Math.round(next * 10) / 10 after adding/subtracting — this is not redundant, it is a defense against floating-point accumulation: 0.5 stepping is fine in JS (0.5 → 1.0 → 1.5), but traps like 0.1 + 0.2 !== 0.3 appear with other step values; keeping one decimal place uniformly pins the result back to a clean decimal. getStarFill’s check order is full → half → empty: full stars first, then half, guaranteeing that with value=2.5 the 3rd star is half rather than empty.

getRateAriaText interpolates {n} via formatMessage, with the text template coming from the locale pack (rate.ariaText), and the number is rounded the same way before being turned into a string — the aria text and the visual fill must be based on the same normalized value.

Function Signature Returns Purpose
clampRate (value: number | undefined, max: number) => number clamped value non-numbers become 0, upper bound is max
stepRate (value, direction: 1 | -1, allowHalf, max) => number stepped value half-star 0.5 stepping, one decimal place
getStarFill (value, index, allowHalf) => 'full' | 'half' | 'empty' fill tier the index-th star (1-based)
getRateAriaText (value, template: string) => string aria text interpolates {n} stars
getRateClassNames (state: LhRateState & { readonly, disabled }) => string[] class names readonly/disabled modifiers
getRateAria (options: { value, max, disabled?, ariaValueText? }) => LhAriaAttrs aria attribute bag slider semantics
export interface LhRateState {
max: number
value: number
allowHalf: boolean
}
export const LH_RATE_MAX_DEFAULT = 5

getRateAria emits slider semantics (not radiogroup): a rate’s value is continuously adjustable (especially in half-star mode), and the APG rating pattern is often implemented as a slider. Keyboard interaction follows the slider convention: ←/→ decreases/increases by one step.

getRateAria({ value: 4.5, max: 5 })
// { role: 'slider', 'aria-valuenow': 4.5, 'aria-valuemin': 0, 'aria-valuemax': 5 }
getRateAria({ value: 0, max: 5, disabled: true, ariaValueText: '0 stars' })
// { role: 'slider', 'aria-valuenow': 0, 'aria-valuemin': 0, 'aria-valuemax': 5,
// 'aria-disabled': true, 'aria-valuetext': '0 stars' }

aria-valuetext is announced in preference to valuenow — the screen reader says “4 and a half stars” instead of “4.5.” Each star button additionally needs an aria-label (generated by the UI layer with getRateAriaText(value, t('rate.ariaText'))).

import { getStarFill, stepRate, clampRate } from '@lh-ui/share/core/rate'
getStarFill(3.5, 3, true) // 'full'
getStarFill(3.5, 4, true) // 'half'
getStarFill(3.5, 4, false) // 'empty' (in whole-star mode, the 4th star of 3.5 is empty)
getStarFill(3.5, 5, true) // 'empty'
stepRate(3, 1, true, 5) // 3.5
stepRate(3.5, 1, true, 5) // 4
stepRate(0, -1, true, 5) // 0 (clamped)
clampRate(undefined, 5) // 0
clampRate(9, 5) // 5
<!-- Vue3 -->
<div :class="getRateClassNames({ max, value, allowHalf, readonly, disabled })">
<div
class="lh-rate__star-list"
v-bind="getRateAria({ value, max, disabled, ariaValueText: getRateAriaText(value, t('rate.ariaText')) })"
tabindex="0"
@keydown.left="onStep(-1)"
@keydown.right="onStep(1)"
>
<button
v-for="i in max"
:key="i"
type="button"
class="lh-rate__star"
:class="`lh-rate__star--${getStarFill(value, i, allowHalf)}`"
@click="onSelect(i)"
>
<LhIcon :name="getStarFill(value, i, allowHalf) === 'empty' ? 'star' : 'star-fill'" />
</button>
</div>
</div>
// React: in half-star mode, hover state decides the value; on mobile, whole stars are clicked
// Stepping: const next = stepRate(value, dir, allowHalf, max); onChange(clampRate(next, max))
// uniapp: <view> iterating max stars; aria support is limited on non-h5 targets, and
// getRateAria's undefined attributes are simply ignored
Class Semantics
.lh-rate Base class
.lh-rate--readonly Readonly (hides the hover highlight, disables the pointer)
.lh-rate--disabled Disabled
.lh-rate__star-list Star group container (tabindex and keyboard events go here)
.lh-rate__star Single star button
.lh-rate__star--full / --half / --empty Fill tiers (half uses a half-occlusion overlay)
.lh-rate__text Score text (like “3.5 points”)

The star color is --lh-warning-color (yellow); empty stars use --lh-fill-color / --lh-border-color.

  • getStarFill’s index is 1-based; iterate with v-for="i in max" (inclusive of max) rather than v-for="i in max - 1".
  • Consistency between half and whole stars: after allowHalf changes, stepRate and getStarFill must be passed the same value, otherwise the tiers from clicking and from keyboard stepping will not line up.
  • Semantics of the readonly state: readonly only switches the class name (visually disabling interaction), and getRateAria does not emit aria-disabled — a readonly rate should still be recognized by screen readers as an understandable rating value (via aria-valuetext), which distinguishes it from disabled.
  • The boundary of max: LH_RATE_MAX_DEFAULT is 5; the UI layer may change max, but too many stars (like 20) overflow on narrow screens, so wrap them yourself.
  • value is decimal but allowHalf is false: core does not validate this combination; getStarFill treats 3.5 as “3 full stars + a 4th empty star” — this is a configuration error by the caller, visually appearing as truncation.
@lh-ui/share/core/rate → getStarFill( )
返回值
(填好参数后点运行)

Copyright © 2024-2026 FengMo WenXue