diff --git a/packages/devtools/src/__tests__/component-pick.test.ts b/packages/devtools/src/__tests__/component-pick.test.ts
index 1818b359..8c370a3d 100644
--- a/packages/devtools/src/__tests__/component-pick.test.ts
+++ b/packages/devtools/src/__tests__/component-pick.test.ts
@@ -110,6 +110,37 @@ describe('startComponentPick', () => {
expect(result.ok && elementById(result.id)).toBe(card);
});
+ it('follows the pointer between components inside one shadow root', async () => {
+ document.body.innerHTML = ``;
+ const root = document.querySelector('app-root')!;
+ const shell = document.querySelector('app-shell')!;
+ shell.attachShadow({ mode: 'open' }).innerHTML =
+ 'Map';
+ const card = shell.shadowRoot!.querySelector('app-trip-card')!;
+ const map = shell.shadowRoot!.querySelector('app-map')!;
+ const instances = new Map([
+ [root, {}],
+ [shell, {}],
+ [card, new _TripCard()],
+ [map, {}],
+ ]);
+ const ng: ComponentDebugNg = { getComponent: (el) => instances.get(el) ?? null };
+ const highlight = { show: vi.fn(), clear: vi.fn() };
+ const pick = startComponentPick({ getNg: () => ng, highlight, timeoutMs: 1000 });
+ const move = (el: Element) =>
+ el.dispatchEvent(new MouseEvent('pointermove', { bubbles: true, composed: true }));
+
+ move(card.querySelector('button')!);
+ expect(highlight.show).toHaveBeenLastCalledWith(card);
+ move(map.querySelector('b')!);
+ expect(highlight.show).toHaveBeenLastCalledWith(map);
+ // Moving within the same component does not redraw the box.
+ move(map.querySelector('b')!);
+ expect(highlight.show).toHaveBeenCalledTimes(2);
+ pick.cancel();
+ await pick.result;
+ });
+
it('ends on Escape, on cancel and after the timeout', async () => {
const first = setup();
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
diff --git a/packages/devtools/src/component-pick.ts b/packages/devtools/src/component-pick.ts
index 164a6a47..bf9f7727 100644
--- a/packages/devtools/src/component-pick.ts
+++ b/packages/devtools/src/component-pick.ts
@@ -38,13 +38,22 @@ export function startComponentPick(options: PickOptions): {
let finish: (result: ComponentPickResult) => void = () => {};
const result = new Promise((resolve) => {
+ // Moving between elements inside one shadow root fires no mouseover at the
+ // document, since both ends retarget to the host, so pointermove also updates
+ // the box. It is only redrawn when the component under the pointer changes.
+ let shown: Element | null | undefined;
const onHover = (event: Event) => {
const host = hostOf(event);
- if (host instanceof HTMLElement) options.highlight.show(host);
+ const next = host instanceof HTMLElement ? host : null;
+ if (next === shown) return;
+ shown = next;
+ if (next) options.highlight.show(next);
else options.highlight.clear();
};
const onLeave = (event: MouseEvent) => {
- if (!event.relatedTarget) options.highlight.clear();
+ if (event.relatedTarget) return;
+ shown = undefined;
+ options.highlight.clear();
};
const onClick = (event: Event) => {
if (ownElement(event.target)) return;
@@ -77,6 +86,7 @@ export function startComponentPick(options: PickOptions): {
finish = () => {};
doc.removeEventListener('click', onClick, true);
doc.removeEventListener('mouseover', onHover, true);
+ doc.removeEventListener('pointermove', onHover, true);
doc.removeEventListener('mouseout', onLeave, true);
doc.removeEventListener('keydown', onKey, true);
clearTimeout(timer);
@@ -87,6 +97,7 @@ export function startComponentPick(options: PickOptions): {
popup?.setAttribute('data-picking', '');
doc.addEventListener('click', onClick, true);
doc.addEventListener('mouseover', onHover, true);
+ doc.addEventListener('pointermove', onHover, true);
doc.addEventListener('mouseout', onLeave, true);
doc.addEventListener('keydown', onKey, true);
});