diff --git a/projects/core/src/internal/decorators/scoped-registry.test.ts b/projects/core/src/internal/decorators/scoped-registry.test.ts index ccb035a626..0fe895e55a 100644 --- a/projects/core/src/internal/decorators/scoped-registry.test.ts +++ b/projects/core/src/internal/decorators/scoped-registry.test.ts @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 /* eslint-disable @typescript-eslint/no-unsafe-function-type */ +import { LitElement } from 'lit'; +import { html as staticHtml, unsafeStatic } from 'lit/static-html.js'; import { describe, expect, it, beforeEach, afterEach } from 'vitest'; +import { createFixture, removeFixture } from '@internals/testing'; import { GlobalStateService } from '../services/global.service.js'; import type { ElementDefinition } from '../types/index.js'; import { supportsScopedRegistry } from '../utils/dom.js'; @@ -87,4 +90,35 @@ describe('scopedRegistry', () => { expect((element.shadowRootOptions as ShadowRootInit).mode).toBe('closed'); }); + + it.skipIf(!supportsScopedRegistry)('should use the scoped shadow root as the Lit creation scope', async () => { + const child = createMockElement(); + const childTag = unsafeStatic(child.metadata.tag); + const hostTag = `nve-test-scoped-host-${uid}-${counter++}`; + const host = unsafeStatic(hostTag); + + class HostElement extends LitElement { + static metadata = { version: '0.0.0', tag: hostTag }; + static elementDefinitions = { [child.metadata.tag]: child }; + + render() { + return staticHtml`<${childTag}>`; + } + } + + scopedRegistry()(HostElement as unknown as Function); + customElements.define(hostTag, HostElement); + + const fixture = await createFixture(staticHtml`<${host}>`); + const element = fixture.querySelector(hostTag)!; + + try { + await element.updateComplete; + + expect(typeof element.renderOptions.creationScope?.importNode).toBe('function'); + expect(element.shadowRoot!.querySelector(child.metadata.tag)).toBeInstanceOf(child); + } finally { + removeFixture(fixture); + } + }); }); diff --git a/projects/core/src/internal/decorators/scoped-registry.ts b/projects/core/src/internal/decorators/scoped-registry.ts index c2d66b6d6a..9fe5363b7a 100644 --- a/projects/core/src/internal/decorators/scoped-registry.ts +++ b/projects/core/src/internal/decorators/scoped-registry.ts @@ -1,10 +1,50 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +import type { RenderOptions } from 'lit'; import { GlobalStateService } from '../services/global.service.js'; import type { ElementDefinition, LegacyDecoratorTarget } from '../types/index.js'; import { defineElement, supportsScopedRegistry } from '../utils/dom.js'; +interface ScopedRegistryHost extends HTMLElement { + createRenderRoot?: () => HTMLElement | DocumentFragment; + renderOptions?: RenderOptions; +} + +const litCreationScopeElements = new WeakSet(); + +/** Lit passes a legacy `deep` boolean, but scoped registries require `ImportNodeOptions`. https://html.spec.whatwg.org/multipage/custom-elements.html#scoped-custom-element-registries */ +function createScopedCreationScope(ownerDocument: Document, customElementRegistry: CustomElementRegistry) { + return { + importNode: (node: Node, deep = false) => + ownerDocument.importNode(node, { + customElementRegistry, + selfOnly: !deep + }) + } satisfies NonNullable; +} + +function attachLitCreationScope(element: ElementDefinition, customElementRegistry: CustomElementRegistry) { + if (litCreationScopeElements.has(element)) return; + + const host = element.prototype as ScopedRegistryHost; + const createRenderRoot = host.createRenderRoot; + if (!createRenderRoot) return; + + litCreationScopeElements.add(element); + Object.defineProperty(host, 'createRenderRoot', { + configurable: true, + value(this: ScopedRegistryHost) { + const renderRoot = createRenderRoot.call(this); + if (renderRoot instanceof ShadowRoot) { + this.renderOptions ??= {}; + this.renderOptions.creationScope = createScopedCreationScope(renderRoot.ownerDocument, customElementRegistry); + } + return renderRoot; + } + }); +} + /** decorator which registers element dependencies with the scoped custom element registry when available */ export function scopedRegistry(): ClassDecorator { return (target: LegacyDecoratorTarget) => { @@ -16,6 +56,7 @@ export function scopedRegistry(): ClassDecorator { configurable: true, value: { ...(element.shadowRootOptions ?? { mode: 'open' }), customElementRegistry } }); + attachLitCreationScope(element, customElementRegistry); } defineElement(element, customElementRegistry); }; diff --git a/projects/core/src/internal/utils/focus.ts b/projects/core/src/internal/utils/focus.ts index c348be4564..6a4bd73cba 100644 --- a/projects/core/src/internal/utils/focus.ts +++ b/projects/core/src/internal/utils/focus.ts @@ -84,7 +84,10 @@ export function onListboxActivate( e.preventDefault(); }); - element.addEventListener('pointerup', (e: PointerEvent) => { + // Chrome's LightDismissFromClick runs auto-popover light dismiss from click; + // opening on pointerup can close immediately in the same gesture. + // https://issues.chromium.org/issues/408010435 + element.addEventListener('click', (e: PointerEvent) => { e.preventDefault(); if (!element.disabled) {