Skip to content

Commit

Permalink
ensure we see the "relatedTarget" in Safari
Browse files Browse the repository at this point in the history
Safari doesn't fire a `focus` event when clicking a button, therefore it
does not become the `document.activeElement`, and events like `blur` or
`focus` doesn't set the button as the `event.relatedTarget`.

Keeping track of a history like this solves that problem. We already had
the code for the `FocusTrap` component.
  • Loading branch information
RobinMalfait committed Aug 31, 2023
1 parent 318485a commit 07f05f5
Show file tree
Hide file tree
Showing 6 changed files with 70 additions and 63 deletions.
15 changes: 10 additions & 5 deletions packages/@headlessui-react/src/components/combobox/combobox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { useOutsideClick } from '../../hooks/use-outside-click'
import { useResolveButtonType } from '../../hooks/use-resolve-button-type'
import { useSyncRefs } from '../../hooks/use-sync-refs'
import { useTreeWalker } from '../../hooks/use-tree-walker'
import { history } from '../../utils/active-element-history'

import { calculateActiveIndex, Focus } from '../../utils/calculate-active-index'
import { disposables } from '../../utils/disposables'
Expand Down Expand Up @@ -1043,14 +1044,16 @@ function InputFn<
})

let handleBlur = useEvent((event: ReactFocusEvent) => {
let relatedTarget =
(event.relatedTarget as HTMLElement) ?? history.find((x) => x !== event.currentTarget)
isTyping.current = false

// Focus is moved into the list, we don't want to close yet.
if (data.optionsRef.current?.contains(event.relatedTarget)) {
if (data.optionsRef.current?.contains(relatedTarget)) {
return
}

if (data.buttonRef.current?.contains(event.relatedTarget)) {
if (data.buttonRef.current?.contains(relatedTarget)) {
return
}

Expand Down Expand Up @@ -1078,8 +1081,10 @@ function InputFn<
})

let handleFocus = useEvent((event: ReactFocusEvent) => {
if (data.buttonRef.current?.contains(event.relatedTarget as HTMLElement)) return
if (data.optionsRef.current?.contains(event.relatedTarget as HTMLElement)) return
let relatedTarget =
(event.relatedTarget as HTMLElement) ?? history.find((x) => x !== event.currentTarget)
if (data.buttonRef.current?.contains(relatedTarget)) return
if (data.optionsRef.current?.contains(relatedTarget)) return
if (data.disabled) return

if (!data.immediate) return
Expand Down Expand Up @@ -1214,7 +1219,7 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
}
})

let handleClick = useEvent((event: ReactMouseEvent) => {
let handleClick = useEvent((event: ReactMouseEvent<HTMLButtonElement>) => {
if (isDisabledReactIssue7711(event.currentTarget)) return event.preventDefault()
if (data.comboboxState === ComboboxState.Open) {
actions.closeCombobox()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ import { useEventListener } from '../../hooks/use-event-listener'
import { microTask } from '../../utils/micro-task'
import { useWatch } from '../../hooks/use-watch'
import { useDisposables } from '../../hooks/use-disposables'
import { onDocumentReady } from '../../utils/document-ready'
import { useOnUnmount } from '../../hooks/use-on-unmount'
import { history } from '../../utils/active-element-history'

type Containers =
// Lazy resolved containers
Expand Down Expand Up @@ -212,29 +212,6 @@ export let FocusTrap = Object.assign(FocusTrapRoot, {

// ---

let history: HTMLElement[] = []
onDocumentReady(() => {
function handle(e: Event) {
if (!(e.target instanceof HTMLElement)) return
if (e.target === document.body) return
if (history[0] === e.target) return

history.unshift(e.target)

// Filter out DOM Nodes that don't exist anymore
history = history.filter((x) => x != null && x.isConnected)
history.splice(10) // Only keep the 10 most recent items
}

window.addEventListener('click', handle, { capture: true })
window.addEventListener('mousedown', handle, { capture: true })
window.addEventListener('focus', handle, { capture: true })

document.body.addEventListener('click', handle, { capture: true })
document.body.addEventListener('mousedown', handle, { capture: true })
document.body.addEventListener('focus', handle, { capture: true })
})

function useRestoreElement(enabled: boolean = true) {
let localHistory = useRef<HTMLElement[]>(history.slice())

Expand Down
24 changes: 24 additions & 0 deletions packages/@headlessui-react/src/utils/active-element-history.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { onDocumentReady } from './document-ready'

export let history: HTMLElement[] = []
onDocumentReady(() => {
function handle(e: Event) {
if (!(e.target instanceof HTMLElement)) return
if (e.target === document.body) return
if (history[0] === e.target) return

history.unshift(e.target)

// Filter out DOM Nodes that don't exist anymore
history = history.filter((x) => x != null && x.isConnected)
history.splice(10) // Only keep the 10 most recent items
}

window.addEventListener('click', handle, { capture: true })
window.addEventListener('mousedown', handle, { capture: true })
window.addEventListener('focus', handle, { capture: true })

document.body.addEventListener('click', handle, { capture: true })
document.body.addEventListener('mousedown', handle, { capture: true })
document.body.addEventListener('focus', handle, { capture: true })
})
20 changes: 10 additions & 10 deletions packages/@headlessui-vue/src/components/combobox/combobox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import { useTrackedPointer } from '../../hooks/use-tracked-pointer'
import { isMobile } from '../../utils/platform'
import { disposables } from '../../utils/disposables'
import { getOwnerDocument } from '../../utils/owner'
import { history } from '../../utils/active-element-history'

function defaultComparator<T>(a: T, z: T): boolean {
return a === z
Expand Down Expand Up @@ -996,20 +997,16 @@ export let ComboboxInput = defineComponent({
}

function handleBlur(event: FocusEvent) {
let relatedTarget =
(event.relatedTarget as HTMLElement) ?? history.find((x) => x !== event.currentTarget)
isTyping.value = false

// Focus is moved into the list, we don't want to close yet.
if (
event.relatedTarget instanceof Node &&
dom(api.optionsRef)?.contains(event.relatedTarget)
) {
if (dom(api.optionsRef)?.contains(relatedTarget)) {
return
}

if (
event.relatedTarget instanceof Node &&
dom(api.buttonRef)?.contains(event.relatedTarget)
) {
if (dom(api.buttonRef)?.contains(relatedTarget)) {
return
}

Expand Down Expand Up @@ -1037,8 +1034,11 @@ export let ComboboxInput = defineComponent({
}

function handleFocus(event: FocusEvent) {
if (dom(api.buttonRef)?.contains(event.relatedTarget as HTMLElement)) return
if (dom(api.optionsRef)?.contains(event.relatedTarget as HTMLElement)) return
let relatedTarget =
(event.relatedTarget as HTMLElement) ?? history.find((x) => x !== event.currentTarget)

if (dom(api.buttonRef)?.contains(relatedTarget)) return
if (dom(api.optionsRef)?.contains(relatedTarget)) return
if (api.disabled.value) return

if (!api.immediate.value) return
Expand Down
25 changes: 1 addition & 24 deletions packages/@headlessui-vue/src/components/focus-trap/focus-trap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { useTabDirection, Direction as TabDirection } from '../../hooks/use-tab-
import { getOwnerDocument } from '../../utils/owner'
import { useEventListener } from '../../hooks/use-event-listener'
import { microTask } from '../../utils/micro-task'
import { onDocumentReady } from '../../utils/document-ready'
import { history } from '../../utils/active-element-history'

type Containers =
// Lazy resolved containers
Expand Down Expand Up @@ -209,29 +209,6 @@ export let FocusTrap = Object.assign(
{ features: Features }
)

let history: HTMLElement[] = []
onDocumentReady(() => {
function handle(e: Event) {
if (!(e.target instanceof HTMLElement)) return
if (e.target === document.body) return
if (history[0] === e.target) return

history.unshift(e.target)

// Filter out DOM Nodes that don't exist anymore
history = history.filter((x) => x != null && x.isConnected)
history.splice(10) // Only keep the 10 most recent items
}

window.addEventListener('click', handle, { capture: true })
window.addEventListener('mousedown', handle, { capture: true })
window.addEventListener('focus', handle, { capture: true })

document.body.addEventListener('click', handle, { capture: true })
document.body.addEventListener('mousedown', handle, { capture: true })
document.body.addEventListener('focus', handle, { capture: true })
})

function useRestoreElement(enabled: Ref<boolean>) {
let localHistory = ref<HTMLElement[]>(history.slice())

Expand Down
24 changes: 24 additions & 0 deletions packages/@headlessui-vue/src/utils/active-element-history.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { onDocumentReady } from './document-ready'

export let history: HTMLElement[] = []
onDocumentReady(() => {
function handle(e: Event) {
if (!(e.target instanceof HTMLElement)) return
if (e.target === document.body) return
if (history[0] === e.target) return

history.unshift(e.target)

// Filter out DOM Nodes that don't exist anymore
history = history.filter((x) => x != null && x.isConnected)
history.splice(10) // Only keep the 10 most recent items
}

window.addEventListener('click', handle, { capture: true })
window.addEventListener('mousedown', handle, { capture: true })
window.addEventListener('focus', handle, { capture: true })

document.body.addEventListener('click', handle, { capture: true })
document.body.addEventListener('mousedown', handle, { capture: true })
document.body.addEventListener('focus', handle, { capture: true })
})

0 comments on commit 07f05f5

Please sign in to comment.