Skip to content

Commit

Permalink
feat (core): support shadow roots in useActiveElement
Browse files Browse the repository at this point in the history
Assuming you have a DOM structure like so:

```
body
  my-element
    #shadow-root
      input
```

When the `input` becomes active, the outer document's `activeElement`
will actually be `my-element`, _not_ the input.

Each shadow root has its own `activeElement` in case you want to get
hold of the inner element in these cases.

This adds support to `useActiveElement` such that you can pass a shadow
root as the document to observe the active element of.
  • Loading branch information
43081j committed Dec 28, 2022
1 parent 9a57746 commit 576af4b
Show file tree
Hide file tree
Showing 3 changed files with 68 additions and 4 deletions.
2 changes: 1 addition & 1 deletion packages/core/_configurable.ts
Expand Up @@ -11,7 +11,7 @@ export interface ConfigurableDocument {
/*
* Specify a custom `document` instance, e.g. working with iframes or in testing environments.
*/
document?: Document
document?: DocumentOrShadowRoot
}

export interface ConfigurableNavigator {
Expand Down
61 changes: 61 additions & 0 deletions packages/core/useActiveElement/index.test.ts
@@ -0,0 +1,61 @@
import { useActiveElement } from '.'

describe('useActiveElement', () => {
let shadowHost: HTMLElement
let input: HTMLInputElement
let shadowInput: HTMLInputElement
let shadowRoot: ShadowRoot

beforeEach(() => {
shadowHost = document.createElement('div')
shadowRoot = shadowHost.attachShadow({ mode: 'open' })
input = document.createElement('input')
shadowInput = input.cloneNode() as HTMLInputElement
shadowRoot.appendChild(shadowInput)
document.body.appendChild(input)
document.body.appendChild(shadowHost)
})

afterEach(() => {
shadowHost.remove()
input.remove()
})

it('should be defined', () => {
expect(useActiveElement).toBeDefined()
})

it('should initialise correctly', () => {
const activeElement = useActiveElement()

expect(activeElement.value).to.equal(document.body)
})

it('should initialise with already-active element', () => {
input.focus()

const activeElement = useActiveElement()

expect(activeElement.value).to.equal(input)
})

it('should accept custom document', () => {
const activeElement = useActiveElement({ document: shadowRoot })

shadowInput.focus()

expect(activeElement.value).to.equal(shadowInput)
})

it('should observe focus/blur events', () => {
const activeElement = useActiveElement()

input.focus()

expect(activeElement.value).to.equal(input)

input.blur()

expect(activeElement.value).to.equal(document.body)
})
})
9 changes: 6 additions & 3 deletions packages/core/useActiveElement/index.ts
@@ -1,19 +1,22 @@
import { computedWithControl } from '@vueuse/shared'
import { useEventListener } from '../useEventListener'
import type { ConfigurableWindow } from '../_configurable'
import type { ConfigurableDocument, ConfigurableWindow } from '../_configurable'
import { defaultWindow } from '../_configurable'

export type UseActiveElementOptions = ConfigurableWindow & ConfigurableDocument

/**
* Reactive `document.activeElement`
*
* @see https://vueuse.org/useActiveElement
* @param options
*/
export function useActiveElement<T extends HTMLElement>(options: ConfigurableWindow = {}) {
export function useActiveElement<T extends HTMLElement>(options: UseActiveElementOptions = {}) {
const { window = defaultWindow } = options
const document = options.document ?? window?.document
const activeElement = computedWithControl(
() => null,
() => window?.document.activeElement as T | null | undefined,
() => document?.activeElement as T | null | undefined,
)

if (window) {
Expand Down

0 comments on commit 576af4b

Please sign in to comment.