From e5fa31d01e6cb0aec1e780a80b5c656754a3eabb Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Sun, 1 Nov 2026 10:38:25 -0800 Subject: [PATCH] feat(controls): render every segmented group with UiSegmented Street Level's fit and panorama filters and Recent Imagery's mode render through the kit's UiSegmented, and their owners answer the choice through the Street Level store (setPano, setRenderMode) and the readout's onSetMode. The imperative segmentedControl.js adapter and its test go. UiSegmented moves focus at once on a key, a radio group from the option holding focus and an arrows group from the selection, and asks for a change only when the key lands somewhere new. --- CHANGELOG.md | 5 +- docs/CURRENT-STATE.md | 15 +- docs/VUE-MIGRATION.md | 6 +- scripts/format-scope.json | 1 - scripts/package-boundaries.json | 4 +- src/ui/kit/UiSegmented.test.mjs | 26 ++- src/ui/kit/UiSegmented.vue | 31 ++-- src/ui/recentImagery.js | 13 +- src/ui/recentImagery/RecentImageryReadout.vue | 38 ++-- src/ui/segmentedControl.js | 109 ----------- src/ui/segmentedControl.test.mjs | 174 ------------------ src/ui/streetLevelControls.js | 36 +--- src/ui/views/panels/StreetLevelPanel.vue | 41 +++-- 13 files changed, 119 insertions(+), 380 deletions(-) delete mode 100644 src/ui/segmentedControl.js delete mode 100644 src/ui/segmentedControl.test.mjs diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c0810d7f..1b06ef58d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,7 +55,10 @@ `initGeoFileImport` takes `connect(actions)` the same way and no longer listens on the Import button, picker or list, nor restores focus itself: the `GeoImport` view does both, and its `publish` need not settle once the - list renders. `syncScopeControls` (`./ui/display`) and + list renders. Street Level's fit and panorama filters and Recent Imagery's + mode are `UiSegmented` too: the Street Level store's owner answers + `setPano` and `setRenderMode`, and `RecentImageryReadout` takes + `onSetMode`. `syncScopeControls` (`./ui/display`) and `shouldHideCollapsedRightPanels` (`./ui/layout`) are deprecated: the Display view and the rail now own what they did. `./ui/input` imports Vue (samehkhamis). diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index c32945e05..f56875125 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -1495,14 +1495,15 @@ Browser acceptance: `QA_BASE_URL=http://127.0.0.1:4179 node scripts/qa-action-bu ## Segmented selectors -Segmented selectors use the controlled DOM adapter in `ui/segmentedControl.js` -and the shared `gev-segmented` / `gev-segment` surface. Model coverage, label +Segmented selectors are the kit's `UiSegmented` (`ui/kit/UiSegmented.vue`) on +the shared `gev-segmented` / `gev-segment` surface. Model coverage, label allocation, drawing shape, Street View fit/panorama filters and imagery mode keep -their feature-owned values, actions and persistence. Painting a restored value -does not emit a change or move focus. Existing keyboard policies remain explicit: -Street View uses arrows/Home/End with one tab stop, imagery mode uses arrows from -its controlled selection, and Display keeps its existing native button behavior. -Pure value navigation is separate from DOM rendering for reuse by a future view. +their feature-owned values, actions and persistence: the view renders the +owner's value and sends a choice back through its store. Rendering a restored +value does not emit a change or move focus. Each group names its keyboard +policy: Street View uses `radio` (arrows/Home/End with one tab stop), imagery +mode `arrows` from its controlled selection, and Display `none`, its native +button behavior. 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. diff --git a/docs/VUE-MIGRATION.md b/docs/VUE-MIGRATION.md index 8523d8e85..91db14fa7 100644 --- a/docs/VUE-MIGRATION.md +++ b/docs/VUE-MIGRATION.md @@ -51,7 +51,11 @@ its own classes beside it. They contain no application state. owner hides the body by writing the root's `collapsed` class at once, and there is no `closable`, since Core's panels collapse rather than close. `UiSegmented` adds `keyboard` (`none`, `arrows` as in ui-vue, or `radio` with -Home and End), since each of Core's groups keeps its own keyboard policy. +Home and End), since each of Core's groups keeps its own keyboard policy, and +`itemClass`/`activeClass` for Core's segment classes. A key moves focus at +once and asks for a change only when it lands on another option. Every +segmented group in Core is a `UiSegmented`; the old `segmentedControl.js` +adapter is gone. Controls share one skin, so every panel reads the same. Every scrolling surface carries the kit's `.ui-scroll` (ui-vue's name), which draws the thin diff --git a/scripts/format-scope.json b/scripts/format-scope.json index e44dd9a1a..ed08a6430 100644 --- a/scripts/format-scope.json +++ b/scripts/format-scope.json @@ -199,7 +199,6 @@ "scripts/qa-scope-hud.mjs", "scripts/qa-segmented-controls.mjs", "src/ui/displayControls.js", - "src/ui/segmentedControl.test.mjs", "src/ui/styles/segmented-control.css", "src/ui/cameraOrientationControls.js", "src/ui/cameraOrientationControls.test.mjs", diff --git a/scripts/package-boundaries.json b/scripts/package-boundaries.json index f1f0de351..d62687ac8 100644 --- a/scripts/package-boundaries.json +++ b/scripts/package-boundaries.json @@ -929,7 +929,6 @@ "src/ui/contextSubscriptions.js", "src/ui/contextTransactions.js", "src/ui/displayControls.js", - "src/ui/segmentedControl.js", "src/ui/effects.js", "src/ui/frameRateMonitor.js", "src/ui/layerPanel.js", @@ -2653,7 +2652,6 @@ "src/ui/contextSubscriptions.js", "src/ui/contextTransactions.js", "src/ui/displayControls.js", - "src/ui/segmentedControl.js", "src/ui/effects.js", "src/ui/frameRateMonitor.js", "src/ui/layerPanel.js", @@ -3017,6 +3015,7 @@ "src/ui/kit/UiTooltip.vue", "src/ui/kit/rail.js", "src/ui/kit/UiRail.vue", + "src/ui/kit/UiSegmented.vue", "src/ui/PanelRailView.vue", "src/ui/panelNavigation.js", "src/ui/panelAccordion.js", @@ -3820,7 +3819,6 @@ "src/ui/visualSettings.js", "src/ui/stores/display.js", "src/ui/vueRuntime.js", - "src/ui/segmentedControl.js", "src/i18n/marker.js", "src/i18n/index.js", "src/i18n/locale.js", diff --git a/src/ui/kit/UiSegmented.test.mjs b/src/ui/kit/UiSegmented.test.mjs index 2dbb5bf26..f15e217c6 100644 --- a/src/ui/kit/UiSegmented.test.mjs +++ b/src/ui/kit/UiSegmented.test.mjs @@ -81,12 +81,22 @@ test('keyboard arrows: one roving stop, arrows skip disabled options and wrap', test('keyboard radio: Home and End reach the ends', async () => { const wrapper = mountGroup({ keyboard: 'radio', modelValue: 'c' }); - await wrapper.get('#seg-a').trigger('keydown', { key: 'Home' }); - await wrapper.get('#seg-a').trigger('keydown', { key: 'End' }); + const [a, , c] = wrapper.findAll('button'); + await c.trigger('keydown', { key: 'Home' }); + await wrapper.setProps({ modelValue: 'a' }); + await a.trigger('keydown', { key: 'End' }); assert.deepEqual(wrapper.emitted('update:modelValue'), [['a'], ['c']]); wrapper.unmount(); }); +test('a key that lands where it started moves focus without a change', async () => { + const wrapper = mountGroup({ keyboard: 'radio', modelValue: 'a' }); + await wrapper.get('#seg-a').trigger('keydown', { key: 'Home' }); + assert.equal(wrapper.emitted('update:modelValue'), undefined); + assert.ok(document.activeElement === wrapper.get('#seg-a').element); + wrapper.unmount(); +}); + test('a disabled group answers nothing and leaves the tab order', async () => { const wrapper = mountGroup({ disabled: true }); assert.deepEqual(tabStops(wrapper), ['-1', '-1', '-1']); @@ -95,3 +105,15 @@ test('a disabled group answers nothing and leaves the tab order', async () => { assert.equal(wrapper.emitted('update:modelValue'), undefined); wrapper.unmount(); }); + +test('a radio group moves from the focused option, an arrows group from the selection', async () => { + // The selection was restored to c while a keeps focus. + const radio = mountGroup({ keyboard: 'radio', modelValue: 'c' }); + await radio.get('#seg-a').trigger('keydown', { key: 'ArrowLeft' }); + assert.deepEqual(radio.emitted('update:modelValue'), [['c']]); + radio.unmount(); + const arrows = mountGroup({ keyboard: 'arrows', modelValue: 'c' }); + await arrows.get('#seg-a').trigger('keydown', { key: 'ArrowLeft' }); + assert.deepEqual(arrows.emitted('update:modelValue'), [['a']]); + arrows.unmount(); +}); diff --git a/src/ui/kit/UiSegmented.vue b/src/ui/kit/UiSegmented.vue index 13b690eeb..959faf3b2 100644 --- a/src/ui/kit/UiSegmented.vue +++ b/src/ui/kit/UiSegmented.vue @@ -1,5 +1,5 @@