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 @@