mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 02:14:01 +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
+2
-1
@@ -68,7 +68,8 @@
|
||||
kit's `UiButton`, which gains `pressedClass`; so are the rail card
|
||||
chips, map source chips and location pills (which now report
|
||||
`aria-pressed`), and the voice card's chips
|
||||
are the kit's new `UiChip`. `syncScopeControls`
|
||||
are the kit's new `UiChip`. The selects are the kit's new `UiSelect`.
|
||||
`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).
|
||||
|
||||
@@ -65,6 +65,13 @@ tilt-map button, whose owners still write their state into the DOM. Core's
|
||||
pressable chips and pills (rail card chips, map sources, cities and places)
|
||||
are `UiButton`s too; `UiChip` is ui-vue's non-interactive label chip, used by
|
||||
the voice card.
|
||||
`UiSelect` renders the bare native select (Core's labels sit outside it or
|
||||
come as an `aria-label`) with ui-vue's `v-model`, `options` and `disabled`;
|
||||
options that carry their own attributes come through the slot, a caller's
|
||||
`@change` still reaches the select, and `hold` works as in `UiSlider`.
|
||||
Without `hold`, a select takes its model back after its own change renders,
|
||||
so a pick its owner refuses goes back; an owner that answers later (the SDR
|
||||
gain) takes `hold`. Every select in the views is a `UiSelect`.
|
||||
`UiSlider` keeps the native range input where ui-vue draws a
|
||||
`role="slider"` track, so Core's keyboard, form behaviour and styling stay;
|
||||
it takes ui-vue's `v-model`, `min`, `max`, `step` and `disabled`, emits a
|
||||
|
||||
@@ -1134,6 +1134,8 @@
|
||||
"src/ui/kit/UiButton.test.mjs",
|
||||
"src/ui/kit/UiChip.vue",
|
||||
"src/ui/kit/UiChip.test.mjs",
|
||||
"src/ui/kit/UiSelect.js",
|
||||
"src/ui/kit/UiSelect.test.mjs",
|
||||
"src/ui/rail/RailChips.vue",
|
||||
"src/ui/displayPanelActions.test.mjs",
|
||||
"src/testSupport/vueDom.mjs",
|
||||
|
||||
@@ -135,7 +135,8 @@
|
||||
"src/ui/rail/panelState.js",
|
||||
"src/sdr/gain.js",
|
||||
"src/ui/kit/UiSlider.js",
|
||||
"src/ui/kit/UiChip.vue"
|
||||
"src/ui/kit/UiChip.vue",
|
||||
"src/ui/kit/UiSelect.js"
|
||||
],
|
||||
"external": ["vue", "pinia"]
|
||||
},
|
||||
|
||||
@@ -94,7 +94,7 @@ test('the Draw control markup carries the ids the tool binds to', () => {
|
||||
"act('toggleDraw')",
|
||||
"act('setDrawShape', $event)",
|
||||
"act('setDrawLabel', $event.target.value)",
|
||||
"act('setDrawColor', $event.target.value)",
|
||||
"act('setDrawColor', $event)",
|
||||
"act('clearDraw')",
|
||||
]) {
|
||||
assert.ok(html.includes(action), `DisplayPanel must call ${action}`);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup>
|
||||
import { storeToRefs } from 'pinia';
|
||||
import UiSelect from '../kit/UiSelect.js';
|
||||
import { useCctvStore } from '../stores/cctv.js';
|
||||
|
||||
// The camera picker: the cameras in view and a closing note for those the
|
||||
@@ -19,7 +20,7 @@ const vPick = { mounted: pick, updated: pick };
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<select v-pick="picker?.selection" :disabled="picker?.disabled"
|
||||
<UiSelect v-pick="picker?.selection" :disabled="picker?.disabled"
|
||||
><option
|
||||
v-for="option in picker?.options"
|
||||
:key="option.value"
|
||||
@@ -27,6 +28,6 @@ const vPick = { mounted: pick, updated: pick };
|
||||
>{{ option.label }}</option
|
||||
><option v-if="picker?.note" value="" disabled data-cctv-outside>{{
|
||||
picker.note
|
||||
}}</option></select
|
||||
}}</option></UiSelect
|
||||
>
|
||||
</template>
|
||||
@@ -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();
|
||||
});
|
||||
@@ -11,6 +11,7 @@ import SceneOptions from './SceneOptions.vue';
|
||||
import SceneShotList from './SceneShotList.vue';
|
||||
import CctvPanel from './panels/CctvPanel.vue';
|
||||
import StreetLevelPanel from './panels/StreetLevelPanel.vue';
|
||||
import UiSelect from '../kit/UiSelect.js';
|
||||
|
||||
// Scene sharing (src/scenes/sharing.js) offers its authoring bar and presents
|
||||
// its dialog through this store; without this view, neither is drawn.
|
||||
@@ -52,7 +53,7 @@ async function importChosen(event) {
|
||||
<RailPanel id="scene-panel" start-collapsed v-class-toggle="{ running: scene?.running, 'has-shots': scene?.hasShots }" :title="t('AUTHORING')" :hint="t('Create and edit scenes')" scroll>
|
||||
<p class="scene-step-heading" data-i18n>SCENE</p>
|
||||
<div class="scene-controls">
|
||||
<select class="gev-field" id="scene-select" aria-label="Scene" data-i18n-aria-label :disabled="off('select')" @change="scenes.act('selectScene', $event.currentTarget.value)"><SceneOptions /></select>
|
||||
<UiSelect class="gev-field" id="scene-select" aria-label="Scene" data-i18n-aria-label :disabled="off('select')" @update:model-value="scenes.act('selectScene', $event)"><SceneOptions /></UiSelect>
|
||||
<UiButton id="scene-new-btn" type="button" :disabled="off('new')" @click="scenes.act('create')" class="gev-action-button scene-btn" title="Start a new, empty scene" data-i18n data-i18n-title>NEW SCENE</UiButton>
|
||||
<UiButton tone="alert" id="scene-delete-btn" type="button" :disabled="off('delete')" @click="scenes.act('deleteScene')" class="gev-action-button gev-action-button--danger scene-btn scene-btn-danger" title="Delete this scene and all of its shots" data-i18n data-i18n-title>DELETE</UiButton>
|
||||
</div>
|
||||
|
||||
@@ -10,6 +10,7 @@ import UiSlider from '../../kit/UiSlider.js';
|
||||
import GeoImport from './GeoImport.vue';
|
||||
import { useDisplayStore } from '../../stores/display.js';
|
||||
import UiButton from '../../kit/UiButton.vue';
|
||||
import UiSelect from '../../kit/UiSelect.js';
|
||||
|
||||
// The Display panel. VisualSettings presents the HUD, Sonar, Scope,
|
||||
// Celestial, detection, bloom and sharpen, AircraftDisplay the 3D group and
|
||||
@@ -68,11 +69,11 @@ const allocationOptions = [
|
||||
</button>
|
||||
<div class="pp-slider-row" :class="{ visible: hud?.visible }" id="hud-layout-row">
|
||||
<span class="pp-slider-mini-label" data-i18n>Layout</span>
|
||||
<select id="hud-layout-select" @change="act('setHudLayout', $event.target.value)" class="pp-select gev-field gev-field--compact" v-range-value="hud?.variant ?? 'tactical'" aria-label="HUD layout" data-i18n-aria-label>
|
||||
<UiSelect id="hud-layout-select" class="pp-select gev-field gev-field--compact" :model-value="hud?.variant ?? 'tactical'" @update:model-value="act('setHudLayout', $event)" aria-label="HUD layout" data-i18n-aria-label>
|
||||
<option value="tactical" data-i18n>Tactical</option>
|
||||
<option value="minimal" data-i18n>Minimal</option>
|
||||
<option value="cyber" data-i18n>Cyber</option>
|
||||
</select>
|
||||
</UiSelect>
|
||||
</div>
|
||||
<div class="pp-slider-row cyber-sonar-control cyber-sonar-toggle-row" id="cyber-sonar-row">
|
||||
<span class="pp-slider-mini-label" data-i18n>Sonar</span>
|
||||
@@ -176,13 +177,13 @@ const allocationOptions = [
|
||||
</div>
|
||||
<div class="pp-slider-row" :class="{ visible: draw?.active }" id="draw-label-row">
|
||||
<input type="text" class="pp-text-input" id="draw-label-input" v-range-value="draw?.label ?? ''" @input="act('setDrawLabel', $event.target.value)" maxlength="120" placeholder="Label (optional)" aria-label="Label for the drawn shape" data-i18n-placeholder data-i18n-aria-label autocomplete="off" spellcheck="false">
|
||||
<select class="pp-select draw-color-select gev-field gev-field--compact" id="draw-color-select" v-range-value="draw?.color ?? 'primary'" @change="act('setDrawColor', $event.target.value)" aria-label="Colour of the drawn shape" data-i18n-aria-label>
|
||||
<UiSelect class="pp-select draw-color-select gev-field gev-field--compact" id="draw-color-select" :model-value="draw?.color ?? 'primary'" @update:model-value="act('setDrawColor', $event)" aria-label="Colour of the drawn shape" data-i18n-aria-label>
|
||||
<option value="primary" data-i18n>Primary</option>
|
||||
<option value="amber" data-i18n>Amber</option>
|
||||
<option value="cyan" data-i18n>Cyan</option>
|
||||
<option value="green" data-i18n>Green</option>
|
||||
<option value="red" data-i18n>Red</option>
|
||||
</select>
|
||||
</UiSelect>
|
||||
<button class="pp-mode-btn draw-clear-btn" id="draw-clear" type="button" @click="act('clearDraw')" title="Remove every mark from the board" data-i18n-title data-i18n>Clear</button>
|
||||
<span class="draw-hint" id="draw-hint" aria-live="polite">{{ draw?.hint ?? '' }}</span>
|
||||
</div>
|
||||
|
||||
@@ -5,11 +5,11 @@ import { t } from '../../../i18n/index.js';
|
||||
import RailPanel from '../../rail/RailPanel.vue';
|
||||
import { vClassToggle } from '../../rail/classToggle.js';
|
||||
import UiSlider from '../../kit/UiSlider.js';
|
||||
import { vRangeValue } from '../../rail/rangeValue.js';
|
||||
import { useRadioStore } from '../../stores/radio.js';
|
||||
import RadioTuner from './RadioTuner.vue';
|
||||
import SdrCard from './SdrCard.vue';
|
||||
import UiButton from '../../kit/UiButton.vue';
|
||||
import UiSelect from '../../kit/UiSelect.js';
|
||||
|
||||
// The Radio panel inside Context. RadioControls presents the directory row,
|
||||
// station card, transport, volume and playback line and publishes them;
|
||||
@@ -41,9 +41,9 @@ const radioWords = {
|
||||
<div class="radio-directory-row">
|
||||
<UiButton id="radio-enable-btn" @click="act('toggleRadio', 'panel')" class="panel-layer-toggle" :pressed="radio?.enable.active ?? false" pressed-class="active" :aria-label="radio?.enable.label" :aria-disabled="radio ? String(radio.enable.busy) : undefined" :aria-busy="radio ? String(radio.enable.busy) : undefined">{{ radio ? radio.enable.text : t('ENABLE') }}</UiButton>
|
||||
<label class="radio-filter-label" for="radio-filter" data-i18n>STATION TAG</label>
|
||||
<select class="gev-field" id="radio-filter" @change="setFilter($event.target)" aria-label="Filter stations by station tag" :disabled="radio ? radio.filter.disabled : true" v-bind="radio?.filter.color ? { style: { color: radio.filter.color } } : {}" v-range-value="radio ? radio.filter.value : 'all'" data-i18n-aria-label>
|
||||
<UiSelect class="gev-field" id="radio-filter" @change="setFilter($event.target)" aria-label="Filter stations by station tag" :disabled="radio ? radio.filter.disabled : true" v-bind="radio?.filter.color ? { style: { color: radio.filter.color } } : {}" :model-value="radio ? radio.filter.value : 'all'" data-i18n-aria-label>
|
||||
<option v-if="!radio?.filter.options" value="all">{{ t('All', { context: 'radio tag' }) }}</option><template v-else><option v-for="option in radio.filter.options" :key="option.id" :value="option.id" :data-radio-color="option.color" :style="{ color: option.color }" :aria-label="option.label">● {{ option.label }}</option></template>
|
||||
</select>
|
||||
</UiSelect>
|
||||
</div>
|
||||
<div id="radio-station-card" class="radio-station-card" aria-live="polite">
|
||||
<strong id="radio-station-name">{{ radio ? radio.station.name : t('NO STATION SELECTED') }}</strong>
|
||||
|
||||
@@ -9,6 +9,7 @@ import { vFieldValue } from '../../rail/fieldValue.js';
|
||||
import UiSlider from '../../kit/UiSlider.js';
|
||||
import { useRadioStore } from '../../stores/radio.js';
|
||||
import UiButton from '../../kit/UiButton.vue';
|
||||
import UiSelect from '../../kit/UiSelect.js';
|
||||
|
||||
// The local RTL-SDR card in the Radio panel. LocalSdrControls presents it and
|
||||
// publishes it; until it does, the card shows its initial markup. Its
|
||||
@@ -40,10 +41,10 @@ const tune = () => act('tuneSdr', frequency.value.value);
|
||||
</div>
|
||||
<div class="sdr-gain-row">
|
||||
<label for="sdr-gain-select" data-i18n>GAIN</label>
|
||||
<select class="gev-field" id="sdr-gain-select" @change="act('setSdrGain', $event.target.value)" aria-label="RTL-SDR tuner gain for the current mode" :disabled="sdr?.gainDisabled" v-field-value="sdr ? sdr.gain : 'auto'" data-i18n-aria-label>
|
||||
<UiSelect class="gev-field" id="sdr-gain-select" hold :model-value="sdr ? sdr.gain : 'auto'" @update:model-value="act('setSdrGain', $event)" aria-label="RTL-SDR tuner gain for the current mode" :disabled="sdr?.gainDisabled" data-i18n-aria-label>
|
||||
<option value="auto" data-i18n>AUTO</option>
|
||||
<option v-for="gain in GAINS" :key="gain" :value="gain" translate="no">{{ localSdrGainOptionLabel(gain) }}</option>
|
||||
</select>
|
||||
</UiSelect>
|
||||
</div>
|
||||
<div id="sdr-adsb-stats" class="sdr-adsb-stats" role="status" aria-live="off" :hidden="sdr ? sdr.statsHidden : true">
|
||||
<span><output id="sdr-stat-rate">{{ sdr ? sdr.stats.rate : '--' }}</output> <span data-i18n>MSG/S</span></span>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useVoiceStore } from '../../stores/voice.js';
|
||||
import VoiceCard from './VoiceCard.vue';
|
||||
import VoiceVisualizer from './VoiceVisualizer.vue';
|
||||
import UiButton from '../../kit/UiButton.vue';
|
||||
import UiSelect from '../../kit/UiSelect.js';
|
||||
|
||||
// The voice control in the command dock, as its presenter
|
||||
// (src/voice/control.js) holds it. The session owners write the presenter;
|
||||
@@ -52,7 +53,7 @@ const rootData = (root) => ({
|
||||
<div id="gev-voice-control" v-bind="rootData(view.root)" v-class-toggle="{ 'error-dismissed': view.root.errorDismissed }" @pointerleave="emit('pointerleave', $event)" @pointerenter="emit('pointerenter', $event)">
|
||||
<label id="gev-voice-provider-field" class="gev-voice-provider-field" :hidden="view.provider.hidden">
|
||||
<span>{{ t('Voice provider') }}</span>
|
||||
<select id="gev-voice-provider" class="gev-field" :aria-label="t('Voice provider')" :title="view.provider.title" :value="view.provider.value" @change="emit('provider-change', $event.target.value)"><option v-for="option in view.provider.options" :key="option.value" :value="option.value">{{ option.label }}</option></select>
|
||||
<UiSelect id="gev-voice-provider" class="gev-field" :aria-label="t('Voice provider')" :title="view.provider.title" :model-value="view.provider.value" @update:model-value="emit('provider-change', $event)"><option v-for="option in view.provider.options" :key="option.value" :value="option.value">{{ option.label }}</option></UiSelect>
|
||||
</label>
|
||||
<button v-if="view.panel" id="gev-voice-settings-button" class="gev-voice-settings-button" type="button" :aria-label="t('Voice Settings')" aria-controls="gev-voice-panel" :aria-expanded="String(view.voicePanel.settingsExpanded)" @click="emit('settings-button')"><span class="material-symbols-outlined gev-voice-settings-icon" aria-hidden="true">settings</span></button>
|
||||
<div class="gev-voice-heading">
|
||||
@@ -96,10 +97,10 @@ const rootData = (root) => ({
|
||||
<div class="gev-voice-timeout-group">
|
||||
<label class="gev-voice-preference gev-voice-setting-title" for="gev-voice-timeout-select">{{ t('Voice inactivity timeout') }}</label>
|
||||
<div class="gev-voice-timeout-controls">
|
||||
<select id="gev-voice-timeout-select" class="gev-field" aria-describedby="gev-voice-timeout-note gev-voice-timeout-status" :value="view.preferences.timeoutChoice" @change="emit('timeout-select', $event.target.value)">
|
||||
<UiSelect id="gev-voice-timeout-select" class="gev-field" aria-describedby="gev-voice-timeout-note gev-voice-timeout-status" :model-value="view.preferences.timeoutChoice" @update:model-value="emit('timeout-select', $event)">
|
||||
<option value="3">3 {{ t('Minutes') }}</option><option value="5">5 {{ t('Minutes') }}</option><option value="10">10 {{ t('Minutes') }}</option><option value="15">15 {{ t('Minutes') }}</option>
|
||||
<option value="custom">{{ t('Custom') }}</option><option value="0">{{ t('None') }}</option>
|
||||
</select>
|
||||
</UiSelect>
|
||||
<label class="gev-voice-timeout-unit" for="gev-voice-timeout-input">{{ t('Minutes') }}</label>
|
||||
<input id="gev-voice-timeout-input" type="number" min="1" max="60" step="1" aria-describedby="gev-voice-timeout-note" v-owned-value="{ text: view.preferences.timeoutMinutes, revision: view.preferences.revision }" :hidden="view.preferences.timeoutInputHidden" @change="emit('timeout-input', $event.target.value)" />
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user