mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-05 14:19:04 +00:00
@@ -0,0 +1,299 @@
|
||||
# Issue #154 — transient hover не залипает после touch
|
||||
|
||||
- **Issue:** https://github.com/Matysh/houseplan-card/issues/154
|
||||
- **Статус документа:** готово к будущей реализации; issue остаётся на `S3-spec`
|
||||
- **Приоритет:** P1
|
||||
- **Тип:** bug/polish, обычный трек
|
||||
- **Пользовательское изменение:** да
|
||||
|
||||
## 1. Проблема и требуемый результат
|
||||
|
||||
Мобильный браузер после tap может синтезировать mouse-события и сохранять CSS
|
||||
`:hover`. Одновременно room hover в View управляется `mouseenter`/`mouseleave`,
|
||||
а touch sequence не гарантирует `mouseleave`. Текущий `_notePointer()` закрывает
|
||||
`_tip`, но не очищает `_hoverRoom` и не нейтрализует CSS hover.
|
||||
|
||||
После исправления touch/pen показывает только краткий pressed feedback на время
|
||||
жеста. После его завершения transient hover отсутствует. Настоящие mouse и
|
||||
trackpad сохраняют обычный hover, а на гибридном устройстве последующий mouse
|
||||
input восстанавливает его после touch без reload.
|
||||
|
||||
## 2. Термины и границы состояния
|
||||
|
||||
**Transient hover** существует только из-за текущего hover-capable pointer:
|
||||
|
||||
- `_hoverRoom`, room fill/outline и room tooltip;
|
||||
- device marker lift/shadow/brightness;
|
||||
- opening, room label и control hover styles;
|
||||
- dialog close/control hover;
|
||||
- аналогичные чисто визуальные CSS `:hover` состояния общих View-компонентов.
|
||||
|
||||
Не являются transient hover и не очищаются этой задачей:
|
||||
|
||||
- `active`, `pressed` во время незавершённого pointer sequence;
|
||||
- selected/checked/current состояния редакторов;
|
||||
- working, unavailable, alarm, warning, Glow/pulse и иное HA-derived состояние;
|
||||
- keyboard focus и `:focus-visible`;
|
||||
- открытый по click/tap toggle-popover `hp-help`;
|
||||
- открытый dialog либо выполненное action.
|
||||
|
||||
## 3. Scope
|
||||
|
||||
Обязательный охват основного View:
|
||||
|
||||
- room floor/fill/outline/label/tooltip;
|
||||
- device marker, opening и vacuum marker;
|
||||
- controls поверх плана и close/action controls диалога;
|
||||
- tap комнаты из будущей #152;
|
||||
- long press, pinch, multi-touch и pointer capture cancellation;
|
||||
- touch → mouse/trackpad на гибридном устройстве;
|
||||
- смена режима, visibility и lifecycle компонента.
|
||||
|
||||
Общие компоненты редакторов получают исправление, если используют тот же
|
||||
pointer-modality gate без специальной адаптации. Полная поддержка touch-editing
|
||||
остаётся вне scope согласно `docs/TOUCH-SUPPORT.md`.
|
||||
|
||||
## 4. Не входит в задачу
|
||||
|
||||
- изменение semantic device/room state;
|
||||
- новый visual design hover/pressed/focus;
|
||||
- изменение действий tap, long press или click;
|
||||
- превращение `hp-help` в transient tooltip;
|
||||
- исправление touch settings из #149;
|
||||
- полная переработка gesture arbiter #152;
|
||||
- глобальный `blur()`, снятие DOM focus либо synthetic click suppression,
|
||||
способный отменить пользовательское action.
|
||||
|
||||
## 5. Единый pointer-modality authority
|
||||
|
||||
Houseplan вводит один session-local источник фактической modality:
|
||||
|
||||
```text
|
||||
unknown | mouse | touch | pen
|
||||
```
|
||||
|
||||
Начальное значение — `unknown`; pointer-only hover при нём выключен. Переходы:
|
||||
|
||||
- trusted `PointerEvent` с `pointerType === 'mouse'` включает `mouse`;
|
||||
- trusted `touch` включает `touch` и немедленно очищает transient hover;
|
||||
- trusted `pen` включает `pen` и следует touch policy;
|
||||
- следующий настоящий mouse/trackpad pointer event переводит touch/pen → mouse;
|
||||
- смена mode/space, `visibilitychange` в hidden и disconnect очищают transient
|
||||
hover, не подменяя semantic state.
|
||||
|
||||
Media queries не являются authority: браузер может ошибочно сообщать
|
||||
`(hover: hover)` после touch. Для включения CSS hover одновременно нужны:
|
||||
|
||||
1. последняя фактическая modality `mouse`;
|
||||
2. hover/fine-pointer capability среды.
|
||||
|
||||
Компонент применяет единый class/data-attribute gate к View tree; дочерние
|
||||
shadow components получают ту же modality явным property/attribute contract.
|
||||
Независимые локальные детекторы и глобальный mutable singleton запрещены.
|
||||
|
||||
Modality не сохраняется в localStorage/config и не синхронизируется между
|
||||
карточками. Она не обязана быть reactive product state, если один безопасный
|
||||
DOM gate и явная очистка обновляются без лишнего full render.
|
||||
|
||||
## 6. Synthetic mouse policy
|
||||
|
||||
Touch-generated compatibility `MouseEvent` не может включить mouse modality.
|
||||
Authority дают только Pointer Events с фактическим `pointerType`; существующие
|
||||
`mouseenter`/`mouseleave` не меняют modality.
|
||||
|
||||
JS room hover переводится на `pointerenter`/`pointerleave` либо общий delegated
|
||||
pointer path и устанавливается только для разрешённого mouse modality. Touch и
|
||||
pen никогда не записывают `_hoverRoom`/`_tip` через hover path.
|
||||
|
||||
Не использовать произвольный таймаут «после touch игнорировать mouse N ms» как
|
||||
основной механизм: он ломает быстрый touch → mouse сценарий и зависит от
|
||||
браузера. Если конкретный браузер отправляет compatibility event как
|
||||
`PointerEvent(pointerType='mouse')`, допускается только детерминированная
|
||||
проверка provenance/capabilities этого же input sequence с unit/browser
|
||||
доказательством; wall-clock suppression без идентичности sequence запрещён.
|
||||
|
||||
## 7. Очистка transient hover
|
||||
|
||||
Единый idempotent helper очищает только transient hover state. Он вызывается:
|
||||
|
||||
- в начале каждого touch/pen `pointerdown`;
|
||||
- на соответствующих `pointerup` и `pointercancel`;
|
||||
- при `lostpointercapture`;
|
||||
- после terminal click/action path до следующего painted frame;
|
||||
- при начале multi-touch/pinch и после его завершения/cancel;
|
||||
- при mode/space change;
|
||||
- при `document.visibilityState === 'hidden'`;
|
||||
- при disconnect/remount boundary.
|
||||
|
||||
Повторный вызов — no-op и не должен создавать render loop. Pointer capture
|
||||
снимается только владельцем gesture по текущему contract; hover cleanup не
|
||||
отменяет service call, dialog open или room fit.
|
||||
|
||||
После touch terminal event pressed feedback очищается обычным gesture owner.
|
||||
Transient hover не остаётся дольше одного animation frame и не используется для
|
||||
имитации pressed.
|
||||
|
||||
## 8. CSS contract
|
||||
|
||||
Все пользовательски заметные View `:hover` selectors получают общий modality
|
||||
gate. Как минимум это:
|
||||
|
||||
- room overlay/yard/styled fill;
|
||||
- device normal/alarm lift, shadow и brightness;
|
||||
- opening outline;
|
||||
- room-label controls;
|
||||
- stage controls/options;
|
||||
- dialog close/action controls и общие card controls, видимые в View.
|
||||
|
||||
Один selector не должен случайно смешивать hover с semantic state. Если текущая
|
||||
rule объединяет `:hover` и `:focus-visible`, её разделяют: mouse hover получает
|
||||
modality gate, focus-visible остаётся без него. `:active`/explicit pressed styles
|
||||
также остаются независимы.
|
||||
|
||||
Selectors редакторских handles можно оставить вне обязательного охвата, если
|
||||
они не используются в View/shared component; решение фиксируется inventory в
|
||||
review. Naked View `:hover` после изменения считается source-contract ошибкой.
|
||||
|
||||
## 9. JS hover и tooltip contract
|
||||
|
||||
- `_hoverRoom` и `_tip` не устанавливаются touch/pen событиями.
|
||||
- `pointerleave` реальной мыши очищает принадлежащее target состояние.
|
||||
- touch `pointerdown` очищает старый mouse hover до выполнения tap action.
|
||||
- открытие/закрытие dialog не восстанавливает старый hover snapshot.
|
||||
- tooltip, открытый keyboard focus либо explicit click contract, не должен
|
||||
ошибочно классифицироваться как hover; owner хранится явно.
|
||||
- следующий mouse enter/move заново вычисляет current hit и показывает hover,
|
||||
а не восстанавливает устаревший room/device id.
|
||||
|
||||
Room fit #152 использует canonical hit resolver непосредственно из gesture
|
||||
sequence и не зависит от `_hoverRoom`; очистка hover не должна терять room tap.
|
||||
|
||||
## 10. Touch, gestures и lifecycle
|
||||
|
||||
- Второй pointer немедленно исключает single-tap activation по действующему
|
||||
gesture contract и очищает hover.
|
||||
- Pinch/long press могут выполнить своё существующее действие, но после terminal
|
||||
event не оставляют hover.
|
||||
- `pointercancel`/lost capture всегда безопасны, даже если target удалён renderом.
|
||||
- Tap → dialog → close не возвращает marker lift/shadow из пред-dialog frame.
|
||||
- Pen tap следует touch policy; hover stylus не входит в обязательный контракт.
|
||||
- Kiosk, light/dark theme, Flat/Isometric используют одинаковую modality policy.
|
||||
|
||||
Listener `visibilitychange` регистрируется/удаляется симметрично lifecycle и не
|
||||
создаёт утечку при повторных mount/unmount.
|
||||
|
||||
## 11. Accessibility
|
||||
|
||||
- DOM focus не снимается touch cleanup helper.
|
||||
- `:focus-visible` и keyboard tooltip/action продолжают работать независимо от
|
||||
последней pointer modality.
|
||||
- Touch cleanup не меняет `aria-expanded`, `aria-pressed`, selection либо dialog
|
||||
focus trap.
|
||||
- Mouse hover не является единственным способом получить обязательную
|
||||
информацию или действие.
|
||||
- Reduced motion не влияет на state machine; pressed feedback #22 остаётся
|
||||
кратким и не превращается в hover.
|
||||
|
||||
## 12. Acceptance criteria
|
||||
|
||||
1. После touch tap на каждом обязательном View target transient hover исчезает
|
||||
не позднее следующего frame после завершения pressed feedback.
|
||||
2. `_hoverRoom`/hover-owned `_tip` равны null после touch terminal event.
|
||||
3. Device lift/shadow и CSS hover controls отсутствуют после tap/dialog close.
|
||||
4. Working/alarm/unavailable/Glow/pulse/selected state не изменяется.
|
||||
5. Pinch, long press, multi-touch, cancel и lost capture не оставляют hover.
|
||||
6. Браузер с touch и ложным `(hover: hover)` не показывает sticky hover.
|
||||
7. Настоящий desktop mouse hover визуально и функционально не изменён.
|
||||
8. На hybrid device touch → mouse восстанавливает hover первым настоящим mouse
|
||||
pointer event без reload и без произвольной задержки.
|
||||
9. Keyboard focus и `:focus-visible` сохраняются.
|
||||
10. Cleanup не отменяет click action, dialog, #152 room fit или help popover.
|
||||
11. Mode/space/visibility/disconnect очищают transient hover без listener leak.
|
||||
12. В View/shared CSS не остаётся naked hover selector из inventory.
|
||||
|
||||
## 13. План тестирования
|
||||
|
||||
### Unit/source contract
|
||||
|
||||
- transitions `unknown → mouse/touch/pen → mouse`;
|
||||
- compatibility MouseEvent не включает mouse modality;
|
||||
- touch/pen down/up/cancel и lost capture вызывают idempotent cleanup;
|
||||
- mode/space/visibility/disconnect lifecycle;
|
||||
- cleanup очищает только hover-owned state;
|
||||
- room hover gate и #152 hit независимы;
|
||||
- CSS inventory: View hover selectors требуют modality gate, focus-visible — нет.
|
||||
|
||||
### Browser smoke
|
||||
|
||||
- реальный touch context/CDP touch input: room, device, opening, label, control;
|
||||
- tap → dialog → close и tap при активном semantic device state;
|
||||
- browser context с touch и `(any-hover: hover)`;
|
||||
- two-finger pinch, long press, pointercancel и lost capture;
|
||||
- touch room click-to-fit #152 без sticky room highlight;
|
||||
- touch → hardware/simulated mouse pointer: hover снова появляется;
|
||||
- desktop mouse enter/leave и keyboard Tab/Enter regression;
|
||||
- Flat/Isometric, kiosk, light/dark и visibility round-trip.
|
||||
|
||||
Тест проверяет computed styles и JS state после painted frame, а не только
|
||||
отсутствие class. `dispatchEvent(new MouseEvent(...))` не заменяет настоящий
|
||||
touch browser scenario.
|
||||
|
||||
### Golden
|
||||
|
||||
- touch post-tap screenshot без hover, но с прежним semantic state;
|
||||
- desktop mouse-hover и keyboard-focus screenshots;
|
||||
- visual diff не переакцептует unrelated colors/shadows.
|
||||
|
||||
### Performance
|
||||
|
||||
- pointermove не вызывает full Lit update на каждый пиксель;
|
||||
- modality gate не добавляет unbounded listeners/observers;
|
||||
- canonical pan/pinch smoke сохраняет frame responsiveness;
|
||||
- performance profile перед beta подтверждает отсутствие новых long tasks.
|
||||
|
||||
## 14. План реализации
|
||||
|
||||
1. Провести inventory JS state и View/shared CSS hover selectors.
|
||||
2. Ввести component-local pointer modality authority и lifecycle cleanup.
|
||||
3. Перевести room hover на pointer events с mouse gate.
|
||||
4. Ввести общий CSS modality gate, разделив hover/focus/semantic rules.
|
||||
5. Передать modality в обязательные shadow child components.
|
||||
6. Добавить unit/source-contract и real-touch/hybrid browser tests.
|
||||
7. Прогнать typecheck, unit и build; перед beta — smoke, golden, performance.
|
||||
|
||||
## 15. Документация и release-артефакты
|
||||
|
||||
Поскольку bug виден пользователю, implementation commit обязан иметь
|
||||
`User-Visible: yes` и в том же коммите обновить:
|
||||
|
||||
- `docs/CHANGELOG.md` и `docs/CHANGELOG.ru.md`;
|
||||
- `docs/TOUCH-SUPPORT.md` — pointer modality и отсутствие sticky hover;
|
||||
- `docs/CANVAS.md` — hover/pressed/semantic ownership;
|
||||
- `docs/TESTING.md` — real-touch и hybrid smoke contract.
|
||||
|
||||
Нужны reviewed touch post-tap, desktop-hover и keyboard-focus golden artifacts,
|
||||
а также browser smoke report. Новых пользовательских строк не ожидается; если
|
||||
появятся, обе локали и parity test обязательны.
|
||||
|
||||
## 16. Риски и откат
|
||||
|
||||
| Риск | Мера |
|
||||
| --- | --- |
|
||||
| Touch cleanup отменяет action | state-only helper, gesture smoke |
|
||||
| Mouse hover пропадает на hybrid | event-derived transition back to mouse |
|
||||
| Focus styling попадает под gate | отдельные selectors/source contract |
|
||||
| Semantic state очищается как hover | явный inventory и state ownership tests |
|
||||
| Pointermove вызывает rerender storm | DOM gate без full render per move |
|
||||
| Lifecycle listener течёт | symmetric connect/disconnect test |
|
||||
|
||||
Откат возвращает прежние JS/CSS hover paths. Config, storage и backend data не
|
||||
меняются; миграция и data rollback не нужны.
|
||||
|
||||
## 17. Принятые предположения
|
||||
|
||||
- `touch` и `pen` следуют одинаковой no-hover policy;
|
||||
- initial `unknown` не включает pointer-only hover до фактической мыши;
|
||||
- mouse modality определяется trusted PointerEvent, не compatibility MouseEvent;
|
||||
- media query используется только вторым gate, а не источником modality;
|
||||
- #152 room activation не должна читать `_hoverRoom` как selected hit;
|
||||
- `hp-help`, focus-visible и semantic states не относятся к transient hover.
|
||||
@@ -48,6 +48,7 @@ GitHub Issues и GitHub Projects (v2) остаются единственным
|
||||
| [#131](https://github.com/Matysh/houseplan-card/issues/131) Полный первый кадр View у read-only-пользователя | [131-readonly-cold-start.md](131-readonly-cold-start.md) |
|
||||
| [#138](https://github.com/Matysh/houseplan-card/issues/138) Автозамыкание комнаты по существующей стене | [138-adjacent-room-autoclose.md](138-adjacent-room-autoclose.md) |
|
||||
| [#146](https://github.com/Matysh/houseplan-card/issues/146) Четырёхфазный фон «Следует за Солнцем» | [146-four-phase-sun-background.md](146-four-phase-sun-background.md) |
|
||||
| [#154](https://github.com/Matysh/houseplan-card/issues/154) Touch: hover-состояние залипает после tap | [154-touch-hover-reset.md](154-touch-hover-reset.md) |
|
||||
| [#156](https://github.com/Matysh/houseplan-card/issues/156) Регрессии Full Performance перед v1.64.0 stable | [156-full-performance-regressions.md](156-full-performance-regressions.md) |
|
||||
|
||||
## P2
|
||||
|
||||
Reference in New Issue
Block a user