fix: address device icon review findings

Issue: #212
User-Visible: yes
This commit is contained in:
Sergey Matyunin
2026-08-20 09:14:27 +03:00
parent b7bfc92d4e
commit c5ba699cf1
12 changed files with 140 additions and 106 deletions
File diff suppressed because one or more lines are too long
+6
View File
@@ -37,6 +37,11 @@ const res = await page.evaluate(async () => {
await c.updateComplete;
const preview = sr().querySelector('hp-device-preview');
await preview?.updateComplete;
preview?.dispatchEvent(new PointerEvent('pointerover', {
pointerType: 'mouse', bubbles: true, composed: true,
}));
const previewHoverGatePropagates = c.hasAttribute('data-pointer-hover')
&& preview?.hasAttribute('data-pointer-hover');
const previewFace = face(preview?.renderRoot?.querySelector('.dev'));
const providerShown = /demo/i.test(preview?.renderRoot?.querySelector('.previewfacts')?.textContent || '');
@@ -168,6 +173,7 @@ const res = await page.evaluate(async () => {
return {
allFacesPresent: !!planFace && !!previewFace && !!staticFace,
previewHoverGatePropagates,
planPreviewEqual: JSON.stringify(planFace) === JSON.stringify(previewFace),
planStaticEqual: JSON.stringify(planFace) === JSON.stringify(staticFace),
providerShown,
+2 -2
View File
@@ -239,7 +239,7 @@ const res = await page.evaluate(async () => {
const poolBeforeHover = sr().querySelector('.glow-pool');
const gradientBeforeHover = sr().querySelector('radialGradient[id^="hp-glow-"]');
const roomEl = sr().querySelector('.room');
roomEl?.dispatchEvent(new MouseEvent('mouseenter'));
roomEl?.dispatchEvent(new PointerEvent('pointerenter', { pointerType: 'mouse' }));
await c.updateComplete;
const hoverFill = sr().querySelector('.room-hover-fill');
const hoverFillLayer = sr().querySelector('.room-hover-fill-layer');
@@ -263,7 +263,7 @@ const res = await page.evaluate(async () => {
// late outline must follow it; absence is not a layer-order failure.
&& (!wallLayer
|| !!(wallLayer.compareDocumentPosition(hoverOutlineLayer) & Node.DOCUMENT_POSITION_FOLLOWING));
roomEl?.dispatchEvent(new MouseEvent('mouseleave'));
roomEl?.dispatchEvent(new PointerEvent('pointerleave', { pointerType: 'mouse' }));
await c.updateComplete;
return out;
});
+4 -4
View File
@@ -45,15 +45,15 @@ const res = await page.evaluate(async () => {
const lbl = [...sr().querySelectorAll('.roomlabel')].find((l) => l.textContent.includes(model.name));
out.cardShowsSource = lbl ? lbl.textContent.includes('30.2°') : false;
const modelIndex = c._spaceModel().rooms.findIndex((r) => r.id === editedId);
[...sr().querySelectorAll('.room')][modelIndex].dispatchEvent(new MouseEvent('mousemove', {
bubbles: true, composed: true, clientX: 200, clientY: 200,
[...sr().querySelectorAll('.room')][modelIndex].dispatchEvent(new PointerEvent('pointermove', {
pointerType: 'mouse', bubbles: true, composed: true, clientX: 200, clientY: 200,
}));
await c.updateComplete;
out.tooltipShowsHumiditySource = c._tip?.hum === 47
&& (sr().querySelector('.tip')?.textContent || '').includes(`${c._t('tip.hum_avg')} 47%`);
model.settings = { ...model.settings, hum_source: 'entity:sensor.missing_room_hum' };
[...sr().querySelectorAll('.room')][modelIndex].dispatchEvent(new MouseEvent('mousemove', {
bubbles: true, composed: true, clientX: 210, clientY: 210,
[...sr().querySelectorAll('.room')][modelIndex].dispatchEvent(new PointerEvent('pointermove', {
pointerType: 'mouse', bubbles: true, composed: true, clientX: 210, clientY: 210,
}));
await c.updateComplete;
out.invalidHumiditySourceIsOmitted = c._tip?.hum == null
+21 -8
View File
@@ -41,7 +41,9 @@ const res = await page.evaluate(async () => {
// тултип комнаты: площадь и агрегаты температуры/влажности в стабильном порядке
const rooms = [...sr().querySelectorAll('.room')];
const room = rooms[0];
room.dispatchEvent(new MouseEvent('mousemove', { bubbles: true, composed: true, clientX: 200, clientY: 200 }));
room.dispatchEvent(new PointerEvent('pointermove', {
pointerType: 'mouse', bubbles: true, composed: true, clientX: 200, clientY: 200,
}));
await c.updateComplete;
out.tipTemp = c._tip?.temp;
out.tipHum = c._tip?.hum;
@@ -60,26 +62,37 @@ const res = await page.evaluate(async () => {
out.tipHasAreaLine = (sr().querySelector('.tip')?.textContent || '').includes(expectedArea);
// Комната без источника влажности не получает пустую строку.
rooms[1].dispatchEvent(new MouseEvent('mousemove', { bubbles: true, composed: true, clientX: 210, clientY: 210 }));
rooms[1].dispatchEvent(new PointerEvent('pointermove', {
pointerType: 'mouse', bubbles: true, composed: true, clientX: 210, clientY: 210,
}));
await c.updateComplete;
out.noHumidityOmitted = c._tip?.hum == null
&& !(sr().querySelector('.tip')?.textContent || '').includes('средняя влажность');
// Общий device tooltip остаётся без room humidity.
sr().querySelector('.dev').dispatchEvent(new MouseEvent('mousemove', {
bubbles: true, composed: true, clientX: 220, clientY: 220,
sr().querySelector('.dev').dispatchEvent(new PointerEvent('pointermove', {
pointerType: 'mouse', bubbles: true, composed: true, clientX: 220, clientY: 220,
}));
await c.updateComplete;
out.deviceTooltipHasNoHumidity = c._tip?.hum == null
&& !(sr().querySelector('.tip')?.textContent || '').includes('средняя влажность');
// После первого touch/pen hover-подсказки по-прежнему не создаются.
c.constructor._touchSeen = true;
// Touch/pen input never creates hover-only tooltips.
room.dispatchEvent(new PointerEvent('pointerdown', {
pointerType: 'touch', pointerId: 91, bubbles: true, composed: true,
clientX: 230, clientY: 230,
}));
c._tip = null;
room.dispatchEvent(new MouseEvent('mousemove', { bubbles: true, composed: true, clientX: 230, clientY: 230 }));
room.dispatchEvent(new PointerEvent('pointermove', {
pointerType: 'touch', pointerId: 91, bubbles: true, composed: true,
clientX: 230, clientY: 230,
}));
await c.updateComplete;
out.noHoverSuppressesTooltip = c._tip === null;
c.constructor._touchSeen = false;
room.dispatchEvent(new PointerEvent('pointerup', {
pointerType: 'touch', pointerId: 91, bubbles: true, composed: true,
clientX: 230, clientY: 230,
}));
// диалог: радио заливки, компактные поля, ширина
c._openSpaceDialog('edit', 'f1'); await c.updateComplete;
out.fillRadios = sr().querySelectorAll('input[name="fillmode"]').length;
File diff suppressed because one or more lines are too long
+24 -24
View File
File diff suppressed because one or more lines are too long
+3 -2
View File
@@ -6,8 +6,9 @@
shrinking the 44×44 px interaction floor, and wide Text markers keep uniform
capsule insets. A real toggle/run dispatch gives a smooth 5% press response
for 200 ms (with a reduced-motion alternative). Touch and pen no longer leave
room, marker or shared-control hover stuck; a real mouse restores hover on
hybrid hardware without reloading ([#212](https://github.com/Matysh/houseplan-card/issues/212)).
room, marker, Device preview or shared-control hover stuck; a real mouse
restores hover on hybrid hardware without reloading
([#212](https://github.com/Matysh/houseplan-card/issues/212)).
## v1.65.0-beta.7 — 2026-08-20
+4 -3
View File
@@ -12,9 +12,10 @@
без уменьшения области нажатия 44×44 px; широкий Text-маркер получил ровные
отступы капсулы. Реальная отправка toggle/run-команды даёт плавный отклик
уменьшением на 5% за 0,2 с с отдельным вариантом для reduced motion. Касание
пальцем или пером больше не оставляет hover комнаты, маркера и общих контролов
«прилипшим», а настоящая мышь возвращает hover на гибридном устройстве без
перезагрузки ([#212](https://github.com/Matysh/houseplan-card/issues/212)).
пальцем или пером больше не оставляет hover комнаты, маркера, Device preview
и общих контролов «прилипшим», а настоящая мышь возвращает hover на гибридном
устройстве без перезагрузки
([#212](https://github.com/Matysh/houseplan-card/issues/212)).
## v1.65.0-beta.7 — 2026-08-20
+11 -11
View File
@@ -1,7 +1,7 @@
{
"version": 1,
"fixture": "synthetic-only",
"sourceFingerprint": "040ec09a494568481fd2a4853a4dd8183c7950152dc9674d0a458a323f8bdf21",
"sourceFingerprint": "d7e3504b796d334b3d6646bcf34f7effb2d465add3e9ddabeb23b696f9e271a9",
"captureScriptSha256": "34f2219790d46efd8250e7a1bd829cb8fc0b0547e1260635fefa52407551b41b",
"command": "npm run build && node demo/docs/capture.mjs",
"scenarios": {
@@ -13,7 +13,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "040ec09a494568481fd2a4853a4dd8183c7950152dc9674d0a458a323f8bdf21",
"sourceSha256": "d7e3504b796d334b3d6646bcf34f7effb2d465add3e9ddabeb23b696f9e271a9",
"imageSha256": "8780c6f05a9f2af55b97512dfa8d277bb77ab9ae0dafb50640cc3237028055dc"
},
"view-touch": {
@@ -24,7 +24,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "040ec09a494568481fd2a4853a4dd8183c7950152dc9674d0a458a323f8bdf21",
"sourceSha256": "d7e3504b796d334b3d6646bcf34f7effb2d465add3e9ddabeb23b696f9e271a9",
"imageSha256": "5e41c81ded9eb67deea49840c260eaaa404cab61abf014a1a275586dafe66e53"
},
"space-create": {
@@ -35,7 +35,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "040ec09a494568481fd2a4853a4dd8183c7950152dc9674d0a458a323f8bdf21",
"sourceSha256": "d7e3504b796d334b3d6646bcf34f7effb2d465add3e9ddabeb23b696f9e271a9",
"imageSha256": "c53db2e5c642a5549c13f3c93a5b359fed69bdb2621bf71a243a877ffcb95e6b"
},
"room-contour-close": {
@@ -46,7 +46,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "040ec09a494568481fd2a4853a4dd8183c7950152dc9674d0a458a323f8bdf21",
"sourceSha256": "d7e3504b796d334b3d6646bcf34f7effb2d465add3e9ddabeb23b696f9e271a9",
"imageSha256": "2f13ea645af306eee9cc7c52699c928377b72f412b08c1a3db6beb848ace1b51"
},
"plan-context-tray": {
@@ -57,7 +57,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "040ec09a494568481fd2a4853a4dd8183c7950152dc9674d0a458a323f8bdf21",
"sourceSha256": "d7e3504b796d334b3d6646bcf34f7effb2d465add3e9ddabeb23b696f9e271a9",
"imageSha256": "a6c526fede11bc3503fd2384bd4a3f6afa008f481c5c47578f06cae6c81c6f9b"
},
"device-editor": {
@@ -68,7 +68,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "040ec09a494568481fd2a4853a4dd8183c7950152dc9674d0a458a323f8bdf21",
"sourceSha256": "d7e3504b796d334b3d6646bcf34f7effb2d465add3e9ddabeb23b696f9e271a9",
"imageSha256": "bfba4647c04bdf4b46dc9cea3b42e349b3ea67007bb38e16880983dbee14653e"
},
"device-display-preview": {
@@ -79,7 +79,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "040ec09a494568481fd2a4853a4dd8183c7950152dc9674d0a458a323f8bdf21",
"sourceSha256": "d7e3504b796d334b3d6646bcf34f7effb2d465add3e9ddabeb23b696f9e271a9",
"imageSha256": "8cdfe00ead5558d013265617104997fdd138337322aa82f6a3e1d15eeb36cf5e"
},
"background-editor": {
@@ -90,7 +90,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "040ec09a494568481fd2a4853a4dd8183c7950152dc9674d0a458a323f8bdf21",
"sourceSha256": "d7e3504b796d334b3d6646bcf34f7effb2d465add3e9ddabeb23b696f9e271a9",
"imageSha256": "d7cfe70551d9260169df8efd832e32ed7df99c7f60b55d1fd4a8322bd4c47175"
},
"room-card": {
@@ -101,7 +101,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "040ec09a494568481fd2a4853a4dd8183c7950152dc9674d0a458a323f8bdf21",
"sourceSha256": "d7e3504b796d334b3d6646bcf34f7effb2d465add3e9ddabeb23b696f9e271a9",
"imageSha256": "ab29f6fdb31d3117e935a93a237105d42a5103687ac892ef426a4887ae7960c4"
},
"device-info": {
@@ -112,7 +112,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "040ec09a494568481fd2a4853a4dd8183c7950152dc9674d0a458a323f8bdf21",
"sourceSha256": "d7e3504b796d334b3d6646bcf34f7effb2d465add3e9ddabeb23b696f9e271a9",
"imageSha256": "30bf3f2a1311fc2bffeac6359773925fea22150df2845015c630e009016f095f"
}
}
+4 -3
View File
@@ -484,6 +484,7 @@ const LS_ZOOM = 'houseplan_card_zoom_v1';
const LS_NAV = 'houseplan_card_nav_v1'; // last space only; editor sessions never survive page navigation
const LS_KIOSK = 'houseplan_card_kiosk_v1'; // per-SCREEN size multipliers (each wall tablet differs)
const LS_VIEW = 'houseplan_card_view_v1'; // presentation preference per space, Labs-only
const POINTER_HOVER_TARGET_SELECTOR = 'hp-dialog, hp-help, hp-color-opacity, hp-device-preview';
const NORM_W = 1000; // side of the render space — the canvas is square (v1.48.0)
/** Short semantic-event / direct-terminal-transition window. Event uses
three sequential 1.1 s waves; motion cool-down itself never animates. */
@@ -5849,7 +5850,7 @@ class HouseplanCard extends LitElement {
private _syncPointerHoverTargets(): void {
const enabled = this._pointerModality.hoverEnabled;
for (const target of this.renderRoot.querySelectorAll<HTMLElement>(
'hp-dialog, hp-help, hp-color-opacity',
POINTER_HOVER_TARGET_SELECTOR,
)) {
target.toggleAttribute('data-pointer-hover', enabled);
}
@@ -5859,11 +5860,11 @@ class HouseplanCard extends LitElement {
if (node.nodeType !== Node.ELEMENT_NODE) return;
const element = node as HTMLElement;
const enabled = this._pointerModality.hoverEnabled;
if (element.matches('hp-dialog, hp-help, hp-color-opacity')) {
if (element.matches(POINTER_HOVER_TARGET_SELECTOR)) {
element.toggleAttribute('data-pointer-hover', enabled);
}
for (const target of element.querySelectorAll<HTMLElement>(
'hp-dialog, hp-help, hp-color-opacity',
POINTER_HOVER_TARGET_SELECTOR,
)) {
target.toggleAttribute('data-pointer-hover', enabled);
}
+13 -1
View File
@@ -41,8 +41,20 @@ test('issue 212 removes the global touch latch and gates every shared hover sele
assert.doesNotMatch(card, /private static _touchSeen/);
assert.match(card, /PointerModalityController/);
assert.match(card, /_clearTransientHover/);
assert.match(
card,
/POINTER_HOVER_TARGET_SELECTOR\s*=\s*'[^']*hp-device-preview[^']*'/,
'device preview must receive the card-owned pointer hover gate',
);
for (const name of ['styles.ts', 'hp-dialog.ts', 'hp-help.ts', 'hp-color-opacity.ts', 'space-card.ts']) {
for (const name of [
'styles.ts',
'hp-dialog.ts',
'hp-help.ts',
'hp-color-opacity.ts',
'hp-device-preview.ts',
'space-card.ts',
]) {
const text = source(name);
const selectors = [...text.matchAll(/(^|\})([^{}]+)\{/gm)].map((match) => match[2]);
const naked = selectors.flatMap((selector) => selector.split(','))