mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
fix(stairs): лестница-переход без рамки фокуса (#686)
Решение владельца 2026-09-28: «убрать рамку фокуса полностью». Навигационная лестница View — фокусируемый `<g role="link" tabindex="0">` (#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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -8,6 +8,11 @@
|
||||
|
||||
## Не выпущено
|
||||
|
||||
- Лестница-переход в режиме просмотра больше не показывает чёрно-белую рамку
|
||||
фокуса браузера; до неё по-прежнему можно дойти клавишей Tab и перейти по
|
||||
Enter или Пробелу
|
||||
([#686](https://github.com/Matysh/houseplan-card/issues/686)).
|
||||
|
||||
## v1.78.0-beta.7 — 2026-09-28
|
||||
|
||||
- У лестниц появились два собственных цвета: для линий и для заливки, а также
|
||||
|
||||
+3
-1
@@ -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.
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user