Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโ€™ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(core/tooltip|dropdown): find trigger if in same shadow DOM #1560

Merged
merged 19 commits into from
Nov 25, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
f00da48
fix(core/tooltip|dropdown): find trigger if in same shadow DOM
nuke-ellington Nov 12, 2024
0b57c26
refactor(core/dropdown): use util fn
nuke-ellington Nov 12, 2024
f5d4e83
fix(core/tooltip|dropdown): pass host element to discovery logic
nuke-ellington Nov 19, 2024
c40be99
Merge branch 'main' into fix/1885-tooltip-shadow-dom
nuke-ellington Nov 19, 2024
920236d
refactor(core/dropdown): undo merge changes
nuke-ellington Nov 20, 2024
25333ce
refactor(core/dropdown): undo merge changes
nuke-ellington Nov 20, 2024
df9d71f
refactor(core/dropdown): undo merge changes
nuke-ellington Nov 20, 2024
deb3f8b
test(core/tooltip): add test for regular shadow dom
nuke-ellington Nov 20, 2024
3a0aa01
Merge branch 'main' into fix/1885-tooltip-shadow-dom
jul-lam Nov 21, 2024
b0eaa04
Create eighty-kangaroos-judge.md
nuke-ellington Nov 21, 2024
bf6abf7
test(core/tooltop): strict mode
nuke-ellington Nov 21, 2024
1ffef3a
refactor(core): prevent unneccesary dom query
nuke-ellington Nov 21, 2024
c859c61
Update packages/core/src/components/utils/find-element.ts
nuke-ellington Nov 21, 2024
4025868
refactor(core): remove blank
nuke-ellington Nov 21, 2024
b3664b1
fix: sonar lint
matthiashader Nov 21, 2024
218bbf2
Merge branch 'main' into fix/1885-tooltip-shadow-dom
matthiashader Nov 21, 2024
23ab030
Merge remote-tracking branch 'origin/main' into fix/1885-tooltip-shadโ€ฆ
danielleroux Nov 25, 2024
e2e02aa
refactor: search on host component
danielleroux Nov 25, 2024
b987358
Merge branch 'main' into fix/1885-tooltip-shadow-dom
jul-lam Nov 25, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions packages/core/component-doc.json
Original file line number Diff line number Diff line change
Expand Up @@ -5225,7 +5225,7 @@
"references": {
"ElementReference": {
"location": "import",
"path": "src/components/utils/element-reference",
"path": "../utils/element-reference",
"id": "src/components/utils/element-reference.ts::ElementReference"
}
}
Expand Down Expand Up @@ -5452,7 +5452,7 @@
"references": {
"ElementReference": {
"location": "import",
"path": "src/components/utils/element-reference",
"path": "../utils/element-reference",
"id": "src/components/utils/element-reference.ts::ElementReference"
}
}
Expand Down Expand Up @@ -16147,8 +16147,8 @@
"references": {
"ElementReference": {
"location": "import",
"path": "../utils/element-reference",
"id": "src/components/utils/element-reference.ts::ElementReference"
"path": "src/components",
"id": "src/components.d.ts::ElementReference"
}
}
},
Expand Down Expand Up @@ -17929,6 +17929,11 @@
"docstring": "",
"path": "src/components/toast/toast-utils.ts"
},
"src/components.d.ts::ElementReference": {
"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": "",
Expand Down
8 changes: 4 additions & 4 deletions packages/core/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,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 as ElementReference1 } from "./components/utils/element-reference";
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";
Expand Down Expand Up @@ -86,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 as ElementReference1 } from "./components/utils/element-reference";
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";
Expand Down Expand Up @@ -2273,7 +2273,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<void>;
/**
Expand Down Expand Up @@ -6530,7 +6530,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.
Expand Down
40 changes: 3 additions & 37 deletions packages/core/src/components/dropdown/dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,12 @@ import {
hasDropdownItemWrapperImplemented,
} from './dropdown-controller';
import { AlignedPlacement } from './placement';
import { findElement } from '../utils/find-element';
import {
addDisposableEventListener,
DisposableEventListener,
} from '../utils/disposable-event-listener';
import { ElementReference } from 'src/components/utils/element-reference';
import { ElementReference } from '../utils/element-reference';

let sequenceId = 0;

Expand Down Expand Up @@ -320,7 +321,7 @@ export class Dropdown implements ComponentInterface, DropdownInterface {
}

private async resolveElement(element: ElementReference) {
const el = await this.findElement(element);
const el = await findElement(element);

return this.checkForSubmenuAnchor(el);
}
Expand All @@ -344,41 +345,6 @@ export class Dropdown implements ComponentInterface, DropdownInterface {
return element;
}

private findElement(element: ElementReference): Promise<Element | undefined> {
if (element instanceof Promise) {
return element;
}

if (typeof element === 'object') {
return Promise.resolve(element);
}

if (typeof element != 'string') {
return Promise.resolve(undefined);
}

const selector = `#${element}`;
return new Promise((resolve) => {
const el = document.querySelector(selector);
if (el !== null) {
return resolve(el);
}

const observer = new MutationObserver(() => {
const el = document.querySelector(selector);
if (el) {
resolve(el);
observer.disconnect();
}
});

observer.observe(document.body, {
childList: true,
subtree: true,
});
});
}

private async resolveAnchorElement() {
if (this.anchor) {
this.anchorElement = await this.resolveElement(this.anchor);
Expand Down
47 changes: 47 additions & 0 deletions packages/core/src/components/tooltip/test/tooltip.ct.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,53 @@ test('renders', async ({ mount, page }) => {
await expect(tooltip).toBeVisible();
});

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(`
<ix-blind>
<ix-tooltip for=".test">tooltip</ix-tooltip>
<ix-button class="test">button</ix-button>
</ix-blind>
`);

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(`
Expand Down
5 changes: 3 additions & 2 deletions packages/core/src/components/tooltip/tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ import {
import { OnListener } from '../utils/listener';
import { tooltipController } from './tooltip-controller';
import { IxOverlayComponent } from '../utils/overlay';
import { ElementReference } from '../utils/element-reference';
import { resolveSelector } from '../utils/find-element';
import { ElementReference } from 'src/components';

type ArrowPosition = {
top?: string;
Expand Down Expand Up @@ -255,7 +256,7 @@ export class Tooltip implements IxOverlayComponent {

private async queryAnchorElements(): Promise<Array<HTMLElement> | undefined> {
if (typeof this.for === 'string') {
return Promise.resolve(Array.from(document.querySelectorAll(this.for)));
return resolveSelector(this.for, this.hostElement);
}

if (this.for instanceof HTMLElement) {
Expand Down
147 changes: 147 additions & 0 deletions packages/core/src/components/utils/find-element.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
/*
* 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
* @param hostElement The element to start the search from
* @returns Promise with the resolved elements
*/
export async function resolveSelector(
selector: string,
hostElement?: HTMLElement
): Promise<HTMLElement[] | undefined> {
const elementsInLightDom: HTMLElement[] = Array.from(
document.querySelectorAll(selector)
);

if (elementsInLightDom.length > 0) {
return Promise.resolve(elementsInLightDom);
}

if (hostElement === undefined) {
return Promise.resolve(undefined);
}

const shadowRoot = getRootFor(hostElement);

if (shadowRoot === undefined || !(shadowRoot instanceof ShadowRoot)) {
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)
);

return Promise.resolve(elementsInShadowRoot);
nuke-ellington marked this conversation as resolved.
Show resolved Hide resolved
}

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;
}

/**
* 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 && !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;
}

return parent;
}

export function waitForSelector(
selector: string,
node = document,
hostElement?: HTMLElement
): Promise<Element> {
return new Promise((resolve) => {
const waitForElements = () => {
resolveSelector(selector, hostElement).then((elements) => {
if (elements && elements.length > 0) {
resolve(elements[0]);
observer?.disconnect();
}
});
};

waitForElements();

const observer = new MutationObserver(() => {
waitForElements();
});

observer.observe(node.body, {
childList: true,
subtree: true,
});
});
}

/**
* 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<HTMLElement>,
hostElement?: HTMLElement
): Promise<Element> {
if (element instanceof Promise) {
return element;
}

if (typeof element === 'object') {
return Promise.resolve(element);
}

const selector = `#${element}`;
return waitForSelector(selector, document, hostElement);
}
Loading