|
1 | 1 | <template> |
2 | | - <div class="root-container"> |
3 | | - <div class="slider-component"> |
4 | | - <div class="slide-container"> |
5 | | - <div class="snap-points-wrapper"> |
6 | | - <div class="snap-points"> |
7 | | - <div |
8 | | - v-for="snapPoint in snapPoints" |
9 | | - :key="snapPoint" |
10 | | - class="snap-point" |
11 | | - :class="{ green: snapPoint <= currentValue, 'opacity-0': disabled }" |
12 | | - :style="{ left: ((snapPoint - min) / (max - min)) * 100 + '%' }" |
13 | | - ></div> |
14 | | - </div> |
15 | | - </div> |
16 | | - <input |
17 | | - ref="input" |
18 | | - v-model="currentValue" |
19 | | - type="range" |
20 | | - :min="min" |
21 | | - :max="max" |
22 | | - :step="step" |
23 | | - class="slider" |
24 | | - :class="{ |
25 | | - disabled: disabled, |
26 | | - }" |
27 | | - :disabled="disabled" |
28 | | - :style="{ |
29 | | - '--current-value': currentValue, |
30 | | - '--min-value': min, |
31 | | - '--max-value': max, |
32 | | - }" |
33 | | - @input="onInputWithSnap(($event.target as HTMLInputElement).value)" |
34 | | - /> |
35 | | - <div class="slider-range"> |
36 | | - <span> {{ min }} {{ unit }} </span> |
37 | | - <span> {{ max }} {{ unit }} </span> |
| 2 | + <div class="flex flex-row items-center w-full"> |
| 3 | + <div class="w-full relative"> |
| 4 | + <div class="absolute top-0 h-1/2 w-full"> |
| 5 | + <div |
| 6 | + class="relative inline-block align-middle w-[calc(100%-0.75rem)] h-3 left-[calc(0.75rem/2)]" |
| 7 | + > |
| 8 | + <div |
| 9 | + v-for="snapPoint in snapPoints" |
| 10 | + :key="snapPoint" |
| 11 | + class="absolute inline-block w-1 h-full rounded-sm -translate-x-1/2" |
| 12 | + :class="{ |
| 13 | + 'opacity-0': disabled, |
| 14 | + }" |
| 15 | + :style="{ |
| 16 | + left: ((snapPoint - min) / (max - min)) * 100 + '%', |
| 17 | + backgroundColor: |
| 18 | + snapPoint <= currentValue ? 'var(--color-brand)' : 'var(--color-base)', |
| 19 | + }" |
| 20 | + ></div> |
38 | 21 | </div> |
39 | 22 | </div> |
| 23 | + <input |
| 24 | + ref="input" |
| 25 | + v-model="currentValue" |
| 26 | + type="range" |
| 27 | + :min="min" |
| 28 | + :max="max" |
| 29 | + :step="step" |
| 30 | + class="slider relative rounded-sm h-1 w-full p-0 min-h-0 shadow-none outline-none align-middle appearance-none" |
| 31 | + :class="{ |
| 32 | + 'opacity-50 cursor-not-allowed': disabled, |
| 33 | + }" |
| 34 | + :disabled="disabled" |
| 35 | + :style="{ |
| 36 | + '--current-value': currentValue, |
| 37 | + '--min-value': min, |
| 38 | + '--max-value': max, |
| 39 | + }" |
| 40 | + @input="onInputWithSnap(($event.target as HTMLInputElement).value)" |
| 41 | + /> |
| 42 | + <div class="flex flex-row justify-between text-xs m-0"> |
| 43 | + <span> {{ min }} {{ unit }} </span> |
| 44 | + <span> {{ max }} {{ unit }} </span> |
| 45 | + </div> |
40 | 46 | </div> |
41 | | - <input |
42 | | - ref="value" |
43 | | - :value="currentValue" |
| 47 | + <StyledInput |
| 48 | + :model-value="String(currentValue)" |
44 | 49 | type="number" |
45 | | - class="slider-input" |
| 50 | + class="w-24 ml-3" |
46 | 51 | :disabled="disabled" |
47 | 52 | :min="min" |
48 | 53 | :max="max" |
|
53 | 58 | </template> |
54 | 59 |
|
55 | 60 | <script setup lang="ts"> |
56 | | -import { ref } from 'vue' |
| 61 | +import { ref, watch } from 'vue' |
| 62 | +
|
| 63 | +import StyledInput from './StyledInput.vue' |
57 | 64 |
|
58 | 65 | const emit = defineEmits<{ 'update:modelValue': [number] }>() |
59 | 66 |
|
@@ -83,6 +90,13 @@ const props = withDefaults(defineProps<Props>(), { |
83 | 90 |
|
84 | 91 | const currentValue = ref(Math.max(props.min, props.modelValue)) |
85 | 92 |
|
| 93 | +watch( |
| 94 | + () => props.modelValue, |
| 95 | + (newValue) => { |
| 96 | + currentValue.value = Math.max(props.min, newValue ?? props.min) |
| 97 | + }, |
| 98 | +) |
| 99 | +
|
86 | 100 | const inputValueValid = (inputValue: number) => { |
87 | 101 | let newValue = inputValue || props.min |
88 | 102 |
|
@@ -115,135 +129,63 @@ const onInput = (value: string) => { |
115 | 129 | </script> |
116 | 130 |
|
117 | 131 | <style lang="scss" scoped> |
118 | | -.root-container { |
119 | | - --transition-speed: 0.2s; |
120 | | -
|
121 | | - @media (prefers-reduced-motion) { |
122 | | - --transition-speed: 0s; |
123 | | - } |
124 | | -
|
125 | | - display: flex; |
126 | | - flex-direction: row; |
127 | | - align-items: center; |
128 | | - width: 100%; |
129 | | -} |
130 | | -
|
131 | | -.slider-component, |
132 | | -.slide-container { |
133 | | - width: 100%; |
134 | | -
|
135 | | - position: relative; |
136 | | -} |
137 | | -
|
138 | | -.slider-component .slide-container .slider { |
| 132 | +.slider { |
139 | 133 | -webkit-appearance: none; |
140 | 134 | appearance: none; |
141 | | - position: relative; |
142 | | -
|
143 | | - border-radius: var(--radius-sm); |
144 | | - height: 0.25rem; |
145 | | - width: 100%; |
146 | | - padding: 0; |
147 | | - min-height: 0px; |
148 | | - box-shadow: none; |
149 | | -
|
150 | 135 | background: linear-gradient( |
151 | | - to right, |
152 | | - var(--color-brand), |
153 | | - var(--color-brand) |
154 | | - calc((var(--current-value) - var(--min-value)) / (var(--max-value) - var(--min-value)) * 100%), |
155 | | - var(--color-base) |
156 | | - calc((var(--current-value) - var(--min-value)) / (var(--max-value) - var(--min-value)) * 100%), |
157 | | - var(--color-base) 100% |
158 | | - ); |
159 | | - background-size: 100% 100%; |
160 | | - outline: none; |
161 | | - vertical-align: middle; |
162 | | -} |
163 | | -
|
164 | | -.slider-component .slide-container .slider::-webkit-slider-thumb { |
165 | | - -webkit-appearance: none; |
166 | | - appearance: none; |
167 | | - width: 0.75rem; |
168 | | - height: 0.75rem; |
169 | | - background: var(--color-brand); |
170 | | - cursor: pointer; |
171 | | - border-radius: 50%; |
172 | | - transition: var(--transition-speed); |
173 | | -} |
174 | | -
|
175 | | -.slider-component .slide-container .slider::-moz-range-thumb { |
176 | | - border: none; |
177 | | - width: 0.75rem; |
178 | | - height: 0.75rem; |
179 | | - background: var(--color-brand); |
180 | | - cursor: pointer; |
181 | | - border-radius: 50%; |
182 | | - transition: var(--transition-speed); |
183 | | -} |
184 | | -
|
185 | | -.slider-component .slide-container .slider:hover::-webkit-slider-thumb:not(.disabled) { |
186 | | - width: 1rem; |
187 | | - height: 1rem; |
188 | | - transition: var(--transition-speed); |
189 | | -} |
190 | | -
|
191 | | -.slider-component .slide-container .slider:hover::-moz-range-thumb:not(.disabled) { |
192 | | - width: 1rem; |
193 | | - height: 1rem; |
194 | | - transition: var(--transition-speed); |
195 | | -} |
196 | | -
|
197 | | -.slider-component .slide-container .snap-points-wrapper { |
198 | | - position: absolute; |
199 | | - height: 50%; |
200 | | - width: 100%; |
201 | | -
|
202 | | - .snap-points { |
203 | | - position: relative; |
204 | | - display: inline-block; |
205 | | -
|
206 | | - vertical-align: middle; |
207 | | -
|
208 | | - width: calc(100% - 0.75rem); |
| 136 | + to right, |
| 137 | + var(--color-brand) 0%, |
| 138 | + var(--color-brand) |
| 139 | + calc( |
| 140 | + (var(--current-value) - var(--min-value)) / (var(--max-value) - var(--min-value)) * 100% |
| 141 | + ), |
| 142 | + var(--color-base) |
| 143 | + calc( |
| 144 | + (var(--current-value) - var(--min-value)) / (var(--max-value) - var(--min-value)) * 100% |
| 145 | + ), |
| 146 | + var(--color-base) 100% |
| 147 | + ) |
| 148 | + 100% 100% no-repeat; |
| 149 | +
|
| 150 | + &::-webkit-slider-thumb { |
| 151 | + -webkit-appearance: none; |
| 152 | + appearance: none; |
| 153 | + width: 0.75rem; |
209 | 154 | height: 0.75rem; |
210 | | -
|
211 | | - left: calc(0.75rem / 2); |
212 | | -
|
213 | | - .snap-point { |
214 | | - position: absolute; |
215 | | - display: inline-block; |
216 | | -
|
217 | | - width: 0.25rem; |
218 | | - height: 100%; |
219 | | - border-radius: var(--radius-sm); |
220 | | -
|
221 | | - background-color: var(--color-base); |
222 | | -
|
223 | | - transform: translateX(calc(-0.25rem / 2)); |
224 | | -
|
225 | | - &.green { |
226 | | - background-color: var(--color-brand); |
227 | | - } |
| 155 | + background: var(--color-brand); |
| 156 | + border-radius: 50%; |
| 157 | + transition: |
| 158 | + width 0.2s, |
| 159 | + height 0.2s; |
| 160 | +
|
| 161 | + @media (prefers-reduced-motion: reduce) { |
| 162 | + transition: none; |
228 | 163 | } |
229 | 164 | } |
230 | | -} |
231 | 165 |
|
232 | | -.slider-input { |
233 | | - width: 6rem; |
234 | | - margin-left: 0.75rem; |
235 | | -} |
| 166 | + &::-moz-range-thumb { |
| 167 | + border: none; |
| 168 | + width: 0.75rem; |
| 169 | + height: 0.75rem; |
| 170 | + background: var(--color-brand); |
| 171 | + border-radius: 50%; |
| 172 | + transition: |
| 173 | + width 0.2s, |
| 174 | + height 0.2s; |
| 175 | +
|
| 176 | + @media (prefers-reduced-motion: reduce) { |
| 177 | + transition: none; |
| 178 | + } |
| 179 | + } |
236 | 180 |
|
237 | | -.slider-range { |
238 | | - display: flex; |
239 | | - flex-direction: row; |
240 | | - justify-content: space-between; |
241 | | - font-size: 0.75rem; |
242 | | - margin: 0; |
243 | | -} |
| 181 | + &:hover:not(:disabled)::-webkit-slider-thumb, |
| 182 | + &:hover:not(:disabled)::-moz-range-thumb { |
| 183 | + width: 1rem; |
| 184 | + height: 1rem; |
| 185 | + } |
244 | 186 |
|
245 | | -.disabled { |
246 | | - opacity: 0.5; |
247 | | - cursor: not-allowed; |
| 187 | + &:disabled { |
| 188 | + pointer-events: none; |
| 189 | + } |
248 | 190 | } |
249 | 191 | </style> |
0 commit comments