From f00da48be7b9d277d07fae1db6ac41c685e44287 Mon Sep 17 00:00:00 2001 From: Lukas Maurer Date: Tue, 12 Nov 2024 10:50:55 +0100 Subject: [PATCH 01/14] fix(core/tooltip|dropdown): find trigger if in same shadow DOM --- .../core/src/components/dropdown/dropdown.tsx | 45 +++++-- .../src/components/tooltip/test/tooltip.ct.ts | 17 +++ .../core/src/components/tooltip/tooltip.tsx | 3 +- .../core/src/components/utils/find-element.ts | 122 ++++++++++++++++++ 4 files changed, 174 insertions(+), 13 deletions(-) create mode 100644 packages/core/src/components/utils/find-element.ts diff --git a/packages/core/src/components/dropdown/dropdown.tsx b/packages/core/src/components/dropdown/dropdown.tsx index da687a12621..1197682c2e8 100644 --- a/packages/core/src/components/dropdown/dropdown.tsx +++ b/packages/core/src/components/dropdown/dropdown.tsx @@ -38,6 +38,7 @@ import { hasDropdownItemWrapperImplemented, } from './dropdown-controller'; import { AlignedPlacement } from './placement'; +import { resolveSelector } from '../utils/find-element'; let sequenceId = 0; @@ -343,21 +344,41 @@ export class Dropdown implements ComponentInterface, DropdownInterface { } const selector = `#${element}`; - return new Promise((resolve) => { - if (document.querySelector(selector)) { - return resolve(document.querySelector(selector)); + + const resolve = (el: Element[]) => { + if (el?.length > 1) { + console.warn( + `ix-dropdown: Ambiguous selector ${selector}. Multiple elements found:`, + el + ); + } else if (el && el[0]) { + return el[0]; } + }; - const observer = new MutationObserver(() => { - if (document.querySelector(selector)) { - resolve(document.querySelector(selector)); - observer.disconnect(); - } - }); + return resolveSelector(selector, this.hostElement).then((el) => { + let element = resolve(el); - observer.observe(document.body, { - childList: true, - subtree: true, + if (element) { + return element; + } + + return new Promise(() => { + const observer = new MutationObserver(() => { + resolveSelector(selector, this.hostElement).then((el) => { + element = resolve(el); + + if (element) { + observer.disconnect(); + return element; + } + }); + + observer.observe(document.body, { + childList: true, + subtree: true, + }); + }); }); }); } diff --git a/packages/core/src/components/tooltip/test/tooltip.ct.ts b/packages/core/src/components/tooltip/test/tooltip.ct.ts index dcf9ca02e1c..986455188f7 100644 --- a/packages/core/src/components/tooltip/test/tooltip.ct.ts +++ b/packages/core/src/components/tooltip/test/tooltip.ct.ts @@ -23,6 +23,23 @@ test('renders', async ({ mount, page }) => { await expect(tooltip).toBeVisible(); }); +test('renders in shadow DOM', async ({ mount, page }) => { + await mount(` + + tooltip + button + + `); + + const tooltip = page.locator('ix-tooltip'); + const button = page.locator('ix-button'); + + await button.hover(); + + await expect(tooltip).toHaveClass(/hydrated/); + await expect(tooltip).toBeVisible(); +}); + test.describe('a11y', () => { test('closes on ESC', async ({ mount, page }) => { await mount(` diff --git a/packages/core/src/components/tooltip/tooltip.tsx b/packages/core/src/components/tooltip/tooltip.tsx index 7c37cf4ad46..3e40ed3e3ca 100644 --- a/packages/core/src/components/tooltip/tooltip.tsx +++ b/packages/core/src/components/tooltip/tooltip.tsx @@ -28,6 +28,7 @@ import { import { OnListener } from '../utils/listener'; import { tooltipController } from './tooltip-controller'; import { IxOverlayComponent } from '../utils/overlay'; +import { resolveSelector } from '../utils/find-element'; type ArrowPosition = { top?: string; @@ -254,7 +255,7 @@ export class Tooltip implements IxOverlayComponent { private async queryAnchorElements(): Promise | undefined> { if (typeof this.for === 'string') { - return Promise.resolve(Array.from(document.querySelectorAll(this.for))); + return resolveSelector(this.for); } if (this.for instanceof HTMLElement) { diff --git a/packages/core/src/components/utils/find-element.ts b/packages/core/src/components/utils/find-element.ts new file mode 100644 index 00000000000..debc969e655 --- /dev/null +++ b/packages/core/src/components/utils/find-element.ts @@ -0,0 +1,122 @@ +/* + * SPDX-FileCopyrightText: 2024 Siemens AG + * + * SPDX-License-Identifier: MIT + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * Will try to resolve the selector in the light dom, shadow dom or slot + * @param selector The selector to resolve + * @returns Promse with the resolved elements + */ +export async function resolveSelector( + selector: string, + hostElement?: HTMLElement +): Promise { + const elementsInLightDom: HTMLElement[] = Array.from( + document.querySelectorAll(selector) + ); + + if (elementsInLightDom.length === 0) { + const shadowRoot = getRootFor(hostElement); + + if (shadowRoot === undefined) { + return Promise.resolve(undefined); + } + + const slots = shadowRoot.querySelectorAll('slot'); + let elementsInSlot: HTMLElement[] = []; + + slots.forEach((slot) => { + const assignedElements = slot.assignedElements({ flatten: true }); + assignedElements.forEach((element) => { + if (element.matches(selector)) { + elementsInSlot.push(element as HTMLElement); + } else if (element.querySelector(selector)) { + elementsInSlot.push(element.querySelector(selector) as HTMLElement); + } + }); + }); + + if (elementsInSlot.length > 0) { + return Promise.resolve(elementsInSlot); + } + + const elementsInShadowRoot: HTMLElement[] = Array.from( + shadowRoot.querySelectorAll(selector) + ); + + return Promise.resolve(elementsInShadowRoot); + } else { + return Promise.resolve(elementsInLightDom); + } +} + +/** + * Walk up the DOM to find the nearest shadow root + * @param element The element to get the root for + * @param parent This will determine how far up the DOM to travel to find the root + * @returns The root element + */ +export function getRootFor(element: HTMLElement, parent = document.body) { + if (!element.parentElement) { + return undefined; + } + + let currentNode = element.parentElement; + + while (currentNode) { + if (currentNode.shadowRoot) { + return currentNode.shadowRoot; + } + + currentNode = currentNode.parentElement; + } + + return parent; +} + +export function waitForSelector( + selector: string, + node = document +): Promise { + return new Promise((resolve) => { + if (node.querySelector(selector)) { + return resolve(node.querySelector(selector)); + } + + const observer = new MutationObserver(() => { + if (node.querySelector(selector)) { + resolve(node.querySelector(selector)); + observer.disconnect(); + } + }); + + observer.observe(node.body, { + childList: true, + subtree: true, + }); + }); +} + +export function findElement( + element: string | HTMLElement | Promise +): Promise { + if (element instanceof Promise) { + return element; + } + + if (typeof element === 'object') { + return Promise.resolve(element); + } + + if (typeof element != 'string') { + return; + } + + const selector = `#${element}`; + return waitForSelector(selector); +} From 0b57c26d83a6e90936bb3f95bf54f8982aeabff8 Mon Sep 17 00:00:00 2001 From: Lukas Maurer Date: Tue, 12 Nov 2024 11:00:35 +0100 Subject: [PATCH 02/14] refactor(core/dropdown): use util fn --- .../core/src/components/dropdown/dropdown.tsx | 59 +------------------ 1 file changed, 2 insertions(+), 57 deletions(-) diff --git a/packages/core/src/components/dropdown/dropdown.tsx b/packages/core/src/components/dropdown/dropdown.tsx index 1197682c2e8..1991f918caf 100644 --- a/packages/core/src/components/dropdown/dropdown.tsx +++ b/packages/core/src/components/dropdown/dropdown.tsx @@ -38,7 +38,7 @@ import { hasDropdownItemWrapperImplemented, } from './dropdown-controller'; import { AlignedPlacement } from './placement'; -import { resolveSelector } from '../utils/find-element'; +import { findElement } from '../utils/find-element'; let sequenceId = 0; @@ -304,7 +304,7 @@ export class Dropdown implements ComponentInterface, DropdownInterface { private async resolveElement( element: string | HTMLElement | Promise ) { - const el = await this.findElement(element); + const el = await findElement(element); return this.checkForSubmenuAnchor(el); } @@ -328,61 +328,6 @@ export class Dropdown implements ComponentInterface, DropdownInterface { return element; } - private findElement( - element: string | HTMLElement | Promise - ): Promise { - if (element instanceof Promise) { - return element; - } - - if (typeof element === 'object') { - return Promise.resolve(element); - } - - if (typeof element != 'string') { - return; - } - - const selector = `#${element}`; - - const resolve = (el: Element[]) => { - if (el?.length > 1) { - console.warn( - `ix-dropdown: Ambiguous selector ${selector}. Multiple elements found:`, - el - ); - } else if (el && el[0]) { - return el[0]; - } - }; - - return resolveSelector(selector, this.hostElement).then((el) => { - let element = resolve(el); - - if (element) { - return element; - } - - return new Promise(() => { - const observer = new MutationObserver(() => { - resolveSelector(selector, this.hostElement).then((el) => { - element = resolve(el); - - if (element) { - observer.disconnect(); - return element; - } - }); - - observer.observe(document.body, { - childList: true, - subtree: true, - }); - }); - }); - }); - } - @Watch('show') async changedShow(newShow: boolean) { if (newShow) { From f5d4e83677d77f379df0f8c85dd2d301ee8c222f Mon Sep 17 00:00:00 2001 From: Lukas Maurer Date: Tue, 19 Nov 2024 17:01:54 +0100 Subject: [PATCH 03/14] fix(core/tooltip|dropdown): pass host element to discovery logic --- .../core/src/components/dropdown/dropdown.tsx | 2 +- .../core/src/components/tooltip/tooltip.tsx | 2 +- .../core/src/components/utils/find-element.ts | 111 +++++++++++------- 3 files changed, 70 insertions(+), 45 deletions(-) diff --git a/packages/core/src/components/dropdown/dropdown.tsx b/packages/core/src/components/dropdown/dropdown.tsx index 1991f918caf..5665281f2e2 100644 --- a/packages/core/src/components/dropdown/dropdown.tsx +++ b/packages/core/src/components/dropdown/dropdown.tsx @@ -304,7 +304,7 @@ export class Dropdown implements ComponentInterface, DropdownInterface { private async resolveElement( element: string | HTMLElement | Promise ) { - const el = await findElement(element); + const el = await findElement(element, this.hostElement); return this.checkForSubmenuAnchor(el); } diff --git a/packages/core/src/components/tooltip/tooltip.tsx b/packages/core/src/components/tooltip/tooltip.tsx index 3e40ed3e3ca..a1954024eb1 100644 --- a/packages/core/src/components/tooltip/tooltip.tsx +++ b/packages/core/src/components/tooltip/tooltip.tsx @@ -255,7 +255,7 @@ export class Tooltip implements IxOverlayComponent { private async queryAnchorElements(): Promise | undefined> { if (typeof this.for === 'string') { - return resolveSelector(this.for); + return resolveSelector(this.for, this.hostElement); } if (this.for instanceof HTMLElement) { diff --git a/packages/core/src/components/utils/find-element.ts b/packages/core/src/components/utils/find-element.ts index debc969e655..692e31422b6 100644 --- a/packages/core/src/components/utils/find-element.ts +++ b/packages/core/src/components/utils/find-element.ts @@ -10,7 +10,8 @@ /** * Will try to resolve the selector in the light dom, shadow dom or slot * @param selector The selector to resolve - * @returns Promse with the resolved elements + * @param hostElement The element to start the search from + * @returns Promise with the resolved elements */ export async function resolveSelector( selector: string, @@ -20,39 +21,49 @@ export async function resolveSelector( document.querySelectorAll(selector) ); - if (elementsInLightDom.length === 0) { - const shadowRoot = getRootFor(hostElement); + if (elementsInLightDom.length > 0) { + return Promise.resolve(elementsInLightDom); + } - if (shadowRoot === undefined) { - return Promise.resolve(undefined); - } + if (hostElement === undefined) { + return Promise.resolve(undefined); + } - const slots = shadowRoot.querySelectorAll('slot'); - let elementsInSlot: HTMLElement[] = []; + const shadowRoot = getRootFor(hostElement); - slots.forEach((slot) => { - const assignedElements = slot.assignedElements({ flatten: true }); - assignedElements.forEach((element) => { - if (element.matches(selector)) { - elementsInSlot.push(element as HTMLElement); - } else if (element.querySelector(selector)) { - elementsInSlot.push(element.querySelector(selector) as HTMLElement); - } - }); - }); - - if (elementsInSlot.length > 0) { - return Promise.resolve(elementsInSlot); - } + if (shadowRoot === undefined || !(shadowRoot instanceof ShadowRoot)) { + return Promise.resolve(undefined); + } - const elementsInShadowRoot: HTMLElement[] = Array.from( - shadowRoot.querySelectorAll(selector) - ); + let elementsInSlot: HTMLElement[] = getSlottedElements(shadowRoot, selector); - return Promise.resolve(elementsInShadowRoot); - } else { - return Promise.resolve(elementsInLightDom); + if (elementsInSlot.length > 0) { + return Promise.resolve(elementsInSlot); } + + const elementsInShadowRoot: HTMLElement[] = Array.from( + shadowRoot.querySelectorAll(selector) + ); + + return Promise.resolve(elementsInShadowRoot); +} + +function getSlottedElements(shadowRoot: ShadowRoot, selector: string) { + const slots = shadowRoot.querySelectorAll('slot'); + let elementsInSlot: HTMLElement[] = []; + + slots.forEach((slot) => { + const assignedElements = slot.assignedElements({ flatten: true }); + assignedElements.forEach((element) => { + if (element.matches(selector)) { + elementsInSlot.push(element as HTMLElement); + } else if (element.querySelector(selector)) { + elementsInSlot.push(element.querySelector(selector) as HTMLElement); + } + }); + }); + + return elementsInSlot; } /** @@ -62,15 +73,21 @@ export async function resolveSelector( * @returns The root element */ export function getRootFor(element: HTMLElement, parent = document.body) { - if (!element.parentElement) { + if (!element.parentElement && !element.parentNode) { return undefined; } + if (element.parentNode instanceof ShadowRoot) { + return element.parentNode; + } + let currentNode = element.parentElement; while (currentNode) { if (currentNode.shadowRoot) { return currentNode.shadowRoot; + } else if (currentNode.parentNode instanceof ShadowRoot) { + return currentNode.parentNode; } currentNode = currentNode.parentElement; @@ -81,18 +98,23 @@ export function getRootFor(element: HTMLElement, parent = document.body) { export function waitForSelector( selector: string, - node = document + node = document, + hostElement?: HTMLElement ): Promise { return new Promise((resolve) => { - if (node.querySelector(selector)) { - return resolve(node.querySelector(selector)); - } + const waitForElements = () => { + resolveSelector(selector, hostElement).then((elements) => { + if (elements && elements.length > 0) { + resolve(elements[0]); + observer?.disconnect(); + } + }); + }; + + waitForElements(); const observer = new MutationObserver(() => { - if (node.querySelector(selector)) { - resolve(node.querySelector(selector)); - observer.disconnect(); - } + waitForElements(); }); observer.observe(node.body, { @@ -102,8 +124,15 @@ export function waitForSelector( }); } +/** + * Find an element by ID or reference + * @param element The element to find + * @param hostElement The element to start the search from + * @returns A promise that will resolve to the element + */ export function findElement( - element: string | HTMLElement | Promise + element: string | HTMLElement | Promise, + hostElement?: HTMLElement ): Promise { if (element instanceof Promise) { return element; @@ -113,10 +142,6 @@ export function findElement( return Promise.resolve(element); } - if (typeof element != 'string') { - return; - } - const selector = `#${element}`; - return waitForSelector(selector); + return waitForSelector(selector, document, hostElement); } From 920236d7b577d8779aa24074e4623eced7e89c7d Mon Sep 17 00:00:00 2001 From: Lukas Maurer Date: Wed, 20 Nov 2024 09:31:09 +0100 Subject: [PATCH 04/14] refactor(core/dropdown): undo merge changes --- packages/core/component-doc.json | 56 +++++++++---------- packages/core/src/components.d.ts | 22 ++++---- .../core/src/components/dropdown/dropdown.tsx | 7 +-- 3 files changed, 41 insertions(+), 44 deletions(-) diff --git a/packages/core/component-doc.json b/packages/core/component-doc.json index eb3cbe2cf3b..3d82a8b2f56 100644 --- a/packages/core/component-doc.json +++ b/packages/core/component-doc.json @@ -5220,13 +5220,16 @@ "name": "anchor", "type": "HTMLElement | Promise | string", "complexType": { - "original": "ElementReference", + "original": "string | HTMLElement | Promise", "resolved": "HTMLElement | Promise | string", "references": { - "ElementReference": { - "location": "import", - "path": "src/components/utils/element-reference", - "id": "src/components/utils/element-reference.ts::ElementReference" + "HTMLElement": { + "location": "global", + "id": "global::HTMLElement" + }, + "Promise": { + "location": "global", + "id": "global::Promise" } } }, @@ -5246,7 +5249,7 @@ "type": "string" } ], - "optional": true, + "optional": false, "required": false }, { @@ -5447,13 +5450,16 @@ "name": "trigger", "type": "HTMLElement | Promise | string", "complexType": { - "original": "ElementReference", + "original": "string | HTMLElement | Promise", "resolved": "HTMLElement | Promise | string", "references": { - "ElementReference": { - "location": "import", - "path": "src/components/utils/element-reference", - "id": "src/components/utils/element-reference.ts::ElementReference" + "HTMLElement": { + "location": "global", + "id": "global::HTMLElement" + }, + "Promise": { + "location": "global", + "id": "global::Promise" } } }, @@ -5473,7 +5479,7 @@ "type": "string" } ], - "optional": true, + "optional": false, "required": false } ], @@ -16140,15 +16146,15 @@ "props": [ { "name": "for", - "type": "HTMLElement | Promise | string", + "type": "ElementReference", "complexType": { "original": "ElementReference", - "resolved": "HTMLElement | Promise | string", + "resolved": "ElementReference", "references": { "ElementReference": { "location": "import", - "path": "../utils/element-reference", - "id": "src/components/utils/element-reference.ts::ElementReference" + "path": "src/components", + "id": "src/components.d.ts::unknown" } } }, @@ -16159,13 +16165,7 @@ "docsTags": [], "values": [ { - "type": "HTMLElement" - }, - { - "type": "Promise" - }, - { - "type": "string" + "type": "ElementReference" } ], "optional": true, @@ -17909,11 +17909,6 @@ "docstring": "", "path": "src/components/category-filter/input-state.ts" }, - "src/components/utils/element-reference.ts::ElementReference": { - "declaration": "export type ElementReference = string | HTMLElement | Promise;", - "docstring": "", - "path": "src/components/utils/element-reference.ts" - }, "src/components/flip-tile/flip-tile-state.ts::FlipTileState": { "declaration": "export enum FlipTileState {\n None = 'none',\n Info = 'info',\n Warning = 'warning',\n Alarm = 'alarm',\n Primary = 'primary',\n}", "docstring": "", @@ -17929,6 +17924,11 @@ "docstring": "", "path": "src/components/toast/toast-utils.ts" }, + "src/components.d.ts::unknown": { + "declaration": "any", + "docstring": "", + "path": "src/components.d.ts" + }, "../../node_modules/.pnpm/@stencil+core@4.17.2/node_modules/@stencil/core/internal/stencil-core/index.d.ts::Element": { "declaration": "any", "docstring": "", diff --git a/packages/core/src/components.d.ts b/packages/core/src/components.d.ts index b3d310448ac..fa7583d9189 100644 --- a/packages/core/src/components.d.ts +++ b/packages/core/src/components.d.ts @@ -24,7 +24,6 @@ import { DateTimeCardCorners } from "./components/date-time-card/date-time-card" import { DateChangeEvent } from "./components/date-picker/date-picker"; import { DateTimeCardCorners as DateTimeCardCorners1 } from "./components/date-time-card/date-time-card"; import { DateTimeDateChangeEvent, DateTimeSelectEvent } from "./components/datetime-picker/datetime-picker"; -import { ElementReference } from "./components/utils/element-reference"; import { CloseBehavior } from "./components/dropdown/dropdown-controller"; import { AlignedPlacement, Side } from "./components/dropdown/placement"; import { DropdownButtonVariant } from "./components/dropdown-button/dropdown-button"; @@ -43,7 +42,7 @@ import { TabClickDetail } from "./components/tab-item/tab-item"; import { TimePickerCorners } from "./components/time-picker/time-picker"; import { ToastConfig, ToastType } from "./components/toast/toast-utils"; import { ShowToastResult } from "./components/toast/toast-container"; -import { ElementReference as ElementReference1 } from "./components/utils/element-reference"; +import { ElementReference } from "./components.d"; import { Element } from "@stencil/core"; import { TreeContext, TreeItemContext, TreeModel, UpdateCallback } from "./components/tree/tree-model"; import { TextDecoration, TypographyColors, TypographyFormat } from "./components/typography/typography"; @@ -67,7 +66,6 @@ export { DateTimeCardCorners } from "./components/date-time-card/date-time-card" export { DateChangeEvent } from "./components/date-picker/date-picker"; export { DateTimeCardCorners as DateTimeCardCorners1 } from "./components/date-time-card/date-time-card"; export { DateTimeDateChangeEvent, DateTimeSelectEvent } from "./components/datetime-picker/datetime-picker"; -export { ElementReference } from "./components/utils/element-reference"; export { CloseBehavior } from "./components/dropdown/dropdown-controller"; export { AlignedPlacement, Side } from "./components/dropdown/placement"; export { DropdownButtonVariant } from "./components/dropdown-button/dropdown-button"; @@ -86,7 +84,7 @@ export { TabClickDetail } from "./components/tab-item/tab-item"; export { TimePickerCorners } from "./components/time-picker/time-picker"; export { ToastConfig, ToastType } from "./components/toast/toast-utils"; export { ShowToastResult } from "./components/toast/toast-container"; -export { ElementReference as ElementReference1 } from "./components/utils/element-reference"; +export { ElementReference } from "./components.d"; export { Element } from "@stencil/core"; export { TreeContext, TreeItemContext, TreeModel, UpdateCallback } from "./components/tree/tree-model"; export { TextDecoration, TypographyColors, TypographyFormat } from "./components/typography/typography"; @@ -806,7 +804,7 @@ export namespace Components { /** * Define an anchor element */ - "anchor"?: ElementReference; + "anchor": string | HTMLElement | Promise; /** * Controls if the dropdown will be closed in response to a click event depending on the position of the event relative to the dropdown. If the dropdown is a child of another one, it will be closed with the parent, regardless of its own close behavior. */ @@ -821,12 +819,12 @@ export namespace Components { /** * Move dropdown along main axis of alignment */ - "offset"?: { + "offset": { mainAxis?: number; crossAxis?: number; alignmentAxis?: number; }; - "overwriteDropdownStyle"?: (delegate: { + "overwriteDropdownStyle": (delegate: { dropdownRef: HTMLElement; triggerRef?: HTMLElement; }) => Promise>; @@ -850,7 +848,7 @@ export namespace Components { /** * Define an element that triggers the dropdown. A trigger can either be a string that will be interpreted as id attribute or a DOM element. */ - "trigger"?: ElementReference; + "trigger": string | HTMLElement | Promise; /** * Update position of dropdown */ @@ -2273,7 +2271,7 @@ export namespace Components { /** * CSS selector for hover trigger element e.g. `for="[data-my-custom-select]"` */ - "for"?: ElementReference1; + "for"?: ElementReference; "hideDelay": number; "hideTooltip": () => Promise; /** @@ -4942,7 +4940,7 @@ declare namespace LocalJSX { /** * Define an anchor element */ - "anchor"?: ElementReference; + "anchor"?: string | HTMLElement | Promise; /** * Controls if the dropdown will be closed in response to a click event depending on the position of the event relative to the dropdown. If the dropdown is a child of another one, it will be closed with the parent, regardless of its own close behavior. */ @@ -4989,7 +4987,7 @@ declare namespace LocalJSX { /** * Define an element that triggers the dropdown. A trigger can either be a string that will be interpreted as id attribute or a DOM element. */ - "trigger"?: ElementReference; + "trigger"?: string | HTMLElement | Promise; } /** * @since 1.3.0 @@ -6530,7 +6528,7 @@ declare namespace LocalJSX { /** * CSS selector for hover trigger element e.g. `for="[data-my-custom-select]"` */ - "for"?: ElementReference1; + "for"?: ElementReference; "hideDelay"?: number; /** * Define if the user can access the tooltip via mouse. diff --git a/packages/core/src/components/dropdown/dropdown.tsx b/packages/core/src/components/dropdown/dropdown.tsx index 67acbb26955..07b88fff61e 100644 --- a/packages/core/src/components/dropdown/dropdown.tsx +++ b/packages/core/src/components/dropdown/dropdown.tsx @@ -39,6 +39,7 @@ import { import { AlignedPlacement } from './placement'; import { findElement } from '../utils/find-element'; import { addDisposableEventListener } from '../utils/disposable-event-listener'; +import { ElementReference } from '../utils/element-reference'; let sequenceId = 0; @@ -71,7 +72,7 @@ export class Dropdown implements ComponentInterface, DropdownInterface { /** * Define an anchor element */ - @Prop() anchor: string | HTMLElement; + @Prop() anchor: string | HTMLElement | Promise; /** * Controls if the dropdown will be closed in response to a click event depending on the position of the event relative to the dropdown. @@ -301,9 +302,7 @@ export class Dropdown implements ComponentInterface, DropdownInterface { } } - private async resolveElement( - element: string | HTMLElement | Promise - ) { + private async resolveElement(element: ElementReference) { const el = await findElement(element); return this.checkForSubmenuAnchor(el); From 25333cef8c314cfb59e9ef6cff4db0f0109979b7 Mon Sep 17 00:00:00 2001 From: Lukas Maurer Date: Wed, 20 Nov 2024 09:48:34 +0100 Subject: [PATCH 05/14] refactor(core/dropdown): undo merge changes --- .../core/src/components/dropdown/dropdown.tsx | 111 +++++++++++------- 1 file changed, 71 insertions(+), 40 deletions(-) diff --git a/packages/core/src/components/dropdown/dropdown.tsx b/packages/core/src/components/dropdown/dropdown.tsx index 07b88fff61e..5c8bb66b1b5 100644 --- a/packages/core/src/components/dropdown/dropdown.tsx +++ b/packages/core/src/components/dropdown/dropdown.tsx @@ -38,7 +38,10 @@ import { } from './dropdown-controller'; import { AlignedPlacement } from './placement'; import { findElement } from '../utils/find-element'; -import { addDisposableEventListener } from '../utils/disposable-event-listener'; +import { + addDisposableEventListener, + DisposableEventListener, +} from '../utils/disposable-event-listener'; import { ElementReference } from '../utils/element-reference'; let sequenceId = 0; @@ -67,12 +70,12 @@ export class Dropdown implements ComponentInterface, DropdownInterface { * Define an element that triggers the dropdown. * A trigger can either be a string that will be interpreted as id attribute or a DOM element. */ - @Prop() trigger: string | HTMLElement | Promise; + @Prop() trigger?: ElementReference; /** * Define an anchor element */ - @Prop() anchor: string | HTMLElement | Promise; + @Prop() anchor?: ElementReference; /** * Controls if the dropdown will be closed in response to a click event depending on the position of the event relative to the dropdown. @@ -100,7 +103,7 @@ export class Dropdown implements ComponentInterface, DropdownInterface { * * @internal */ - @Prop() offset: { + @Prop() offset?: { mainAxis?: number; crossAxis?: number; alignmentAxis?: number; @@ -109,7 +112,7 @@ export class Dropdown implements ComponentInterface, DropdownInterface { /** * @internal */ - @Prop() overwriteDropdownStyle: (delegate: { + @Prop() overwriteDropdownStyle?: (delegate: { dropdownRef: HTMLElement; triggerRef?: HTMLElement; }) => Promise>; @@ -127,22 +130,24 @@ export class Dropdown implements ComponentInterface, DropdownInterface { /** * Fire event after visibility of dropdown has changed */ - @Event() showChanged: EventEmitter; + @Event() showChanged!: EventEmitter; - private autoUpdateCleanup: () => void = null; + private autoUpdateCleanup?: () => void; private triggerElement?: Element; private anchorElement?: Element; - private dropdownRef: HTMLElement; + private dropdownRef?: HTMLElement; private localUId = `dropdown-${sequenceId++}`; private assignedSubmenu: string[] = []; - private arrowFocusController: ArrowFocusController; + private arrowFocusController?: ArrowFocusController; private focusDropdownItemBind = this.focusDropdownItem.bind(this); - private itemObserver = new MutationObserver(() => { - this.arrowFocusController.items = this.dropdownItems; + private itemObserver? = new MutationObserver(() => { + if (this.arrowFocusController) { + this.arrowFocusController.items = this.dropdownItems; + } }); connectedCallback(): void { @@ -169,16 +174,29 @@ export class Dropdown implements ComponentInterface, DropdownInterface { dropdownController.dismiss(this); dropdownController.disconnected(this); + if (this.arrowFocusController) { + this.arrowFocusController?.disconnect(); + this.arrowFocusController = undefined; + } + + if (this.itemObserver) { + this.itemObserver.disconnect(); + this.itemObserver = undefined; + } + if (this.disposeClickListener) { this.disposeClickListener(); + this.disposeClickListener = undefined; } if (this.disposeKeyListener) { this.disposeKeyListener(); + this.disposeKeyListener = undefined; } if (this.autoUpdateCleanup) { this.autoUpdateCleanup(); + this.autoUpdateCleanup = undefined; } } @@ -217,11 +235,11 @@ export class Dropdown implements ComponentInterface, DropdownInterface { } get slotElement() { - return this.hostElement.shadowRoot.querySelector('slot'); + return this.hostElement.shadowRoot!.querySelector('slot'); } - private disposeClickListener?: () => void; - private disposeKeyListener?: () => void; + private disposeClickListener?: DisposableEventListener; + private disposeKeyListener?: DisposableEventListener; private addEventListenersFor() { this.disposeClickListener?.(); @@ -237,10 +255,14 @@ export class Dropdown implements ComponentInterface, DropdownInterface { dropdownController.dismissOthers(this.getId()); }; + if (!this.triggerElement) { + return; + } + this.disposeClickListener = addDisposableEventListener( this.triggerElement, 'click', - (event: PointerEvent) => { + (event: Event) => { if (!event.defaultPrevented) { toggleController(); } @@ -274,7 +296,7 @@ export class Dropdown implements ComponentInterface, DropdownInterface { this.disposeKeyListener = addDisposableEventListener( this.triggerElement, 'keydown', - (event: KeyboardEvent) => { + ((event: KeyboardEvent) => { if (event.key !== 'ArrowDown') { return; } @@ -288,13 +310,11 @@ export class Dropdown implements ComponentInterface, DropdownInterface { setTimeout(() => { this.focusDropdownItem(0); }); - } + }) as EventListener ); } - private async registerListener( - element: string | HTMLElement | Promise - ) { + private async registerListener(element: ElementReference) { this.triggerElement = await this.resolveElement(element); if (this.triggerElement) { this.addEventListenersFor(); @@ -310,7 +330,7 @@ export class Dropdown implements ComponentInterface, DropdownInterface { private async checkForSubmenuAnchor(element: Element) { if (!element) { - return null; + return undefined; } if (hasDropdownItemWrapperImplemented(element)) { @@ -366,7 +386,7 @@ export class Dropdown implements ComponentInterface, DropdownInterface { private destroyAutoUpdate() { if (this.autoUpdateCleanup) { this.autoUpdateCleanup(); - this.autoUpdateCleanup = null; + this.autoUpdateCleanup = undefined; } } @@ -398,7 +418,7 @@ export class Dropdown implements ComponentInterface, DropdownInterface { }; if (!this.suppressAutomaticPlacement) { - positionConfig.middleware.push( + positionConfig.middleware?.push( flip({ fallbackStrategy: 'initialPlacement' }) ); } @@ -406,7 +426,7 @@ export class Dropdown implements ComponentInterface, DropdownInterface { positionConfig.placement = isSubmenu ? 'right-start' : this.placement; positionConfig.middleware = [ - ...positionConfig.middleware, + ...(positionConfig.middleware?.filter(Boolean) || []), inline(), shift(), ]; @@ -419,27 +439,29 @@ export class Dropdown implements ComponentInterface, DropdownInterface { this.autoUpdateCleanup = autoUpdate( this.anchorElement, - this.dropdownRef, + this.hostElement, async () => { - const computeResponse = await computePosition( - this.anchorElement, - this.dropdownRef, - positionConfig - ); - Object.assign(this.dropdownRef.style, { - top: '0', - left: '0', - transform: `translate(${Math.round(computeResponse.x)}px,${Math.round( - computeResponse.y - )}px)`, - }); + if (this.anchorElement) { + const computeResponse = await computePosition( + this.anchorElement, + this.hostElement, + positionConfig + ); + Object.assign(this.hostElement.style, { + top: '0', + left: '0', + transform: `translate(${Math.round( + computeResponse.x + )}px,${Math.round(computeResponse.y)}px)`, + }); + } if (this.overwriteDropdownStyle) { const overwriteStyle = await this.overwriteDropdownStyle({ - dropdownRef: this.dropdownRef, + dropdownRef: this.hostElement, triggerRef: this.triggerElement as HTMLElement, }); - Object.assign(this.dropdownRef.style, overwriteStyle); + Object.assign(this.hostElement.style, overwriteStyle); } }, { @@ -452,11 +474,20 @@ export class Dropdown implements ComponentInterface, DropdownInterface { private focusDropdownItem(index: number) { requestAnimationFrame(() => { - this.dropdownItems[index]?.shadowRoot.querySelector('button').focus(); + const button = + this.dropdownItems[index]?.shadowRoot?.querySelector('button'); + + if (button) { + button.focus(); + } }); } async componentDidLoad() { + if (!this.trigger) { + return; + } + this.changedTrigger(this.trigger); } From df9d71f8dd2f4cd0fd905a30018c7b6799dc681f Mon Sep 17 00:00:00 2001 From: Lukas Maurer Date: Wed, 20 Nov 2024 10:10:19 +0100 Subject: [PATCH 06/14] refactor(core/dropdown): undo merge changes --- packages/core/component-doc.json | 51 ++++++++++--------- packages/core/src/components.d.ts | 18 ++++--- .../core/src/components/dropdown/dropdown.tsx | 38 +++++++------- 3 files changed, 58 insertions(+), 49 deletions(-) diff --git a/packages/core/component-doc.json b/packages/core/component-doc.json index 3d82a8b2f56..e1ddb41d10a 100644 --- a/packages/core/component-doc.json +++ b/packages/core/component-doc.json @@ -5220,16 +5220,13 @@ "name": "anchor", "type": "HTMLElement | Promise | string", "complexType": { - "original": "string | HTMLElement | Promise", + "original": "ElementReference", "resolved": "HTMLElement | Promise | string", "references": { - "HTMLElement": { - "location": "global", - "id": "global::HTMLElement" - }, - "Promise": { - "location": "global", - "id": "global::Promise" + "ElementReference": { + "location": "import", + "path": "../utils/element-reference", + "id": "src/components/utils/element-reference.ts::ElementReference" } } }, @@ -5249,7 +5246,7 @@ "type": "string" } ], - "optional": false, + "optional": true, "required": false }, { @@ -5450,16 +5447,13 @@ "name": "trigger", "type": "HTMLElement | Promise | string", "complexType": { - "original": "string | HTMLElement | Promise", + "original": "ElementReference", "resolved": "HTMLElement | Promise | string", "references": { - "HTMLElement": { - "location": "global", - "id": "global::HTMLElement" - }, - "Promise": { - "location": "global", - "id": "global::Promise" + "ElementReference": { + "location": "import", + "path": "../utils/element-reference", + "id": "src/components/utils/element-reference.ts::ElementReference" } } }, @@ -5479,7 +5473,7 @@ "type": "string" } ], - "optional": false, + "optional": true, "required": false } ], @@ -16146,15 +16140,15 @@ "props": [ { "name": "for", - "type": "ElementReference", + "type": "HTMLElement | Promise | string", "complexType": { "original": "ElementReference", - "resolved": "ElementReference", + "resolved": "HTMLElement | Promise | string", "references": { "ElementReference": { "location": "import", "path": "src/components", - "id": "src/components.d.ts::unknown" + "id": "src/components.d.ts::ElementReference" } } }, @@ -16165,7 +16159,13 @@ "docsTags": [], "values": [ { - "type": "ElementReference" + "type": "HTMLElement" + }, + { + "type": "Promise" + }, + { + "type": "string" } ], "optional": true, @@ -17909,6 +17909,11 @@ "docstring": "", "path": "src/components/category-filter/input-state.ts" }, + "src/components/utils/element-reference.ts::ElementReference": { + "declaration": "export type ElementReference = string | HTMLElement | Promise;", + "docstring": "", + "path": "src/components/utils/element-reference.ts" + }, "src/components/flip-tile/flip-tile-state.ts::FlipTileState": { "declaration": "export enum FlipTileState {\n None = 'none',\n Info = 'info',\n Warning = 'warning',\n Alarm = 'alarm',\n Primary = 'primary',\n}", "docstring": "", @@ -17924,7 +17929,7 @@ "docstring": "", "path": "src/components/toast/toast-utils.ts" }, - "src/components.d.ts::unknown": { + "src/components.d.ts::ElementReference": { "declaration": "any", "docstring": "", "path": "src/components.d.ts" diff --git a/packages/core/src/components.d.ts b/packages/core/src/components.d.ts index fa7583d9189..fa4e4958b1f 100644 --- a/packages/core/src/components.d.ts +++ b/packages/core/src/components.d.ts @@ -24,6 +24,7 @@ import { DateTimeCardCorners } from "./components/date-time-card/date-time-card" import { DateChangeEvent } from "./components/date-picker/date-picker"; import { DateTimeCardCorners as DateTimeCardCorners1 } from "./components/date-time-card/date-time-card"; import { DateTimeDateChangeEvent, DateTimeSelectEvent } from "./components/datetime-picker/datetime-picker"; +import { ElementReference } from "./components/utils/element-reference"; import { CloseBehavior } from "./components/dropdown/dropdown-controller"; import { AlignedPlacement, Side } from "./components/dropdown/placement"; import { DropdownButtonVariant } from "./components/dropdown-button/dropdown-button"; @@ -42,7 +43,7 @@ import { TabClickDetail } from "./components/tab-item/tab-item"; import { TimePickerCorners } from "./components/time-picker/time-picker"; import { ToastConfig, ToastType } from "./components/toast/toast-utils"; import { ShowToastResult } from "./components/toast/toast-container"; -import { ElementReference } from "./components.d"; +import { ElementReference as ElementReference1 } from "./components.d"; import { Element } from "@stencil/core"; import { TreeContext, TreeItemContext, TreeModel, UpdateCallback } from "./components/tree/tree-model"; import { TextDecoration, TypographyColors, TypographyFormat } from "./components/typography/typography"; @@ -66,6 +67,7 @@ export { DateTimeCardCorners } from "./components/date-time-card/date-time-card" export { DateChangeEvent } from "./components/date-picker/date-picker"; export { DateTimeCardCorners as DateTimeCardCorners1 } from "./components/date-time-card/date-time-card"; export { DateTimeDateChangeEvent, DateTimeSelectEvent } from "./components/datetime-picker/datetime-picker"; +export { ElementReference } from "./components/utils/element-reference"; export { CloseBehavior } from "./components/dropdown/dropdown-controller"; export { AlignedPlacement, Side } from "./components/dropdown/placement"; export { DropdownButtonVariant } from "./components/dropdown-button/dropdown-button"; @@ -84,7 +86,7 @@ export { TabClickDetail } from "./components/tab-item/tab-item"; export { TimePickerCorners } from "./components/time-picker/time-picker"; export { ToastConfig, ToastType } from "./components/toast/toast-utils"; export { ShowToastResult } from "./components/toast/toast-container"; -export { ElementReference } from "./components.d"; +export { ElementReference as ElementReference1 } from "./components.d"; export { Element } from "@stencil/core"; export { TreeContext, TreeItemContext, TreeModel, UpdateCallback } from "./components/tree/tree-model"; export { TextDecoration, TypographyColors, TypographyFormat } from "./components/typography/typography"; @@ -804,7 +806,7 @@ export namespace Components { /** * Define an anchor element */ - "anchor": string | HTMLElement | Promise; + "anchor"?: ElementReference; /** * Controls if the dropdown will be closed in response to a click event depending on the position of the event relative to the dropdown. If the dropdown is a child of another one, it will be closed with the parent, regardless of its own close behavior. */ @@ -819,12 +821,12 @@ export namespace Components { /** * Move dropdown along main axis of alignment */ - "offset": { + "offset"?: { mainAxis?: number; crossAxis?: number; alignmentAxis?: number; }; - "overwriteDropdownStyle": (delegate: { + "overwriteDropdownStyle"?: (delegate: { dropdownRef: HTMLElement; triggerRef?: HTMLElement; }) => Promise>; @@ -848,7 +850,7 @@ export namespace Components { /** * Define an element that triggers the dropdown. A trigger can either be a string that will be interpreted as id attribute or a DOM element. */ - "trigger": string | HTMLElement | Promise; + "trigger"?: ElementReference; /** * Update position of dropdown */ @@ -4940,7 +4942,7 @@ declare namespace LocalJSX { /** * Define an anchor element */ - "anchor"?: string | HTMLElement | Promise; + "anchor"?: ElementReference; /** * Controls if the dropdown will be closed in response to a click event depending on the position of the event relative to the dropdown. If the dropdown is a child of another one, it will be closed with the parent, regardless of its own close behavior. */ @@ -4987,7 +4989,7 @@ declare namespace LocalJSX { /** * Define an element that triggers the dropdown. A trigger can either be a string that will be interpreted as id attribute or a DOM element. */ - "trigger"?: string | HTMLElement | Promise; + "trigger"?: ElementReference; } /** * @since 1.3.0 diff --git a/packages/core/src/components/dropdown/dropdown.tsx b/packages/core/src/components/dropdown/dropdown.tsx index 5c8bb66b1b5..6fa6880e713 100644 --- a/packages/core/src/components/dropdown/dropdown.tsx +++ b/packages/core/src/components/dropdown/dropdown.tsx @@ -137,12 +137,10 @@ export class Dropdown implements ComponentInterface, DropdownInterface { private triggerElement?: Element; private anchorElement?: Element; - private dropdownRef?: HTMLElement; private localUId = `dropdown-${sequenceId++}`; private assignedSubmenu: string[] = []; private arrowFocusController?: ArrowFocusController; - private focusDropdownItemBind = this.focusDropdownItem.bind(this); private itemObserver? = new MutationObserver(() => { if (this.arrowFocusController) { @@ -328,7 +326,7 @@ export class Dropdown implements ComponentInterface, DropdownInterface { return this.checkForSubmenuAnchor(el); } - private async checkForSubmenuAnchor(element: Element) { + private async checkForSubmenuAnchor(element?: Element) { if (!element) { return undefined; } @@ -347,12 +345,18 @@ export class Dropdown implements ComponentInterface, DropdownInterface { return element; } + private async resolveAnchorElement() { + if (this.anchor) { + this.anchorElement = await this.resolveElement(this.anchor); + } else if (this.trigger) { + this.anchorElement = await this.resolveElement(this.trigger); + } + } + @Watch('show') async changedShow(newShow: boolean) { if (newShow) { - this.anchorElement = await (this.anchor - ? this.resolveElement(this.anchor) - : this.resolveElement(this.trigger)); + await this.resolveAnchorElement(); if (this.anchorElement) { this.applyDropdownPosition(); @@ -360,11 +364,11 @@ export class Dropdown implements ComponentInterface, DropdownInterface { this.arrowFocusController = new ArrowFocusController( this.dropdownItems, - this.dropdownRef, - this.focusDropdownItemBind + this.hostElement, + (index) => this.focusDropdownItem(index) ); - this.itemObserver.observe(this.dropdownRef, { + this.itemObserver?.observe(this.hostElement, { childList: true, subtree: true, }); @@ -373,13 +377,13 @@ export class Dropdown implements ComponentInterface, DropdownInterface { } else { this.destroyAutoUpdate(); this.arrowFocusController?.disconnect(); - this.itemObserver.disconnect(); + this.itemObserver?.disconnect(); this.disposeKeyListener?.(); } } @Watch('trigger') - changedTrigger(newTriggerValue: string | HTMLElement | Promise) { + changedTrigger(newTriggerValue: ElementReference) { this.registerListener(newTriggerValue); } @@ -407,9 +411,6 @@ export class Dropdown implements ComponentInterface, DropdownInterface { if (!this.anchorElement) { return; } - if (!this.dropdownRef) { - return; - } const isSubmenu = this.isAnchorSubmenu(); let positionConfig: Partial = { @@ -437,6 +438,10 @@ export class Dropdown implements ComponentInterface, DropdownInterface { this.destroyAutoUpdate(); + if (!this.anchorElement) { + return; + } + this.autoUpdateCleanup = autoUpdate( this.anchorElement, this.hostElement, @@ -493,9 +498,7 @@ export class Dropdown implements ComponentInterface, DropdownInterface { async componentDidRender() { await this.applyDropdownPosition(); - this.anchorElement = await (this.anchor - ? this.resolveElement(this.anchor) - : this.resolveElement(this.trigger)); + await this.resolveAnchorElement(); } private isTriggerElement(element: HTMLElement) { @@ -542,7 +545,6 @@ export class Dropdown implements ComponentInterface, DropdownInterface { return ( (this.dropdownRef = ref)} class={{ 'dropdown-menu': true, show: this.show, From deb3f8bc050883440dbb5c33a0ba39dd396684d6 Mon Sep 17 00:00:00 2001 From: Lukas Maurer Date: Wed, 20 Nov 2024 17:10:09 +0100 Subject: [PATCH 07/14] test(core/tooltip): add test for regular shadow dom --- .../src/components/tooltip/test/tooltip.ct.ts | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/packages/core/src/components/tooltip/test/tooltip.ct.ts b/packages/core/src/components/tooltip/test/tooltip.ct.ts index 986455188f7..09f97a952dd 100644 --- a/packages/core/src/components/tooltip/test/tooltip.ct.ts +++ b/packages/core/src/components/tooltip/test/tooltip.ct.ts @@ -24,6 +24,36 @@ test('renders', async ({ mount, page }) => { }); test('renders in shadow DOM', async ({ mount, page }) => { + await mount(``); + + await page.evaluate(() => { + customElements.define('test-component', class extends HTMLElement {}); + const testComponent = document.createElement('test-component'); + testComponent.attachShadow({ mode: 'open' }); + + const tooltip = document.createElement('ix-tooltip'); + tooltip.innerHTML = 'tooltip'; + tooltip.for = '.test'; + + const button = document.createElement('ix-button'); + button.innerHTML = 'button'; + button.classList.add('test'); + + document.querySelector('#mount').appendChild(testComponent); + testComponent.shadowRoot.appendChild(button); + testComponent.shadowRoot.appendChild(tooltip); + }); + + const tooltip = page.locator('ix-tooltip'); + const button = page.locator('ix-button'); + + await button.hover(); + + await expect(tooltip).toHaveClass(/hydrated/); + await expect(tooltip).toBeVisible(); +}); + +test('renders in slot', async ({ mount, page }) => { await mount(` tooltip From b0eaa04efc1117d20679bb5f41c20e0d0e7c2a53 Mon Sep 17 00:00:00 2001 From: Lukas Maurer Date: Thu, 21 Nov 2024 10:51:02 +0100 Subject: [PATCH 08/14] Create eighty-kangaroos-judge.md --- .changeset/eighty-kangaroos-judge.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/eighty-kangaroos-judge.md diff --git a/.changeset/eighty-kangaroos-judge.md b/.changeset/eighty-kangaroos-judge.md new file mode 100644 index 00000000000..aeb22407767 --- /dev/null +++ b/.changeset/eighty-kangaroos-judge.md @@ -0,0 +1,5 @@ +--- +"@siemens/ix": patch +--- + +Enable discovery of trigger elements if in same shadow DOM for ix-tooltip and ix-dropdown. From bf6abf7ce8400af8def33fdd9ff454b17a2ad3c4 Mon Sep 17 00:00:00 2001 From: Lukas Maurer Date: Thu, 21 Nov 2024 10:49:01 +0100 Subject: [PATCH 09/14] test(core/tooltop): strict mode --- packages/core/src/components/tooltip/test/tooltip.ct.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/core/src/components/tooltip/test/tooltip.ct.ts b/packages/core/src/components/tooltip/test/tooltip.ct.ts index 09f97a952dd..c8be8bdb6c5 100644 --- a/packages/core/src/components/tooltip/test/tooltip.ct.ts +++ b/packages/core/src/components/tooltip/test/tooltip.ct.ts @@ -39,9 +39,9 @@ test('renders in shadow DOM', async ({ mount, page }) => { button.innerHTML = 'button'; button.classList.add('test'); - document.querySelector('#mount').appendChild(testComponent); - testComponent.shadowRoot.appendChild(button); - testComponent.shadowRoot.appendChild(tooltip); + document.querySelector('#mount')!.appendChild(testComponent); + testComponent.shadowRoot?.appendChild(button); + testComponent.shadowRoot?.appendChild(tooltip); }); const tooltip = page.locator('ix-tooltip'); From 1ffef3a6193f133b9b8a593a4a0ff473af2dc2a0 Mon Sep 17 00:00:00 2001 From: Lukas Maurer Date: Thu, 21 Nov 2024 12:01:58 +0100 Subject: [PATCH 10/14] refactor(core): prevent unneccesary dom query --- packages/core/src/components/utils/find-element.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/core/src/components/utils/find-element.ts b/packages/core/src/components/utils/find-element.ts index 692e31422b6..23d32ec6f48 100644 --- a/packages/core/src/components/utils/find-element.ts +++ b/packages/core/src/components/utils/find-element.ts @@ -57,8 +57,12 @@ function getSlottedElements(shadowRoot: ShadowRoot, selector: string) { assignedElements.forEach((element) => { if (element.matches(selector)) { elementsInSlot.push(element as HTMLElement); - } else if (element.querySelector(selector)) { - elementsInSlot.push(element.querySelector(selector) as HTMLElement); + } else { + const elementInSlot = element.querySelector(selector); + + if (elementInSlot) { + elementsInSlot.push(elementInSlot as HTMLElement); + } } }); }); From c859c619447e194ea93cceb89e40cab56731b951 Mon Sep 17 00:00:00 2001 From: Lukas Maurer Date: Thu, 21 Nov 2024 12:07:26 +0100 Subject: [PATCH 11/14] Update packages/core/src/components/utils/find-element.ts Co-authored-by: Julian Lamplmair <151610373+jul-lam@users.noreply.github.com> --- packages/core/src/components/utils/find-element.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/core/src/components/utils/find-element.ts b/packages/core/src/components/utils/find-element.ts index 23d32ec6f48..abf2862d008 100644 --- a/packages/core/src/components/utils/find-element.ts +++ b/packages/core/src/components/utils/find-element.ts @@ -45,7 +45,11 @@ export async function resolveSelector( shadowRoot.querySelectorAll(selector) ); - return Promise.resolve(elementsInShadowRoot); + if (elementsInShadowRoot.length > 0) { + return Promise.resolve(elementsInShadowRoot); + } + + return Promise.resolve(undefined); } function getSlottedElements(shadowRoot: ShadowRoot, selector: string) { From 40258684bf6349f7d641eaf2ff13abfa2635fa83 Mon Sep 17 00:00:00 2001 From: Lukas Maurer Date: Thu, 21 Nov 2024 12:16:00 +0100 Subject: [PATCH 12/14] refactor(core): remove blank --- packages/core/src/components/utils/find-element.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/components/utils/find-element.ts b/packages/core/src/components/utils/find-element.ts index abf2862d008..26a2c7c7360 100644 --- a/packages/core/src/components/utils/find-element.ts +++ b/packages/core/src/components/utils/find-element.ts @@ -48,7 +48,7 @@ export async function resolveSelector( if (elementsInShadowRoot.length > 0) { return Promise.resolve(elementsInShadowRoot); } - + return Promise.resolve(undefined); } From b3664b1698955d015bca031f7b9a6224ddcc192c Mon Sep 17 00:00:00 2001 From: matthiashader <144090716+matthiashader@users.noreply.github.com> Date: Thu, 21 Nov 2024 12:50:30 +0100 Subject: [PATCH 13/14] fix: sonar lint --- packages/core/src/components/tooltip/test/tooltip.ct.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/components/tooltip/test/tooltip.ct.ts b/packages/core/src/components/tooltip/test/tooltip.ct.ts index c8be8bdb6c5..ae3d152d5d3 100644 --- a/packages/core/src/components/tooltip/test/tooltip.ct.ts +++ b/packages/core/src/components/tooltip/test/tooltip.ct.ts @@ -39,7 +39,7 @@ test('renders in shadow DOM', async ({ mount, page }) => { button.innerHTML = 'button'; button.classList.add('test'); - document.querySelector('#mount')!.appendChild(testComponent); + document.querySelector('#mount')?.appendChild(testComponent); testComponent.shadowRoot?.appendChild(button); testComponent.shadowRoot?.appendChild(tooltip); }); From e2e02aa5f92a690b38849e8e1937ebb4f3151380 Mon Sep 17 00:00:00 2001 From: Daniel Leroux Date: Mon, 25 Nov 2024 10:55:56 +0100 Subject: [PATCH 14/14] refactor: search on host component --- .../core/src/components/utils/find-element.ts | 49 ++++++------------- 1 file changed, 15 insertions(+), 34 deletions(-) diff --git a/packages/core/src/components/utils/find-element.ts b/packages/core/src/components/utils/find-element.ts index 26a2c7c7360..54625a72194 100644 --- a/packages/core/src/components/utils/find-element.ts +++ b/packages/core/src/components/utils/find-element.ts @@ -11,18 +11,18 @@ * Will try to resolve the selector in the light dom, shadow dom or slot * @param selector The selector to resolve * @param hostElement The element to start the search from - * @returns Promise with the resolved elements + * @returns Promise with the resolved elements or undefined if not found */ export async function resolveSelector( selector: string, hostElement?: HTMLElement ): Promise { - const elementsInLightDom: HTMLElement[] = Array.from( + const elements: HTMLElement[] = Array.from( document.querySelectorAll(selector) ); - if (elementsInLightDom.length > 0) { - return Promise.resolve(elementsInLightDom); + if (elements.length > 0) { + return Promise.resolve(elements); } if (hostElement === undefined) { @@ -35,43 +35,24 @@ export async function resolveSelector( return Promise.resolve(undefined); } - let elementsInSlot: HTMLElement[] = getSlottedElements(shadowRoot, selector); - - if (elementsInSlot.length > 0) { - return Promise.resolve(elementsInSlot); - } - const elementsInShadowRoot: HTMLElement[] = Array.from( shadowRoot.querySelectorAll(selector) ); - if (elementsInShadowRoot.length > 0) { - return Promise.resolve(elementsInShadowRoot); - } - - return Promise.resolve(undefined); -} - -function getSlottedElements(shadowRoot: ShadowRoot, selector: string) { - const slots = shadowRoot.querySelectorAll('slot'); - let elementsInSlot: HTMLElement[] = []; + const elementsInHost: HTMLElement[] = Array.from( + shadowRoot.host.querySelectorAll(selector) + ); - slots.forEach((slot) => { - const assignedElements = slot.assignedElements({ flatten: true }); - assignedElements.forEach((element) => { - if (element.matches(selector)) { - elementsInSlot.push(element as HTMLElement); - } else { - const elementInSlot = element.querySelector(selector); + const elementsInComponent: HTMLElement[] = [ + ...elementsInHost, + ...elementsInShadowRoot, + ]; - if (elementInSlot) { - elementsInSlot.push(elementInSlot as HTMLElement); - } - } - }); - }); + if (elementsInComponent.length > 0) { + return Promise.resolve(elementsInComponent); + } - return elementsInSlot; + return Promise.resolve(undefined); } /**