diff --git a/packages/radix-vue/src/Dialog/DialogContentImpl.vue b/packages/radix-vue/src/Dialog/DialogContentImpl.vue
index 09050eed7..02c4c29ae 100644
--- a/packages/radix-vue/src/Dialog/DialogContentImpl.vue
+++ b/packages/radix-vue/src/Dialog/DialogContentImpl.vue
@@ -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 & {
/**
@@ -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') {
diff --git a/packages/radix-vue/src/FocusScope/FocusScope.vue b/packages/radix-vue/src/FocusScope/FocusScope.vue
index 216f301ad..66cc0899d 100644
--- a/packages/radix-vue/src/FocusScope/FocusScope.vue
+++ b/packages/radix-vue/src/FocusScope/FocusScope.vue
@@ -1,6 +1,6 @@
@@ -87,7 +84,6 @@ function handleKeydown(event: KeyboardEvent) {
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
@@ -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?.()
}
diff --git a/packages/radix-vue/src/Select/SelectScrollButtonImpl.vue b/packages/radix-vue/src/Select/SelectScrollButtonImpl.vue
index a95470863..f85ab24de 100644
--- a/packages/radix-vue/src/Select/SelectScrollButtonImpl.vue
+++ b/packages/radix-vue/src/Select/SelectScrollButtonImpl.vue
@@ -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: []
@@ -24,7 +24,7 @@ function clearAutoScrollTimer() {
watchEffect(() => {
const activeItem = collectionItems.value.find(
- item => item === document.activeElement,
+ item => item === getActiveElement(),
)
activeItem?.scrollIntoView({ block: 'nearest' })
})
diff --git a/packages/radix-vue/src/Stepper/StepperTrigger.vue b/packages/radix-vue/src/Stepper/StepperTrigger.vue
index d3889f5dc..ba42c2711 100644
--- a/packages/radix-vue/src/Stepper/StepperTrigger.vue
+++ b/packages/radix-vue/src/Stepper/StepperTrigger.vue
@@ -1,6 +1,6 @@
diff --git a/packages/radix-vue/src/shared/getActiveElement.test.ts b/packages/radix-vue/src/shared/getActiveElement.test.ts
new file mode 100644
index 000000000..a0668fa4b
--- /dev/null
+++ b/packages/radix-vue/src/shared/getActiveElement.test.ts
@@ -0,0 +1,48 @@
+import { describe, expect, it } from 'vitest'
+import { getActiveElement } from './getActiveElement'
+
+describe('getActiveElement', () => {
+ it('should return the active element when it is a regular element', () => {
+ const element = createFocusableElement()
+ document.body.appendChild(element)
+ element.focus()
+
+ expect(getActiveElement()).toBe(element)
+
+ document.body.removeChild(element)
+ })
+
+ it('should return the deepest active element in shadow DOM', () => {
+ const host = createShadowDom()
+ const hostElement = createFocusableElement()
+ host.shadowRoot?.appendChild(hostElement)
+
+ const nested = createShadowDom()
+ const nestedElement = createFocusableElement()
+ nested.shadowRoot?.appendChild(nestedElement)
+
+ host.shadowRoot?.appendChild(nested)
+
+ document.body.appendChild(host)
+
+ nestedElement.focus()
+
+ expect(getActiveElement()).toBe(nestedElement)
+
+ document.body.removeChild(host)
+ })
+})
+
+function createFocusableElement() {
+ const button = document.createElement('button')
+ button.innerText = 'Test Button'
+
+ return button
+}
+
+function createShadowDom() {
+ const host = document.createElement('div')
+ host.attachShadow({ mode: 'open' })
+
+ return host
+}
diff --git a/packages/radix-vue/src/shared/getActiveElement.ts b/packages/radix-vue/src/shared/getActiveElement.ts
new file mode 100644
index 000000000..12bc6b45a
--- /dev/null
+++ b/packages/radix-vue/src/shared/getActiveElement.ts
@@ -0,0 +1,12 @@
+export function getActiveElement(): Element | null {
+ let activeElement = document.activeElement
+ if (activeElement == null) {
+ return null
+ }
+
+ while (activeElement != null && activeElement.shadowRoot != null && activeElement.shadowRoot.activeElement != null) {
+ activeElement = activeElement.shadowRoot.activeElement
+ }
+
+ return activeElement
+}
diff --git a/packages/radix-vue/src/shared/index.ts b/packages/radix-vue/src/shared/index.ts
index a7dc5936e..f4eb59834 100644
--- a/packages/radix-vue/src/shared/index.ts
+++ b/packages/radix-vue/src/shared/index.ts
@@ -31,3 +31,4 @@ export { useStateMachine } from './useStateMachine'
export { useTypeahead } from './useTypeahead'
export { withDefault } from './withDefault'
export { useKbd, useTestKbd } from './useKbd'
+export { getActiveElement } from './getActiveElement'
diff --git a/packages/radix-vue/src/shared/trap-focus.ts b/packages/radix-vue/src/shared/trap-focus.ts
index c3d95e6d2..f6601df0b 100644
--- a/packages/radix-vue/src/shared/trap-focus.ts
+++ b/packages/radix-vue/src/shared/trap-focus.ts
@@ -1,3 +1,5 @@
+import { getActiveElement } from './getActiveElement'
+
export function trapFocus(element: HTMLElement) {
if (element) {
const focusableEls = [
@@ -25,14 +27,14 @@ export function trapFocus(element: HTMLElement) {
return
if (e.shiftKey) {
- /* shift + tab */ if (document.activeElement === firstFocusableEl) {
+ /* shift + tab */ if (getActiveElement() === firstFocusableEl) {
lastFocusableEl.focus()
e.preventDefault()
}
}
else {
/* tab */
- if (document.activeElement === lastFocusableEl) {
+ if (getActiveElement() === lastFocusableEl) {
firstFocusableEl.focus()
e.preventDefault()
}
diff --git a/packages/radix-vue/src/shared/useTypeahead.ts b/packages/radix-vue/src/shared/useTypeahead.ts
index a17243282..ae40c24f7 100644
--- a/packages/radix-vue/src/shared/useTypeahead.ts
+++ b/packages/radix-vue/src/shared/useTypeahead.ts
@@ -1,5 +1,6 @@
import { refAutoReset } from '@vueuse/shared'
import type { Ref } from 'vue'
+import { getActiveElement } from './getActiveElement'
const ITEM_TEXT_ATTR = 'data-item-text'
@@ -14,7 +15,7 @@ export function useTypeahead(collections?: Ref) {
search.value = search.value + key
const items = collections?.value ?? fallback!
- const currentItem = document.activeElement
+ const currentItem = getActiveElement()
const itemsWithTextValue = items.map(el => ({
ref: el,