Files
UI/packages/skin/ui/src/rate.ts
T

53 lines
3.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { effect, h, signal } from '@ui-arch/frame';
import type { RateProps } from './types.js';
/*** 评分:星标点选(allowHalf 可记半星,allowClear 再点当前分清零),hover 预览移出还原,键盘 ←→/↑↓ 调节,value 信号双向绑定 ***/
export function rate(props: RateProps): HTMLDivElement {
const value = props.value;
const max = props.max ?? 5; // 星标总数缺省 5
const step = props.allowHalf ? 0.5 : 1; // 半星模式步进 0.5
const hover = signal(0); // 预览值(1 基,0 表示无预览)
const emit = (v: number, allowZero = false): void => { const n = Math.round(Math.min(max, Math.max(allowZero ? 0 : 1, v)) * 2) / 2; const q = props.allowHalf ? n : Math.round(n); value(q); if (props.onChange) props.onChange(q); }; // 写回信号并回调(半星按 0.5 量化;清零仅在 allowZero 时放行)
/*** 计算星标命中值:allowHalf 时左半区记 0.5 分 ***/
const hitOf = (e: MouseEvent, i: number): number => props.allowHalf && e.offsetX < (e.currentTarget as HTMLElement).offsetWidth / 2 ? i - 0.5 : i;
/*** 键盘调节:focus 在整组上,方向键按步进调节(重渲染后仍生效) ***/
const keyNav = (e: KeyboardEvent): void => {
if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight' && e.key !== 'ArrowUp' && e.key !== 'ArrowDown') return; // 卫语句:只拦截方向键
e.preventDefault();
emit(value() + (e.key === 'ArrowRight' || e.key === 'ArrowUp' ? step : -step)); // →/↑ 加一步,←/↓ 减一步
};
const render = (): HTMLElement => {
const shown = hover() || value(); // 预览优先,无预览显示当前值
const stars = [];
for (let i = 1; i <= max; i++) {
const frac = shown - i + 1; // 该星填充比例(1 满格 / 0.5 半格 / 0 灭)
stars.push(h('button', {
class: `ua-rate-star${frac >= 1 ? ' is-on' : frac > 0 ? ' is-half' : ''}`,
type: 'button',
disabled: props.disabled,
'aria-label': `${i} 星`,
onClick: (e: MouseEvent) => { const hit = hitOf(e, i); if (props.allowClear && hit === value()) emit(0, true); else emit(hit); }, // 再点当前分清零(allowClear)
onMouseMove: (e: MouseEvent) => hover(hitOf(e, i)), // 半星模式下按悬停位置细分预览
}, '★'));
}
const node = h('div', { class: 'ua-rate', role: 'slider', tabindex: '0', 'aria-label': '评分', 'aria-valuemin': props.allowClear ? 0 : 1, 'aria-valuemax': max, 'aria-valuenow': value(), onMouseLeave: () => hover(0) }, ...stars) as HTMLDivElement;
node.addEventListener('keydown', keyNav); // 每次渲染都挂:节点被替换后键盘仍可用
return node;
};
let el!: HTMLElement; // 当前渲染产物(effect 首跑赋值)
effect(() => {
const next = render();
if (el) {
const hadFocus = el.contains(document.activeElement); // 重渲染前焦点是否在本组内
el.replaceWith(next);
if (hadFocus) (next as HTMLElement).focus(); // 归还焦点,方向键可继续连按
}
el = next;
}); // value/hover 变化整条重渲染(星标少,开销可忽略)
return el as HTMLDivElement;
}