Skip to content

Commit

Permalink
refactor: progress
Browse files Browse the repository at this point in the history
  • Loading branch information
segunadebayo committed Feb 11, 2025
1 parent 0991f55 commit f2c7800
Show file tree
Hide file tree
Showing 8 changed files with 212 additions and 137 deletions.
71 changes: 68 additions & 3 deletions .xstate/progress.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,44 @@ const {
choose
} = actions;
const fetchMachine = createMachine({
id: "progress",
initial: "idle",
context: {},
props({
props
}) {
const min = props.min ?? 0;
const max = props.max ?? 100;
return {
...props,
max,
min,
defaultValue: props.defaultValue ?? midValue(min, max),
orientation: "horizontal",
translations: {
value: ({
percent
}) => percent === -1 ? "loading..." : `${percent} percent`,
...props.translations
}
};
},
initialState() {
return "idle";
},
context({
bindable,
prop
}) {
return {
value: bindable < number | null > (() => ({
defaultValue: prop("defaultValue"),
value: prop("value"),
onChange(value) {
prop("onValueChange")?.({
value
});
}
}))
};
},
on: {
UPDATE_CONTEXT: {
actions: "updateContext"
Expand All @@ -26,6 +61,36 @@ const fetchMachine = createMachine({
}
}
}
},
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}\``);
}
}
}
}
}, {
actions: {
Expand Down
8 changes: 4 additions & 4 deletions examples/next-ts/pages/progress.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ import { useControls } from "../hooks/use-controls"
export default function Page() {
const controls = useControls(progressControls)

const [state, send] = useMachine(progress.machine({ id: useId() }), {
context: controls.context,
const service = useMachine(progress.machine, {
id: useId(),
})

const api = progress.connect(state, send, normalizeProps)
const api = progress.connect(service, normalizeProps)

return (
<>
Expand Down Expand Up @@ -41,7 +41,7 @@ export default function Page() {
</main>

<Toolbar controls={controls.ui}>
<StateVisualizer state={state} />
<StateVisualizer state={service} />
</Toolbar>
</>
)
Expand Down
6 changes: 3 additions & 3 deletions packages/machines/progress/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ export { connect } from "./progress.connect"
export { machine } from "./progress.machine"
export * from "./progress.props"
export type {
MachineApi as Api,
UserDefinedContext as Context,
ProgressApi as Api,
ProgressService as Service,
ProgressProps as Props,
ElementIds,
IntlTranslations,
Orientation,
ProgressState,
Service,
ValueChangeDetails,
ValueTranslationDetails,
ViewProps,
Expand Down
64 changes: 33 additions & 31 deletions packages/machines/progress/src/progress.connect.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
import type { NormalizeProps, PropTypes } from "@zag-js/types"
import { parts } from "./progress.anatomy"
import { dom } from "./progress.dom"
import type { MachineApi, MachineContext, ProgressState, Send, State } from "./progress.types"
import * as dom from "./progress.dom"
import type { ProgressApi, ProgressService, ProgressState } from "./progress.types"

export function connect<T extends PropTypes>(state: State, send: Send, normalize: NormalizeProps<T>): MachineApi<T> {
const percent = state.context.percent
const percentAsString = state.context.isIndeterminate ? "" : `${percent}%`
export function connect<T extends PropTypes>(service: ProgressService, normalize: NormalizeProps<T>): ProgressApi<T> {
const { context, computed, prop, send, scope } = service
const percent = computed("percent")
const percentAsString = computed("isIndeterminate") ? "" : `${percent}%`

const max = state.context.max
const min = state.context.min
const max = prop("max")
const min = prop("min")

const orientation = state.context.orientation
const translations = state.context.translations
const indeterminate = state.context.isIndeterminate
const orientation = prop("orientation")
const translations = prop("translations")
const indeterminate = computed("isIndeterminate")

const value = state.context.value
const valueAsString = translations.value({ value, max, percent, min })
const value = context.get("value")
const valueAsString = translations?.value({ value, max, percent, min }) ?? ""
const progressState = getProgressState(value, max)

const progressbarProps = {
Expand All @@ -29,7 +30,7 @@ export function connect<T extends PropTypes>(state: State, send: Send, normalize
"data-state": progressState,
}

const circleProps = getCircleProps(state.context)
const circleProps = getCircleProps(service)

return {
value,
Expand All @@ -51,9 +52,9 @@ export function connect<T extends PropTypes>(state: State, send: Send, normalize

getRootProps() {
return normalize.element({
dir: state.context.dir,
dir: prop("dir"),
...parts.root.attrs,
id: dom.getRootId(state.context),
id: dom.getRootId(scope),
"data-max": max,
"data-value": value ?? undefined,
"data-state": progressState,
Expand All @@ -66,46 +67,46 @@ export function connect<T extends PropTypes>(state: State, send: Send, normalize

getLabelProps() {
return normalize.element({
dir: state.context.dir,
id: dom.getLabelId(state.context),
dir: prop("dir"),
id: dom.getLabelId(scope),
...parts.label.attrs,
"data-orientation": orientation,
})
},

getValueTextProps() {
return normalize.element({
dir: state.context.dir,
dir: prop("dir"),
"aria-live": "polite",
...parts.valueText.attrs,
})
},

getTrackProps() {
return normalize.element({
dir: state.context.dir,
id: dom.getTrackId(state.context),
dir: prop("dir"),
id: dom.getTrackId(scope),
...parts.track.attrs,
...progressbarProps,
})
},

getRangeProps() {
return normalize.element({
dir: state.context.dir,
dir: prop("dir"),
...parts.range.attrs,
"data-orientation": orientation,
"data-state": progressState,
style: {
[state.context.isHorizontal ? "width" : "height"]: indeterminate ? undefined : `${percent}%`,
[computed("isHorizontal") ? "width" : "height"]: indeterminate ? undefined : `${percent}%`,
},
})
},

getCircleProps() {
return normalize.element({
dir: state.context.dir,
id: dom.getCircleId(state.context),
dir: prop("dir"),
id: dom.getCircleId(scope),
...parts.circle.attrs,
...progressbarProps,
...circleProps.root,
Expand All @@ -114,7 +115,7 @@ export function connect<T extends PropTypes>(state: State, send: Send, normalize

getCircleTrackProps() {
return normalize.element({
dir: state.context.dir,
dir: prop("dir"),
"data-orientation": orientation,
...parts.circleTrack.attrs,
...circleProps.track,
Expand All @@ -123,7 +124,7 @@ export function connect<T extends PropTypes>(state: State, send: Send, normalize

getCircleRangeProps() {
return normalize.element({
dir: state.context.dir,
dir: prop("dir"),
...parts.circleRange.attrs,
...circleProps.range,
"data-state": progressState,
Expand All @@ -132,7 +133,7 @@ export function connect<T extends PropTypes>(state: State, send: Send, normalize

getViewProps(props) {
return normalize.element({
dir: state.context.dir,
dir: prop("dir"),
...parts.view.attrs,
"data-state": props.state,
hidden: props.state !== progressState,
Expand All @@ -145,7 +146,8 @@ function getProgressState(value: number | null, maxValue: number): ProgressState
return value == null ? "indeterminate" : value === maxValue ? "complete" : "loading"
}

function getCircleProps(ctx: MachineContext) {
function getCircleProps(service: ProgressService) {
const { context, computed } = service
const circleProps = {
style: {
"--radius": "calc(var(--size) / 2 - var(--thickness) / 2)",
Expand All @@ -165,14 +167,14 @@ function getCircleProps(ctx: MachineContext) {
},
track: circleProps,
range: {
opacity: ctx.value === 0 ? 0 : undefined,
opacity: context.get("value") === 0 ? 0 : undefined,
style: {
...circleProps.style,
"--percent": ctx.percent,
"--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: ctx.isIndeterminate ? undefined : `var(--circumference)`,
strokeDasharray: computed("isIndeterminate") ? undefined : `var(--circumference)`,
transformOrigin: "center",
transform: "rotate(-90deg)",
},
Expand Down
13 changes: 5 additions & 8 deletions packages/machines/progress/src/progress.dom.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import { createScope } from "@zag-js/dom-query"
import type { MachineContext as Ctx } from "./progress.types"
import type { Scope } from "@zag-js/core"

export const dom = createScope({
getRootId: (ctx: Ctx) => ctx.ids?.root ?? `progress-${ctx.id}`,
getTrackId: (ctx: Ctx) => ctx.ids?.track ?? `progress-${ctx.id}-track`,
getLabelId: (ctx: Ctx) => ctx.ids?.label ?? `progress-${ctx.id}-label`,
getCircleId: (ctx: Ctx) => ctx.ids?.circle ?? `progress-${ctx.id}-circle`,
})
export const getRootId = (ctx: Scope) => ctx.ids?.root ?? `progress-${ctx.id}`
export const getTrackId = (ctx: Scope) => ctx.ids?.track ?? `progress-${ctx.id}-track`
export const getLabelId = (ctx: Scope) => ctx.ids?.label ?? `progress-${ctx.id}-label`
export const getCircleId = (ctx: Scope) => ctx.ids?.circle ?? `progress-${ctx.id}-circle`
Loading

0 comments on commit f2c7800

Please sign in to comment.