diff --git a/packages/core/_configurable.ts b/packages/core/_configurable.ts index d361ac0f2ba..fbc1d44fc5c 100644 --- a/packages/core/_configurable.ts +++ b/packages/core/_configurable.ts @@ -14,6 +14,13 @@ export interface ConfigurableDocument { document?: Document } +export interface ConfigurableDocumentOrShadowRoot { + /* + * Specify a custom `document` instance or a shadow root, e.g. working with iframes or in testing environments. + */ + document?: DocumentOrShadowRoot +} + export interface ConfigurableNavigator { /* * Specify a custom `navigator` instance, e.g. working with iframes or in testing environments. diff --git a/packages/core/useActiveElement/index.test.ts b/packages/core/useActiveElement/index.test.ts new file mode 100644 index 00000000000..ada9774fe8e --- /dev/null +++ b/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) + }) +}) diff --git a/packages/core/useActiveElement/index.ts b/packages/core/useActiveElement/index.ts index e9feb5c5f39..4976bfb3448 100644 --- a/packages/core/useActiveElement/index.ts +++ b/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 { ConfigurableDocumentOrShadowRoot, ConfigurableWindow } from '../_configurable' import { defaultWindow } from '../_configurable' +export interface UseActiveElementOptions extends ConfigurableWindow, ConfigurableDocumentOrShadowRoot {} + /** * Reactive `document.activeElement` * * @see https://vueuse.org/useActiveElement * @param options */ -export function useActiveElement(options: ConfigurableWindow = {}) { +export function useActiveElement(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) { diff --git a/packages/core/useEventListener/index.ts b/packages/core/useEventListener/index.ts index b2ee006e1c4..6b7bab0e010 100644 --- a/packages/core/useEventListener/index.ts +++ b/packages/core/useEventListener/index.ts @@ -63,7 +63,7 @@ export function useEventListener( * @param options */ export function useEventListener( - target: Document, + target: DocumentOrShadowRoot, event: Arrayable, listener: Arrayable<(this: Document, ev: DocumentEventMap[E]) => any>, options?: boolean | AddEventListenerOptions