Skip to content

Commit 76c09d0

Browse files
authored
fix(overlay): follow the pointer inside a shadow root while picking (#306)
Moving between elements in one shadow root fires no mouseover at the document, so the pick highlight stayed on the first component. The picker now also listens for pointermove and redraws only when the component under the pointer changes.
1 parent 71450fa commit 76c09d0

2 files changed

Lines changed: 44 additions & 2 deletions

File tree

‎packages/devtools/src/__tests__/component-pick.test.ts‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,37 @@ describe('startComponentPick', () => {
110110
expect(result.ok && elementById(result.id)).toBe(card);
111111
});
112112

113+
it('follows the pointer between components inside one shadow root', async () => {
114+
document.body.innerHTML = `<app-root ng-version="22.0.0"><app-shell></app-shell></app-root>`;
115+
const root = document.querySelector('app-root')!;
116+
const shell = document.querySelector('app-shell')!;
117+
shell.attachShadow({ mode: 'open' }).innerHTML =
118+
'<app-trip-card><button>Book</button></app-trip-card><app-map><b>Map</b></app-map>';
119+
const card = shell.shadowRoot!.querySelector('app-trip-card')!;
120+
const map = shell.shadowRoot!.querySelector('app-map')!;
121+
const instances = new Map<Element, object>([
122+
[root, {}],
123+
[shell, {}],
124+
[card, new _TripCard()],
125+
[map, {}],
126+
]);
127+
const ng: ComponentDebugNg = { getComponent: (el) => instances.get(el) ?? null };
128+
const highlight = { show: vi.fn(), clear: vi.fn() };
129+
const pick = startComponentPick({ getNg: () => ng, highlight, timeoutMs: 1000 });
130+
const move = (el: Element) =>
131+
el.dispatchEvent(new MouseEvent('pointermove', { bubbles: true, composed: true }));
132+
133+
move(card.querySelector('button')!);
134+
expect(highlight.show).toHaveBeenLastCalledWith(card);
135+
move(map.querySelector('b')!);
136+
expect(highlight.show).toHaveBeenLastCalledWith(map);
137+
// Moving within the same component does not redraw the box.
138+
move(map.querySelector('b')!);
139+
expect(highlight.show).toHaveBeenCalledTimes(2);
140+
pick.cancel();
141+
await pick.result;
142+
});
143+
113144
it('ends on Escape, on cancel and after the timeout', async () => {
114145
const first = setup();
115146
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));

‎packages/devtools/src/component-pick.ts‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,22 @@ export function startComponentPick(options: PickOptions): {
3838

3939
let finish: (result: ComponentPickResult) => void = () => {};
4040
const result = new Promise<ComponentPickResult>((resolve) => {
41+
// Moving between elements inside one shadow root fires no mouseover at the
42+
// document, since both ends retarget to the host, so pointermove also updates
43+
// the box. It is only redrawn when the component under the pointer changes.
44+
let shown: Element | null | undefined;
4145
const onHover = (event: Event) => {
4246
const host = hostOf(event);
43-
if (host instanceof HTMLElement) options.highlight.show(host);
47+
const next = host instanceof HTMLElement ? host : null;
48+
if (next === shown) return;
49+
shown = next;
50+
if (next) options.highlight.show(next);
4451
else options.highlight.clear();
4552
};
4653
const onLeave = (event: MouseEvent) => {
47-
if (!event.relatedTarget) options.highlight.clear();
54+
if (event.relatedTarget) return;
55+
shown = undefined;
56+
options.highlight.clear();
4857
};
4958
const onClick = (event: Event) => {
5059
if (ownElement(event.target)) return;
@@ -77,6 +86,7 @@ export function startComponentPick(options: PickOptions): {
7786
finish = () => {};
7887
doc.removeEventListener('click', onClick, true);
7988
doc.removeEventListener('mouseover', onHover, true);
89+
doc.removeEventListener('pointermove', onHover, true);
8090
doc.removeEventListener('mouseout', onLeave, true);
8191
doc.removeEventListener('keydown', onKey, true);
8292
clearTimeout(timer);
@@ -87,6 +97,7 @@ export function startComponentPick(options: PickOptions): {
8797
popup?.setAttribute('data-picking', '');
8898
doc.addEventListener('click', onClick, true);
8999
doc.addEventListener('mouseover', onHover, true);
100+
doc.addEventListener('pointermove', onHover, true);
90101
doc.addEventListener('mouseout', onLeave, true);
91102
doc.addEventListener('keydown', onKey, true);
92103
});

0 commit comments

Comments
 (0)