Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
RobinMalfait committed Mar 1, 2022
1 parent cdf883d commit 88564da
Show file tree
Hide file tree
Showing 15 changed files with 159 additions and 44 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import { useResolveButtonType } from '../../hooks/use-resolve-button-type'
import { useLatestValue } from '../../hooks/use-latest-value'
import { useTreeWalker } from '../../hooks/use-tree-walker'
import { sortByDomNode } from '../../utils/focus-management'
import { getOwnerDocument } from '../../utils/owner-document'
import { getOwnerDocument } from '../../utils/owner'

enum ComboboxStates {
Open,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import { useWindowEvent } from '../../hooks/use-window-event'
import { useOpenClosed, State } from '../../internal/open-closed'
import { useServerHandoffComplete } from '../../hooks/use-server-handoff-complete'
import { StackProvider, StackMessage } from '../../internal/stack-context'
import { getOwnerDocument } from '../../utils/owner-document'
import { getOwnerDocument } from '../../utils/owner'

enum DialogStates {
Open,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import { Keys } from '../keyboard'
import { isDisabledReactIssue7711 } from '../../utils/bugs'
import { OpenClosedProvider, State, useOpenClosed } from '../../internal/open-closed'
import { useResolveButtonType } from '../../hooks/use-resolve-button-type'
import { getOwnerDocument } from '../../utils/owner-document'
import { getOwnerDocument } from '../../utils/owner'

enum DisclosureStates {
Open,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import { isFocusableElement, FocusableMode, sortByDomNode } from '../../utils/fo
import { useWindowEvent } from '../../hooks/use-window-event'
import { useOpenClosed, State, OpenClosedProvider } from '../../internal/open-closed'
import { useResolveButtonType } from '../../hooks/use-resolve-button-type'
import { getOwnerDocument } from '../../utils/owner-document'
import { getOwnerDocument } from '../../utils/owner'

enum ListboxStates {
Open,
Expand Down
2 changes: 1 addition & 1 deletion packages/@headlessui-react/src/components/menu/menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import { useWindowEvent } from '../../hooks/use-window-event'
import { useTreeWalker } from '../../hooks/use-tree-walker'
import { useOpenClosed, State, OpenClosedProvider } from '../../internal/open-closed'
import { useResolveButtonType } from '../../hooks/use-resolve-button-type'
import { getOwnerDocument } from '../../utils/owner-document'
import { getOwnerDocument } from '../../utils/owner'

enum MenuStates {
Open,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import {
import { useWindowEvent } from '../../hooks/use-window-event'
import { OpenClosedProvider, State, useOpenClosed } from '../../internal/open-closed'
import { useResolveButtonType } from '../../hooks/use-resolve-button-type'
import { getOwnerDocument } from '../../utils/owner-document'
import { getOwnerDocument } from '../../utils/owner'

enum PopoverStates {
Open,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { useIsoMorphicEffect } from '../../hooks/use-iso-morphic-effect'
import { usePortalRoot } from '../../internal/portal-force-root'
import { useServerHandoffComplete } from '../../hooks/use-server-handoff-complete'
import { useSyncRefs } from '../../hooks/use-sync-refs'
import { getOwnerDocument } from '../../utils/owner-document'
import { getOwnerDocument } from '../../utils/owner'

function usePortalTarget(ref: MutableRefObject<HTMLElement>): HTMLElement | null {
let forceInRoot = usePortalRoot()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import { Label, useLabels } from '../../components/label/label'
import { Description, useDescriptions } from '../../components/description/description'
import { useTreeWalker } from '../../hooks/use-tree-walker'
import { useSyncRefs } from '../../hooks/use-sync-refs'
import { getOwnerDocument } from '../../utils/owner-document'
import { getOwnerDocument } from '../../utils/owner'

interface Option {
id: string
Expand Down
58 changes: 34 additions & 24 deletions packages/@headlessui-react/src/hooks/use-focus-trap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { Keys } from '../components/keyboard'
import { focusElement, focusIn, Focus, FocusResult } from '../utils/focus-management'
import { useWindowEvent } from './use-window-event'
import { useIsMounted } from './use-is-mounted'
import { getOwnerDocument } from '../utils/owner-document'
import { getOwnerDocument } from '../utils/owner'

export enum Features {
/** No features enabled for the `useFocusTrap` hook. */
Expand Down Expand Up @@ -42,10 +42,9 @@ export function useFocusTrap(
containers?: MutableRefObject<Set<MutableRefObject<HTMLElement | null>>>
} = {}
) {
let ownerDocument = getOwnerDocument(container)
let restoreElement = useRef<HTMLElement | null>(
typeof window !== 'undefined'
? (getOwnerDocument(container).activeElement as HTMLElement | null)
: null
typeof window !== 'undefined' ? (ownerDocument.activeElement as HTMLElement | null) : null
)
let previousActiveElement = useRef<HTMLElement | null>(null)
let mounted = useIsMounted()
Expand All @@ -57,8 +56,8 @@ export function useFocusTrap(
useEffect(() => {
if (!featuresRestoreFocus) return

restoreElement.current = getOwnerDocument(container).activeElement as HTMLElement
}, [featuresRestoreFocus, container])
restoreElement.current = ownerDocument.activeElement as HTMLElement
}, [featuresRestoreFocus, ownerDocument])

// Restore the focus when we unmount the component.
useEffect(() => {
Expand All @@ -75,7 +74,7 @@ export function useFocusTrap(
if (!featuresInitialFocus) return
if (!container.current) return

let activeElement = getOwnerDocument(container).activeElement as HTMLElement
let activeElement = ownerDocument.activeElement as HTMLElement

if (initialFocus?.current) {
if (initialFocus?.current === activeElement) {
Expand All @@ -96,27 +95,37 @@ export function useFocusTrap(
}
}

previousActiveElement.current = getOwnerDocument(container).activeElement as HTMLElement
}, [container, initialFocus, featuresInitialFocus, container])
previousActiveElement.current = ownerDocument.activeElement as HTMLElement
}, [container, container.current, initialFocus, featuresInitialFocus, ownerDocument])

// Handle `Tab` & `Shift+Tab` keyboard events
useWindowEvent('keydown', (event) => {
if (!(features & Features.TabLock)) return
useWindowEvent(
'keydown',
(event) => {
console.assert(
container.current?.contains?.(event.target as HTMLElement),
'The event target is not a child of the container'
)

if (!container.current) return
if (event.key !== Keys.Tab) return
if (!(features & Features.TabLock)) return

event.preventDefault()
if (!container.current) return
if (event.key !== Keys.Tab) return

if (
focusIn(
container.current,
(event.shiftKey ? Focus.Previous : Focus.Next) | Focus.WrapAround
) === FocusResult.Success
) {
previousActiveElement.current = getOwnerDocument(container).activeElement as HTMLElement
}
})
event.preventDefault()

if (
focusIn(
container.current,
(event.shiftKey ? Focus.Previous : Focus.Next) | Focus.WrapAround
) === FocusResult.Success
) {
previousActiveElement.current = ownerDocument.activeElement as HTMLElement
}
},
undefined,
container
)

// Prevent programmatically escaping the container
useWindowEvent(
Expand Down Expand Up @@ -148,7 +157,8 @@ export function useFocusTrap(
focusElement(previousActiveElement.current)
}
},
true
true,
container
)
}

Expand Down
2 changes: 1 addition & 1 deletion packages/@headlessui-react/src/hooks/use-inert-others.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { MutableRefObject } from 'react'
import { getOwnerDocument } from '../utils/owner-document'
import { getOwnerDocument } from '../utils/owner'
import { useIsoMorphicEffect } from './use-iso-morphic-effect'

let interactables = new Set<HTMLElement>()
Expand Down
2 changes: 1 addition & 1 deletion packages/@headlessui-react/src/hooks/use-tree-walker.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useRef, useEffect } from 'react'
import { useIsoMorphicEffect } from './use-iso-morphic-effect'
import { getOwnerDocument } from '../utils/owner-document'
import { getOwnerDocument } from '../utils/owner'

type AcceptNode = (
node: HTMLElement
Expand Down
23 changes: 14 additions & 9 deletions packages/@headlessui-react/src/hooks/use-window-event.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
import { useEffect, useRef } from 'react'
import { MutableRefObject, useEffect } from 'react'

import { getOwnerWindow } from '../utils/owner'
import { useLatestValue } from './use-latest-value'

export function useWindowEvent<TType extends keyof WindowEventMap>(
type: TType,
listener: (this: Window, ev: WindowEventMap[TType]) => any,
options?: boolean | AddEventListenerOptions
listener: (ev: WindowEventMap[TType]) => any,
options?: boolean | AddEventListenerOptions,
contextElement: Element | MutableRefObject<Element | null> | null = null
) {
let listenerRef = useRef(listener)
listenerRef.current = listener
let listenerRef = useLatestValue(listener)
let target = getOwnerWindow(contextElement)
console.log(target, target.document)

useEffect(() => {
function handler(event: WindowEventMap[TType]) {
listenerRef.current.call(window, event)
listenerRef.current(event)
}

window.addEventListener(type, handler, options)
return () => window.removeEventListener(type, handler, options)
}, [type, options])
target.addEventListener(type, handler, options)
return () => target.removeEventListener(type, handler, options)
}, [type, options, target])
}
2 changes: 1 addition & 1 deletion packages/@headlessui-react/src/utils/focus-management.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { match } from './match'
import { getOwnerDocument } from './owner-document'
import { getOwnerDocument } from './owner'

// Credit:
// - https://stackoverflow.com/a/30753870
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,10 @@ export function getOwnerDocument<T extends Element | MutableRefObject<Element |

return document
}

export function getOwnerWindow<T extends Element | MutableRefObject<Element | null>>(
element: T | null | undefined
) {
let ownerDocument = getOwnerDocument(element)
return ownerDocument.defaultView ?? window
}
93 changes: 93 additions & 0 deletions packages/playground-react/pages/tmp.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { createPortal } from 'react-dom'
import { FocusTrap } from '@headlessui/react'

function Window({ onClose, children }: { onClose: () => void; children: (window: Window) => any }) {
const newWindow = useMemo(() => window.open('', 'window', 'width=300'), [])

const [_, _forceUpdate] = useState({})
const forceUpdate = useCallback(() => _forceUpdate({}), [])

useEffect(() => {
if (newWindow) return () => newWindow.close()
}, [newWindow])

useEffect(() => {
if (newWindow) {
newWindow.addEventListener('beforeunload', onClose)
newWindow.addEventListener('load', forceUpdate)

return () => {
newWindow.removeEventListener('load', forceUpdate)
newWindow.removeEventListener('beforeunload', onClose)
}
}

// document is required to re-add these event listeners after initial
// navigation
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [newWindow, newWindow?.document, onClose, forceUpdate])

return newWindow ? createPortal(<>{children(newWindow)}</>, newWindow.document.body) : null
}

function FocusTrapper() {
const [focusTrapped, setFocusTrapped] = useState(false)

const contents = (
<>
<label>
<input
type="checkbox"
checked={focusTrapped}
onChange={(e) => setFocusTrapped(e.target.checked)}
/>
Trap focus!
</label>
<button>Hello 1</button>
<button>Hello 2</button>
<button>Hello 3</button>
</>
)

return focusTrapped ? <FocusTrap>{contents}</FocusTrap> : contents
}

export default function App() {
const [windowOpen, setWindowOpen] = useState(false)
const onWindowClose = useCallback(() => setWindowOpen(false), [])

const windowRender = useCallback(
() => (
<>
<FocusTrapper />
<label>
<input
type="checkbox"
checked={windowOpen}
onChange={(e) => setWindowOpen(e.target.checked)}
/>
Open window
</label>
</>
),
[windowOpen]
)

return (
<div className="App">
<FocusTrapper />
<div>
<label>
<input
type="checkbox"
checked={windowOpen}
onChange={(e) => setWindowOpen(e.target.checked)}
/>
Open window
</label>
</div>
{windowOpen && <Window onClose={onWindowClose}>{windowRender}</Window>}
</div>
)
}

0 comments on commit 88564da

Please sign in to comment.