feat(ui): slider now supports mixed/undefined state

This commit is contained in:
upsiflu 2025-03-25 19:53:13 +01:00
parent 0c0fd24ebd
commit 1948d9455b
2 changed files with 51 additions and 15 deletions

View File

@ -13,14 +13,18 @@ const props = defineProps<{
const keys = computed(() => Object.keys(props.options) as T[]) const keys = computed(() => Object.keys(props.options) as T[])
const model = defineModel<T>({ required: true }) const model = defineModel<T | undefined>({ required: true })
const index = computed({ const index = computed({
get () { get () {
return keys.value.indexOf(model.value) return model.value
? keys.value.indexOf(model.value)
: undefined
}, },
set (newIndex) { set (newIndex) {
model.value = keys.value[newIndex] model.value = newIndex
? keys.value[newIndex]
: undefined
} }
}) })
@ -38,7 +42,8 @@ onMounted(() => {
:style="` :style="`
--step-size: calc(100% / ${keys.length + 2}); --step-size: calc(100% / ${keys.length + 2});
--slider-width: calc(var(--step-size) * ${keys.length - 1} + 16px); --slider-width: calc(var(--step-size) * ${keys.length - 1} + 16px);
--current-step: ${index}; --slider-opacity: ${ index === undefined ? .5 : 1 };
--current-step: ${ index === undefined ? keys.length - 1 : index };
`" `"
> >
<!-- Label --> <!-- Label -->
@ -66,7 +71,7 @@ onMounted(() => {
v-for="key in keys" v-for="key in keys"
:key="key" :key="key"
:class="[$style.key, { [$style.current]: key === model } ]" :class="[$style.key, { [$style.current]: key === model } ]"
style="flex-basis:var(--step-size)" style="flex-basis: var(--step-size); padding-bottom: 8px;"
type="button" type="button"
tabindex="-1" tabindex="-1"
@click="() => { model = key; input.focus(); }" @click="() => { model = key; input.focus(); }"
@ -74,7 +79,6 @@ onMounted(() => {
{{ key }} {{ key }}
</button> </button>
</Layout> </Layout>
<Spacer size-8 />
<!-- Slider --> <!-- Slider -->
@ -83,12 +87,15 @@ onMounted(() => {
ref="input" ref="input"
v-model="index" v-model="index"
type="range" type="range"
style="width: var(--slider-width); opacity: .001;" style="width: var(--slider-width); opacity: .001; cursor: pointer;"
:max="keys.length - 1" :max="keys.length - 1"
:autofocus="autofocus || undefined" :autofocus="autofocus || undefined"
> >
<div :class="$style.range" /> <div :class="$style.range" />
<div :class="$style.pin" /> <div
v-if="model !== undefined"
:class="$style.pin"
/>
</span> </span>
<Spacer size-8 /> <Spacer size-8 />
@ -104,6 +111,7 @@ onMounted(() => {
><Markdown :md="options[key]" /></span> ><Markdown :md="options[key]" /></span>
</span> </span>
<span <span
v-if="model !== undefined"
style="position: absolute;" style="position: absolute;"
:class="$style.description" :class="$style.description"
><Markdown :md="options[model]" /></span> ><Markdown :md="options[model]" /></span>
@ -147,6 +155,7 @@ onMounted(() => {
top: 11px; top: 11px;
transition: all .1s; transition: all .1s;
pointer-events: none; pointer-events: none;
opacity: var(--slider-opacity);
} }
input:focus~.range { input:focus~.range {
// focused style // focused style

View File

@ -11,6 +11,8 @@ const options = {
} as const } as const
const option = ref<keyof typeof options>('pod') const option = ref<keyof typeof options>('pod')
const optionWithUndefined = ref<keyof typeof options | undefined>(undefined)
</script> </script>
```ts ```ts
@ -49,25 +51,50 @@ You can either specify the `label` prop or add custom Html into the `#label` slo
Add the prop `autofocus` to focus the slider as soon as it renders. Make sure to only autofocus one element per page. Add the prop `autofocus` to focus the slider as soon as it renders. Make sure to only autofocus one element per page.
## Undefined state
If you want to aggregate potentially mixed states, or start with no initial selection,
you can set v-model to `undefined`.
```ts
const optionWithUndefined = ref<keyof typeof options | undefined>(undefined)
```
```vue-html
<Slider
v-model="optionWithUndefined"
label="Privacy level?"
:options="options"
/>
```
<Spacer />
<Slider
v-model="optionWithUndefined"
label="Privacy level?"
:options="options"
/>
--- ---
Functionality: **Functionality**
- Define a list of `[value:string, description:markdown]` pairs (props) - Define a possible values
- Select one value as active (model) - Select zero or one values as active (`v-model`)
User interaction: **User interaction**
- It mimics the functionality of a single `range` input: - It mimics the functionality of a single `range` input:
- to be operated with arrow keys or mouse - to be operated with arrow keys or mouse
- focus is indicated - focus is indicated
- ticks are indicated - ticks are indicated
Design: **Design**
- A pin (same as in the toggle component) - A pin (same as in the toggle component)
- a range (very faint) - a range (very faint)
- Ticks - Ticks?
- Constant dimensions, fitting the largest text box - Constant dimensions, fitting the largest text box
- Not to be confused with a pearls navigation patterns (list of dots; indicates temporal range) - Not to be confused with a pearls navigation patterns (list of dots; indicates temporal range)