Skip to content

Commit ff15701

Browse files
committed
chore: prep for nested nav menu
1 parent 8443a9c commit ff15701

File tree

7 files changed

+498
-20
lines changed

7 files changed

+498
-20
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
import * as navigationMenu from "@zag-js/navigation-menu"
2+
import { normalizeProps, useMachine } from "@zag-js/react"
3+
import { ChevronDown } from "lucide-react"
4+
import { useId } from "react"
5+
import { Presence } from "../components/presence"
6+
import { StateVisualizer } from "../components/state-visualizer"
7+
import { Toolbar } from "../components/toolbar"
8+
import { useEffectOnce } from "../hooks/use-effect-once"
9+
10+
export default function Page() {
11+
const rootService = useMachine(navigationMenu.machine, { id: useId() })
12+
const rootMenu = navigationMenu.connect(rootService, normalizeProps)
13+
14+
const productService = useMachine(navigationMenu.machine, { id: useId(), value: "extensibility" })
15+
const productSubmenu = navigationMenu.connect(productService, normalizeProps)
16+
17+
const companyService = useMachine(navigationMenu.machine, { id: useId(), value: "customers" })
18+
const companySubmenu = navigationMenu.connect(companyService, normalizeProps)
19+
20+
const renderLinks = (menu: typeof rootMenu, opts: { value: string; items: string[] }) => {
21+
const { value, items } = opts
22+
return items.map((item, index) => (
23+
<a href="#" key={`${value}-${item}-${index}`} {...menu.getLinkProps({ value })}>
24+
{item}
25+
</a>
26+
))
27+
}
28+
29+
useEffectOnce(() => {
30+
productSubmenu.setParent(rootService)
31+
rootMenu.setChild(productService)
32+
})
33+
34+
useEffectOnce(() => {
35+
companySubmenu.setParent(rootService)
36+
rootMenu.setChild(companyService)
37+
})
38+
39+
return (
40+
<>
41+
<main className="navigation-menu nested">
42+
<Navbar>
43+
<div {...rootMenu.getRootProps()}>
44+
<div {...rootMenu.getListProps()}>
45+
<div {...rootMenu.getItemProps({ value: "products" })}>
46+
<button {...rootMenu.getTriggerProps({ value: "products" })}>
47+
Products
48+
<ChevronDown />
49+
</button>
50+
</div>
51+
52+
<div {...rootMenu.getItemProps({ value: "company" })}>
53+
<button {...rootMenu.getTriggerProps({ value: "company" })}>
54+
Company
55+
<ChevronDown />
56+
</button>
57+
</div>
58+
59+
<div {...rootMenu.getItemProps({ value: "developers", disabled: true })}>
60+
<button {...rootMenu.getTriggerProps({ value: "developers", disabled: true })}>
61+
Developers
62+
<ChevronDown />
63+
</button>
64+
</div>
65+
66+
<div {...rootMenu.getItemProps({ value: "pricing" })}>
67+
<a href="#" {...rootMenu.getLinkProps({ value: "pricing" })}>
68+
Pricing
69+
</a>
70+
</div>
71+
</div>
72+
73+
<Presence {...rootMenu.getViewportProps()}>
74+
<Presence {...rootMenu.getContentProps({ value: "products" })}>
75+
<div {...productSubmenu.getRootProps()}>
76+
<div {...productSubmenu.getIndicatorTrackProps()}>
77+
<div {...productSubmenu.getListProps()}>
78+
<div {...productSubmenu.getItemProps({ value: "extensibility" })}>
79+
<button {...productSubmenu.getTriggerProps({ value: "extensibility" })}>Extensibility</button>
80+
</div>
81+
82+
<div {...productSubmenu.getItemProps({ value: "security" })}>
83+
<button {...productSubmenu.getTriggerProps({ value: "security" })}>Security</button>
84+
</div>
85+
86+
<div {...productSubmenu.getItemProps({ value: "authentication" })}>
87+
<button {...productSubmenu.getTriggerProps({ value: "authentication" })}>Authentication</button>
88+
</div>
89+
<div {...productSubmenu.getIndicatorProps()} />
90+
</div>
91+
</div>
92+
93+
<Presence {...productSubmenu.getViewportProps()}>
94+
<Presence
95+
{...productSubmenu.getContentProps({ value: "extensibility" })}
96+
style={{
97+
gridTemplateColumns: "1.5fr 1fr 1fr",
98+
}}
99+
>
100+
{renderLinks(productSubmenu, {
101+
value: "extensibility",
102+
items: ["Donec quis dui", "Vestibulum", "Nunc dignissim"],
103+
})}
104+
{renderLinks(productSubmenu, {
105+
value: "extensibility",
106+
items: ["Fusce pellentesque", "Aliquam porttitor", "Pellentesque"],
107+
})}
108+
{renderLinks(productSubmenu, {
109+
value: "extensibility",
110+
items: ["Fusce pellentesque", "Aliquam porttitor", "Pellentesque"],
111+
})}
112+
</Presence>
113+
114+
<Presence
115+
{...productSubmenu.getContentProps({ value: "security" })}
116+
style={{
117+
gridTemplateColumns: "1fr 1fr 1fr",
118+
}}
119+
>
120+
{renderLinks(productSubmenu, {
121+
value: "security",
122+
items: ["Fusce pellentesque", "Aliquam porttitor", "Pellentesque", "Vestibulum"],
123+
})}
124+
{renderLinks(productSubmenu, {
125+
value: "security",
126+
items: ["Fusce pellentesque", "Aliquam porttitor", "Pellentesque"],
127+
})}
128+
{renderLinks(productSubmenu, {
129+
value: "security",
130+
items: ["Fusce pellentesque", "Aliquam porttitor"],
131+
})}
132+
</Presence>
133+
134+
<Presence
135+
{...productSubmenu.getContentProps({ value: "authentication" })}
136+
style={{
137+
gridTemplateColumns: "1.5fr 1fr 1fr",
138+
}}
139+
>
140+
{renderLinks(productSubmenu, {
141+
value: "authentication",
142+
items: ["Donec quis dui", "Vestibulum", "Nunc dignissim"],
143+
})}
144+
{renderLinks(productSubmenu, {
145+
value: "authentication",
146+
items: ["Fusce pellentesque", "Aliquam porttitor", "Pellentesque"],
147+
})}
148+
{renderLinks(productSubmenu, {
149+
value: "authentication",
150+
items: ["Fusce pellentesque", "Aliquam porttitor", "Pellentesque"],
151+
})}
152+
</Presence>
153+
</Presence>
154+
</div>
155+
</Presence>
156+
157+
<Presence {...rootMenu.getContentProps({ value: "company" })}>
158+
<div {...companySubmenu.getRootProps()}>
159+
<div {...companySubmenu.getIndicatorTrackProps()}>
160+
<div {...companySubmenu.getListProps()}>
161+
<div {...companySubmenu.getItemProps({ value: "customers" })}>
162+
<button {...companySubmenu.getTriggerProps({ value: "customers" })}>Customers</button>
163+
</div>
164+
165+
<div {...companySubmenu.getItemProps({ value: "partners" })}>
166+
<button {...companySubmenu.getTriggerProps({ value: "partners" })}>Partners</button>
167+
</div>
168+
169+
<div {...companySubmenu.getItemProps({ value: "enterprise" })}>
170+
<button {...companySubmenu.getTriggerProps({ value: "enterprise" })}>Enterprise</button>
171+
</div>
172+
</div>
173+
<div {...companySubmenu.getIndicatorProps()} />
174+
</div>
175+
176+
<Presence {...companySubmenu.getViewportProps()}>
177+
<Presence
178+
{...companySubmenu.getContentProps({ value: "customers" })}
179+
style={{
180+
gridTemplateColumns: "1.5fr 1fr",
181+
}}
182+
>
183+
{renderLinks(companySubmenu, {
184+
value: "customers",
185+
items: ["Donec quis dui", "Vestibulum", "Nunc dignissim"],
186+
})}
187+
{renderLinks(companySubmenu, {
188+
value: "customers",
189+
items: ["Fusce pellentesque", "Aliquam porttitor", "Pellentesque"],
190+
})}
191+
</Presence>
192+
193+
<Presence
194+
{...companySubmenu.getContentProps({ value: "partners" })}
195+
style={{
196+
gridTemplateColumns: "1fr 1fr",
197+
}}
198+
>
199+
{renderLinks(companySubmenu, {
200+
value: "partners",
201+
items: ["Fusce pellentesque", "Aliquam porttitor", "Pellentesque", "Vestibulum"],
202+
})}
203+
{renderLinks(companySubmenu, {
204+
value: "partners",
205+
items: ["Fusce pellentesque", "Aliquam porttitor", "Pellentesque"],
206+
})}
207+
</Presence>
208+
209+
<Presence
210+
{...companySubmenu.getContentProps({ value: "enterprise" })}
211+
style={{
212+
gridTemplateColumns: "1.5fr 1fr",
213+
}}
214+
>
215+
{renderLinks(companySubmenu, {
216+
value: "enterprise",
217+
items: ["Donec quis dui", "Vestibulum", "Nunc dignissim"],
218+
})}
219+
{renderLinks(companySubmenu, {
220+
value: "enterprise",
221+
items: ["Fusce pellentesque", "Aliquam porttitor", "Pellentesque"],
222+
})}
223+
</Presence>
224+
</Presence>
225+
</div>
226+
</Presence>
227+
228+
<Presence {...rootMenu.getContentProps({ value: "developers" })}>
229+
{renderLinks(rootMenu, {
230+
value: "developers",
231+
items: ["Donec quis dui", "Vestibulum", "Fusce pellentesque", "Aliquam porttitor"],
232+
})}
233+
{renderLinks(rootMenu, {
234+
value: "developers",
235+
items: ["Fusce pellentesque", "Aliquam porttitor"],
236+
})}
237+
</Presence>
238+
</Presence>
239+
</div>
240+
</Navbar>
241+
</main>
242+
243+
<Toolbar viz>
244+
<StateVisualizer state={rootService} label="root" />
245+
<StateVisualizer state={productService} label="product" />
246+
<StateVisualizer state={companyService} label="company" />
247+
</Toolbar>
248+
</>
249+
)
250+
}
251+
252+
const Navbar = ({ children }: { children: React.ReactNode }) => {
253+
return (
254+
<div
255+
style={{
256+
position: "relative",
257+
display: "flex",
258+
boxSizing: "border-box",
259+
alignItems: "center",
260+
padding: "15px 20px",
261+
justifyContent: "space-between",
262+
width: "100%",
263+
backgroundColor: "white",
264+
boxShadow: "0 50px 100px -20px rgba(50,50,93,0.1),0 30px 60px -30px rgba(0,0,0,0.2)",
265+
}}
266+
>
267+
<button>Logo</button>
268+
{children}
269+
<button>Login</button>
270+
</div>
271+
)
272+
}

packages/machines/navigation-menu/src/navigation-menu.machine.ts

+13-17
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import type { Rect, Size } from "@zag-js/types"
55
import { callAll, ensureProps, setRafTimeout } from "@zag-js/utils"
66
import { autoReset } from "./auto-reset"
77
import * as dom from "./navigation-menu.dom"
8-
import type { NavigationMenuSchema, NavigationMenuService } from "./navigation-menu.types"
8+
import type { NavigationMenuSchema } from "./navigation-menu.types"
99

1010
const { createMachine, guards } = setup<NavigationMenuSchema>()
1111

@@ -62,18 +62,12 @@ export const machine = createMachine({
6262
})),
6363

6464
// nesting
65-
parent: bindable<NavigationMenuService | null>(() => ({
66-
defaultValue: null,
67-
})),
68-
children: bindable<Record<string, NavigationMenuService | null>>(() => ({
69-
defaultValue: {},
70-
})),
7165
}
7266
},
7367

7468
computed: {
75-
isRootMenu: ({ context }) => context.get("parent") == null,
76-
isSubmenu: ({ context }) => context.get("parent") != null,
69+
isRootMenu: ({ refs }) => refs.get("parent") == null,
70+
isSubmenu: ({ refs }) => refs.get("parent") != null,
7771
},
7872

7973
watch({ track, action, context }) {
@@ -90,6 +84,8 @@ export const machine = createMachine({
9084
tabOrderCleanup: null,
9185
contentCleanup: null,
9286
triggerCleanup: null,
87+
parent: null,
88+
children: {},
9389
}
9490
},
9591

@@ -238,8 +234,8 @@ export const machine = createMachine({
238234
guards: {
239235
isItemOpen: ({ context, event }) => context.get("value") === event.value,
240236
wasItemOpen: ({ context, event }) => context.get("previousValue") === event.value,
241-
isRootMenu: ({ context }) => context.get("parent") == null,
242-
isSubmenu: ({ context }) => context.get("parent") != null,
237+
isRootMenu: ({ refs }) => refs.get("parent") == null,
238+
isSubmenu: ({ refs }) => refs.get("parent") != null,
243239
},
244240

245241
effects: {
@@ -430,14 +426,14 @@ export const machine = createMachine({
430426
restoreTabOrder({ refs }) {
431427
refs.get("tabOrderCleanup")?.()
432428
},
433-
setParentMenu({ context, event }) {
434-
context.set("parent", event.parent)
429+
setParentMenu({ refs, event }) {
430+
refs.set("parent", event.parent)
435431
},
436-
setChildMenu({ context, event }) {
437-
context.set("children", (prev) => ({ ...prev, [event.id]: event.value }))
432+
setChildMenu({ refs, event }) {
433+
refs.set("children", { ...refs.get("children"), [event.id]: event.value })
438434
},
439-
propagateClose({ context, prop }) {
440-
const menus = Object.values(context.get("children"))
435+
propagateClose({ refs, prop }) {
436+
const menus = Object.values(refs.get("children"))
441437
menus.forEach((child) => {
442438
child?.send({ type: "ROOT.CLOSE", src: prop("id")! })
443439
})

packages/machines/navigation-menu/src/navigation-menu.types.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ export interface NavigationMenuSchema {
8787
pointerMoveOpenedRef: AutoReset<string | null>
8888
clickCloseRef: string | null
8989
wasEscapeClose: boolean
90+
parent: NavigationMenuService | null
91+
children: Record<string, NavigationMenuService | null>
9092
}
9193
context: {
9294
value: string | null
@@ -96,8 +98,6 @@ export interface NavigationMenuSchema {
9698
contentNode: HTMLElement | null
9799
triggerRect: Rect | null
98100
triggerNode: HTMLElement | null
99-
parent: NavigationMenuService | null
100-
children: Record<string, NavigationMenuService | null>
101101
}
102102
action: string
103103
effect: string

packages/utilities/stringify-state/src/index.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const pick = (obj: Dict, keys: string[]) =>
1111
const hasProp = (v: any, prop: string) => Object.prototype.hasOwnProperty.call(v, prop)
1212

1313
const isTimeObject = (v: any) => hasProp(v, "hour") && hasProp(v, "minute") && hasProp(v, "second")
14+
const isMachine = (v: any) => ["state", "context", "scope"].every((prop) => hasProp(v, prop))
1415

1516
export function stringifyState(state: Dict, omit?: string[]) {
1617
const code = JSON.stringify(
@@ -48,8 +49,12 @@ export function stringifyState(state: Dict, omit?: string[]) {
4849
return Array.from(value)
4950
}
5051

52+
if (isMachine(value)) {
53+
return `Machine: ${value.scope.id}`
54+
}
55+
5156
switch (value?.toString()) {
52-
case "[object Machine]":
57+
case "[object ShadowRoot]":
5358
const id = value.state.context.id ?? value.id
5459
return `Machine: ${id}`
5560

0 commit comments

Comments
 (0)