Explain display setup in Settings, stop iOS form zoom, lock the page behind the drawer
CI / verify (pull_request) Successful in 1m12s
CI / infrastructure-smoke (pull_request) Successful in 49s
CI / package-release (pull_request) Successful in 47s
CI / migration-smoke (pull_request) Successful in 2m18s
CI / image-build (pull_request) Successful in 1m3s
CI / deploy-smoke (same-origin) (pull_request) Successful in 49s
CI / package-publication (pull_request) Successful in 54s
CI / deploy-smoke (split) (pull_request) Successful in 3m38s
CI / verify (pull_request) Successful in 1m12s
CI / infrastructure-smoke (pull_request) Successful in 49s
CI / package-release (pull_request) Successful in 47s
CI / migration-smoke (pull_request) Successful in 2m18s
CI / image-build (pull_request) Successful in 1m3s
CI / deploy-smoke (same-origin) (pull_request) Successful in 49s
CI / package-publication (pull_request) Successful in 54s
CI / deploy-smoke (split) (pull_request) Successful in 3m38s
A second real-iPhone review of the Stage 1 Dashboard found three defects. This fixes them, and ADR 0065 records the decisions. - Settings -> Displays now sets up a screen in one place. A "Set up a new screen" panel shows the platform display page to open on the screen, a Copy link button, and Add display directly beneath it; the step-by-step version is folded into a <details>. The addresses come from a new GET /api/v1/devices/setup, which requires core.devices.manage and lists each running display module's /display/<module-id> page. Nothing names Signage. With no display module installed, the panel says so and offers no Add display. - iOS Safari zoomed into the pairing field because the shared .ek-control rendered at 14px. On phones and touch screens it now renders at max(1rem, 16px). The viewport is untouched, so zoom stays available, and a test guards that. The shared focus trap now returns focus with preventScroll, so closing a dialog no longer moves the page under the phone header. - While the phone drawer is open, the page behind it no longer scrolls. The root's overflow is hidden, a non-passive touchmove listener cancels drags outside the drawer's list and overscroll past its ends, and root overscroll is turned off. The list itself still scrolls. Closing restores the exact scroll position unless another page opened. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
5f9b536bc6
commit
906c015ca8
@@ -38,7 +38,7 @@ export interface FocusTrapOptions {
|
||||
* - While active, Tab from the last focusable element wraps to the first and Shift+Tab from the
|
||||
* first wraps to the last, so focus cannot walk out into the page behind.
|
||||
* - On deactivation or unmount, returns focus to whatever had it before — the control that opened
|
||||
* the surface — if that control is still in the document.
|
||||
* the surface — if that control is still in the document, without scrolling the page.
|
||||
*
|
||||
* Returns the `onKeyDown` handler the container must carry. It works inside an isolated module root
|
||||
* as well as in a document: it only ever looks inside its own container and at the container's own
|
||||
@@ -68,7 +68,10 @@ export function useFocusTrap(
|
||||
const target = restoreTo.current;
|
||||
restoreTo.current = null;
|
||||
if (target?.isConnected) {
|
||||
target.focus();
|
||||
// Without scrolling: the control was on screen when it opened the surface, and scrolling it
|
||||
// into view again — past a sticky header's scroll padding — would move the page under a
|
||||
// person who just closed a dialog (ADR 0065).
|
||||
target.focus({ preventScroll: true });
|
||||
}
|
||||
};
|
||||
}, [active, containerRef, initialFocus]);
|
||||
|
||||
@@ -177,6 +177,19 @@ export const EKKLESIA_UI_STYLESHEET = `
|
||||
border-color: ${designToken("border.strong")};
|
||||
}
|
||||
|
||||
/*
|
||||
* Text a person types is never smaller than 16px on a phone or a touch screen. iOS Safari zooms the
|
||||
* whole page into any focused text field whose font is smaller than that, and leaves it zoomed after
|
||||
* the field closes, so the compact desktop size would turn every form on an iPhone into a zoomed-in
|
||||
* page (ADR 0065). The fix is the font, never the viewport: zoom stays available to everyone who
|
||||
* needs it. Desktop keeps its compact size.
|
||||
*/
|
||||
@media (max-width: 47.999em), (pointer: coarse) {
|
||||
.ek-control {
|
||||
font-size: max(${designToken("typography.size-md")}, 16px);
|
||||
}
|
||||
}
|
||||
|
||||
.ek-control::placeholder {
|
||||
color: ${designToken("text.muted")};
|
||||
}
|
||||
|
||||
@@ -251,6 +251,19 @@ describe("ConfirmationDialog", () => {
|
||||
expect(document.activeElement).toBe(byTestId("opener"));
|
||||
});
|
||||
|
||||
it("returns focus without scrolling the page (ADR 0065)", () => {
|
||||
openHarness({ onCancel: vi.fn() });
|
||||
const opener = byTestId("opener");
|
||||
const focus = vi.spyOn(opener, "focus");
|
||||
|
||||
press(dialog(), "Escape");
|
||||
|
||||
// The opener was on screen when it opened the dialog; scrolling it into view again would move a
|
||||
// page with a sticky header under the person who just closed the dialog.
|
||||
expect(focus).toHaveBeenCalledWith({ preventScroll: true });
|
||||
expect(document.activeElement).toBe(opener);
|
||||
});
|
||||
|
||||
it("does not close on Escape or Cancel while the confirmed action is in progress", () => {
|
||||
const onCancel = vi.fn();
|
||||
openHarness({ dialog: { busy: true }, onCancel });
|
||||
|
||||
@@ -159,3 +159,21 @@ describe("choice indicators are drawn from tokens (ADR 0064)", () => {
|
||||
expect(EKKLESIA_UI_STYLESHEET).toMatch(/@media \(forced-colors: active\)/u);
|
||||
});
|
||||
});
|
||||
|
||||
describe("text controls never make iOS Safari zoom (ADR 0065)", () => {
|
||||
it("renders typed text at 16px or more on phones and touch screens, and compact on desktop", () => {
|
||||
// iOS Safari zooms the page into a focused field whose font is under 16px.
|
||||
expect(ruleBody(".ek-control")).toContain("font-size: var(--ekklesia-typography-size-sm)");
|
||||
const phone =
|
||||
/@media \(max-width: 47\.999em\), \(pointer: coarse\) \{\s*\.ek-control \{([^}]*)\}/u.exec(
|
||||
EKKLESIA_UI_STYLESHEET,
|
||||
)?.[1];
|
||||
expect(phone).toContain("font-size: max(var(--ekklesia-typography-size-md), 16px)");
|
||||
});
|
||||
|
||||
it("puts the phone rule after the base rule, so it wins", () => {
|
||||
expect(EKKLESIA_UI_STYLESHEET.indexOf("(pointer: coarse)")).toBeGreaterThan(
|
||||
EKKLESIA_UI_STYLESHEET.indexOf("\n.ek-control {"),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user