diff --git a/changelog.d/1301-shared-segmented-selectors.md b/changelog.d/1301-shared-segmented-selectors.md index 96c1ee20d..379b8c716 100644 --- a/changelog.d/1301-shared-segmented-selectors.md +++ b/changelog.d/1301-shared-segmented-selectors.md @@ -1,4 +1,5 @@ - Unify segmented selectors for model coverage, detection allocation, drawing shape, Street View and imagery comparison while preserving their existing behavior. Keep compact labels such as FILL and FLAT on one line, and let - collapsed Weather release space to the open rail panel (mazy1998, #1301). + collapsed Weather release space to the open rail panel. Reserve the full mode + button height in Recent Imagery so keyboard focus is not clipped (mazy1998, #1301). diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 419e3efb6..224c9a02a 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -1237,6 +1237,8 @@ Pure value navigation is separate from DOM rendering for reuse by a future view. Segments keep their word minimum width: compact inline groups such as FIT/FILL and ALL/360°/FLAT must not break short labels mid-word. Collapsed Weather follows the same exclusive-rail hiding rule as the other readouts. +The imagery mode row reserves 26px for the 24px buttons and group borders so +the complete button and inset keyboard-focus outline remain visible. Browser acceptance: `QA_BASE_URL=http://127.0.0.1:4173 node scripts/qa-segmented-controls.mjs`. ## Application shortcuts and shader parameter controls diff --git a/scripts/qa-segmented-controls.mjs b/scripts/qa-segmented-controls.mjs index cfa8e51d6..8612c2f23 100644 --- a/scripts/qa-segmented-controls.mjs +++ b/scripts/qa-segmented-controls.mjs @@ -274,6 +274,42 @@ try { cyber.consistent, JSON.stringify(cyber), ); + await page.evaluate(async () => { + const { dataManager, styleManager } = window.__godsEyeView; + await dataManager.setEnabled('recent-imagery', true); + styleManager.setPanelCollapsed('recent-imagery-panel', false, { + persist: false, + syncShare: false, + }); + }); + for (const width of [1440, 620, 390]) { + await page.setViewport({ width, height: 900 }); + await page.focus('.ri-mode-btn[aria-checked="true"]'); + const modeLayout = await page.evaluate(() => { + const group = document.getElementById('ri-mode'); + const bounds = group.getBoundingClientRect(); + const style = getComputedStyle(group); + const top = bounds.top + parseFloat(style.borderTopWidth); + const bottom = bounds.bottom - parseFloat(style.borderBottomWidth); + const buttons = [...group.querySelectorAll('[role="radio"]')]; + return { + visible: bounds.width > 0 && bounds.height > 0, + contained: buttons.every((button) => { + const rect = button.getBoundingClientRect(); + return rect.top >= top - 0.5 && rect.bottom <= bottom + 0.5; + }), + groupHeight: bounds.height, + buttonHeights: buttons.map( + (button) => button.getBoundingClientRect().height, + ), + }; + }); + check( + `imagery mode buttons and focus outline fit their row at ${width}px`, + modeLayout.visible && modeLayout.contained, + JSON.stringify(modeLayout), + ); + } check('no uncaught browser errors', errors.length === 0, errors.join(' | ')); } catch (error) { console.error('Browser errors:', errors); diff --git a/src/ui/recentImagery.test.mjs b/src/ui/recentImagery.test.mjs index a77556797..fe9496688 100644 --- a/src/ui/recentImagery.test.mjs +++ b/src/ui/recentImagery.test.mjs @@ -412,7 +412,7 @@ test('the body is a fixed stack of seven blocks in a fixed order, each with a fi assert.match(rule('.recent-imagery-readout .ri-card'), /height: 112px/); assert.match( rule('.recent-imagery-readout .ri-selection'), - /grid-template-rows: 20px 20px 20px/, + /grid-template-rows: 26px 20px 20px/, ); assert.match(rule('.recent-imagery-readout .ri-slot'), /height: 20px/); assert.match(rule('.recent-imagery-readout .ri-controls'), /height: 22px/); @@ -445,7 +445,7 @@ test('no control changes place in the tree, no block is hidden and every fixed-h /** * Every block that must keep its height: the node carrying the class, that * class, and the rule it gets its height from. The mode row is the first - * 20 px track of the selection grid. The details header has no height of + * 26 px track of the selection grid. The details header has no height of * its own: it is the last block, so it moves nothing below it, and its * title is held to one line. */ @@ -491,7 +491,7 @@ test('no control changes place in the tree, no block is hidden and every fixed-h mode.parentNode.children[0] === mode ? mode.parentNode : null, 'ri-selection', readout('.ri-selection'), - /grid-template-rows: 20px 20px 20px/, + /grid-template-rows: 26px 20px 20px/, ], 'ri-slot-a': [ f.byId('ri-slot-a'), diff --git a/src/ui/styles/recent-imagery.css b/src/ui/styles/recent-imagery.css index 95c149a3c..66b351d26 100644 --- a/src/ui/styles/recent-imagery.css +++ b/src/ui/styles/recent-imagery.css @@ -309,10 +309,10 @@ html:not([lang='en']) display: none; } -/* 4. The selection bar: mode, then two rows that are always there. */ +/* 4. The selection bar: 24px mode buttons plus borders, then two stable rows. */ .recent-imagery-readout .ri-selection { display: grid; - grid-template-rows: 20px 20px 20px; + grid-template-rows: 26px 20px 20px; row-gap: 4px; padding: 5px; border: 1px solid var(--weather-card-border);