From 2c0c861a0e13b2365cf0aa3191d8ad70afc0518e Mon Sep 17 00:00:00 2001 From: Segun Adebayo Date: Wed, 27 Nov 2024 22:21:25 +0000 Subject: [PATCH] chore: wip --- examples/next-ts/pages/dialog.tsx | 25 +++++------- packages/machines/dialog/package.json | 2 +- .../machines/dialog/src/dialog.machine.ts | 40 ++++++------------- packages/utilities/focus-trap/src/index.ts | 10 +++-- .../utilities/interact-outside/src/index.ts | 21 +++++++++- pnpm-lock.yaml | 6 +-- 6 files changed, 54 insertions(+), 50 deletions(-) diff --git a/examples/next-ts/pages/dialog.tsx b/examples/next-ts/pages/dialog.tsx index 6c26bc0763..ac2576760a 100644 --- a/examples/next-ts/pages/dialog.tsx +++ b/examples/next-ts/pages/dialog.tsx @@ -1,29 +1,24 @@ import * as dialog from "@zag-js/dialog" import { Portal, normalizeProps, useMachine } from "@zag-js/react" +import { useState } from "react" export default function Dialog() { + const [nextContent, setNextContent] = useState(false) const [state, send] = useMachine(dialog.machine({ id: "1" })) const api = dialog.connect(state, send, normalizeProps) return (
- {api.open && ( - -
-
-
-

Edit profile

-

Make changes to your profile here. Click save when you are done.

-
- - -
- -
+ +
+
+
+ {!nextContent && } + {nextContent && }
- - )} +
+
) } diff --git a/packages/machines/dialog/package.json b/packages/machines/dialog/package.json index 380b8b5f05..605356afdf 100644 --- a/packages/machines/dialog/package.json +++ b/packages/machines/dialog/package.json @@ -34,7 +34,7 @@ "@zag-js/dismissable": "workspace:*", "@zag-js/remove-scroll": "workspace:*", "@zag-js/types": "workspace:*", - "focus-trap": "7.6.2" + "@zag-js/focus-trap": "workspace:*" }, "devDependencies": { "clean-package": "2.2.0" diff --git a/packages/machines/dialog/src/dialog.machine.ts b/packages/machines/dialog/src/dialog.machine.ts index 3fd613fbac..586fcc2fac 100644 --- a/packages/machines/dialog/src/dialog.machine.ts +++ b/packages/machines/dialog/src/dialog.machine.ts @@ -1,10 +1,10 @@ import { ariaHidden } from "@zag-js/aria-hidden" import { createMachine } from "@zag-js/core" import { trackDismissableElement } from "@zag-js/dismissable" -import { nextTick, raf } from "@zag-js/dom-query" +import { raf } from "@zag-js/dom-query" +import { trapFocus } from "@zag-js/focus-trap" import { preventBodyScroll } from "@zag-js/remove-scroll" import { compact } from "@zag-js/utils" -import { createFocusTrap, type FocusTrap } from "focus-trap" import { dom } from "./dialog.dom" import type { MachineContext, MachineState, UserDefinedContext } from "./dialog.types" @@ -132,32 +132,18 @@ export function machine(userContext: UserDefinedContext) { }, trapFocus(ctx) { if (!ctx.trapFocus || !ctx.modal) return - let trap: FocusTrap - - const cleanup = nextTick(() => { - const contentEl = dom.getContentEl(ctx) - if (!contentEl) return - trap = createFocusTrap(contentEl, { - document: dom.getDoc(ctx), - escapeDeactivates: false, - preventScroll: true, - fallbackFocus: contentEl, - returnFocusOnDeactivate: !!ctx.restoreFocus, - allowOutsideClick: true, - initialFocus: ctx.initialFocusEl?.() ?? undefined, - setReturnFocus(triggerEl) { - return ctx.finalFocusEl?.() ?? triggerEl - }, - }) - - try { - trap.activate() - } catch {} + const contentEl = () => dom.getContentEl(ctx) + return trapFocus(contentEl, { + escapeDeactivates: false, + preventScroll: true, + fallbackFocus: contentEl()!, + returnFocusOnDeactivate: !!ctx.restoreFocus, + allowOutsideClick: true, + initialFocus: ctx.initialFocusEl?.() ?? undefined, + setReturnFocus(triggerEl) { + return ctx.finalFocusEl?.() ?? triggerEl + }, }) - return () => { - trap?.deactivate() - cleanup() - } }, hideContentBelow(ctx) { if (!ctx.modal) return diff --git a/packages/utilities/focus-trap/src/index.ts b/packages/utilities/focus-trap/src/index.ts index 2d45dc3d44..6e50c2a20d 100644 --- a/packages/utilities/focus-trap/src/index.ts +++ b/packages/utilities/focus-trap/src/index.ts @@ -1,4 +1,4 @@ -import { nextTick, getDocument } from "@zag-js/dom-query" +import { getDocument, raf } from "@zag-js/dom-query" import { createFocusTrap, type FocusTrap, type Options } from "focus-trap" type ElementOrGetter = HTMLElement | null | (() => HTMLElement | null) @@ -7,17 +7,19 @@ export interface TrapFocusOptions extends Omit {} export function trapFocus(el: ElementOrGetter, options: TrapFocusOptions = {}) { let trap: FocusTrap | undefined - nextTick(() => { + + raf(() => { const contentEl = typeof el === "function" ? el() : el if (!contentEl) return trap = createFocusTrap(contentEl, { + delayInitialFocus: false, escapeDeactivates: false, allowOutsideClick: true, preventScroll: true, returnFocusOnDeactivate: true, - document: getDocument(contentEl), fallbackFocus: contentEl, ...options, + document: getDocument(contentEl), }) try { @@ -29,3 +31,5 @@ export function trapFocus(el: ElementOrGetter, options: TrapFocusOptions = {}) { trap?.deactivate() } } + +export type { FocusTrap } diff --git a/packages/utilities/interact-outside/src/index.ts b/packages/utilities/interact-outside/src/index.ts index 712a92eeda..f347ca3abc 100644 --- a/packages/utilities/interact-outside/src/index.ts +++ b/packages/utilities/interact-outside/src/index.ts @@ -156,11 +156,15 @@ function trackInteractOutsideImpl(node: MaybeElement, options: InteractOutsideOp cleanups.add(frames.addEventListener("pointerdown", onPointerDown, true)) }, 0) + let hadLastActiveElement = false + let activeElement: Element | null = doc.activeElement + function onFocusin(event: FocusEvent) { + activeElement = doc.activeElement // const func = defer ? raf : (v: any) => v() func(() => { - if (!node || !isEventOutside(event)) return + if (!node || !isEventOutside(event) || hadLastActiveElement) return if (onFocusOutside || onInteractOutside) { const handler = callAll(onFocusOutside, onInteractOutside) as EventListener @@ -183,10 +187,25 @@ function trackInteractOutsideImpl(node: MaybeElement, options: InteractOutsideOp cleanups.add(parentWin.addEventListener("focusin", onFocusin, true)) cleanups.add(frames.addEventListener("focusin", onFocusin, true)) + const obs = new win.MutationObserver((entries) => { + entries.forEach((entry) => { + if (!entry.removedNodes.length) return + const removedNodes = Array.from(entry.removedNodes) + hadLastActiveElement = removedNodes.some((node) => node.contains(activeElement)) + }) + + if (hadLastActiveElement && !node.contains(activeElement) && doc.activeElement !== node) { + node?.focus({ preventScroll: true }) + } + }) + + obs.observe(node, { childList: true, subtree: true }) + return () => { clearTimeout(timer) pointerdownCleanups.forEach((fn) => fn()) cleanups.forEach((fn) => fn()) + obs.disconnect() } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c50794bb1a..473c7be827 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2729,6 +2729,9 @@ importers: '@zag-js/dom-query': specifier: workspace:* version: link:../../utilities/dom-query + '@zag-js/focus-trap': + specifier: workspace:* + version: link:../../utilities/focus-trap '@zag-js/remove-scroll': specifier: workspace:* version: link:../../utilities/remove-scroll @@ -2738,9 +2741,6 @@ importers: '@zag-js/utils': specifier: workspace:* version: link:../../utilities/core - focus-trap: - specifier: 7.6.2 - version: 7.6.2 devDependencies: clean-package: specifier: 2.2.0