mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 02:44:02 +00:00
feat(kit): render the sliders with UiSlider
UiSlider takes v-model, min, max, step and disabled over Core's native range input, emits numbers, and with `hold` never rewrites a focused handle and takes the model back on blur. It is a functional render function, so the portable style parameter rows use it too. The Display, Radio, RTL-SDR, Cockpit, mission replay and style parameter sliders render with it; server-rendered markup keeps their default values.
This commit is contained in:
1 parent
fa2dc9fc87
commit
5019bd9e8a
18 files changed
+241
-50
No files matched your search
@@ -0,0 +1,63 @@
|
||||
import { h, withDirectives } from 'vue';
|
||||
import { vFieldValue } from '../rail/fieldValue.js';
|
||||
import { vRangeValue } from '../rail/rangeValue.js';
|
||||
|
||||
// Server-rendered markup carries the model as the input's value.
|
||||
const ssrValue = ({ value }) => (value == null ? {} : { value });
|
||||
const LIVE = { ...vRangeValue, getSSRProps: ssrValue };
|
||||
const HOLD = { ...vFieldValue, getSSRProps: ssrValue };
|
||||
|
||||
/** Put the model back on a held input that lost focus. */
|
||||
function takeModel(input, props) {
|
||||
const next = props.modelValue;
|
||||
if (next != null && input.value !== String(next)) input.value = String(next);
|
||||
}
|
||||
|
||||
/**
|
||||
* Core's slider, with @halfpixel/ui-vue UiSlider's v-model, min, max, step
|
||||
* and disabled so the two kits can merge. Core keeps the native range input
|
||||
* (its keyboard, form and styling) where ui-vue draws a `role="slider"`
|
||||
* track. The input carries Core's `gev-quantitative-slider` class ahead of
|
||||
* the caller's, the caller's attributes come first, as in the markup this
|
||||
* replaced, and the accessible name comes as an `aria-label` attribute so
|
||||
* the attribute pass translates it. The value emits as a number.
|
||||
*
|
||||
* The input takes the model's value only when it differs (`v-range-value`);
|
||||
* with `hold`, never while it is focused (`v-field-value`), so a render
|
||||
* cannot move a handle a person is dragging, and it takes the model back
|
||||
* when it loses focus: a model left unchanged renders nothing. Owners take
|
||||
* every value their slider can reach, some only after a delay, so an input
|
||||
* is never put back on its own. It is a functional component written as a
|
||||
* render function, so portable entries need no component compiler. Not
|
||||
* mirrored yet: `label`, `format`, `markers` and `endLabels`.
|
||||
*/
|
||||
export default function UiSlider(props, { attrs, emit }) {
|
||||
const { class: className, ...rest } = attrs;
|
||||
return withDirectives(
|
||||
h('input', {
|
||||
...rest,
|
||||
class: ['gev-quantitative-slider', className],
|
||||
type: 'range',
|
||||
min: props.min,
|
||||
max: props.max,
|
||||
step: props.step,
|
||||
disabled: props.disabled,
|
||||
onInput: (event) => emit('update:modelValue', Number(event.target.value)),
|
||||
onBlur: props.hold
|
||||
? (event) => takeModel(event.target, props)
|
||||
: undefined,
|
||||
}),
|
||||
[[props.hold ? HOLD : LIVE, props.modelValue]],
|
||||
);
|
||||
}
|
||||
|
||||
UiSlider.props = {
|
||||
modelValue: { type: [Number, String], default: undefined },
|
||||
min: { type: [Number, String], required: true },
|
||||
max: { type: [Number, String], required: true },
|
||||
step: { type: [Number, String], default: undefined },
|
||||
disabled: { type: Boolean, default: false },
|
||||
hold: { type: Boolean, default: false },
|
||||
};
|
||||
UiSlider.emits = ['update:modelValue'];
|
||||
UiSlider.inheritAttrs = false;
|
||||
@@ -0,0 +1,107 @@
|
||||
import { after, before, test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { installVueDom } from '../../testSupport/vueDom.mjs';
|
||||
|
||||
let mount;
|
||||
let UiSlider;
|
||||
let uninstall;
|
||||
before(async () => {
|
||||
uninstall = installVueDom();
|
||||
({ mount } = await import('@vue/test-utils'));
|
||||
UiSlider = (await import('./UiSlider.js')).default;
|
||||
});
|
||||
after(() => uninstall());
|
||||
|
||||
function mountSlider(props = {}, attrs = {}) {
|
||||
return mount(UiSlider, {
|
||||
attachTo: document.body,
|
||||
props: { min: 0, max: 100, modelValue: 40, ...props },
|
||||
attrs,
|
||||
});
|
||||
}
|
||||
|
||||
test('a native range input with Core’s class beside the caller’s', () => {
|
||||
const wrapper = mountSlider(
|
||||
{ step: 5 },
|
||||
{ id: 'vol', class: 'pp-slider', 'aria-label': 'Volume' },
|
||||
);
|
||||
const input = wrapper.get('input');
|
||||
assert.equal(input.attributes('type'), 'range');
|
||||
assert.equal(input.attributes('class'), 'gev-quantitative-slider pp-slider');
|
||||
assert.equal(input.attributes('id'), 'vol');
|
||||
assert.equal(input.attributes('aria-label'), 'Volume');
|
||||
assert.equal(input.attributes('step'), '5');
|
||||
assert.equal(input.attributes('value'), undefined);
|
||||
assert.equal(input.element.value, '40');
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
test('no step attribute unless one is given, and disabled reaches the input', () => {
|
||||
const wrapper = mountSlider({ disabled: true });
|
||||
const input = wrapper.get('input');
|
||||
assert.equal(input.attributes('step'), undefined);
|
||||
assert.equal(input.element.disabled, true);
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
test('a drag emits the value as a number', async () => {
|
||||
const wrapper = mountSlider({ min: 0.25, max: 4, step: 0.25, modelValue: 1 });
|
||||
const input = wrapper.get('input');
|
||||
input.element.value = '2.5';
|
||||
await input.trigger('input');
|
||||
assert.deepEqual(wrapper.emitted('update:modelValue'), [[2.5]]);
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
test('the model moves the handle, even while focused, unless it holds', async () => {
|
||||
const live = mountSlider();
|
||||
live.element.focus();
|
||||
await live.setProps({ modelValue: 70 });
|
||||
assert.equal(live.element.value, '70');
|
||||
live.unmount();
|
||||
|
||||
const held = mountSlider({ hold: true });
|
||||
held.element.focus();
|
||||
held.element.value = '55';
|
||||
await held.setProps({ modelValue: 70 });
|
||||
assert.equal(held.element.value, '55');
|
||||
held.element.blur();
|
||||
await held.setProps({ modelValue: 80 });
|
||||
assert.equal(held.element.value, '80');
|
||||
held.unmount();
|
||||
});
|
||||
|
||||
test('a held slider with no model keeps its value', async () => {
|
||||
const wrapper = mountSlider({ hold: true, modelValue: 30 });
|
||||
await wrapper.setProps({ modelValue: null });
|
||||
assert.equal(wrapper.element.value, '30');
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
test('a held handle takes the model back when it loses focus', async () => {
|
||||
const wrapper = mountSlider({ hold: true, modelValue: 60 });
|
||||
const input = wrapper.get('input');
|
||||
input.element.focus();
|
||||
input.element.value = '35';
|
||||
await input.trigger('input');
|
||||
await wrapper.setProps({ modelValue: 60 });
|
||||
assert.equal(input.element.value, '35');
|
||||
input.element.blur();
|
||||
await input.trigger('blur');
|
||||
assert.equal(input.element.value, '60');
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
test('an input stays until the owner answers, however late', async () => {
|
||||
for (const hold of [false, true]) {
|
||||
const wrapper = mountSlider({ hold, modelValue: 40 });
|
||||
const input = wrapper.get('input');
|
||||
input.element.value = '70';
|
||||
await input.trigger('input');
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
assert.equal(input.element.value, '70', `hold: ${hold}`);
|
||||
await wrapper.setProps({ modelValue: 70 });
|
||||
assert.equal(input.element.value, '70', `hold: ${hold}`);
|
||||
wrapper.unmount();
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user