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

View File

@ -11,6 +11,8 @@ const options = {
} as const
const option = ref<keyof typeof options>('pod')
const optionWithUndefined = ref<keyof typeof options | undefined>(undefined)
</script>
```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.
## 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)
- Select one value as active (model)
- Define a possible values
- Select zero or one values as active (`v-model`)
User interaction:
**User interaction**
- It mimics the functionality of a single `range` input:
- to be operated with arrow keys or mouse
- focus is indicated
- ticks are indicated
Design:
**Design**
- A pin (same as in the toggle component)
- a range (very faint)
- Ticks
- Ticks?
- Constant dimensions, fitting the largest text box
- Not to be confused with a pearls navigation patterns (list of dots; indicates temporal range)