@cfasim-ui/docs 0.3.16 → 0.3.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/NumberInput/NumberInput.md +165 -0
- package/components/NumberInput/NumberInput.vue +161 -84
- package/components/index.ts +1 -0
- package/index.json +1 -1
- package/package.json +1 -1
- package/theme/base.css +0 -5
|
@@ -12,6 +12,16 @@ const days = ref(10)
|
|
|
12
12
|
const population = ref(100000)
|
|
13
13
|
const coverage = ref(0.5)
|
|
14
14
|
const r0 = ref(3.5)
|
|
15
|
+
const ageRange = ref([18, 65])
|
|
16
|
+
const coverageRange = ref([0.2, 0.8])
|
|
17
|
+
const minAge = ref(18)
|
|
18
|
+
const maxAge = ref(65)
|
|
19
|
+
const dayMs = 24 * 60 * 60 * 1000
|
|
20
|
+
const dateStart = Date.UTC(2024, 0, 1)
|
|
21
|
+
const dateEnd = Date.UTC(2024, 11, 31)
|
|
22
|
+
const dateRange = ref([Date.UTC(2024, 2, 1), Date.UTC(2024, 8, 30)])
|
|
23
|
+
const formatDate = (ms) =>
|
|
24
|
+
new Date(ms).toLocaleDateString("en-US", { month: "short", day: "numeric" })
|
|
15
25
|
</script>
|
|
16
26
|
|
|
17
27
|
<ComponentDemo>
|
|
@@ -116,6 +126,157 @@ const days = ref(10);
|
|
|
116
126
|
</template>
|
|
117
127
|
</ComponentDemo>
|
|
118
128
|
|
|
129
|
+
### Range slider
|
|
130
|
+
|
|
131
|
+
Bind `v-model:range` with a `[low, high]` tuple to render a two-handle
|
|
132
|
+
slider. Range mode is enabled automatically by the binding — there's no
|
|
133
|
+
explicit toggle prop.
|
|
134
|
+
|
|
135
|
+
<ComponentDemo>
|
|
136
|
+
<div style="width: 300px">
|
|
137
|
+
<NumberInput
|
|
138
|
+
v-model:range="ageRange"
|
|
139
|
+
label="Age range"
|
|
140
|
+
:min="0"
|
|
141
|
+
:max="100"
|
|
142
|
+
number-type="integer"
|
|
143
|
+
/>
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
<template #code>
|
|
147
|
+
|
|
148
|
+
```vue
|
|
149
|
+
<script setup>
|
|
150
|
+
import { ref } from "vue";
|
|
151
|
+
const ageRange = ref([18, 65]);
|
|
152
|
+
</script>
|
|
153
|
+
|
|
154
|
+
<NumberInput
|
|
155
|
+
v-model:range="ageRange"
|
|
156
|
+
label="Age range"
|
|
157
|
+
:min="0"
|
|
158
|
+
:max="100"
|
|
159
|
+
number-type="integer"
|
|
160
|
+
/>
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
</template>
|
|
164
|
+
</ComponentDemo>
|
|
165
|
+
|
|
166
|
+
### Range slider with split bindings
|
|
167
|
+
|
|
168
|
+
When your state stores the bounds in separate refs (rather than as a tuple),
|
|
169
|
+
bind them directly with `v-model:lower` and `v-model:upper`. You can bind
|
|
170
|
+
either pair or combine them with `v-model:range` — writes from the component
|
|
171
|
+
go to every bound sink.
|
|
172
|
+
|
|
173
|
+
<ComponentDemo>
|
|
174
|
+
<div style="width: 300px">
|
|
175
|
+
<NumberInput
|
|
176
|
+
v-model:lower="minAge"
|
|
177
|
+
v-model:upper="maxAge"
|
|
178
|
+
label="Age range (split)"
|
|
179
|
+
:min="0"
|
|
180
|
+
:max="100"
|
|
181
|
+
number-type="integer"
|
|
182
|
+
/>
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
<template #code>
|
|
186
|
+
|
|
187
|
+
```vue
|
|
188
|
+
<script setup>
|
|
189
|
+
import { ref } from "vue";
|
|
190
|
+
const minAge = ref(18);
|
|
191
|
+
const maxAge = ref(65);
|
|
192
|
+
</script>
|
|
193
|
+
|
|
194
|
+
<NumberInput
|
|
195
|
+
v-model:lower="minAge"
|
|
196
|
+
v-model:upper="maxAge"
|
|
197
|
+
label="Age range"
|
|
198
|
+
:min="0"
|
|
199
|
+
:max="100"
|
|
200
|
+
number-type="integer"
|
|
201
|
+
/>
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
</template>
|
|
205
|
+
</ComponentDemo>
|
|
206
|
+
|
|
207
|
+
Range mode works with `percent` and `live` as well:
|
|
208
|
+
|
|
209
|
+
<ComponentDemo>
|
|
210
|
+
<div style="width: 300px">
|
|
211
|
+
<NumberInput
|
|
212
|
+
v-model:range="coverageRange"
|
|
213
|
+
label="Coverage range"
|
|
214
|
+
percent
|
|
215
|
+
live
|
|
216
|
+
:max="1"
|
|
217
|
+
/>
|
|
218
|
+
</div>
|
|
219
|
+
|
|
220
|
+
<template #code>
|
|
221
|
+
|
|
222
|
+
```vue
|
|
223
|
+
<NumberInput
|
|
224
|
+
v-model:range="coverageRange"
|
|
225
|
+
label="Coverage range"
|
|
226
|
+
percent
|
|
227
|
+
live
|
|
228
|
+
:max="1"
|
|
229
|
+
/>
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
</template>
|
|
233
|
+
</ComponentDemo>
|
|
234
|
+
|
|
235
|
+
### Custom slider display
|
|
236
|
+
|
|
237
|
+
Pass `slider-display` (a `(value: number) => string` function) to format the
|
|
238
|
+
thumb labels and the min/max labels however you like. The internal model is
|
|
239
|
+
still a number — only the displayed text changes. This applies to single
|
|
240
|
+
sliders and ranges; the regular text input is unaffected.
|
|
241
|
+
|
|
242
|
+
<ComponentDemo>
|
|
243
|
+
<div style="width: 300px">
|
|
244
|
+
<NumberInput
|
|
245
|
+
v-model:range="dateRange"
|
|
246
|
+
label="Date range"
|
|
247
|
+
:min="dateStart"
|
|
248
|
+
:max="dateEnd"
|
|
249
|
+
:step="dayMs"
|
|
250
|
+
:slider-display="formatDate"
|
|
251
|
+
/>
|
|
252
|
+
</div>
|
|
253
|
+
|
|
254
|
+
<template #code>
|
|
255
|
+
|
|
256
|
+
```vue
|
|
257
|
+
<script setup>
|
|
258
|
+
import { ref } from "vue";
|
|
259
|
+
const dayMs = 24 * 60 * 60 * 1000;
|
|
260
|
+
const dateStart = Date.UTC(2024, 0, 1);
|
|
261
|
+
const dateEnd = Date.UTC(2024, 11, 31);
|
|
262
|
+
const dateRange = ref([Date.UTC(2024, 2, 1), Date.UTC(2024, 8, 30)]);
|
|
263
|
+
const formatDate = (ms) =>
|
|
264
|
+
new Date(ms).toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
|
265
|
+
</script>
|
|
266
|
+
|
|
267
|
+
<NumberInput
|
|
268
|
+
v-model:range="dateRange"
|
|
269
|
+
label="Date range"
|
|
270
|
+
:min="dateStart"
|
|
271
|
+
:max="dateEnd"
|
|
272
|
+
:step="dayMs"
|
|
273
|
+
:slider-display="formatDate"
|
|
274
|
+
/>
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
</template>
|
|
278
|
+
</ComponentDemo>
|
|
279
|
+
|
|
119
280
|
### Live slider
|
|
120
281
|
|
|
121
282
|
With `live`, the model updates while dragging the slider thumb rather than only on release.
|
|
@@ -284,6 +445,9 @@ the input visually.
|
|
|
284
445
|
| Name | Type |
|
|
285
446
|
|------|------|
|
|
286
447
|
| `v-model` | `number` |
|
|
448
|
+
| `v-model:range` | `NumberRange` |
|
|
449
|
+
| `v-model:lower` | `number` |
|
|
450
|
+
| `v-model:upper` | `number` |
|
|
287
451
|
|
|
288
452
|
## Props
|
|
289
453
|
|
|
@@ -302,4 +466,5 @@ the input visually.
|
|
|
302
466
|
| `numberType` | `"integer" \| "float"` | No | — |
|
|
303
467
|
| `required` | `boolean` | No | — |
|
|
304
468
|
| `decimals` | `number` | No | — |
|
|
469
|
+
| `sliderDisplay` | `(value: number) => string` | No | — |
|
|
305
470
|
|
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { ref, watch, computed } from "vue";
|
|
2
|
+
import { ref, watch, computed, onMounted, getCurrentInstance } from "vue";
|
|
3
3
|
import { SliderRoot, SliderTrack, SliderRange, SliderThumb } from "reka-ui";
|
|
4
4
|
import Hint from "../Hint/Hint.vue";
|
|
5
5
|
|
|
6
|
+
export type NumberRange = [number, number];
|
|
7
|
+
|
|
8
|
+
// The default `v-model` is always a scalar number. For range mode, bind
|
|
9
|
+
// `v-model:range` (a tuple) and/or the split `v-model:lower`/`v-model:upper`.
|
|
10
|
+
// Mode is auto-detected from which v-model bindings the parent provides;
|
|
11
|
+
// no explicit toggle prop. Precedence on read: lower/upper > range >
|
|
12
|
+
// slider defaults.
|
|
6
13
|
const model = defineModel<number>();
|
|
14
|
+
const range = defineModel<NumberRange>("range");
|
|
15
|
+
const lower = defineModel<number>("lower");
|
|
16
|
+
const upper = defineModel<number>("upper");
|
|
7
17
|
|
|
8
18
|
const props = defineProps<{
|
|
9
19
|
label?: string;
|
|
@@ -19,8 +29,43 @@ const props = defineProps<{
|
|
|
19
29
|
numberType?: "integer" | "float";
|
|
20
30
|
required?: boolean;
|
|
21
31
|
decimals?: number;
|
|
32
|
+
// Custom formatter for slider thumb labels and min/max labels. Overrides
|
|
33
|
+
// the default percent/decimal formatting when provided. Only consulted in
|
|
34
|
+
// slider/range mode — the text input keeps its own number-shaped formatting.
|
|
35
|
+
sliderDisplay?: (value: number) => string;
|
|
22
36
|
}>();
|
|
23
37
|
|
|
38
|
+
function isRangeValue(v: unknown): v is NumberRange {
|
|
39
|
+
return Array.isArray(v) && v.length === 2;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Auto-detect range mode from the parent's v-model bindings. We check both
|
|
43
|
+
// the listener (which Vue attaches as `onUpdate:<name>` on the vnode) and
|
|
44
|
+
// the initial value — that way one-way `:range="x"` bindings also work.
|
|
45
|
+
// Determined once at setup; mode doesn't change for the component's life.
|
|
46
|
+
const instance = getCurrentInstance();
|
|
47
|
+
const vnodeProps = instance?.vnode.props;
|
|
48
|
+
const isRange =
|
|
49
|
+
!!vnodeProps?.["onUpdate:range"] ||
|
|
50
|
+
!!vnodeProps?.["onUpdate:lower"] ||
|
|
51
|
+
!!vnodeProps?.["onUpdate:upper"] ||
|
|
52
|
+
range.value !== undefined ||
|
|
53
|
+
lower.value !== undefined ||
|
|
54
|
+
upper.value !== undefined;
|
|
55
|
+
|
|
56
|
+
// Range implies slider — a two-handle range has no sensible text-input form.
|
|
57
|
+
const isSlider = computed(() => !!props.slider || isRange);
|
|
58
|
+
|
|
59
|
+
// Warn if the parent bound the default `v-model` in range mode — it will
|
|
60
|
+
// never receive updates, which is almost always a bug.
|
|
61
|
+
onMounted(() => {
|
|
62
|
+
if (isRange && !!vnodeProps?.["onUpdate:modelValue"]) {
|
|
63
|
+
console.warn(
|
|
64
|
+
"[NumberInput] In range mode, the default `v-model` is unused. Bind `v-model:range` or `v-model:lower`/`v-model:upper` instead.",
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
24
69
|
const sliderMin = computed(() => props.min ?? (props.percent ? 0 : 0));
|
|
25
70
|
const sliderMax = computed(() => props.max ?? (props.percent ? 1 : 100));
|
|
26
71
|
const sliderStep = computed(() => props.step ?? (props.percent ? 0.01 : 1));
|
|
@@ -68,6 +113,7 @@ function roundToDecimals(v: number, d: number): number {
|
|
|
68
113
|
|
|
69
114
|
function formatSliderValue(v: number | undefined) {
|
|
70
115
|
if (v == null) return "";
|
|
116
|
+
if (props.sliderDisplay) return props.sliderDisplay(v);
|
|
71
117
|
const d = displayDecimals.value;
|
|
72
118
|
if (props.percent) return (v * 100).toFixed(d) + "%";
|
|
73
119
|
return v.toLocaleString("en-US", {
|
|
@@ -121,13 +167,50 @@ function stripCommas(s: string): string {
|
|
|
121
167
|
return s.replace(/,/g, "");
|
|
122
168
|
}
|
|
123
169
|
|
|
124
|
-
|
|
125
|
-
|
|
170
|
+
// Resolve the current value across all bindings:
|
|
171
|
+
// - In range mode: lower/upper take precedence; falls back per-side to
|
|
172
|
+
// `range`; finally to slider min/max. The default `v-model` is
|
|
173
|
+
// unused in this mode.
|
|
174
|
+
// - In single mode: just the default `v-model`.
|
|
175
|
+
function effectiveValue(): number | NumberRange | undefined {
|
|
176
|
+
if (isRange) {
|
|
177
|
+
const tuple = range.value;
|
|
178
|
+
const lo = lower.value ?? tuple?.[0];
|
|
179
|
+
const hi = upper.value ?? tuple?.[1];
|
|
180
|
+
if (lo !== undefined || hi !== undefined) {
|
|
181
|
+
return [lo ?? sliderMin.value, hi ?? sliderMax.value];
|
|
182
|
+
}
|
|
183
|
+
return undefined;
|
|
184
|
+
}
|
|
185
|
+
return model.value;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Initial single-value display string. The text input isn't rendered in
|
|
189
|
+
// range mode, so `local` is only consulted in single mode.
|
|
190
|
+
const initialEffective = effectiveValue();
|
|
191
|
+
const initialSingle =
|
|
192
|
+
typeof initialEffective === "number" ? initialEffective : undefined;
|
|
193
|
+
const local = ref(formatForDisplay(toDisplay(initialSingle)));
|
|
194
|
+
|
|
195
|
+
// Slider state is always an array, even in single mode (reka-ui's API).
|
|
196
|
+
// In range mode it holds [low, high]; in single mode it holds [value].
|
|
197
|
+
function modelToSliderArray(v: number | NumberRange | undefined): number[] {
|
|
198
|
+
if (isRange) {
|
|
199
|
+
if (isRangeValue(v)) return [v[0], v[1]];
|
|
200
|
+
return [sliderMin.value, sliderMax.value];
|
|
201
|
+
}
|
|
202
|
+
if (typeof v === "number") return [v];
|
|
203
|
+
return [sliderMin.value];
|
|
204
|
+
}
|
|
205
|
+
const sliderArrayLocal = ref<number[]>(modelToSliderArray(initialEffective));
|
|
126
206
|
const validationError = ref<string>();
|
|
127
207
|
|
|
128
|
-
watch(model, (
|
|
129
|
-
|
|
130
|
-
|
|
208
|
+
watch([model, range, lower, upper], () => {
|
|
209
|
+
const v = effectiveValue();
|
|
210
|
+
if (!isRange && !isRangeValue(v)) {
|
|
211
|
+
local.value = formatForDisplay(toDisplay(v as number | undefined));
|
|
212
|
+
}
|
|
213
|
+
sliderArrayLocal.value = modelToSliderArray(v);
|
|
131
214
|
validationError.value = validate(v);
|
|
132
215
|
});
|
|
133
216
|
|
|
@@ -179,21 +262,17 @@ function onBlur() {
|
|
|
179
262
|
|
|
180
263
|
let liveTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
181
264
|
function onInputEvent() {
|
|
182
|
-
if (!props.live ||
|
|
265
|
+
if (!props.live || isSlider.value) return;
|
|
183
266
|
if (liveTimeout) clearTimeout(liveTimeout);
|
|
184
267
|
liveTimeout = setTimeout(commit, 300);
|
|
185
268
|
}
|
|
186
269
|
function onChangeEvent() {
|
|
187
|
-
if (!props.live ||
|
|
270
|
+
if (!props.live || isSlider.value) return;
|
|
188
271
|
if (liveTimeout) clearTimeout(liveTimeout);
|
|
189
272
|
commit();
|
|
190
273
|
}
|
|
191
274
|
|
|
192
|
-
|
|
193
|
-
// for required / min / max errors — used on commit, programmatic updates,
|
|
194
|
-
// and arrow-key stepping.
|
|
195
|
-
function validate(v: number | undefined): string | undefined {
|
|
196
|
-
if (v == null) return props.required ? "Required" : undefined;
|
|
275
|
+
function validateScalar(v: number): string | undefined {
|
|
197
276
|
const display = toDisplay(v) as number;
|
|
198
277
|
if (inputMin.value != null && display < inputMin.value) {
|
|
199
278
|
return `Min ${inputMin.value}${props.percent ? "%" : ""}`;
|
|
@@ -204,11 +283,28 @@ function validate(v: number | undefined): string | undefined {
|
|
|
204
283
|
return undefined;
|
|
205
284
|
}
|
|
206
285
|
|
|
286
|
+
// Single source of truth for required / min / max errors — used on commit,
|
|
287
|
+
// programmatic updates, and arrow-key stepping. In range mode, returns the
|
|
288
|
+
// first failing handle's error, suffixed with "(lower)" or "(upper)".
|
|
289
|
+
function validate(v: number | NumberRange | undefined): string | undefined {
|
|
290
|
+
if (v == null) return props.required ? "Required" : undefined;
|
|
291
|
+
if (isRangeValue(v)) {
|
|
292
|
+
const lo = validateScalar(v[0]);
|
|
293
|
+
if (lo) return `${lo} (lower)`;
|
|
294
|
+
const hi = validateScalar(v[1]);
|
|
295
|
+
if (hi) return `${hi} (upper)`;
|
|
296
|
+
return undefined;
|
|
297
|
+
}
|
|
298
|
+
return validateScalar(v);
|
|
299
|
+
}
|
|
300
|
+
|
|
207
301
|
function commit() {
|
|
208
|
-
//
|
|
302
|
+
// commit() is only reachable when !isSlider (only text-input events call
|
|
303
|
+
// it). Default `v-model` is scalar-only.
|
|
304
|
+
const current = model.value;
|
|
209
305
|
if (local.value.trim() === "") {
|
|
210
306
|
model.value = undefined;
|
|
211
|
-
|
|
307
|
+
sliderArrayLocal.value = modelToSliderArray(undefined);
|
|
212
308
|
validationError.value = validate(undefined);
|
|
213
309
|
return;
|
|
214
310
|
}
|
|
@@ -219,8 +315,8 @@ function commit() {
|
|
|
219
315
|
// turn pure garbage ("abc") into 0. Reset to the current model value so
|
|
220
316
|
// invalid input doesn't linger in the field.
|
|
221
317
|
if (!/\d/.test(cleaned)) {
|
|
222
|
-
local.value = formatForDisplay(toDisplay(
|
|
223
|
-
validationError.value = validate(
|
|
318
|
+
local.value = formatForDisplay(toDisplay(current));
|
|
319
|
+
validationError.value = validate(current);
|
|
224
320
|
return;
|
|
225
321
|
}
|
|
226
322
|
if (cleaned !== local.value) {
|
|
@@ -240,22 +336,43 @@ function commit() {
|
|
|
240
336
|
if (error) return;
|
|
241
337
|
|
|
242
338
|
model.value = next;
|
|
243
|
-
|
|
339
|
+
sliderArrayLocal.value = [next];
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function commitSliderArray(v: number[], asModel: boolean): void {
|
|
343
|
+
const coerced = v.map(coerceInteger);
|
|
344
|
+
sliderArrayLocal.value = coerced;
|
|
345
|
+
if (!isRange) {
|
|
346
|
+
local.value = formatForDisplay(toDisplay(coerced[0]));
|
|
347
|
+
}
|
|
348
|
+
if (asModel) {
|
|
349
|
+
if (isRange) {
|
|
350
|
+
// Emit to all range sinks; consumers without a matching v-model just
|
|
351
|
+
// ignore their `update:*` event. The default `v-model` is unused in
|
|
352
|
+
// range mode.
|
|
353
|
+
range.value = [coerced[0], coerced[1]] as NumberRange;
|
|
354
|
+
lower.value = coerced[0];
|
|
355
|
+
upper.value = coerced[1];
|
|
356
|
+
} else {
|
|
357
|
+
model.value = coerced[0];
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function thumbAriaLabel(i: number): string | undefined {
|
|
363
|
+
if (!props.label) return undefined;
|
|
364
|
+
if (!isRange) return props.label;
|
|
365
|
+
return i === 0 ? `${props.label} (lower)` : `${props.label} (upper)`;
|
|
244
366
|
}
|
|
245
367
|
|
|
246
368
|
function onSliderUpdate(v: number[] | undefined) {
|
|
247
369
|
if (!v) return;
|
|
248
|
-
|
|
249
|
-
sliderLocal.value = val;
|
|
250
|
-
local.value = formatForDisplay(toDisplay(val));
|
|
251
|
-
if (props.live) {
|
|
252
|
-
model.value = val;
|
|
253
|
-
}
|
|
370
|
+
commitSliderArray(v, !!props.live);
|
|
254
371
|
}
|
|
255
372
|
|
|
256
373
|
function onSliderCommit(v: number[] | undefined) {
|
|
257
374
|
if (!v) return;
|
|
258
|
-
|
|
375
|
+
commitSliderArray(v, true);
|
|
259
376
|
}
|
|
260
377
|
|
|
261
378
|
function onArrowStep(event: KeyboardEvent, direction: 1 | -1) {
|
|
@@ -268,71 +385,26 @@ function onArrowStep(event: KeyboardEvent, direction: 1 | -1) {
|
|
|
268
385
|
if (inputMin.value != null) next = Math.max(next, inputMin.value);
|
|
269
386
|
if (inputMax.value != null) next = Math.min(next, inputMax.value);
|
|
270
387
|
local.value = formatForDisplay(next);
|
|
271
|
-
|
|
272
|
-
|
|
388
|
+
const nextModel = fromDisplay(next);
|
|
389
|
+
model.value = nextModel;
|
|
390
|
+
sliderArrayLocal.value = [nextModel];
|
|
273
391
|
}
|
|
274
392
|
</script>
|
|
275
393
|
|
|
276
394
|
<template>
|
|
277
|
-
<
|
|
395
|
+
<component
|
|
396
|
+
:is="props.label ? 'label' : 'div'"
|
|
397
|
+
:class="props.label ? 'input-label' : undefined"
|
|
398
|
+
>
|
|
278
399
|
<span
|
|
400
|
+
v-if="props.label"
|
|
279
401
|
class="input-label-row"
|
|
280
402
|
:class="{ 'visually-hidden': props.hideLabel }"
|
|
281
403
|
>
|
|
282
404
|
{{ props.label }}
|
|
283
405
|
<Hint v-if="props.hint && !props.hideLabel" :text="props.hint" />
|
|
284
406
|
</span>
|
|
285
|
-
<span v-if="!
|
|
286
|
-
<input
|
|
287
|
-
type="text"
|
|
288
|
-
:inputmode="props.numberType === 'integer' ? 'numeric' : 'decimal'"
|
|
289
|
-
v-model="local"
|
|
290
|
-
:placeholder="props.placeholder"
|
|
291
|
-
:aria-invalid="!!validationError"
|
|
292
|
-
:aria-required="props.required || undefined"
|
|
293
|
-
:required="props.required"
|
|
294
|
-
@blur="onBlur"
|
|
295
|
-
@keydown.enter="commit"
|
|
296
|
-
@keydown.up="onArrowStep($event, 1)"
|
|
297
|
-
@keydown.down="onArrowStep($event, -1)"
|
|
298
|
-
@input="
|
|
299
|
-
reformatInput($event);
|
|
300
|
-
onInputEvent();
|
|
301
|
-
"
|
|
302
|
-
@change="onChangeEvent"
|
|
303
|
-
/>
|
|
304
|
-
<span v-if="props.percent" class="input-suffix">%</span>
|
|
305
|
-
</span>
|
|
306
|
-
<span v-if="validationError" class="input-error" role="alert">
|
|
307
|
-
{{ validationError }}
|
|
308
|
-
</span>
|
|
309
|
-
<div v-if="props.slider" class="slider-container">
|
|
310
|
-
<SliderRoot
|
|
311
|
-
class="slider-root"
|
|
312
|
-
:model-value="sliderLocal != null ? [sliderLocal] : [sliderMin]"
|
|
313
|
-
:min="sliderMin"
|
|
314
|
-
:max="sliderMax"
|
|
315
|
-
:step="sliderStep"
|
|
316
|
-
@update:model-value="onSliderUpdate"
|
|
317
|
-
@value-commit="onSliderCommit"
|
|
318
|
-
>
|
|
319
|
-
<SliderTrack class="slider-track">
|
|
320
|
-
<SliderRange class="slider-range" />
|
|
321
|
-
</SliderTrack>
|
|
322
|
-
<SliderThumb class="slider-thumb" :aria-label="props.label">
|
|
323
|
-
<span class="slider-current">
|
|
324
|
-
{{ formatSliderValue(sliderLocal) }}
|
|
325
|
-
</span>
|
|
326
|
-
</SliderThumb>
|
|
327
|
-
</SliderRoot>
|
|
328
|
-
<div class="slider-labels">
|
|
329
|
-
<span>{{ formatSliderValue(sliderMin) }}</span>
|
|
330
|
-
<span>{{ formatSliderValue(sliderMax) }}</span>
|
|
331
|
-
</div>
|
|
332
|
-
</div>
|
|
333
|
-
</label>
|
|
334
|
-
<div v-else>
|
|
335
|
-
<span v-if="!props.slider" class="input-wrapper">
|
|
407
|
+
<span v-if="!isSlider" class="input-wrapper">
|
|
336
408
|
<input
|
|
337
409
|
type="text"
|
|
338
410
|
:inputmode="props.numberType === 'integer' ? 'numeric' : 'decimal'"
|
|
@@ -356,10 +428,10 @@ function onArrowStep(event: KeyboardEvent, direction: 1 | -1) {
|
|
|
356
428
|
<span v-if="validationError" class="input-error" role="alert">
|
|
357
429
|
{{ validationError }}
|
|
358
430
|
</span>
|
|
359
|
-
<div v-if="
|
|
431
|
+
<div v-if="isSlider" class="slider-container">
|
|
360
432
|
<SliderRoot
|
|
361
433
|
class="slider-root"
|
|
362
|
-
:model-value="
|
|
434
|
+
:model-value="sliderArrayLocal"
|
|
363
435
|
:min="sliderMin"
|
|
364
436
|
:max="sliderMax"
|
|
365
437
|
:step="sliderStep"
|
|
@@ -369,9 +441,14 @@ function onArrowStep(event: KeyboardEvent, direction: 1 | -1) {
|
|
|
369
441
|
<SliderTrack class="slider-track">
|
|
370
442
|
<SliderRange class="slider-range" />
|
|
371
443
|
</SliderTrack>
|
|
372
|
-
<SliderThumb
|
|
444
|
+
<SliderThumb
|
|
445
|
+
v-for="(v, i) in sliderArrayLocal"
|
|
446
|
+
:key="i"
|
|
447
|
+
class="slider-thumb"
|
|
448
|
+
:aria-label="thumbAriaLabel(i)"
|
|
449
|
+
>
|
|
373
450
|
<span class="slider-current">
|
|
374
|
-
{{ formatSliderValue(
|
|
451
|
+
{{ formatSliderValue(v) }}
|
|
375
452
|
</span>
|
|
376
453
|
</SliderThumb>
|
|
377
454
|
</SliderRoot>
|
|
@@ -380,7 +457,7 @@ function onArrowStep(event: KeyboardEvent, direction: 1 | -1) {
|
|
|
380
457
|
<span>{{ formatSliderValue(sliderMax) }}</span>
|
|
381
458
|
</div>
|
|
382
459
|
</div>
|
|
383
|
-
</
|
|
460
|
+
</component>
|
|
384
461
|
</template>
|
|
385
462
|
|
|
386
463
|
<style scoped>
|
package/components/index.ts
CHANGED
|
@@ -10,6 +10,7 @@ export { default as Hint } from "./Hint/Hint.vue";
|
|
|
10
10
|
export { default as Icon } from "./Icon/Icon.vue";
|
|
11
11
|
export { default as LightDarkToggle } from "./LightDarkToggle/LightDarkToggle.vue";
|
|
12
12
|
export { default as NumberInput } from "./NumberInput/NumberInput.vue";
|
|
13
|
+
export type { NumberRange } from "./NumberInput/NumberInput.vue";
|
|
13
14
|
export { default as SelectBox } from "./SelectBox/SelectBox.vue";
|
|
14
15
|
export type { SelectOption } from "./SelectBox/SelectBox.vue";
|
|
15
16
|
export { default as SidebarLayout } from "./SidebarLayout/SidebarLayout.vue";
|
package/index.json
CHANGED
package/package.json
CHANGED