From c455e7c82dfd358377eca2f60c38184c42acc79d Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 28 Sep 2026 08:28:00 +0300 Subject: [PATCH] =?UTF-8?q?fix(stairs):=20=D0=BB=D0=B5=D1=81=D1=82=D0=BD?= =?UTF-8?q?=D0=B8=D1=86=D0=B0-=D0=BF=D0=B5=D1=80=D0=B5=D1=85=D0=BE=D0=B4?= =?UTF-8?q?=20=D0=B1=D0=B5=D0=B7=20=D1=80=D0=B0=D0=BC=D0=BA=D0=B8=20=D1=84?= =?UTF-8?q?=D0=BE=D0=BA=D1=83=D1=81=D0=B0=20(#686)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Решение владельца 2026-09-28: «убрать рамку фокуса полностью». Навигационная лестница View — фокусируемый `` (#676), а своих стилей фокуса у `.hp-stair` не было, поэтому браузер рисовал чёрно-белое кольцо по прямоугольнику символа. Теперь `.hp-stair:focus` и `:focus-visible` — `outline: none`: ни кольца браузера, ни замены. Лестница остаётся в порядке Tab, Enter/Пробел по-прежнему переходят; hover и выделение в редакторе не тронуты. Свидетель — `demo/smoke_stairs.mjs`: снимок области лестницы с запасом на кольцо без фокуса и с клавиатурным фокусом (`:focus-visible`) обязан совпасть байт в байт; Enter на сфокусированной лестнице переводит на целевой этаж. Без правила смок красный (`focusedStairPaintsNoFrame`). STAIRS.md и оба changelog. Issue: #686 User-Visible: yes Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd --- demo/smoke_stairs.mjs | 51 +++++++++++++++++++++++++++++++++++++++ docs/CHANGELOG.md | 5 ++++ docs/CHANGELOG.ru.md | 5 ++++ docs/STAIRS.md | 4 ++- src/styles/plan.styles.ts | 5 ++++ 5 files changed, 69 insertions(+), 1 deletion(-) diff --git a/demo/smoke_stairs.mjs b/demo/smoke_stairs.mjs index 47d851bb..9f618607 100644 --- a/demo/smoke_stairs.mjs +++ b/demo/smoke_stairs.mjs @@ -641,5 +641,56 @@ const out = await page.evaluate(async () => { return result; }); +// #686: a focused stair link paints no focus frame — neither the browser's +// ring nor a replacement — and it still follows with Enter. The oracle is +// pixels: the stair's area with a margin for the ring is captured unfocused +// and with keyboard focus, and the two frames must be byte-identical. +const focusProbe = await page.evaluate(async () => { + const card = window.__card; + const hp = window.__hpTest; + await hp.setVolumetricView(false); + await hp.switchSpace('f1'); + await hp.setMode('view'); + await hp.settled(); + const node = card.renderRoot.querySelector('[data-hp="stair"][data-target-state="active"]'); + const rect = node?.getBoundingClientRect(); + return node ? { id: node.getAttribute('data-id'), x: rect.x, y: rect.y, w: rect.width, h: rect.height } : null; +}); +out.focusProbeHasActiveStair = !!focusProbe && focusProbe.w > 0 && focusProbe.h > 0; +if (focusProbe) { + const margin = 8; + const clip = { + x: Math.max(0, Math.floor(focusProbe.x - margin)), y: Math.max(0, Math.floor(focusProbe.y - margin)), + width: Math.ceil(focusProbe.w + 2 * margin), height: Math.ceil(focusProbe.h + 2 * margin), + }; + await page.mouse.move(1, 1); + const shot = () => page.screenshot({ clip, animations: 'disabled', caret: 'hide' }); + const unfocused = await shot(); + // A real key press puts the page into keyboard modality, so the following + // programmatic focus matches :focus-visible exactly like Tab would. + await page.keyboard.press('Shift'); + const focused = await page.evaluate(async (id) => { + const node = window.__card.renderRoot.querySelector(`[data-hp="stair"][data-id="${id}"]`); + node?.focus(); + await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve))); + return { + active: window.__card.renderRoot.activeElement === node, + visible: !!node?.matches(':focus-visible'), + }; + }, focusProbe.id); + const withFocus = await shot(); + out.keyboardFocusReachesStair = focused.active && focused.visible; + out.focusedStairPaintsNoFrame = focused.active && unfocused.equals(withFocus); + await page.keyboard.press('Enter'); + out.focusedStairEnterNavigates = await page.evaluate(async () => { + const card = window.__card; + for (let guard = 0; card._cameraTransition?.active && guard < 90; guard++) + await new Promise((resolve) => requestAnimationFrame(resolve)); + await window.__hpTest.settled(); + return card.renderRoot.querySelector('[data-hp="space-tab"][aria-current="page"]') + ?.getAttribute('data-id') === 'garden'; + }); +} + checkAll(out); await finish(browser, out); diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 78957a5f..fd54e0b6 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -2,6 +2,11 @@ ## Unreleased +- A floor-link stair in View no longer shows the browser's black-and-white + focus frame when it receives focus; it can still be reached with Tab and + followed with Enter or Space + ([#686](https://github.com/Matysh/houseplan-card/issues/686)). + ## v1.78.0-beta.7 — 2026-09-28 - Stairs now have two independent line and fill colours with opacity controls. A diff --git a/docs/CHANGELOG.ru.md b/docs/CHANGELOG.ru.md index 86c40881..f762841b 100755 --- a/docs/CHANGELOG.ru.md +++ b/docs/CHANGELOG.ru.md @@ -8,6 +8,11 @@ ## Не выпущено +- Лестница-переход в режиме просмотра больше не показывает чёрно-белую рамку + фокуса браузера; до неё по-прежнему можно дойти клавишей Tab и перейти по + Enter или Пробелу + ([#686](https://github.com/Matysh/houseplan-card/issues/686)). + ## v1.78.0-beta.7 — 2026-09-28 - У лестниц появились два собственных цвета: для линий и для заливки, а также diff --git a/docs/STAIRS.md b/docs/STAIRS.md index 4215358c..792e8e9f 100644 --- a/docs/STAIRS.md +++ b/docs/STAIRS.md @@ -57,7 +57,9 @@ state); a missing, self, deleted or fixed-floor target shows no tooltip. In an ordinary multi-space card, a clean click/tap or keyboard activation on a valid stair switches to the target tab and restores that floor's remembered -camera. Pan, pinch, long press, swipe and pointer cancellation do not navigate. +camera. A focused stair link draws no focus indicator — neither the browser's +ring nor an outline of its own (owner's decision, #686); it stays in the Tab +order, and Enter or Space still follow it. Pan, pinch, long press, swipe and pointer cancellation do not navigate. In a card configured with `floor`, stairs are visible but inert. The target floor receives no automatic stair, highlight or camera centring. diff --git a/src/styles/plan.styles.ts b/src/styles/plan.styles.ts index f8510d5f..b1b2a20b 100644 --- a/src/styles/plan.styles.ts +++ b/src/styles/plan.styles.ts @@ -1590,6 +1590,11 @@ export const planStyles = css` .hp-stair-hit { fill: transparent; stroke: transparent; stroke-width: 14px; pointer-events: none; } .hp-stair.input-enabled .hp-stair-hit { pointer-events: all; } .hp-stair.navigable { cursor: pointer; } + /* #686 (owner, 2026-09-28): a stair link shows no focus indicator at all — + neither the browser's ring nor a replacement. It stays focusable, and + Enter/Space still follow it (#676). */ + .hp-stair:focus, + .hp-stair:focus-visible { outline: none; } :host([data-pointer-hover]) .hp-stair.navigable:hover .hp-stair-outline { stroke: var(--hp-accent); stroke-opacity: 1;