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
Changes from all 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
5 changes: 5 additions & 0 deletions .changeset/eighty-kangaroos-judge.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@siemens/ix": patch
---

Enable discovery of trigger elements if in same shadow DOM for ix-tooltip and ix-dropdown.
13 changes: 9 additions & 4 deletions packages/core/component-doc.json
Original file line number Diff line number Diff line change
@@ -6453,7 +6453,7 @@
"references": {
"ElementReference": {
"location": "import",
"path": "src/components/utils/element-reference",
"path": "../utils/element-reference",
"id": "src/components/utils/element-reference.ts::ElementReference"
}
}
@@ -6680,7 +6680,7 @@
"references": {
"ElementReference": {
"location": "import",
"path": "src/components/utils/element-reference",
"path": "../utils/element-reference",
"id": "src/components/utils/element-reference.ts::ElementReference"
}
}
@@ -20120,8 +20120,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"
}
}
},
@@ -21945,6 +21945,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": "",
8 changes: 4 additions & 4 deletions packages/core/src/components.d.ts
Original file line number Diff line number Diff line change
@@ -46,7 +46,7 @@ import { TextareaResizeBehavior } from "./components/input/textarea";
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";
@@ -92,7 +92,7 @@ export { TextareaResizeBehavior } from "./components/input/textarea";
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";
@@ -2998,7 +2998,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>;
/**
@@ -8225,7 +8225,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.
40 changes: 3 additions & 37 deletions packages/core/src/components/dropdown/dropdown.tsx
Original file line number Diff line number Diff line change
@@ -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;

@@ -322,7 +323,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);
}
@@ -346,41 +347,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);
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
@@ -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(`
5 changes: 3 additions & 2 deletions packages/core/src/components/tooltip/tooltip.tsx
Original file line number Diff line number Diff line change
@@ -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;
@@ -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) {
136 changes: 136 additions & 0 deletions packages/core/src/components/utils/find-element.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/*
* 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 or undefined if not found
*/
export async function resolveSelector(
selector: string,
hostElement?: HTMLElement
): Promise<HTMLElement[] | undefined> {
const elements: HTMLElement[] = Array.from(
document.querySelectorAll(selector)
);

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

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

const shadowRoot = getRootFor(hostElement);

if (shadowRoot === undefined || !(shadowRoot instanceof ShadowRoot)) {
return Promise.resolve(undefined);
}

const elementsInShadowRoot: HTMLElement[] = Array.from(
shadowRoot.querySelectorAll(selector)
);

const elementsInHost: HTMLElement[] = Array.from(
shadowRoot.host.querySelectorAll(selector)
);

const elementsInComponent: HTMLElement[] = [
...elementsInHost,
...elementsInShadowRoot,
];

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

return Promise.resolve(undefined);
}

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

Unchanged files with check annotations Beta

});
}
async function fallbackRemoveViewFromRootDom(view: any) {

Check warning on line 38 in packages/react/src/delegate.ts

GitHub Actions / build

Unexpected any. Specify a different type
const parent = view.parentElement;
const id = parent.id;
if (id in mountedRootNodes) {
}
export class ReactFrameworkDelegate implements FrameworkDelegate {
attachViewToPortal?: (id: string, view: any) => Promise<Element>;

Check warning on line 49 in packages/react/src/delegate.ts

GitHub Actions / build

Unexpected any. Specify a different type
removeViewFromPortal?: (id: string) => void;
resolvePortalInitPromise: (() => void) | undefined;
);
}
async attachView(view: any): Promise<any> {

Check warning on line 62 in packages/react/src/delegate.ts

GitHub Actions / build

Unexpected any. Specify a different type

Check warning on line 62 in packages/react/src/delegate.ts

GitHub Actions / build

Unexpected any. Specify a different type
const id = createViewInstance();
if (!this.isUsingReactPortal) {
console.error('Portal could not be initialized');
}
async removeView(view: any): Promise<void> {

Check warning on line 78 in packages/react/src/delegate.ts

GitHub Actions / build

Unexpected any. Specify a different type
if (!this.removeViewFromPortal) {
return fallbackRemoveViewFromRootDom(view);
}
import { IxModal } from '../components';
export interface ModalRef {
close: <T = any>(result: T) => void;

Check warning on line 13 in packages/react/src/modal/modal.tsx

GitHub Actions / build

Unexpected any. Specify a different type
dismiss: <T = any>(result?: T) => void;

Check warning on line 14 in packages/react/src/modal/modal.tsx

GitHub Actions / build

Unexpected any. Specify a different type
modalElement: HTMLIxModalElement | null;
}
Record<string, (value: Element | PromiseLike<Element>) => void>
>({});
const viewRefs = useRef<Record<string, any>>({});

Check warning on line 23 in packages/react/src/modal/portal.tsx

GitHub Actions / build

Unexpected any. Specify a different type
const [views, setViews] = useState<Record<string, any>>({});

Check warning on line 24 in packages/react/src/modal/portal.tsx

GitHub Actions / build

Unexpected any. Specify a different type
useEffect(() => {
const addOverlay = (id: string, view: any) => {

Check warning on line 27 in packages/react/src/modal/portal.tsx

GitHub Actions / build

Unexpected any. Specify a different type
const _views = { ...viewRefs.current };
_views[id] = view;
setViews(_views);