From d3ac60e0e24fa659c501b421cffb6a57dd62ac4f Mon Sep 17 00:00:00 2001 From: James Garbutt <43081j@users.noreply.github.com> Date: Wed, 28 Dec 2022 17:56:56 +0000 Subject: [PATCH 1/2] feat (core): support shadow roots in useActiveElement 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. --- packages/core/_configurable.ts | 2 +- packages/core/useActiveElement/index.test.ts | 61 ++++++++++++++++++++ packages/core/useActiveElement/index.ts | 9 ++- 3 files changed, 68 insertions(+), 4 deletions(-) create mode 100644 packages/core/useActiveElement/index.test.ts diff --git a/packages/core/_configurable.ts b/packages/core/_configurable.ts index d361ac0f2ba..a3945872a7e 100644 --- a/packages/core/_configurable.ts +++ b/packages/core/_configurable.ts @@ -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 { 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 62e0983a8f7..b8e6377e530 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 { 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(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) { From fbae8e196c7270ebd6ca89d9ab06efeb7b2f1dc9 Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Tue, 3 Jan 2023 14:25:42 +0100 Subject: [PATCH 2/2] chore: update --- packages/core/_configurable.ts | 7 +++++++ packages/core/useActiveElement/index.ts | 4 ++-- packages/core/useEventListener/index.ts | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/core/_configurable.ts b/packages/core/_configurable.ts index a3945872a7e..fbc1d44fc5c 100644 --- a/packages/core/_configurable.ts +++ b/packages/core/_configurable.ts @@ -11,6 +11,13 @@ export interface ConfigurableDocument { /* * Specify a custom `document` instance, e.g. working with iframes or in testing environments. */ + 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 } diff --git a/packages/core/useActiveElement/index.ts b/packages/core/useActiveElement/index.ts index b8e6377e530..280a2245e32 100644 --- a/packages/core/useActiveElement/index.ts +++ b/packages/core/useActiveElement/index.ts @@ -1,9 +1,9 @@ import { computedWithControl } from '@vueuse/shared' import { useEventListener } from '../useEventListener' -import type { ConfigurableDocument, ConfigurableWindow } from '../_configurable' +import type { ConfigurableDocumentOrShadowRoot, ConfigurableWindow } from '../_configurable' import { defaultWindow } from '../_configurable' -export type UseActiveElementOptions = ConfigurableWindow & ConfigurableDocument +export interface UseActiveElementOptions extends ConfigurableWindow, ConfigurableDocumentOrShadowRoot {} /** * Reactive `document.activeElement` 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