Skip to content

Commit

Permalink
progress on slider snap and range
Browse files Browse the repository at this point in the history
  • Loading branch information
CarelessCourage committed Sep 22, 2024
1 parent d3f12b4 commit ccfda8f
Show file tree
Hide file tree
Showing 10 changed files with 311 additions and 144 deletions.
116 changes: 27 additions & 89 deletions packages/nobel/components/ui/Slider/Slider.vue
Original file line number Diff line number Diff line change
@@ -1,71 +1,42 @@
<script setup lang="ts">
import { ref, computed, watch, useTemplateRef } from 'vue'
import { useMouse, useMousePressed, onKeyStroke } from '@vueuse/core'
import { gsap } from 'gsap'
import Button from '../Button/Button.vue'
import IconHome from '../../Icons/IconHome.vue'
import { useMod } from './useMod'
import { watch, useTemplateRef } from 'vue'
import { useSliderKeys } from './useSliderKeys'
import { useSliderValue } from './useSliderValue'
import SnapPoints from './SnapPoints.vue'
import SliderHandle from './SliderHandle.vue'
const slider = useTemplateRef<HTMLDivElement>('slider')
const track = useTemplateRef<HTMLDivElement>('track')
const range = useTemplateRef<HTMLDivElement>('range')
const handle = useTemplateRef<HTMLDivElement>('handle')
const { x } = useMouse()
const { pressed } = useMousePressed({ target: slider })
const value = ref(50)
const snapPoints = ref([0, 25, 50, 75, 100])
const modifier = ref(false)
function nearestSnapPoint(value: number) {
return snapPoints.value.reduce((a, b) => (Math.abs(b - value) < Math.abs(a - value) ? b : a))
}
watch(pressed, (pressed) => {
// when finished press gsap value to nearest snap point
if (pressed) return
gsap.to(value, {
value: nearestSnapPoint(value.value),
duration: 0.2,
ease: 'power2.inOut'
})
})
const percent = computed(() => {
const trackBox = track.value?.getBoundingClientRect()
return trackBox ? Math.round(((x.value - trackBox.left) / trackBox.width) * 100) : 0
const { size, left, pressed, updateSlider, leftHandleClicked, snapPoints } = useSliderValue({
slider,
track
})
useMod({ track: slider, value })
useSliderKeys(slider, { size, snapPoints })
watch(percent, (percent) => {
if (!pressed.value) return
value.value = percent
watch(pressed, (isPressed) => {
if (isPressed) return
leftHandleClicked.value = false
})
</script>

<template>
<p>{{ percent }}</p>
<p>{{ value }}</p>
<p>{{ modifier }}</p>
<div ref="slider" class="slider-wrapper" @mousedown="() => (value = percent)">
<p>{{ left }}</p>
<p>{{ size }}</p>
<div
ref="slider"
tabindex="1"
class="slider-wrapper border focus"
@mousedown="() => updateSlider()"
>
<div ref="track" class="track">
<div ref="range" class="range">
<Button ref="handle" size="mini" variant="secondary" class="handle">
<IconHome size="mini" />
</Button>
<div class="range">
<SliderHandle @mousedown="leftHandleClicked = true" variant="secondary" side="left" />
<SliderHandle variant="primary" side="right" />
</div>

<div class="snap-points">
<div
v-for="snapPoint in snapPoints"
:key="snapPoint"
class="snapPoint"
:class="{ active: nearestSnapPoint(value) === snapPoint }"
:style="{ left: snapPoint + '%' }"
></div>
</div>
<SnapPoints :value="size" :snapPoints="snapPoints" />
</div>
</div>
</template>
Expand All @@ -82,7 +53,7 @@ watch(percent, (percent) => {
--track-height: calc(var(--block-small) / 2);
height: var(--block-big);
padding: 0px calc(var(--handle-size) / 2);
padding: var(--space-2) calc(var(--space-2) + var(--track-height) / 2.5);
border-radius: var(--radius);
background: var(--base-10);
}
Expand All @@ -105,42 +76,9 @@ watch(percent, (percent) => {
position: relative;
z-index: 2;
height: 100%;
width: calc(v-bind(value) * 1%);
width: calc(v-bind(size) * 1%);
margin-left: calc(v-bind(left) * 1%);
background: var(--accent-100);
border-radius: var(--radius);
}
.slider-wrapper .track .range .handle {
position: absolute;
right: calc(0px - var(--handle-size) / 2);
}
.slider-wrapper .snap-points {
display: flex;
justify-content: center;
align-items: center;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.slider-wrapper .snapPoint {
display: flex;
justify-content: center;
align-items: center;
position: absolute;
transform: translateX(-50%);
background-color: var(--base-100);
border-radius: var(--radius);
height: var(--block-small);
aspect-ratio: 1 / 1;
transition: var(--time);
}
.slider-wrapper .snapPoint.active {
height: var(--block);
background-color: var(--accent-100);
}
</style>
27 changes: 27 additions & 0 deletions packages/nobel/components/ui/Slider/SliderHandle.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<script setup lang="ts">
import Button from '../Button/Button.vue'
import IconHome from '../../Icons/IconHome.vue'
defineProps<{
variant: 'primary' | 'secondary' | 'base'
side: 'left' | 'right'
}>()
</script>

<template>
<Button size="mini" :variant="variant" class="handle" :class="side">
<IconHome size="mini" />
</Button>
</template>

<style scoped lang="scss">
.slider-wrapper .track .range .handle.right {
position: absolute;
right: calc(0px - var(--handle-size) / 2);
}
.slider-wrapper .track .range .handle.left {
position: absolute;
left: calc(0px - var(--handle-size) / 2);
}
</style>
50 changes: 50 additions & 0 deletions packages/nobel/components/ui/Slider/SnapPoints.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<script setup lang="ts">
import { nearestSnapPoint } from './utils'
defineProps<{
value: number
snapPoints: number[]
}>()
</script>

<template>
<div class="snap-points">
<div
v-for="snapPoint in snapPoints"
:key="snapPoint"
class="snapPoint"
:class="{ active: nearestSnapPoint(value, snapPoints) === snapPoint }"
:style="{ left: snapPoint + '%' }"
></div>
</div>
</template>

<style scoped lang="scss">
.slider-wrapper .snap-points {
display: flex;
justify-content: center;
align-items: center;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.slider-wrapper .snapPoint {
display: flex;
justify-content: center;
align-items: center;
position: absolute;
transform: translateX(-50%);
background-color: var(--accent-60);
border-radius: var(--radius);
height: var(--block-small);
aspect-ratio: 1 / 1;
transition: var(--time);
}
.slider-wrapper .snapPoint.active {
height: var(--block);
background-color: var(--accent-100);
}
</style>
52 changes: 0 additions & 52 deletions packages/nobel/components/ui/Slider/useMod.ts

This file was deleted.

56 changes: 56 additions & 0 deletions packages/nobel/components/ui/Slider/useSliderKeys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { ref, computed } from 'vue'
import type { Ref, ShallowRef } from 'vue'
import { onKeyStroke } from '@vueuse/core'
import { clamp, switchPoint } from './utils'

interface UseSliderKeys {
size: Ref<number>
snapPoints: Ref<number[]>
}

export function useSliderKeys(
slider: Readonly<ShallowRef<HTMLDivElement | null>> | null,
{ size, snapPoints }: UseSliderKeys
) {
const modifier = ref(false)
const hasSnapPoints = computed(() => snapPoints.value.length > 0)

function setValue(percent: number, add = 0) {
const adder = modifier.value ? add * 10 : add
size.value = clamp(percent + adder, 0, 100)
}

function snapOrMove(value: number, add = 0) {
hasSnapPoints.value ? setValue(switchPoint(value, snapPoints.value, add)) : setValue(value, add)
}

function shiftMod(e: KeyboardEvent) {
e.preventDefault()
modifier.value = !modifier.value
}

onKeyStroke('Shift', shiftMod, {
eventName: 'keydown',
target: slider
})

onKeyStroke('Shift', shiftMod, {
eventName: 'keyup',
target: slider
})

onKeyStroke('ArrowLeft', () => snapOrMove(size.value, -1), {
eventName: 'keydown',
target: slider
})

onKeyStroke('ArrowRight', () => snapOrMove(size.value, 1), {
eventName: 'keydown',
target: slider
})

return {
modifier,
setValue
}
}
25 changes: 25 additions & 0 deletions packages/nobel/components/ui/Slider/useSliderSnap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { ref, watch } from 'vue'
import type { Ref } from 'vue'
import { gsap } from 'gsap'
import { nearestSnapPoint } from './utils'

interface UseSliderSnap {
pressed: Ref<boolean>
size: Ref<number>
}

export function useSliderSnap(points: number[], { pressed, size }: UseSliderSnap) {
const snapPoints = ref(points)

watch(pressed, (pressed) => {
// when finished press gsap value to nearest snap point
if (pressed) return
gsap.to(size, {
value: nearestSnapPoint(size.value, snapPoints.value),
duration: 0.2,
ease: 'power2.inOut'
})
})

return { snapPoints }
}
Loading

0 comments on commit ccfda8f

Please sign in to comment.