Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 22 additions & 6 deletions examples/next-ts/pages/segment-control.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as radio from "@zag-js/radio-group"
import { normalizeProps, useMachine } from "@zag-js/react"
import { radioControls, radioData } from "@zag-js/shared"
import { radioControls, segmentControlData } from "@zag-js/shared"
import { useId } from "react"
import { StateVisualizer } from "../components/state-visualizer"
import { Toolbar } from "../components/toolbar"
Expand All @@ -13,6 +13,7 @@ export default function Page() {
id: useId(),
name: "fruit",
orientation: "horizontal",
defaultValue: "orange",
...controls.context,
})

Expand All @@ -23,16 +24,31 @@ export default function Page() {
<main className="segmented-control">
<div {...api.getRootProps()}>
<div {...api.getIndicatorProps()} />
{radioData.map((opt) => (
<label key={opt.id} data-testid={`radio-${opt.id}`} {...api.getItemProps({ value: opt.id })}>
<span data-testid={`label-${opt.id}`} {...api.getItemTextProps({ value: opt.id })}>
{segmentControlData.map((opt) => (
<label
key={opt.id}
data-testid={`radio-${opt.id}`}
{...api.getItemProps({ value: opt.id, disabled: opt.disabled })}
>
<span
data-testid={`label-${opt.id}`}
{...api.getItemTextProps({ value: opt.id, disabled: opt.disabled })}
>
{opt.label}
</span>
<input data-testid={`input-${opt.id}`} {...api.getItemHiddenInputProps({ value: opt.id })} />
<input
data-testid={`input-${opt.id}`}
{...api.getItemHiddenInputProps({ value: opt.id, disabled: opt.disabled })}
/>
</label>
))}
</div>
<button onClick={api.clearValue}>reset</button>

<div style={{ display: "flex", flexDirection: "row" }}>
<button onClick={api.clearValue}>Reset</button>
<button onClick={() => api.clearValue()}>Clear</button>
<button onClick={() => api.setValue("orange")}>Set to Oranges</button>
</div>
</main>

<Toolbar controls={controls.ui}>
Expand Down
8 changes: 6 additions & 2 deletions examples/next-ts/pages/tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export default function Page() {

const service = useMachine(tabs.machine, {
id: useId(),
defaultValue: "nils",
defaultValue: "evelyn",
...controls.context,
})

Expand All @@ -24,7 +24,11 @@ export default function Page() {
<div {...api.getIndicatorProps()} />
<div {...api.getListProps()}>
{tabsData.map((data) => (
<button {...api.getTriggerProps({ value: data.id })} key={data.id} data-testid={`${data.id}-tab`}>
<button
{...api.getTriggerProps({ value: data.id, disabled: data.disabled })}
key={data.id}
data-testid={`${data.id}-tab`}
>
{data.label}
</button>
))}
Expand Down
4 changes: 3 additions & 1 deletion packages/machines/radio-group/src/radio-group.connect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,12 +196,14 @@ export function connect<T extends PropTypes>(

getIndicatorProps() {
const rect = context.get("indicatorRect")
const disabled = context.get("indicatorDisabled")

return normalize.element({
id: dom.getIndicatorId(scope),
...parts.indicator.attrs,
dir: prop("dir"),
hidden: context.get("value") == null,
"data-disabled": dataAttr(groupDisabled),
"data-disabled": dataAttr(groupDisabled) || dataAttr(disabled),
"data-orientation": prop("orientation"),
style: {
"--transition-property": "left, top, width, height",
Expand Down
5 changes: 5 additions & 0 deletions packages/machines/radio-group/src/radio-group.dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,8 @@ export const resolveRect = (rect: Record<"width" | "height" | "left" | "top", nu
left: `${rect.left}px`,
top: `${rect.top}px`,
})

export const getIsTriggerDisabled = (ctx: Scope, value: string) => {
const el = ctx.getById(getItemId(ctx, value))
return el?.getAttribute("data-disabled") === ""
}
11 changes: 9 additions & 2 deletions packages/machines/radio-group/src/radio-group.machine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export const machine = createMachine<RadioGroupSchema>({
indicatorRect: bindable<Partial<IndicatorRect>>(() => ({
defaultValue: {},
})),
indicatorDisabled: bindable(() => ({ defaultValue: false })),
canIndicatorTransition: bindable<boolean>(() => ({
defaultValue: false,
})),
Expand All @@ -62,15 +63,15 @@ export const machine = createMachine<RadioGroupSchema>({
isDisabled: ({ prop, context }) => !!prop("disabled") || context.get("fieldsetDisabled"),
},

entry: ["syncIndicatorRect", "syncSsr"],
entry: ["syncIndicatorDisabled", "syncIndicatorRect", "syncSsr"],

exit: ["cleanupObserver"],

effects: ["trackFormControlState", "trackFocusVisible"],

watch({ track, action, context }) {
track([() => context.get("value")], () => {
action(["setIndicatorTransition", "syncIndicatorRect", "syncInputElements"])
action(["setIndicatorTransition", "syncIndicatorDisabled", "syncIndicatorRect", "syncInputElements"])
})
},

Expand Down Expand Up @@ -148,6 +149,12 @@ export const machine = createMachine<RadioGroupSchema>({
syncSsr({ context }) {
context.set("ssr", false)
},
syncIndicatorDisabled({ context, scope }) {
const value = context.get("value")
if (value) {
context.set("indicatorDisabled", dom.getIsTriggerDisabled(scope, value))
}
},
syncIndicatorRect({ context, scope, refs }) {
refs.get("indicatorCleanup")?.()

Expand Down
4 changes: 4 additions & 0 deletions packages/machines/radio-group/src/radio-group.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ interface PrivateContext {
* The active tab indicator's dom rect
*/
indicatorRect: Partial<IndicatorRect>
/**
* Whether the active tab indicator is disabled
*/
indicatorDisabled: boolean
/**
* Whether the active tab indicator's rect can transition
*/
Expand Down
3 changes: 3 additions & 0 deletions packages/machines/tabs/src/tabs.connect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,11 +185,14 @@ export function connect<T extends PropTypes>(service: Service<TabsSchema>, norma
getIndicatorProps() {
const indicatorRect = context.get("indicatorRect")
const indicatorTransition = context.get("indicatorTransition")
const indicatorDisabled = context.get("indicatorDisabled")

return normalize.element({
id: dom.getIndicatorId(scope),
...parts.indicator.attrs,
dir: prop("dir"),
"data-orientation": prop("orientation"),
"data-disabled": dataAttr(indicatorDisabled),
style: {
"--transition-property": "left, right, top, bottom, width, height",
"--left": indicatorRect.left,
Expand Down
5 changes: 5 additions & 0 deletions packages/machines/tabs/src/tabs.dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,8 @@ export const resolveRect = (rect: Record<"width" | "height" | "left" | "top", nu
left: `${rect.left}px`,
top: `${rect.top}px`,
})

export const getIsTriggerDisabled = (ctx: Scope, id: string) => {
const el = getTriggerEl(ctx, id)
return el?.hasAttribute("disabled") || el?.getAttribute("aria-disabled") === "true"
}
17 changes: 15 additions & 2 deletions packages/machines/tabs/src/tabs.machine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,19 @@ export const machine = createMachine({
indicatorRect: bindable(() => ({
defaultValue: { left: "0px", top: "0px", width: "0px", height: "0px" },
})),
indicatorDisabled: bindable(() => ({ defaultValue: false })),
}
},

watch({ context, prop, track, action }) {
track([() => context.get("value")], () => {
action(["allowIndicatorTransition", "syncIndicatorRect", "syncTabIndex", "navigateIfNeeded"])
action([
"allowIndicatorTransition",
"syncIndicatorDisabled",
"syncIndicatorRect",
"syncTabIndex",
"navigateIfNeeded",
])
})
track([() => prop("dir"), () => prop("orientation")], () => {
action(["syncIndicatorRect"])
Expand All @@ -73,7 +80,7 @@ export const machine = createMachine({
},
},

entry: ["syncIndicatorRect", "syncTabIndex", "syncSsr"],
entry: ["syncIndicatorDisabled", "syncIndicatorRect", "syncTabIndex", "syncSsr"],

exit: ["cleanupObserver"],

Expand Down Expand Up @@ -268,6 +275,12 @@ export const machine = createMachine({
syncSsr({ context }) {
context.set("ssr", false)
},
syncIndicatorDisabled({ context, scope }) {
const value = context.get("value")
if (value) {
context.set("indicatorDisabled", dom.getIsTriggerDisabled(scope, value))
}
},
syncIndicatorRect({ context, refs, scope }) {
const cleanup = refs.get("indicatorCleanup")
if (cleanup) cleanup()
Expand Down
1 change: 1 addition & 0 deletions packages/machines/tabs/src/tabs.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ export type TabsSchema = {
focusedValue: string | null
indicatorTransition: boolean
indicatorRect: { left: string; top: string; width: string; height: string }
indicatorDisabled: boolean
}
refs: {
indicatorCleanup: VoidFunction | null | undefined
Expand Down
9 changes: 9 additions & 0 deletions shared/src/css/segmented-control.css
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,18 @@
padding: 8px;
width: 100%;
text-align: center;

&[data-disabled] {
opacity: 0.5;
cursor: not-allowed;
}
}

.segmented-control [data-part="indicator"] {
width: var(--width);
height: var(--height);

&[data-disabled] {
opacity: 0.5;
}
}
8 changes: 8 additions & 0 deletions shared/src/css/tabs.css
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@
background: blue;
color: white;
}
&[disabled] {
opacity: 0.5;
pointer-events: none;
}
}

[data-scope="tabs"][data-part="content"] {
Expand All @@ -38,6 +42,10 @@
[data-scope="tabs"][data-part="indicator"] {
background-color: red;
z-index: 10;

&[data-disabled] {
opacity: 0.5;
}
}

[data-scope="tabs"][data-part="indicator"][data-orientation="horizontal"] {
Expand Down
18 changes: 18 additions & 0 deletions shared/src/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,17 @@ export const tabsData = [
piano, upright piano, Roland Juno-60, Rhodes piano, drum machine, and Moog Taurus.
`,
},
{
id: "evelyn",
label: "Evelyn Glennie",
content: `
Evelyn Glennie is a Scottish percussionist and composer who has been profoundly deaf since the age of 12. She
is known for her ability to feel music through vibrations and has performed internationally, becoming one
of the world's leading solo percussionists. Her work has inspired many and challenged perceptions of
disability in music.
`,
disabled: true,
},
{
id: "agnes",
label: "Agnes Obel",
Expand Down Expand Up @@ -129,6 +140,13 @@ export const radioData = [
{ id: "grape", label: "Grapes" },
]

export const segmentControlData = [
{ id: "apple", label: "Apples" },
{ id: "orange", label: "Oranges", disabled: true },
{ id: "mango", label: "Mangoes" },
{ id: "grape", label: "Grapes" },
]

export const carouselData = [
"https://picsum.photos/seed/a/500/300",
"https://picsum.photos/seed/b/500/300",
Expand Down
Loading