@zag-js/rating-group
Version:
Core logic for the rating-group widget implemented as a state machine
191 lines (190 loc) • 6.13 kB
JavaScript
// src/rating-group.connect.ts
import { ariaAttr, dataAttr, getEventKey, getEventPoint, getRelativePoint, isLeftClick } from "@zag-js/dom-query";
import * as dom from "./rating-group.dom.mjs";
import { parts } from "./rating-group.anatomy.mjs";
function connect(service, normalize) {
const { context, send, prop, scope, computed } = service;
const interactive = computed("isInteractive");
const disabled = computed("isDisabled");
const readOnly = !!prop("readOnly");
const required = !!prop("required");
const value = context.get("value");
const hoveredValue = context.get("hoveredValue");
const translations = prop("translations");
function getItemState(props) {
const currentValue = computed("isHovering") ? hoveredValue : value;
const equal = Math.ceil(currentValue) === props.index;
const highlighted = props.index <= currentValue || equal;
const half = equal && Math.abs(currentValue - props.index) === 0.5;
return {
highlighted,
half,
checked: equal || value <= 0 && props.index === 1
};
}
return {
hovering: computed("isHovering"),
value,
hoveredValue,
count: prop("count"),
items: Array.from({ length: prop("count") }).map((_, index) => index + 1),
setValue(value2) {
send({ type: "SET_VALUE", value: value2 });
},
clearValue() {
send({ type: "CLEAR_VALUE" });
},
getRootProps() {
return normalize.element({
...parts.root.attrs,
dir: prop("dir"),
id: dom.getRootId(scope)
});
},
getHiddenInputProps() {
return normalize.input({
name: prop("name"),
form: prop("form"),
type: "text",
hidden: true,
disabled,
readOnly,
required: prop("required"),
id: dom.getHiddenInputId(scope),
defaultValue: value
});
},
getLabelProps() {
return normalize.label({
...parts.label.attrs,
dir: prop("dir"),
id: dom.getLabelId(scope),
"data-disabled": dataAttr(disabled),
"data-required": dataAttr(required),
htmlFor: dom.getHiddenInputId(scope),
onClick(event) {
if (event.defaultPrevented) return;
if (!interactive) return;
event.preventDefault();
const radioEl = dom.getRadioEl(scope, Math.max(1, context.get("value")));
radioEl?.focus({ preventScroll: true });
}
});
},
getControlProps() {
return normalize.element({
id: dom.getControlId(scope),
...parts.control.attrs,
dir: prop("dir"),
role: "radiogroup",
"aria-orientation": "horizontal",
"aria-labelledby": dom.getLabelId(scope),
"aria-readonly": ariaAttr(readOnly),
"data-readonly": dataAttr(readOnly),
"data-disabled": dataAttr(disabled),
onPointerMove(event) {
if (!interactive) return;
if (event.pointerType === "touch") return;
send({ type: "GROUP_POINTER_OVER" });
},
onPointerLeave(event) {
if (!interactive) return;
if (event.pointerType === "touch") return;
send({ type: "GROUP_POINTER_LEAVE" });
}
});
},
getItemState,
getItemProps(props) {
const { index } = props;
const itemState = getItemState(props);
const valueText = translations.ratingValueText(index);
return normalize.element({
...parts.item.attrs,
dir: prop("dir"),
id: dom.getItemId(scope, index.toString()),
role: "radio",
tabIndex: (() => {
if (readOnly) return itemState.checked ? 0 : void 0;
if (disabled) return void 0;
return itemState.checked ? 0 : -1;
})(),
"aria-roledescription": "rating",
"aria-label": valueText,
"aria-disabled": disabled,
"data-disabled": dataAttr(disabled),
"data-readonly": dataAttr(readOnly),
"aria-setsize": prop("count"),
"aria-checked": itemState.checked,
"data-checked": dataAttr(itemState.checked),
"aria-posinset": index,
"data-highlighted": dataAttr(itemState.highlighted),
"data-half": dataAttr(itemState.half),
onPointerDown(event) {
if (!interactive) return;
if (!isLeftClick(event)) return;
event.preventDefault();
},
onPointerMove(event) {
if (!interactive) return;
const point = getEventPoint(event);
const relativePoint = getRelativePoint(point, event.currentTarget);
const percentX = relativePoint.getPercentValue({
orientation: "horizontal",
dir: prop("dir")
});
const isMidway = percentX < 0.5;
send({ type: "POINTER_OVER", index, isMidway });
},
onKeyDown(event) {
if (event.defaultPrevented) return;
if (!interactive) return;
const keyMap = {
ArrowLeft() {
send({ type: "ARROW_LEFT" });
},
ArrowRight() {
send({ type: "ARROW_RIGHT" });
},
ArrowUp() {
send({ type: "ARROW_LEFT" });
},
ArrowDown() {
send({ type: "ARROW_RIGHT" });
},
Space() {
send({ type: "SPACE", value: index });
},
Home() {
send({ type: "HOME" });
},
End() {
send({ type: "END" });
}
};
const key = getEventKey(event, { dir: prop("dir") });
const exec = keyMap[key];
if (exec) {
event.preventDefault();
exec(event);
}
},
onClick() {
if (!interactive) return;
send({ type: "CLICK", value: index });
},
onFocus() {
if (!interactive) return;
send({ type: "FOCUS" });
},
onBlur() {
if (!interactive) return;
send({ type: "BLUR" });
}
});
}
};
}
export {
connect
};