Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix document.activeElement not being correct when components are use inside custom elements #1570

Merged
merged 3 commits into from
Jan 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions packages/radix-vue/src/Dialog/DialogContentImpl.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type {
DismissableLayerEmits,
DismissableLayerProps,
} from '@/DismissableLayer'
import { useForwardExpose, useId } from '@/shared'
import { getActiveElement, useForwardExpose, useId } from '@/shared'
export type DialogContentImplEmits = DismissableLayerEmits & {
/**
Expand Down Expand Up @@ -54,8 +54,8 @@ onMounted(() => {
rootContext.contentElement = contentElement
// Preserve the `DialogTrigger` element in case it was triggered programmatically
if (document.activeElement !== document.body)
rootContext.triggerElement.value = document.activeElement as HTMLElement
if (getActiveElement() !== document.body)
rootContext.triggerElement.value = getActiveElement() as HTMLElement
})
if (process.env.NODE_ENV !== 'production') {
Expand Down
8 changes: 4 additions & 4 deletions packages/radix-vue/src/FocusScope/FocusScope.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script lang="ts">
import type { PrimitiveProps } from '@/Primitive'
import { useForwardExpose } from '@/shared'
import { getActiveElement, useForwardExpose } from '@/shared'

export type FocusScopeEmits = {
/**
Expand Down Expand Up @@ -142,7 +142,7 @@ watchEffect(async (cleanupFn) => {
if (!container)
return
focusScopesStack.add(focusScope)
const previouslyFocusedElement = document.activeElement as HTMLElement | null
const previouslyFocusedElement = getActiveElement() as HTMLElement | null
const hasFocusedCandidate = container.contains(previouslyFocusedElement)

if (!hasFocusedCandidate) {
Expand All @@ -155,7 +155,7 @@ watchEffect(async (cleanupFn) => {
focusFirst(removeLinks(getTabbableCandidates(container)), {
select: true,
})
if (document.activeElement === previouslyFocusedElement)
if (getActiveElement() === previouslyFocusedElement)
focus(container)
}
}
Expand Down Expand Up @@ -191,7 +191,7 @@ function handleKeyDown(event: KeyboardEvent) {

const isTabKey
= event.key === 'Tab' && !event.altKey && !event.ctrlKey && !event.metaKey
const focusedElement = document.activeElement as HTMLElement | null
const focusedElement = getActiveElement() as HTMLElement | null

if (isTabKey && focusedElement) {
const container = event.currentTarget as HTMLElement
Expand Down
8 changes: 5 additions & 3 deletions packages/radix-vue/src/FocusScope/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { getActiveElement } from '@/shared'

export const AUTOFOCUS_ON_MOUNT = 'focusScope.autoFocusOnMount'
export const AUTOFOCUS_ON_UNMOUNT = 'focusScope.autoFocusOnUnmount'
export const EVENT_OPTIONS = { bubbles: false, cancelable: true }
Expand All @@ -9,10 +11,10 @@ type FocusableTarget = HTMLElement | { focus: () => void }
* Stops when focus has actually moved.
*/
export function focusFirst(candidates: HTMLElement[], { select = false } = {}) {
const previouslyFocusedElement = document.activeElement
const previouslyFocusedElement = getActiveElement()
for (const candidate of candidates) {
focus(candidate, { select })
if (document.activeElement !== previouslyFocusedElement)
if (getActiveElement() !== previouslyFocusedElement)
return true
}
}
Expand Down Expand Up @@ -96,7 +98,7 @@ export function focus(
) {
// only focus if that element is focusable
if (element && element.focus) {
const previouslyFocusedElement = document.activeElement
const previouslyFocusedElement = getActiveElement()
// NOTE: we prevent scrolling on focus, to minimize jarring transitions for users
element.focus({ preventScroll: true })
// only select if its not the same element, it supports selection and we need to select
Expand Down
4 changes: 2 additions & 2 deletions packages/radix-vue/src/Listbox/ListboxVirtualizer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { injectListboxRootContext } from './ListboxRoot.vue'
import { compare, queryCheckedElement } from './utils'
import { MAP_KEY_TO_FOCUS_INTENT } from '@/RovingFocus/utils'
import { refAutoReset } from '@vueuse/shared'
import { findValuesBetween } from '@/shared'
import { findValuesBetween, getActiveElement } from '@/shared'
import { getNextMatch } from '@/shared/useTypeahead'
import { useParentElement } from '@vueuse/core'
import { useCollection } from '@/Collection'
Expand Down Expand Up @@ -186,7 +186,7 @@ rootContext.virtualKeydownHook.on((event) => {
}
else if (!intent && !isMetaKey) {
search.value += event.key
const currentIndex = Number(document.activeElement?.getAttribute('data-index'))
const currentIndex = Number(getActiveElement()?.getAttribute('data-index'))
const currentMatch = optionsWithMetadata.value[currentIndex].textContent
const filteredOptions = optionsWithMetadata.value.map(i => i.textContent)
const next = getNextMatch(filteredOptions, search.value, currentMatch)
Expand Down
3 changes: 2 additions & 1 deletion packages/radix-vue/src/Menu/MenuContentImpl.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type { PopperContentProps } from '@/Popper'

import {
createContext,
getActiveElement,
useArrowNavigation,
useBodyScrollLock,
useCollection,
Expand Down Expand Up @@ -176,7 +177,7 @@ function handleKeyDown(event: KeyboardEvent) {

const el = useArrowNavigation(
event,
document.activeElement as HTMLElement,
getActiveElement() as HTMLElement,
contentElement.value,
{
loop: loop.value,
Expand Down
6 changes: 4 additions & 2 deletions packages/radix-vue/src/Menu/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { getActiveElement } from '@/shared'

export type CheckedState = boolean | 'indeterminate'
export type Direction = 'ltr' | 'rtl'

Expand Down Expand Up @@ -35,13 +37,13 @@ export function getCheckedState(checked: CheckedState) {
}

export function focusFirst(candidates: HTMLElement[]) {
const PREVIOUSLY_FOCUSED_ELEMENT = document.activeElement
const PREVIOUSLY_FOCUSED_ELEMENT = getActiveElement()
for (const candidate of candidates) {
// if focus is already where we want to go, we don't want to keep going through the candidates
if (candidate === PREVIOUSLY_FOCUSED_ELEMENT)
return
candidate.focus()
if (document.activeElement !== PREVIOUSLY_FOCUSED_ELEMENT)
if (getActiveElement() !== PREVIOUSLY_FOCUSED_ELEMENT)
return
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {
FocusOutsideEvent,
} from '@/DismissableLayer'
import type { PointerDownOutsideEvent } from '@/DismissableLayer/utils'
import { getActiveElement, useArrowNavigation, useCollection, useForwardExpose } from '@/shared'

type MotionAttribute = 'to-start' | 'to-end' | 'from-start' | 'from-end'

Expand All @@ -25,7 +26,6 @@ import {
makeTriggerId,
} from './utils'
import { DismissableLayer } from '@/DismissableLayer'
import { useArrowNavigation, useCollection, useForwardExpose } from '@/shared'
import { injectNavigationMenuItemContext } from './NavigationMenuItem.vue'

const props = defineProps<NavigationMenuContentImplProps>()
Expand Down Expand Up @@ -114,7 +114,7 @@ watchEffect((cleanupFn) => {
const handleClose = () => {
menuContext.onItemDismiss()
itemContext.onRootContentClose()
if (content.contains(document.activeElement))
if (content.contains(getActiveElement()))
itemContext.triggerRef.value?.focus()
}
content.addEventListener(EVENT_ROOT_CONTENT_DISMISS, handleClose)
Expand Down Expand Up @@ -145,7 +145,7 @@ function handleKeydown(ev: KeyboardEvent) {
const candidates = getTabbableCandidates(ev.currentTarget as HTMLElement)

if (isTabKey) {
const focusedElement = document.activeElement
const focusedElement = getActiveElement()
const index = candidates.findIndex(
candidate => candidate === focusedElement,
)
Expand All @@ -169,7 +169,7 @@ function handleKeydown(ev: KeyboardEvent) {

const newSelectedElement = useArrowNavigation(
ev,
document.activeElement as HTMLElement,
getActiveElement() as HTMLElement,
undefined,
{ itemsArray: candidates, loop: false, enableIgnoredElement: true },
)
Expand Down
4 changes: 2 additions & 2 deletions packages/radix-vue/src/NavigationMenu/NavigationMenuItem.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script lang="ts">
import type { Ref } from 'vue'
import type { PrimitiveProps } from '@/Primitive'
import { createContext, useArrowNavigation, useCollection, useForwardExpose, useId } from '@/shared'
import { createContext, getActiveElement, useArrowNavigation, useCollection, useForwardExpose, useId } from '@/shared'

export interface NavigationMenuItemProps extends PrimitiveProps {
/**
Expand Down Expand Up @@ -95,7 +95,7 @@ function handleClose() {
}

function handleKeydown(ev: KeyboardEvent) {
const currentFocus = document.activeElement as HTMLElement
const currentFocus = getActiveElement() as HTMLElement
if (ev.keyCode === 32 || ev.key === 'Enter') {
if (context.modelValue.value === value) {
handleClose()
Expand Down
6 changes: 4 additions & 2 deletions packages/radix-vue/src/NavigationMenu/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { getActiveElement } from '@/shared'

export type Orientation = 'vertical' | 'horizontal'
export type Direction = 'ltr' | 'rtl'

Expand Down Expand Up @@ -48,13 +50,13 @@ export function getTabbableCandidates(container: HTMLElement) {
}

export function focusFirst(candidates: HTMLElement[]) {
const previouslyFocusedElement = document.activeElement
const previouslyFocusedElement = getActiveElement()
return candidates.some((candidate) => {
// if focus is already where we want to go, we don't want to keep going through the candidates
if (candidate === previouslyFocusedElement)
return true
candidate.focus()
return document.activeElement !== previouslyFocusedElement
return getActiveElement() !== previouslyFocusedElement
})
}

Expand Down
3 changes: 2 additions & 1 deletion packages/radix-vue/src/NumberField/NumberFieldInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import type { PrimitiveProps } from '@/Primitive'
import { injectNumberFieldRootContext } from './NumberFieldRoot.vue'
import { onMounted, ref, watch } from 'vue'
import { getActiveElement } from '@/shared'

export interface NumberFieldInputProps extends PrimitiveProps {
}
Expand All @@ -19,7 +20,7 @@ const rootContext = injectNumberFieldRootContext()

function handleWheelEvent(event: WheelEvent) {
// only handle when in focus
if (event.target !== document.activeElement)
if (event.target !== getActiveElement())
return

// if on a trackpad, users can scroll in both X and Y at once, check the magnitude of the change
Expand Down
4 changes: 2 additions & 2 deletions packages/radix-vue/src/PinInput/PinInputInput.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script lang="ts">
import { Primitive, type PrimitiveProps, usePrimitiveElement } from '@/Primitive'
import { injectPinInputRootContext } from './PinInputRoot.vue'
import { useArrowNavigation } from '@/shared'
import { getActiveElement, useArrowNavigation } from '@/shared'

export interface PinInputInputProps extends PrimitiveProps {
/** Position of the value this input binds to. */
Expand Down Expand Up @@ -58,7 +58,7 @@ function resetPlaceholder() {
}

function handleKeydown(event: KeyboardEvent) {
useArrowNavigation(event, document.activeElement as HTMLElement, undefined, {
useArrowNavigation(event, getActiveElement() as HTMLElement, undefined, {
itemsArray: inputElements.value,
focus: true,
loop: false,
Expand Down
8 changes: 2 additions & 6 deletions packages/radix-vue/src/RovingFocus/RovingFocusItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export interface RovingFocusItemProps extends PrimitiveProps {
<script setup lang="ts">
import { computed, nextTick, onMounted, onUnmounted } from 'vue'
import { injectRovingFocusGroupContext } from './RovingFocusGroup.vue'
import { Primitive, usePrimitiveElement } from '@/Primitive'
import { Primitive } from '@/Primitive'
import { focusFirst, getFocusIntent, wrapArray } from './utils'
import { useId } from '@/shared'
import { CollectionItem, useCollection } from '@/Collection'
Expand All @@ -31,9 +31,6 @@ const isCurrentTabStop = computed(

const { getItems } = useCollection()

const { primitiveElement, currentElement } = usePrimitiveElement()
const rootNode = computed(() => currentElement.value?.getRootNode() as Document | ShadowRoot)

onMounted(() => {
if (props.focusable)
context.onFocusableItemAdd()
Expand Down Expand Up @@ -79,15 +76,14 @@ function handleKeydown(event: KeyboardEvent) {
: candidateNodes.slice(currentIndex + 1)
}

nextTick(() => focusFirst(candidateNodes, false, rootNode.value))
nextTick(() => focusFirst(candidateNodes))
}
}
</script>

<template>
<CollectionItem>
<Primitive
ref="primitiveElement"
:tabindex="isCurrentTabStop ? 0 : -1"
:data-orientation="context.orientation.value"
:data-active="active"
Expand Down
8 changes: 5 additions & 3 deletions packages/radix-vue/src/RovingFocus/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { getActiveElement } from '@/shared'

export type Orientation = 'horizontal' | 'vertical'
export type Direction = 'ltr' | 'rtl'

Expand Down Expand Up @@ -40,14 +42,14 @@ export function getFocusIntent(
return MAP_KEY_TO_FOCUS_INTENT[key]
}

export function focusFirst(candidates: HTMLElement[], preventScroll = false, rootNode?: Document | ShadowRoot) {
const PREVIOUSLY_FOCUSED_ELEMENT = rootNode?.activeElement ?? document.activeElement
export function focusFirst(candidates: HTMLElement[], preventScroll = false) {
const PREVIOUSLY_FOCUSED_ELEMENT = getActiveElement()
for (const candidate of candidates) {
// if focus is already where we want to go, we don't want to keep going through the candidates
if (candidate === PREVIOUSLY_FOCUSED_ELEMENT)
return
candidate.focus({ preventScroll })
if (document.activeElement !== PREVIOUSLY_FOCUSED_ELEMENT)
if (getActiveElement() !== PREVIOUSLY_FOCUSED_ELEMENT)
return
}
}
Expand Down
4 changes: 2 additions & 2 deletions packages/radix-vue/src/Select/SelectItem.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script lang="ts">
import type { Ref } from 'vue'
import type { PrimitiveProps } from '@/Primitive'
import { createContext, useForwardExpose, useId } from '@/shared'
import { createContext, getActiveElement, useForwardExpose, useId } from '@/shared'

interface SelectItemContext {
value: string
Expand Down Expand Up @@ -84,7 +84,7 @@ async function handlePointerLeave(event: PointerEvent) {
await nextTick()
if (event.defaultPrevented)
return
if (event.currentTarget === document.activeElement)
if (event.currentTarget === getActiveElement())
contentContext.onItemLeave?.()
}

Expand Down
4 changes: 2 additions & 2 deletions packages/radix-vue/src/Select/SelectScrollButtonImpl.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { onBeforeUnmount, ref, watchEffect } from 'vue'
import { SelectContentDefaultContextValue, injectSelectContentContext } from './SelectContentImpl.vue'
import { Primitive } from '@/Primitive'
import { useCollection } from '@/shared'
import { getActiveElement, useCollection } from '@/shared'
export type SelectScrollButtonImplEmits = {
autoScroll: []
Expand All @@ -24,7 +24,7 @@ function clearAutoScrollTimer() {
watchEffect(() => {
const activeItem = collectionItems.value.find(
item => item === document.activeElement,
item => item === getActiveElement(),
)
activeItem?.scrollIntoView({ block: 'nearest' })
})
Expand Down
4 changes: 2 additions & 2 deletions packages/radix-vue/src/Stepper/StepperTrigger.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script lang="ts">
import type { PrimitiveProps } from '@/Primitive'
import { useArrowNavigation, useForwardExpose, useKbd } from '@/shared'
import { getActiveElement, useArrowNavigation, useForwardExpose, useKbd } from '@/shared'
import { computed, onMounted, onUnmounted } from 'vue'

export interface StepperTriggerProps extends PrimitiveProps {
Expand Down Expand Up @@ -54,7 +54,7 @@ function handleKeyDown(event: KeyboardEvent) {
rootContext.changeModelValue(itemContext.step.value)

if ([kbd.ARROW_LEFT, kbd.ARROW_RIGHT, kbd.ARROW_UP, kbd.ARROW_DOWN].includes(event.key)) {
useArrowNavigation(event, document.activeElement as HTMLElement, undefined, {
useArrowNavigation(event, getActiveElement() as HTMLElement, undefined, {
itemsArray: stepperItems.value,
focus: true,
loop: false,
Expand Down
4 changes: 2 additions & 2 deletions packages/radix-vue/src/Toast/ToastRootImpl.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { isClient } from '@vueuse/shared'
import type { PrimitiveProps } from '@/Primitive'
import type { SwipeEvent } from './utils'
import { createContext, useForwardExpose } from '@/shared'
import { createContext, getActiveElement, useForwardExpose } from '@/shared'

export type ToastRootImplEmits = {
close: []
Expand Down Expand Up @@ -98,7 +98,7 @@ function startTimer(duration: number) {
function handleClose() {
// focus viewport if focus is within toast to read the remaining toast
// count to SR users and ensure focus isn't lost
const isFocusInToast = currentElement.value?.contains(document.activeElement)
const isFocusInToast = currentElement.value?.contains(getActiveElement())
if (isFocusInToast)
providerContext.viewport.value?.focus()

Expand Down
Loading
Loading