mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 02:44:02 +00:00
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.
This commit is contained in:
1 parent
42c3cc12b1
commit
e5fa31d01e
13 files changed
+119
-380
No files matched your search
@@ -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();
|
||||
});
|
||||
+20
-11
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { computed, nextTick } from 'vue';
|
||||
import { computed } from 'vue';
|
||||
|
||||
// Core's segmented control, with @halfpixel/ui-vue UiSegmented's
|
||||
// v-model, options, label, stacked and disabled so the two kits can merge.
|
||||
@@ -50,8 +50,12 @@ function choose(index) {
|
||||
if (!option || optionOff(option)) return;
|
||||
emit('update:modelValue', option.value);
|
||||
}
|
||||
/** Select the option `delta` away, wrapping past disabled ones, or `to` an end. */
|
||||
function move({ delta = 0, to = null }) {
|
||||
/**
|
||||
* Select the option `delta` away from `from`, wrapping past disabled ones, or
|
||||
* `to` an end. A key that lands where it started (Home on the first) only
|
||||
* moves focus.
|
||||
*/
|
||||
function move({ delta = 0, to = null, from }) {
|
||||
const count = props.options.length;
|
||||
if (!count || props.disabled) return;
|
||||
const enabled = props.options
|
||||
@@ -62,23 +66,28 @@ function move({ delta = 0, to = null }) {
|
||||
if (to === 'first') target = enabled[0];
|
||||
else if (to === 'last') target = enabled.at(-1);
|
||||
else {
|
||||
target =
|
||||
selectedIndex.value < 0 ? (delta > 0 ? -1 : count) : selectedIndex.value;
|
||||
target = from < 0 ? (delta > 0 ? -1 : count) : from;
|
||||
do target = (((target + delta) % count) + count) % count;
|
||||
while (props.options[target].disabled);
|
||||
}
|
||||
choose(target);
|
||||
// Focus follows the selection once the options re-render.
|
||||
nextTick(() => items[target]?.focus());
|
||||
if (target !== from) choose(target);
|
||||
// Focus moves at once, as a radio group's does; the option is already there.
|
||||
items[target]?.focus();
|
||||
}
|
||||
const STEPS = { ArrowRight: 1, ArrowDown: 1, ArrowLeft: -1, ArrowUp: -1 };
|
||||
function onKey(event) {
|
||||
if (props.keyboard === 'none') return;
|
||||
if (STEPS[event.key]) move({ delta: STEPS[event.key] });
|
||||
// A radio group moves from the option holding focus, `arrows` from the
|
||||
// selection.
|
||||
const from =
|
||||
props.keyboard === 'radio'
|
||||
? items.indexOf(event.target)
|
||||
: selectedIndex.value;
|
||||
if (STEPS[event.key]) move({ delta: STEPS[event.key], from });
|
||||
else if (props.keyboard === 'radio' && event.key === 'Home')
|
||||
move({ to: 'first' });
|
||||
move({ to: 'first', from });
|
||||
else if (props.keyboard === 'radio' && event.key === 'End')
|
||||
move({ to: 'last' });
|
||||
move({ to: 'last', from });
|
||||
else return;
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user