diff --git a/.changeset/healthy-zebras-cough.md b/.changeset/healthy-zebras-cough.md
new file mode 100644
index 0000000000..2d39033460
--- /dev/null
+++ b/.changeset/healthy-zebras-cough.md
@@ -0,0 +1,5 @@
+---
+"@zag-js/popover": patch
+---
+
+Fix issue where popover does not restore focus when open state is changed programmatically
diff --git a/examples/next-ts/pages/popover-controlled.tsx b/examples/next-ts/pages/popover-controlled.tsx
new file mode 100644
index 0000000000..4754155f0a
--- /dev/null
+++ b/examples/next-ts/pages/popover-controlled.tsx
@@ -0,0 +1,40 @@
+import { Fragment, useId, useState } from "react"
+import * as popover from "@zag-js/popover"
+import { useMachine, normalizeProps, Portal } from "@zag-js/react"
+
+export default function Page() {
+ const [open, setOpen] = useState(false)
+
+ const initialContext = {
+ id: useId(),
+ open: false,
+ "open.controlled": true,
+ }
+
+ const [state, send] = useMachine(popover.machine(initialContext), {
+ context: {
+ ...initialContext,
+ onOpenChange: (details) => setOpen(details.open),
+ open: open,
+ },
+ })
+
+ const api = popover.connect(state, send, normalizeProps)
+
+ const Wrapper = api.portalled ? Portal : Fragment
+
+ return (
+
+
+
+
+
+
Presenters
+
Description
+
+
+
+
+
+ )
+}
diff --git a/packages/machines/popover/src/popover.connect.ts b/packages/machines/popover/src/popover.connect.ts
index c4087e1cf8..84a95e2e5d 100644
--- a/packages/machines/popover/src/popover.connect.ts
+++ b/packages/machines/popover/src/popover.connect.ts
@@ -132,7 +132,7 @@ export function connect(state: State, send: Send, normalize
"aria-label": "close",
onClick(event) {
if (event.defaultPrevented) return
- send({ type: "CLOSE", restoreFocus: true })
+ send("CLOSE")
},
})
},
diff --git a/packages/machines/popover/src/popover.machine.ts b/packages/machines/popover/src/popover.machine.ts
index ab0ebd55d8..94ff1014c1 100644
--- a/packages/machines/popover/src/popover.machine.ts
+++ b/packages/machines/popover/src/popover.machine.ts
@@ -242,7 +242,7 @@ export function machine(userContext: UserDefinedContext) {
},
setFinalFocus(ctx, evt) {
const restoreFocus = evt.restoreFocus ?? evt.previousEvent?.restoreFocus
- if (!restoreFocus) return
+ if (restoreFocus != null && !restoreFocus) return
raf(() => {
const element = dom.getTriggerEl(ctx)
element?.focus({ preventScroll: true })