Skip to content

Commit aa7c4d8

Browse files
committed
chore: more progress with nav-menu
1 parent c92f847 commit aa7c4d8

File tree

7 files changed

+75
-46
lines changed

7 files changed

+75
-46
lines changed

examples/next-ts/components/presence.tsx

+3-7
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,20 @@ import * as presence from "@zag-js/presence"
22
import { normalizeProps, useMachine } from "@zag-js/react"
33
import { forwardRef, Ref, RefObject } from "react"
44

5-
function usePresence(present: boolean) {
6-
const service = useMachine(presence.machine, { present })
7-
return presence.connect(service, normalizeProps)
8-
}
9-
105
interface PresenceProps extends React.HTMLAttributes<HTMLDivElement> {}
116

127
export const Presence = forwardRef<HTMLDivElement, PresenceProps>(function Presence(props, ref) {
138
const { hidden, ...rest } = props
149

1510
const present = !hidden
16-
const api = usePresence(present)
11+
const service = useMachine(presence.machine, { present })
12+
const api = presence.connect(service, normalizeProps)
1713

1814
return (
1915
<div
2016
ref={mergeRefs(ref, (node) => api.setNode(node))}
2117
data-scope="presence"
22-
data-state={present ? "open" : "closed"}
18+
data-state={api.skip ? undefined : present ? "open" : "closed"}
2319
hidden={!api.present}
2420
{...rest}
2521
/>

examples/next-ts/pages/navigation-menu-viewport.tsx

+22-12
Original file line numberDiff line numberDiff line change
@@ -79,18 +79,18 @@ export default function Page() {
7979
{renderLinks({
8080
value: "products",
8181
items: [
82-
"Fusce pellentesque",
83-
"Aliquam porttitor",
84-
"Pellentesque",
85-
"Fusce pellentesque",
86-
"Aliquam porttitor",
87-
"Pellentesque",
82+
"Analytics Platform",
83+
"Customer Engagement",
84+
"Marketing Automation",
85+
"Data Integration",
86+
"Enterprise Solutions",
87+
"API Documentation",
8888
],
8989
})}
9090

9191
{renderLinks({
9292
value: "products",
93-
items: ["Fusce pellentesque", "Aliquam porttitor", "Pellentesque"],
93+
items: ["Case Studies", "Success Stories", "Integration Partners", "Security & Compliance"],
9494
})}
9595
</Presence>
9696

@@ -103,12 +103,12 @@ export default function Page() {
103103
>
104104
{renderLinks({
105105
value: "company",
106-
items: ["Fusce pellentesque", "Aliquam porttitor", "Pellentesque", "Aliquam porttitor"],
106+
items: ["About Us", "Leadership Team", "Careers", "Press Releases"],
107107
})}
108108

109109
{renderLinks({
110110
value: "company",
111-
items: ["Fusce pellentesque", "Aliquam porttitor", "Pellentesque"],
111+
items: ["Investors", "Partners", "Corporate Responsibility"],
112112
})}
113113
</Presence>
114114

@@ -121,11 +121,18 @@ export default function Page() {
121121
>
122122
{renderLinks({
123123
value: "developers",
124-
items: ["Donec quis dui", "Vestibulum", "Fusce pellentesque", "Aliquam porttitor"],
124+
items: [
125+
"API Documentation",
126+
"SDKs & Libraries",
127+
"Developer Guides",
128+
"Code Samples",
129+
"Webhooks",
130+
"GraphQL Explorer",
131+
],
125132
})}
126133
{renderLinks({
127134
value: "developers",
128-
items: ["Fusce pellentesque", "Aliquam porttitor"],
135+
items: ["Developer Community", "Changelog", "Status Page", "Rate Limits"],
129136
})}
130137
</Presence>
131138
</Presence>
@@ -135,7 +142,10 @@ export default function Page() {
135142
</main>
136143

137144
<Toolbar controls={controls.ui} viz>
138-
<StateVisualizer state={service} context={["value", "previousValue"]} />
145+
<StateVisualizer
146+
state={service}
147+
context={["value", "previousValue", "triggerRect", "viewportSize", "hasPointerMoveOpened"]}
148+
/>
139149
</Toolbar>
140150
</>
141151
)

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

+18-11
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,13 @@ export function connect<T extends PropTypes>(
1010
): NavigationMenuApi<T> {
1111
const { context, send, prop, scope, computed } = service
1212

13-
const activeTriggerRect = context.get("triggerRect")
13+
const triggerRect = context.get("triggerRect")
1414
const viewportSize = context.get("viewportSize")
1515

1616
const value = context.get("value")
17+
const previousValue = context.get("previousValue")
1718
const open = Boolean(value)
1819

19-
const previousValue = context.get("previousValue")
2020
const isViewportRendered = context.get("isViewportRendered")
2121
const preventTransition = value && !previousValue
2222

@@ -44,7 +44,7 @@ export function connect<T extends PropTypes>(
4444
send({ type: "PARENT.SET", parent })
4545
},
4646
setChild(child) {
47-
send({ type: "CHILD.SET", value: child, id: child.prop("id")! })
47+
send({ type: "CHILD.SET", value: child, id: child.prop("id") })
4848
},
4949

5050
getRootProps() {
@@ -56,10 +56,10 @@ export function connect<T extends PropTypes>(
5656
"data-type": computed("isSubmenu") ? "submenu" : "root",
5757
dir: prop("dir"),
5858
style: {
59-
"--trigger-width": activeTriggerRect != null ? activeTriggerRect.width + "px" : undefined,
60-
"--trigger-height": activeTriggerRect != null ? activeTriggerRect.height + "px" : undefined,
61-
"--trigger-x": activeTriggerRect != null ? activeTriggerRect.x + "px" : undefined,
62-
"--trigger-y": activeTriggerRect != null ? activeTriggerRect.y + "px" : undefined,
59+
"--trigger-width": triggerRect != null ? triggerRect.width + "px" : undefined,
60+
"--trigger-height": triggerRect != null ? triggerRect.height + "px" : undefined,
61+
"--trigger-x": triggerRect != null ? triggerRect.x + "px" : undefined,
62+
"--trigger-y": triggerRect != null ? triggerRect.y + "px" : undefined,
6363
"--viewport-width": viewportSize != null ? viewportSize.width + "px" : undefined,
6464
"--viewport-height": viewportSize != null ? viewportSize.height + "px" : undefined,
6565
},
@@ -128,7 +128,7 @@ export function connect<T extends PropTypes>(
128128
return normalize.button({
129129
...parts.trigger.attrs,
130130
id: itemState.triggerId,
131-
"data-uid": prop("id")!,
131+
"data-uid": prop("id"),
132132
dir: prop("dir"),
133133
disabled: props.disabled,
134134
"data-value": props.value,
@@ -137,10 +137,15 @@ export function connect<T extends PropTypes>(
137137
"data-disabled": dataAttr(props.disabled),
138138
"aria-controls": itemState.contentId,
139139
"aria-expanded": itemState.selected,
140+
onPointerDown() {
141+
send({ type: "TRIGGER.POINTERDOWN" })
142+
},
140143
onPointerEnter() {
144+
if (prop("disableHoverTrigger")) return
141145
send({ type: "TRIGGER.ENTER", value: props.value })
142146
},
143147
onPointerMove(event) {
148+
if (prop("disableHoverTrigger")) return
144149
if (event.pointerType !== "mouse") return
145150
if (itemState.disabled) return
146151
if (context.get("hasPointerMoveOpened") === props.value) return
@@ -149,12 +154,14 @@ export function connect<T extends PropTypes>(
149154
send({ type: "TRIGGER.MOVE", value: props.value })
150155
},
151156
onPointerLeave(event) {
157+
if (prop("disableHoverTrigger")) return
152158
if (event.pointerType !== "mouse") return
153159
if (props.disabled) return
154160
if (computed("isSubmenu")) return
155161
send({ type: "TRIGGER.LEAVE", value: props.value })
156162
},
157163
onClick() {
164+
if (prop("disableClickTrigger")) return
158165
send({ type: "TRIGGER.CLICK", value: props.value })
159166
},
160167
onKeyDown(event) {
@@ -195,8 +202,8 @@ export function connect<T extends PropTypes>(
195202
...parts.link.attrs,
196203
dir: prop("dir"),
197204
"data-value": props.value,
198-
"data-current": dataAttr(props.current),
199-
"aria-current": props.current ? "page" : undefined,
205+
"data-current": dataAttr(props.active),
206+
"aria-current": props.active ? "page" : undefined,
200207
"data-ownedby": dom.getContentId(scope, props.value),
201208
onClick(event) {
202209
const { currentTarget } = event
@@ -275,7 +282,7 @@ export function connect<T extends PropTypes>(
275282
dir: prop("dir"),
276283
hidden: !selected,
277284
"aria-labelledby": itemState.triggerId,
278-
"data-uid": prop("id")!,
285+
"data-uid": prop("id"),
279286
"data-state": selected ? "open" : "closed",
280287
"data-type": computed("isSubmenu") ? "submenu" : "root",
281288
"data-value": props.value,

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

+4-4
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,9 @@ export function setMotionAttr(scope: Scope, value: string | null, previousValue:
105105

106106
const contentEls = getContentEls(scope)
107107
contentEls.forEach((contentEl) => {
108-
const value = contentEl.dataset.value!
109-
const selected = value === value
110-
const prevSelected = prevIndex === values.indexOf(value)
108+
const itemValue = contentEl.dataset.value!
109+
const selected = value === itemValue
110+
const prevSelected = prevIndex === values.indexOf(itemValue)
111111

112112
if (!selected && !prevSelected) {
113113
delete contentEl.dataset.motion
@@ -124,7 +124,7 @@ export function setMotionAttr(scope: Scope, value: string | null, previousValue:
124124
}
125125
// Otherwise we're entering from closed or leaving the list
126126
// entirely and should not animate in any direction
127-
return undefined
127+
return null
128128
})()
129129

130130
if (attribute) {

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

+18-10
Original file line numberDiff line numberDiff line change
@@ -115,14 +115,19 @@ export const machine = createMachine({
115115
},
116116
"TRIGGER.CLICK": [
117117
{
118+
target: "closed",
118119
guard: and("isItemOpen", "isRootMenu"),
119120
actions: ["clearValue", "setClickCloseRef"],
120121
},
121122
{
123+
reenter: true,
122124
target: "open",
123-
actions: ["setValue", "setClickCloseRef"],
125+
actions: ["setValue"],
124126
},
125127
],
128+
"TRIGGER.POINTERDOWN": {
129+
actions: ["clearPreviousValue"],
130+
},
126131
"TRIGGER.FOCUS": {
127132
actions: ["focusTopLevelEl"],
128133
},
@@ -136,7 +141,7 @@ export const machine = createMachine({
136141
entry: ["cleanupObservers", "propagateClose"],
137142
on: {
138143
"TRIGGER.ENTER": {
139-
actions: ["clearCloseRefs"],
144+
actions: ["clearCloseRefs", "clearPreviousValue"],
140145
},
141146
"TRIGGER.MOVE": [
142147
{
@@ -149,19 +154,22 @@ export const machine = createMachine({
149154
actions: ["setPointerMoveRef"],
150155
},
151156
],
157+
"TRIGGER.LEAVE": {
158+
actions: ["clearPointerMoveRef"],
159+
},
152160
},
153161
},
154162

155163
opening: {
156164
effects: ["waitForOpenDelay"],
157165
on: {
158-
OPEN_DELAY: {
166+
"OPEN.DELAY": {
159167
target: "open",
160168
actions: ["setValue"],
161169
},
162170
"TRIGGER.LEAVE": {
163171
target: "closed",
164-
actions: ["clearValue", "clearPointerMoveRef"],
172+
actions: ["clearValue", "clearPreviousValue", "clearPointerMoveRef"],
165173
},
166174
"CONTENT.FOCUS": {
167175
actions: ["focusContent", "restoreTabOrder"],
@@ -191,7 +199,7 @@ export const machine = createMachine({
191199
},
192200
"CONTENT.DISMISS": {
193201
target: "closed",
194-
actions: ["focusTriggerIfNeeded", "clearValue", "clearPointerMoveRef"],
202+
actions: ["focusTriggerIfNeeded", "clearValue", "clearPreviousValue", "clearPointerMoveRef"],
195203
},
196204
"CONTENT.ENTER": {
197205
actions: ["restoreTabOrder"],
@@ -211,13 +219,13 @@ export const machine = createMachine({
211219
tags: ["open"],
212220
effects: ["trackInteractionOutside", "waitForCloseDelay"],
213221
on: {
214-
CLOSE_DELAY: {
222+
"CLOSE.DELAY": {
215223
target: "closed",
216-
actions: ["clearValue"],
224+
actions: ["clearValue", "clearPreviousValue"],
217225
},
218226
"CONTENT.DISMISS": {
219227
target: "closed",
220-
actions: ["focusTriggerIfNeeded", "clearValue", "clearPointerMoveRef"],
228+
actions: ["focusTriggerIfNeeded", "clearValue", "clearPreviousValue", "clearPointerMoveRef"],
221229
},
222230
"CONTENT.ENTER": {
223231
target: "open",
@@ -252,12 +260,12 @@ export const machine = createMachine({
252260
effects: {
253261
waitForOpenDelay: ({ send, prop, event }) => {
254262
return setRafTimeout(() => {
255-
send({ type: "OPEN_DELAY", value: event.value })
263+
send({ type: "OPEN.DELAY", value: event.value })
256264
}, prop("openDelay"))
257265
},
258266
waitForCloseDelay: ({ send, prop, event }) => {
259267
return setRafTimeout(() => {
260-
send({ type: "CLOSE_DELAY", value: event.value })
268+
send({ type: "CLOSE.DELAY", value: event.value })
261269
}, prop("closeDelay"))
262270
},
263271
preserveTabOrder({ context, scope, refs }) {

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

+9-1
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,14 @@ export interface NavigationMenuProps extends DirectionProperty, CommonProperties
6060
* @default 300
6161
*/
6262
closeDelay?: number | undefined
63+
/**
64+
* Whether to disable the click trigger
65+
*/
66+
disableClickTrigger?: boolean | undefined
67+
/**
68+
* Whether to disable the hover trigger
69+
*/
70+
disableHoverTrigger?: boolean | undefined
6371
}
6472

6573
type PropsWithDefault = "openDelay" | "closeDelay" | "dir" | "id"
@@ -130,7 +138,7 @@ export interface LinkProps {
130138
/**
131139
* Whether the link is the current link
132140
*/
133-
current?: boolean | undefined
141+
active?: boolean | undefined
134142
/**
135143
* Function called when the link is selected
136144
*/

shared/src/css/navigation-menu-viewport.css

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
font-size: inherit;
2727
gap: 4px;
2828

29-
& > svg {
29+
& svg {
3030
transition: rotate 200ms ease;
3131
width: 14px;
3232
height: 14px;

0 commit comments

Comments
 (0)