Skip to content

Commit

Permalink
chore: wip
Browse files Browse the repository at this point in the history
  • Loading branch information
segunadebayo committed Nov 27, 2024
1 parent 2c0a8e3 commit 2c0c861
Show file tree
Hide file tree
Showing 6 changed files with 54 additions and 50 deletions.
25 changes: 10 additions & 15 deletions examples/next-ts/pages/dialog.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<main>
<button {...api.getTriggerProps()}> Click me</button>
{api.open && (
<Portal>
<div {...api.getBackdropProps()} />
<div {...api.getPositionerProps()}>
<div {...api.getContentProps()}>
<h2 {...api.getTitleProps()}>Edit profile</h2>
<p {...api.getDescriptionProps()}>Make changes to your profile here. Click save when you are done.</p>
<div>
<input placeholder="Enter name..." />
<button>Save</button>
</div>
<button {...api.getCloseTriggerProps()}>Close</button>
</div>
<Portal>
<div {...api.getBackdropProps()} />
<div {...api.getPositionerProps()}>
<div {...api.getContentProps()}>
{!nextContent && <button onClick={() => setNextContent(true)}>Set next content</button>}
{nextContent && <button onClick={() => setNextContent(false)}>Set previous content</button>}
</div>
</Portal>
)}
</div>
</Portal>
</main>
)
}
2 changes: 1 addition & 1 deletion packages/machines/dialog/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
40 changes: 13 additions & 27 deletions packages/machines/dialog/src/dialog.machine.ts
Original file line number Diff line number Diff line change
@@ -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"

Expand Down Expand Up @@ -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
Expand Down
10 changes: 7 additions & 3 deletions packages/utilities/focus-trap/src/index.ts
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -7,17 +7,19 @@ export interface TrapFocusOptions extends Omit<Options, "document"> {}

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 {
Expand All @@ -29,3 +31,5 @@ export function trapFocus(el: ElementOrGetter, options: TrapFocusOptions = {}) {
trap?.deactivate()
}
}

export type { FocusTrap }
21 changes: 20 additions & 1 deletion packages/utilities/interact-outside/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()
}
}

Expand Down
6 changes: 3 additions & 3 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 2c0c861

Please sign in to comment.