@zag-js/progress
Version:
Core logic for the progress widget implemented as a state machine
306 lines (300 loc) • 8.72 kB
JavaScript
;
var anatomy$1 = require('@zag-js/anatomy');
var core = require('@zag-js/core');
var utils = require('@zag-js/utils');
var types = require('@zag-js/types');
// src/progress.anatomy.ts
var anatomy = anatomy$1.createAnatomy("progress").parts(
"root",
"label",
"track",
"range",
"valueText",
"view",
"circle",
"circleTrack",
"circleRange"
);
var parts = anatomy.build();
// src/progress.dom.ts
var getRootId = (ctx) => ctx.ids?.root ?? `progress-${ctx.id}`;
var getTrackId = (ctx) => ctx.ids?.track ?? `progress-${ctx.id}-track`;
var getLabelId = (ctx) => ctx.ids?.label ?? `progress-${ctx.id}-label`;
var getCircleId = (ctx) => ctx.ids?.circle ?? `progress-${ctx.id}-circle`;
// src/progress.connect.ts
function connect(service, normalize) {
const { context, computed, prop, send, scope } = service;
const percent = computed("percent");
const percentAsString = computed("isIndeterminate") ? "" : computed("formatter").format(percent / 100);
const max = prop("max");
const min = prop("min");
const orientation = prop("orientation");
const translations = prop("translations");
const indeterminate = computed("isIndeterminate");
const value = context.get("value");
const valueAsString = translations?.value({ value, max, percent, min, formatter: computed("formatter") }) ?? "";
const progressState = getProgressState(value, max);
const progressbarProps = {
role: "progressbar",
"aria-label": valueAsString,
"data-max": max,
"aria-valuemin": min,
"aria-valuemax": max,
"aria-valuenow": value ?? void 0,
"data-orientation": orientation,
"data-state": progressState
};
const circleProps2 = getCircleProps(service);
return {
value,
valueAsString,
min,
max,
percent,
percentAsString,
indeterminate,
setValue(value2) {
send({ type: "VALUE.SET", value: value2 });
},
setToMax() {
send({ type: "VALUE.SET", value: max });
},
setToMin() {
send({ type: "VALUE.SET", value: min });
},
getRootProps() {
return normalize.element({
dir: prop("dir"),
...parts.root.attrs,
id: getRootId(scope),
"data-max": max,
"data-value": value ?? void 0,
"data-state": progressState,
"data-orientation": orientation,
style: {
"--percent": indeterminate ? void 0 : percent
}
});
},
getLabelProps() {
return normalize.element({
dir: prop("dir"),
id: getLabelId(scope),
...parts.label.attrs,
"data-orientation": orientation
});
},
getValueTextProps() {
return normalize.element({
dir: prop("dir"),
"aria-live": "polite",
...parts.valueText.attrs
});
},
getTrackProps() {
return normalize.element({
dir: prop("dir"),
id: getTrackId(scope),
...parts.track.attrs,
...progressbarProps
});
},
getRangeProps() {
return normalize.element({
dir: prop("dir"),
...parts.range.attrs,
"data-orientation": orientation,
"data-state": progressState,
style: {
[computed("isHorizontal") ? "width" : "height"]: indeterminate ? void 0 : `${percent}%`
}
});
},
getCircleProps() {
return normalize.element({
dir: prop("dir"),
id: getCircleId(scope),
...parts.circle.attrs,
...progressbarProps,
...circleProps2.root
});
},
getCircleTrackProps() {
return normalize.element({
dir: prop("dir"),
"data-orientation": orientation,
...parts.circleTrack.attrs,
...circleProps2.track
});
},
getCircleRangeProps() {
return normalize.element({
dir: prop("dir"),
...parts.circleRange.attrs,
...circleProps2.range,
"data-state": progressState
});
},
getViewProps(props2) {
return normalize.element({
dir: prop("dir"),
...parts.view.attrs,
"data-state": props2.state,
hidden: props2.state !== progressState
});
}
};
}
function getProgressState(value, maxValue) {
return value == null ? "indeterminate" : value === maxValue ? "complete" : "loading";
}
var circleProps = {
style: {
"--radius": "calc(var(--size) / 2 - var(--thickness) / 2)",
cx: "calc(var(--size) / 2)",
cy: "calc(var(--size) / 2)",
r: "var(--radius)",
fill: "transparent",
strokeWidth: "var(--thickness)"
}
};
var rootProps = {
style: {
width: "var(--size)",
height: "var(--size)"
}
};
function getCircleProps(service) {
const { context, computed } = service;
return {
root: rootProps,
track: circleProps,
range: {
opacity: context.get("value") === 0 ? 0 : void 0,
style: {
...circleProps.style,
"--percent": computed("percent"),
"--circumference": `calc(2 * 3.14159 * var(--radius))`,
"--offset": `calc(var(--circumference) * (100 - var(--percent)) / 100)`,
strokeDashoffset: `calc(var(--circumference) * ((100 - var(--percent)) / 100))`,
strokeDasharray: computed("isIndeterminate") ? void 0 : `var(--circumference)`,
transformOrigin: "center",
transform: "rotate(-90deg)"
}
}
};
}
var machine = core.createMachine({
props({ props: props2 }) {
const min = props2.min ?? 0;
const max = props2.max ?? 100;
return {
orientation: "horizontal",
...props2,
max,
min,
defaultValue: props2.defaultValue !== void 0 ? props2.defaultValue : midValue(min, max),
formatOptions: {
style: "percent",
...props2.formatOptions
},
translations: {
value: ({ value, percent, formatter }) => {
if (value === null) return "loading...";
if (formatter) {
const formatOptions = formatter.resolvedOptions();
const num = formatOptions.style === "percent" ? percent / 100 : value;
return formatter.format(num);
}
return value.toString();
},
...props2.translations
}
};
},
initialState() {
return "idle";
},
entry: ["validateContext"],
context({ bindable, prop }) {
return {
value: bindable(() => ({
defaultValue: prop("defaultValue"),
value: prop("value"),
onChange(value) {
prop("onValueChange")?.({ value });
}
}))
};
},
computed: {
isIndeterminate: ({ context }) => context.get("value") === null,
percent({ context, prop }) {
const value = context.get("value");
if (!utils.isNumber(value)) return -1;
return utils.getValuePercent(value, prop("min"), prop("max")) * 100;
},
formatter: core.memo(
({ prop }) => [prop("locale"), prop("formatOptions")],
([locale, formatOptions]) => new Intl.NumberFormat(locale, formatOptions)
),
isHorizontal: ({ prop }) => prop("orientation") === "horizontal"
},
states: {
idle: {
on: {
"VALUE.SET": {
actions: ["setValue"]
}
}
}
},
implementations: {
actions: {
setValue: ({ context, event, prop }) => {
const value = event.value === null ? null : Math.max(0, Math.min(event.value, prop("max")));
context.set("value", value);
},
validateContext: ({ context, prop }) => {
const max = prop("max");
const min = prop("min");
const value = context.get("value");
if (value == null) return;
if (!isValidNumber(max)) {
throw new Error(`[progress] The max value passed \`${max}\` is not a valid number`);
}
if (!isValidMax(value, max)) {
throw new Error(`[progress] The value passed \`${value}\` exceeds the max value \`${max}\``);
}
if (!isValidMin(value, min)) {
throw new Error(`[progress] The value passed \`${value}\` exceeds the min value \`${min}\``);
}
}
}
}
});
var isValidNumber = (max) => utils.isNumber(max) && !isNaN(max);
var isValidMax = (value, max) => isValidNumber(value) && value <= max;
var isValidMin = (value, min) => isValidNumber(value) && value >= min;
var midValue = (min, max) => min + (max - min) / 2;
var props = types.createProps()([
"dir",
"getRootNode",
"id",
"ids",
"max",
"min",
"orientation",
"translations",
"value",
"onValueChange",
"defaultValue",
"formatOptions",
"locale"
]);
var splitProps = utils.createSplitProps(props);
exports.anatomy = anatomy;
exports.connect = connect;
exports.machine = machine;
exports.props = props;
exports.splitProps = splitProps;