Skip to content

Commit

Permalink
Merge pull request #573 from madeindjs/WF-4
Browse files Browse the repository at this point in the history
feat(ui): Implement range selector. WF-4
  • Loading branch information
ramedina86 authored Oct 22, 2024
2 parents 0aa2c42 + f864eed commit 874d5be
Show file tree
Hide file tree
Showing 13 changed files with 493 additions and 84 deletions.
Binary file added docs/framework/public/components/rangeinput.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 14 additions & 0 deletions src/ui/src/components/core/base/BaseInputSlider.utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { computed, Ref } from "vue";

/**
* Format a number using `toFixed` according to the number of floating number in the `step`
*/
export function useNumberFormatByStep(
value: Ref<number | string>,
step: Ref<number>,
) {
const precision = computed(
() => String(step.value).split(".")[1]?.length ?? 0,
);
return computed(() => Number(value.value).toFixed(precision.value));
}
90 changes: 90 additions & 0 deletions src/ui/src/components/core/base/BaseInputSlider.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<template>
<BaseInputSliderLayout
:min="min"
:max="max"
:popover-display-mode="popoverDisplayMode"
:aria-valuenow="value"
:aria-valuetext="`${displayValue} (${Math.round(progress)}%)`"
>
<div
ref="slider"
class="BaseInputRange__slider"
@mousedown="thumb.handleMouseDown"
>
<div
class="BaseInputRange__slider__progress"
:style="{ width: `calc(${progress}% + ${thumbRadius}px)` }"
></div>
</div>
<BaseInputRangeThumb
ref="thumb"
:value="value"
:min="min"
:max="max"
:step="step"
:popover-display-mode="popoverDisplayMode"
:slider-bounding-rect="sliderBoundingRect"
@update:value="model = $event"
/>
</BaseInputSliderLayout>
</template>

<script setup lang="ts">
import {
computed,
PropType,
ref,
ComponentInstance,
toRef,
watch,
ComputedRef,
} from "vue";
import BaseInputRangeThumb from "./BaseInputSliderThumb.vue";
import BaseInputSliderLayout from "./BaseInputSliderLayout.vue";
import { useBoundingClientRect } from "@/composables/useBoundingClientRect";
import { useNumberFormatByStep } from "./BaseInputSlider.utils";
const thumbRadius = 9;
const props = defineProps({
min: { type: Number, default: 0 },
max: { type: Number, default: 100 },
step: { type: Number, default: 1 },
popoverDisplayMode: {
type: String as PropType<"always" | "onChange">,
default: "onChange",
},
});
const model = defineModel("value", { type: Number, default: 50 });
const slider = ref<HTMLElement>();
const thumb = ref<ComponentInstance<typeof BaseInputRangeThumb>>();
const displayValue = useNumberFormatByStep(model, toRef(props, "step"));
const progress = computed(() => {
if (typeof model.value !== "number") return 50;
return ((model.value - props.min) / (props.max - props.min)) * 100;
});
const sliderBoundingRect = useBoundingClientRect(slider);
// update the `value` if the `min` or `max` change and `value` is outside of the range
watch(
() => props.min,
() => {
if (typeof model.value === "number" && model.value < props.min)
model.value = props.min;
},
{ immediate: true },
);
watch(
() => props.max,
() => {
if (typeof model.value === "number" && model.value > props.max)
model.value = props.max;
},
{ immediate: true },
);
</script>
60 changes: 60 additions & 0 deletions src/ui/src/components/core/base/BaseInputSliderLayout.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<template>
<div
role="slider"
:aria-valuemin="min"
:aria-valuemax="max"
class="BaseInputRange"
:class="{
'BaseInputRange--popover-always-visible':
popoverDisplayMode === 'always',
}"
>
<slot />
</div>
</template>

<script setup lang="ts">
import { PropType } from "vue";
defineProps({
min: { type: Number, default: 0 },
max: { type: Number, default: 100 },
popoverDisplayMode: {
type: String as PropType<"always" | "onChange">,
default: "onChange",
},
});
</script>

<style scoped>
.BaseInputRange {
--thumb-color: var(--accentColor);
--thumb-shadow-color: rgba(228, 231, 237, 0.4);
--slider-color: var(--softenedAccentColor);
--slider-bg-color: var(--separatorColor);
--popover-bg-color: var(--popoverBackgroundColor, rgba(0, 0, 0, 1));
width: 100%;
position: relative;
padding-top: 5px;
padding-bottom: 5px;
}
.BaseInputRange--popover-always-visible {
/* add extra margin to make sure the popover does not overflow on something */
margin-top: 24px;
}
:deep(.BaseInputRange__slider) {
height: 8px;
width: 100%;
border-radius: 4px;
background-color: var(--slider-bg-color);
}
:deep(.BaseInputRange__slider__progress) {
height: 100%;
max-width: 100%;
border-radius: 4px;
background-color: var(--slider-color);
}
</style>
120 changes: 120 additions & 0 deletions src/ui/src/components/core/base/BaseInputSliderRange.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
<template>
<BaseInputSliderLayout
class="BaseInputRangeSlider"
:min="min"
:max="max"
:popover-display-mode="popoverDisplayMode"
:aria-valuetext="displayValue"
>
<div
ref="slider"
class="BaseInputRange__slider"
@mousedown="handleMouseDown"
>
<div
class="BaseInputRange__slider__progress"
:style="{
width: `calc(${progress}% + ${thumbRadius}px)`,
marginLeft: `${progressOffset}%`,
}"
></div>
</div>
<BaseInputRangeThumb
v-for="key in [0, 1]"
:key="key"
ref="thumbs"
:value="value[key]"
:min="min"
:max="max"
:step="step"
:popover-display-mode="popoverDisplayMode"
:slider-bounding-rect="sliderBoundingRect"
@update:value="onUpdate(key ? 1 : 0, $event)"
/>
</BaseInputSliderLayout>
</template>

<script setup lang="ts">
import { ComponentInstance, computed, PropType, ref, toRef, watch } from "vue";
import BaseInputRangeThumb from "./BaseInputSliderThumb.vue";
import { useBoundingClientRect } from "@/composables/useBoundingClientRect";
import BaseInputSliderLayout from "./BaseInputSliderLayout.vue";
import { useNumberFormatByStep } from "./BaseInputSlider.utils";
const thumbRadius = 9;
const props = defineProps({
min: { type: Number, default: 0 },
max: { type: Number, default: 100 },
step: { type: Number, default: 1 },
popoverDisplayMode: {
type: String as PropType<"always" | "onChange">,
default: "onChange",
},
});
const model = defineModel("value", {
type: Array as PropType<number[]>,
default: () => [20, 60],
});
function onUpdate(key: 0 | 1, value: number) {
const copy = [...model.value];
copy[key] = value;
model.value = copy.sort((a, b) => a - b);
}
const slider = ref<HTMLElement>();
const thumbs = ref<ComponentInstance<typeof BaseInputRangeThumb>[]>();
function handleMouseDown(e: MouseEvent) {
const thumb1 = thumbs.value[0];
const thumb2 = thumbs.value[1];
const distance1 = Math.abs(e.x - thumb1.getOffsetLeft());
const distance2 = Math.abs(e.x - thumb2.getOffsetLeft());
distance1 > distance2
? thumb2.handleMouseDown(e)
: thumb1.handleMouseDown(e);
}
const from = computed(() => Math.min(...model.value));
const to = computed(() => Math.max(...model.value));
const step = toRef(props, "step");
const displayFrom = useNumberFormatByStep(from, step);
const displayTo = useNumberFormatByStep(to, step);
const displayValue = computed(() => `from ${displayFrom} to ${displayTo}`);
const progressOffset = computed(() => {
if (typeof from.value !== "number") return 50;
return ((from.value - props.min) / (props.max - props.min)) * 100;
});
const progress = computed(() => {
if (typeof to.value !== "number") return 50;
const value = ((to.value - props.min) / (props.max - props.min)) * 100;
return value - progressOffset.value;
});
const sliderBoundingRect = useBoundingClientRect(slider);
// update the `value` if the `min` or `max` change and `value` is outside of the range
watch(
() => props.min,
() => {
if (!model.value.some((v) => v < props.min)) return;
model.value = model.value.map((v) => (v < props.min ? props.min : v));
},
{ immediate: true },
);
watch(
() => props.max,
() => {
if (!model.value.some((v) => v > props.max)) return;
model.value = model.value.map((v) => (v > props.max ? props.max : v));
},
{ immediate: true },
);
</script>
Loading

0 comments on commit 874d5be

Please sign in to comment.