mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
feat(kit): render the selects with UiSelect
UiSelect renders the bare native select with v-model, options and disabled. Options that carry their own attributes come through the slot, a caller's own change handler still reaches the select, and hold never rewrites a focused select and takes the model back on blur. Without hold, a select takes its model back once its change has rendered, so a pick its owner refuses goes back, as the raw value binding did. The HUD layout, draw colour, RTL-SDR gain, radio station tag, voice provider, voice timeout, scene and CCTV camera selects now render through it.
This commit is contained in:
1 parent
28ca784d49
commit
ecaf723035
13 files changed
+234
-18
No files matched your search
@@ -0,0 +1,68 @@
|
||||
import { h, mergeProps, nextTick, withDirectives } from 'vue';
|
||||
import { vFieldValue } from '../rail/fieldValue.js';
|
||||
import { vRangeValue } from '../rail/rangeValue.js';
|
||||
|
||||
// A null model leaves the select as it is.
|
||||
const live = (el, binding) => {
|
||||
if (binding.value != null) vRangeValue.updated(el, binding);
|
||||
};
|
||||
const LIVE = { mounted: live, updated: live };
|
||||
|
||||
/** Put the model back on the select. */
|
||||
function takeModel(select, props) {
|
||||
const next = props.modelValue;
|
||||
if (next != null && select.value !== String(next))
|
||||
select.value = String(next);
|
||||
}
|
||||
|
||||
/**
|
||||
* Core's select, with @halfpixel/ui-vue UiSelect's v-model, options and
|
||||
* disabled so the two kits can merge. Core renders the bare select: its
|
||||
* labels sit outside it or come as an `aria-label` attribute, so the attribute
|
||||
* pass translates them. Options come from `options` ({ value, label }) or,
|
||||
* where they carry their own attributes, the default slot. A change emits the
|
||||
* chosen value; a caller's own `@change` still reaches the select.
|
||||
*
|
||||
* The select takes the model's value only when it differs (`v-range-value`),
|
||||
* after its options are in place; a null model leaves it as it is. With
|
||||
* `hold`, it never takes it while focused (`v-field-value`), and takes it back
|
||||
* when it loses focus. Without `hold`, it takes the model back once the
|
||||
* change's render has run: an owner that refuses a pick republishes the same
|
||||
* value, which renders nothing, so this puts the refused pick back. The
|
||||
* owners of these selects answer synchronously. Like UiSlider it is a functional render-function
|
||||
* component. Not mirrored yet: `label` and `hideLabel`.
|
||||
*/
|
||||
export default function UiSelect(props, { attrs, emit, slots }) {
|
||||
const options = props.options
|
||||
? props.options.map((option) =>
|
||||
h('option', { key: option.value, value: option.value }, option.label),
|
||||
)
|
||||
: slots.default?.();
|
||||
const select = h(
|
||||
'select',
|
||||
mergeProps(attrs, {
|
||||
disabled: props.disabled,
|
||||
onChange: (event) => {
|
||||
const select = event.target;
|
||||
emit('update:modelValue', select.value);
|
||||
if (!props.hold) void nextTick(() => takeModel(select, props));
|
||||
},
|
||||
onBlur: props.hold
|
||||
? (event) => takeModel(event.target, props)
|
||||
: undefined,
|
||||
}),
|
||||
options,
|
||||
);
|
||||
return withDirectives(select, [
|
||||
[props.hold ? vFieldValue : LIVE, props.modelValue],
|
||||
]);
|
||||
}
|
||||
|
||||
UiSelect.props = {
|
||||
modelValue: { type: [String, Number], default: undefined },
|
||||
options: { type: Array, default: undefined },
|
||||
disabled: { type: Boolean, default: false },
|
||||
hold: { type: Boolean, default: false },
|
||||
};
|
||||
UiSelect.emits = ['update:modelValue'];
|
||||
UiSelect.inheritAttrs = false;
|
||||
@@ -0,0 +1,132 @@
|
||||
import { after, before, test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { installVueDom } from '../../testSupport/vueDom.mjs';
|
||||
|
||||
let mount;
|
||||
let vue;
|
||||
let UiSelect;
|
||||
let uninstall;
|
||||
before(async () => {
|
||||
uninstall = installVueDom();
|
||||
({ mount } = await import('@vue/test-utils'));
|
||||
vue = await import('vue');
|
||||
UiSelect = (await import('./UiSelect.js')).default;
|
||||
});
|
||||
after(() => uninstall());
|
||||
|
||||
const OPTIONS = [
|
||||
{ value: 'a', label: 'Alpha' },
|
||||
{ value: 'b', label: 'Bravo' },
|
||||
{ value: 'c', label: 'Charlie' },
|
||||
];
|
||||
|
||||
function mountSelect(props = {}, attrs = {}) {
|
||||
return mount(UiSelect, {
|
||||
attachTo: document.body,
|
||||
props: { options: OPTIONS, modelValue: 'b', ...props },
|
||||
attrs,
|
||||
});
|
||||
}
|
||||
|
||||
test('a bare select with the caller’s attributes and the model chosen', () => {
|
||||
const wrapper = mountSelect(
|
||||
{},
|
||||
{ id: 'pick', class: 'gev-field', 'aria-label': 'Pick' },
|
||||
);
|
||||
const select = wrapper.get('select');
|
||||
assert.equal(select.attributes('class'), 'gev-field');
|
||||
assert.equal(select.attributes('id'), 'pick');
|
||||
assert.equal(select.attributes('aria-label'), 'Pick');
|
||||
assert.equal(select.attributes('value'), undefined);
|
||||
assert.deepEqual(
|
||||
[...select.element.options].map((o) => [o.value, o.textContent]),
|
||||
[
|
||||
['a', 'Alpha'],
|
||||
['b', 'Bravo'],
|
||||
['c', 'Charlie'],
|
||||
],
|
||||
);
|
||||
assert.equal(select.element.value, 'b');
|
||||
assert.equal(select.element.disabled, false);
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
test('a change emits the value, and a caller’s own change still runs', async () => {
|
||||
const seen = [];
|
||||
const wrapper = mountSelect(
|
||||
{},
|
||||
{ onChange: (event) => seen.push(event.target.value) },
|
||||
);
|
||||
const select = wrapper.get('select');
|
||||
select.element.value = 'c';
|
||||
await select.trigger('change');
|
||||
assert.deepEqual(wrapper.emitted('update:modelValue'), [['c']]);
|
||||
assert.deepEqual(seen, ['c']);
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
test('a refused pick goes back to the model', async () => {
|
||||
const { nextTick } = vue;
|
||||
const wrapper = mountSelect();
|
||||
const select = wrapper.get('select');
|
||||
select.element.value = 'c';
|
||||
await select.trigger('change');
|
||||
await nextTick();
|
||||
assert.equal(select.element.value, 'b');
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
test('the model picks the option, and a null model leaves the select', async () => {
|
||||
const wrapper = mountSelect();
|
||||
const select = wrapper.get('select').element;
|
||||
await wrapper.setProps({ modelValue: 'c' });
|
||||
assert.equal(select.value, 'c');
|
||||
select.value = 'a';
|
||||
await wrapper.setProps({ modelValue: null, disabled: true });
|
||||
assert.equal(select.value, 'a');
|
||||
assert.equal(select.disabled, true);
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
test('a held select keeps a focused pick and takes the model back on blur', async () => {
|
||||
const wrapper = mountSelect({ hold: true });
|
||||
const select = wrapper.get('select').element;
|
||||
select.focus();
|
||||
select.value = 'a';
|
||||
await wrapper.setProps({ modelValue: 'c' });
|
||||
assert.equal(select.value, 'a');
|
||||
select.blur();
|
||||
await wrapper.get('select').trigger('blur');
|
||||
assert.equal(select.value, 'c');
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
test('slotted options render, and the model applies once they arrive', async () => {
|
||||
const { defineComponent, h, ref, nextTick } = vue;
|
||||
const options = ref(['x']);
|
||||
const model = ref('x');
|
||||
const wrapper = mount(
|
||||
defineComponent({
|
||||
setup: () => () =>
|
||||
h(
|
||||
UiSelect,
|
||||
{ modelValue: model.value },
|
||||
{
|
||||
default: () =>
|
||||
options.value.map((value) =>
|
||||
h('option', { key: value, value, 'data-i18n': '' }, value),
|
||||
),
|
||||
},
|
||||
),
|
||||
}),
|
||||
{ attachTo: document.body },
|
||||
);
|
||||
const select = wrapper.get('select').element;
|
||||
assert.equal(select.options[0].hasAttribute('data-i18n'), true);
|
||||
options.value = ['x', 'y'];
|
||||
model.value = 'y';
|
||||
await nextTick();
|
||||
assert.equal(select.options.length, 2);
|
||||
assert.equal(select.value, 'y');
|
||||
wrapper.unmount();
|
||||
});
|
||||
Reference in new issue
Block a user