@cfasim-ui/docs 0.6.3 → 0.7.0
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/charts/BarChart/BarChart.md +7 -8
- package/charts/BarChart/BarChart.vue +13 -7
- package/charts/ChoroplethMap/ChoroplethMap.md +99 -29
- package/charts/ChoroplethMap/ChoroplethMap.vue +456 -127
- package/charts/DataTable/DataTable.vue +1 -7
- package/charts/LineChart/LineChart.md +7 -8
- package/charts/LineChart/LineChart.vue +13 -7
- package/charts/_shared/ChartZoomControls.vue +138 -0
- package/charts/_shared/chartProps.ts +7 -7
- package/charts/_shared/index.ts +4 -15
- package/charts/_shared/seriesCsv.ts +2 -2
- package/charts/_shared/touch.ts +8 -0
- package/charts/_shared/useChartFoundation.ts +0 -1
- package/components/MultiSelect/MultiSelect.md +1 -1
- package/components/MultiSelect/MultiSelect.vue +24 -83
- package/components/NumberInput/NumberInput.md +5 -5
- package/components/NumberInput/NumberInput.vue +12 -59
- package/components/SelectBox/SelectBox.md +1 -0
- package/components/SelectBox/SelectBox.vue +34 -125
- package/components/TextInput/TextInput.md +2 -1
- package/components/TextInput/TextInput.vue +11 -51
- package/components/ToggleGroup/ToggleGroup.md +2 -2
- package/components/ToggleGroup/ToggleGroup.vue +21 -26
- package/components/_internal/FieldLabel.vue +27 -0
- package/components/_internal/field.ts +27 -0
- package/components/_internal/input.css +54 -0
- package/components/_internal/listbox.css +53 -0
- package/components/env.d.ts +4 -0
- package/index.json +8 -8
- package/package.json +1 -1
- package/pyodide/messages.ts +23 -0
- package/pyodide/pyodide.worker.ts +2 -21
- package/pyodide/pyodideWorkerApi.ts +7 -44
- package/shared/index.ts +1 -0
- package/shared/workerError.ts +17 -0
- package/wasm/messages.ts +6 -0
- package/wasm/wasm.worker.ts +1 -7
- package/wasm/wasmWorkerApi.ts +8 -26
|
@@ -3,6 +3,8 @@ import { ref, watch, computed, onMounted, getCurrentInstance } from "vue";
|
|
|
3
3
|
import { SliderRoot, SliderTrack, SliderRange, SliderThumb } from "reka-ui";
|
|
4
4
|
import { formatNumber, type NumberFormat } from "@cfasim-ui/shared";
|
|
5
5
|
import Hint from "../Hint/Hint.vue";
|
|
6
|
+
import type { FieldProps } from "../_internal/field";
|
|
7
|
+
import "../_internal/input.css";
|
|
6
8
|
|
|
7
9
|
export type NumberRange = [number, number];
|
|
8
10
|
|
|
@@ -16,14 +18,11 @@ const range = defineModel<NumberRange>("range");
|
|
|
16
18
|
const lower = defineModel<number>("lower");
|
|
17
19
|
const upper = defineModel<number>("upper");
|
|
18
20
|
|
|
19
|
-
|
|
20
|
-
label?: string;
|
|
21
|
-
hideLabel?: boolean;
|
|
21
|
+
interface Props extends FieldProps {
|
|
22
22
|
placeholder?: string;
|
|
23
23
|
step?: number;
|
|
24
24
|
min?: number;
|
|
25
25
|
max?: number;
|
|
26
|
-
hint?: string;
|
|
27
26
|
percent?: boolean;
|
|
28
27
|
slider?: boolean;
|
|
29
28
|
live?: boolean;
|
|
@@ -42,7 +41,9 @@ const props = defineProps<{
|
|
|
42
41
|
/** @deprecated Use `format` instead. Still honored for slider labels
|
|
43
42
|
* when `format` is unset, but will be removed in a future release. */
|
|
44
43
|
sliderDisplay?: (value: number) => string;
|
|
45
|
-
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const props = defineProps<Props>();
|
|
46
47
|
|
|
47
48
|
function isRangeValue(v: unknown): v is NumberRange {
|
|
48
49
|
return Array.isArray(v) && v.length === 2;
|
|
@@ -75,7 +76,7 @@ onMounted(() => {
|
|
|
75
76
|
}
|
|
76
77
|
});
|
|
77
78
|
|
|
78
|
-
const sliderMin = computed(() => props.min ??
|
|
79
|
+
const sliderMin = computed(() => props.min ?? 0);
|
|
79
80
|
const sliderMax = computed(() => props.max ?? (props.percent ? 1 : 100));
|
|
80
81
|
const sliderStep = computed(() => props.step ?? (props.percent ? 0.01 : 1));
|
|
81
82
|
|
|
@@ -408,11 +409,11 @@ function onArrowStep(event: KeyboardEvent, direction: 1 | -1) {
|
|
|
408
409
|
<template>
|
|
409
410
|
<component
|
|
410
411
|
:is="props.label ? 'label' : 'div'"
|
|
411
|
-
:class="props.label ? 'input-label' : undefined"
|
|
412
|
+
:class="props.label ? 'cfasim-input-label' : undefined"
|
|
412
413
|
>
|
|
413
414
|
<span
|
|
414
415
|
v-if="props.label"
|
|
415
|
-
class="input-label-row"
|
|
416
|
+
class="cfasim-input-label-row"
|
|
416
417
|
:class="{ 'visually-hidden': props.hideLabel }"
|
|
417
418
|
>
|
|
418
419
|
{{ props.label }}
|
|
@@ -421,9 +422,11 @@ function onArrowStep(event: KeyboardEvent, direction: 1 | -1) {
|
|
|
421
422
|
<span v-if="!isSlider" class="input-wrapper">
|
|
422
423
|
<input
|
|
423
424
|
type="text"
|
|
425
|
+
class="cfasim-input"
|
|
424
426
|
:inputmode="props.numberType === 'integer' ? 'numeric' : 'decimal'"
|
|
425
427
|
v-model="local"
|
|
426
428
|
:placeholder="props.placeholder"
|
|
429
|
+
:aria-label="!props.label ? props.ariaLabel : undefined"
|
|
427
430
|
:aria-invalid="!!validationError"
|
|
428
431
|
:aria-required="props.required || undefined"
|
|
429
432
|
:required="props.required"
|
|
@@ -475,67 +478,17 @@ function onArrowStep(event: KeyboardEvent, direction: 1 | -1) {
|
|
|
475
478
|
</template>
|
|
476
479
|
|
|
477
480
|
<style scoped>
|
|
478
|
-
.input-label {
|
|
479
|
-
display: flex;
|
|
480
|
-
flex-direction: column;
|
|
481
|
-
gap: 0.25em;
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
.input-label-row {
|
|
485
|
-
display: flex;
|
|
486
|
-
align-items: center;
|
|
487
|
-
justify-content: space-between;
|
|
488
|
-
}
|
|
489
|
-
|
|
490
481
|
.input-wrapper {
|
|
491
482
|
display: flex;
|
|
492
483
|
align-items: center;
|
|
493
484
|
gap: 0.25em;
|
|
494
485
|
}
|
|
495
486
|
|
|
496
|
-
.input-wrapper input {
|
|
487
|
+
.input-wrapper .cfasim-input {
|
|
497
488
|
flex: 1;
|
|
498
489
|
min-width: 0;
|
|
499
490
|
}
|
|
500
491
|
|
|
501
|
-
input {
|
|
502
|
-
display: block;
|
|
503
|
-
width: 100%;
|
|
504
|
-
height: 2.5em;
|
|
505
|
-
padding: 0 0.75em;
|
|
506
|
-
font-size: inherit;
|
|
507
|
-
background-color: var(--color-bg-0);
|
|
508
|
-
color: var(--color-text);
|
|
509
|
-
border: 1px solid var(--color-border);
|
|
510
|
-
border-radius: 0.375em;
|
|
511
|
-
transition:
|
|
512
|
-
border-color var(--transition-fast),
|
|
513
|
-
box-shadow var(--transition-fast);
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
input:hover {
|
|
517
|
-
border-color: var(--color-border-hover);
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
input:focus {
|
|
521
|
-
outline: none;
|
|
522
|
-
border-color: var(--color-border-focus);
|
|
523
|
-
box-shadow: var(--shadow-focus);
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
input[aria-invalid="true"] {
|
|
527
|
-
border-color: var(--color-error);
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
input[aria-invalid="true"]:focus {
|
|
531
|
-
border-color: var(--color-error);
|
|
532
|
-
box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-error) 25%, transparent);
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
input::placeholder {
|
|
536
|
-
color: var(--color-text-tertiary);
|
|
537
|
-
}
|
|
538
|
-
|
|
539
492
|
.input-suffix {
|
|
540
493
|
color: var(--color-text-secondary);
|
|
541
494
|
font-size: var(--font-size-sm);
|
|
@@ -166,6 +166,7 @@ reka-ui Combobox following the
|
|
|
166
166
|
| `label` | `string` | No | — |
|
|
167
167
|
| `hideLabel` | `boolean` | No | — |
|
|
168
168
|
| `ariaLabel` | `string` | No | — |
|
|
169
|
+
| `hint` | `string` | No | — |
|
|
169
170
|
| `options` | `SelectOption[]` | Yes | — |
|
|
170
171
|
| `placeholder` | `string` | No | — |
|
|
171
172
|
| `autocomplete` | `boolean` | No | — |
|
|
@@ -19,8 +19,11 @@ import {
|
|
|
19
19
|
SelectTrigger,
|
|
20
20
|
SelectValue,
|
|
21
21
|
SelectViewport,
|
|
22
|
-
useId,
|
|
23
22
|
} from "reka-ui";
|
|
23
|
+
import Icon from "../Icon/Icon.vue";
|
|
24
|
+
import FieldLabel from "../_internal/FieldLabel.vue";
|
|
25
|
+
import { useField, type FieldProps } from "../_internal/field";
|
|
26
|
+
import "../_internal/listbox.css";
|
|
24
27
|
|
|
25
28
|
export interface SelectOption {
|
|
26
29
|
value: string;
|
|
@@ -29,17 +32,16 @@ export interface SelectOption {
|
|
|
29
32
|
|
|
30
33
|
const model = defineModel<string>();
|
|
31
34
|
|
|
32
|
-
|
|
33
|
-
label?: string;
|
|
34
|
-
hideLabel?: boolean;
|
|
35
|
-
ariaLabel?: string;
|
|
35
|
+
interface Props extends FieldProps {
|
|
36
36
|
options: SelectOption[];
|
|
37
37
|
placeholder?: string;
|
|
38
38
|
/** Turn the field into a filterable single-select autocomplete (combobox). */
|
|
39
39
|
autocomplete?: boolean;
|
|
40
|
-
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const props = defineProps<Props>();
|
|
41
43
|
|
|
42
|
-
const id =
|
|
44
|
+
const { id, labelId, ariaProps } = useField(props);
|
|
43
45
|
|
|
44
46
|
// Shows the selected option's label in the combobox input when not filtering.
|
|
45
47
|
function displayValue(value: string) {
|
|
@@ -49,14 +51,14 @@ function displayValue(value: string) {
|
|
|
49
51
|
|
|
50
52
|
<template>
|
|
51
53
|
<div class="select-box">
|
|
52
|
-
<
|
|
53
|
-
v-if="label"
|
|
54
|
-
:id="`${id}-label`"
|
|
55
|
-
:for="autocomplete ? `${id}-input` : undefined"
|
|
54
|
+
<FieldLabel
|
|
56
55
|
class="select-label"
|
|
57
|
-
:
|
|
58
|
-
|
|
59
|
-
|
|
56
|
+
:label="label"
|
|
57
|
+
:label-id="labelId"
|
|
58
|
+
:hide-label="hideLabel"
|
|
59
|
+
:hint="hint"
|
|
60
|
+
:html-for="autocomplete ? `${id}-input` : undefined"
|
|
61
|
+
/>
|
|
60
62
|
|
|
61
63
|
<ComboboxRoot
|
|
62
64
|
v-if="autocomplete"
|
|
@@ -70,55 +72,34 @@ function displayValue(value: string) {
|
|
|
70
72
|
class="select-input"
|
|
71
73
|
:display-value="displayValue"
|
|
72
74
|
:placeholder="placeholder"
|
|
73
|
-
|
|
74
|
-
:aria-label="!props.label ? props.ariaLabel : undefined"
|
|
75
|
+
v-bind="ariaProps"
|
|
75
76
|
/>
|
|
76
77
|
<ComboboxTrigger class="select-icon-button" aria-label="Toggle options">
|
|
77
78
|
<span class="select-icon" aria-hidden="true">
|
|
78
|
-
<
|
|
79
|
-
width="12"
|
|
80
|
-
height="12"
|
|
81
|
-
viewBox="0 0 12 12"
|
|
82
|
-
fill="none"
|
|
83
|
-
stroke="currentColor"
|
|
84
|
-
stroke-width="2"
|
|
85
|
-
stroke-linecap="round"
|
|
86
|
-
stroke-linejoin="round"
|
|
87
|
-
>
|
|
88
|
-
<path d="M3 4.5L6 7.5L9 4.5" />
|
|
89
|
-
</svg>
|
|
79
|
+
<Icon icon="keyboard_arrow_down" :size="16" />
|
|
90
80
|
</span>
|
|
91
81
|
</ComboboxTrigger>
|
|
92
82
|
</ComboboxAnchor>
|
|
93
83
|
<ComboboxPortal>
|
|
94
84
|
<ComboboxContent
|
|
95
|
-
class="
|
|
85
|
+
class="cfasim-listbox-content select-content-autocomplete"
|
|
96
86
|
position="popper"
|
|
97
87
|
:side-offset="4"
|
|
98
88
|
:body-lock="false"
|
|
99
89
|
>
|
|
100
|
-
<ComboboxViewport class="
|
|
101
|
-
<ComboboxEmpty class="
|
|
90
|
+
<ComboboxViewport class="cfasim-listbox-viewport">
|
|
91
|
+
<ComboboxEmpty class="cfasim-listbox-empty">
|
|
92
|
+
No matches
|
|
93
|
+
</ComboboxEmpty>
|
|
102
94
|
<ComboboxItem
|
|
103
95
|
v-for="opt in options"
|
|
104
96
|
:key="opt.value"
|
|
105
97
|
:value="opt.value"
|
|
106
|
-
class="
|
|
98
|
+
class="cfasim-listbox-item"
|
|
107
99
|
>
|
|
108
100
|
<span>{{ opt.label }}</span>
|
|
109
|
-
<ComboboxItemIndicator class="
|
|
110
|
-
<
|
|
111
|
-
width="12"
|
|
112
|
-
height="12"
|
|
113
|
-
viewBox="0 0 12 12"
|
|
114
|
-
fill="none"
|
|
115
|
-
stroke="currentColor"
|
|
116
|
-
stroke-width="2"
|
|
117
|
-
stroke-linecap="round"
|
|
118
|
-
stroke-linejoin="round"
|
|
119
|
-
>
|
|
120
|
-
<path d="M2 6L5 9L10 3" />
|
|
121
|
-
</svg>
|
|
101
|
+
<ComboboxItemIndicator class="cfasim-listbox-indicator">
|
|
102
|
+
<Icon icon="check" :size="14" />
|
|
122
103
|
</ComboboxItemIndicator>
|
|
123
104
|
</ComboboxItem>
|
|
124
105
|
</ComboboxViewport>
|
|
@@ -127,55 +108,29 @@ function displayValue(value: string) {
|
|
|
127
108
|
</ComboboxRoot>
|
|
128
109
|
|
|
129
110
|
<SelectRoot v-else v-model="model">
|
|
130
|
-
<SelectTrigger
|
|
131
|
-
class="select-trigger"
|
|
132
|
-
:aria-labelledby="props.label ? `${id}-label` : undefined"
|
|
133
|
-
:aria-label="!props.label ? props.ariaLabel : undefined"
|
|
134
|
-
>
|
|
111
|
+
<SelectTrigger class="select-trigger" v-bind="ariaProps">
|
|
135
112
|
<SelectValue :placeholder="placeholder" />
|
|
136
113
|
<span class="select-icon" aria-hidden="true">
|
|
137
|
-
<
|
|
138
|
-
width="12"
|
|
139
|
-
height="12"
|
|
140
|
-
viewBox="0 0 12 12"
|
|
141
|
-
fill="none"
|
|
142
|
-
stroke="currentColor"
|
|
143
|
-
stroke-width="2"
|
|
144
|
-
stroke-linecap="round"
|
|
145
|
-
stroke-linejoin="round"
|
|
146
|
-
>
|
|
147
|
-
<path d="M3 4.5L6 7.5L9 4.5" />
|
|
148
|
-
</svg>
|
|
114
|
+
<Icon icon="keyboard_arrow_down" :size="16" />
|
|
149
115
|
</span>
|
|
150
116
|
</SelectTrigger>
|
|
151
117
|
<SelectPortal>
|
|
152
118
|
<SelectContent
|
|
153
|
-
class="select-content"
|
|
119
|
+
class="cfasim-listbox-content select-content"
|
|
154
120
|
position="popper"
|
|
155
121
|
:side-offset="4"
|
|
156
122
|
:body-lock="false"
|
|
157
123
|
>
|
|
158
|
-
<SelectViewport class="
|
|
124
|
+
<SelectViewport class="cfasim-listbox-viewport">
|
|
159
125
|
<SelectItem
|
|
160
126
|
v-for="opt in options"
|
|
161
127
|
:key="opt.value"
|
|
162
128
|
:value="opt.value"
|
|
163
|
-
class="
|
|
129
|
+
class="cfasim-listbox-item"
|
|
164
130
|
>
|
|
165
131
|
<SelectItemText>{{ opt.label }}</SelectItemText>
|
|
166
|
-
<SelectItemIndicator class="
|
|
167
|
-
<
|
|
168
|
-
width="12"
|
|
169
|
-
height="12"
|
|
170
|
-
viewBox="0 0 12 12"
|
|
171
|
-
fill="none"
|
|
172
|
-
stroke="currentColor"
|
|
173
|
-
stroke-width="2"
|
|
174
|
-
stroke-linecap="round"
|
|
175
|
-
stroke-linejoin="round"
|
|
176
|
-
>
|
|
177
|
-
<path d="M2 6L5 9L10 3" />
|
|
178
|
-
</svg>
|
|
132
|
+
<SelectItemIndicator class="cfasim-listbox-indicator">
|
|
133
|
+
<Icon icon="check" :size="14" />
|
|
179
134
|
</SelectItemIndicator>
|
|
180
135
|
</SelectItem>
|
|
181
136
|
</SelectViewport>
|
|
@@ -291,14 +246,8 @@ function displayValue(value: string) {
|
|
|
291
246
|
</style>
|
|
292
247
|
|
|
293
248
|
<style>
|
|
249
|
+
/* Sizing only — the dropdown skin lives in _internal/listbox.css. */
|
|
294
250
|
.select-content {
|
|
295
|
-
z-index: 100;
|
|
296
|
-
background: var(--color-bg-0);
|
|
297
|
-
border: 1px solid var(--color-border);
|
|
298
|
-
border-radius: 0.25em;
|
|
299
|
-
box-shadow:
|
|
300
|
-
0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
|
301
|
-
0 2px 4px -2px rgba(0, 0, 0, 0.1);
|
|
302
251
|
min-width: var(--reka-select-trigger-width);
|
|
303
252
|
max-height: var(--reka-select-content-available-height);
|
|
304
253
|
}
|
|
@@ -308,44 +257,4 @@ function displayValue(value: string) {
|
|
|
308
257
|
width: var(--reka-combobox-trigger-width);
|
|
309
258
|
max-height: var(--reka-combobox-content-available-height);
|
|
310
259
|
}
|
|
311
|
-
|
|
312
|
-
.select-viewport {
|
|
313
|
-
padding: 0.25em;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
.select-empty {
|
|
317
|
-
padding: 0.5em;
|
|
318
|
-
font-size: var(--font-size-sm);
|
|
319
|
-
color: var(--color-text-secondary);
|
|
320
|
-
text-align: center;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
.select-item {
|
|
324
|
-
display: flex;
|
|
325
|
-
align-items: center;
|
|
326
|
-
justify-content: space-between;
|
|
327
|
-
gap: 0.5em;
|
|
328
|
-
padding: 0.25em 0.5em;
|
|
329
|
-
border-radius: 0.25em;
|
|
330
|
-
font-size: var(--font-size-sm);
|
|
331
|
-
white-space: nowrap;
|
|
332
|
-
cursor: pointer;
|
|
333
|
-
user-select: none;
|
|
334
|
-
outline: none;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
.select-item[data-highlighted] {
|
|
338
|
-
background: var(--color-primary);
|
|
339
|
-
color: white;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
.select-item[data-state="checked"] {
|
|
343
|
-
font-weight: 600;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
.select-indicator {
|
|
347
|
-
display: flex;
|
|
348
|
-
align-items: center;
|
|
349
|
-
flex-shrink: 0;
|
|
350
|
-
}
|
|
351
260
|
</style>
|
|
@@ -77,6 +77,7 @@ it.
|
|
|
77
77
|
|------|------|----------|---------|
|
|
78
78
|
| `label` | `string` | No | — |
|
|
79
79
|
| `hideLabel` | `boolean` | No | — |
|
|
80
|
-
| `
|
|
80
|
+
| `ariaLabel` | `string` | No | — |
|
|
81
81
|
| `hint` | `string` | No | — |
|
|
82
|
+
| `placeholder` | `string` | No | — |
|
|
82
83
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { ref, watch } from "vue";
|
|
3
3
|
import Hint from "../Hint/Hint.vue";
|
|
4
|
+
import type { FieldProps } from "../_internal/field";
|
|
5
|
+
import "../_internal/input.css";
|
|
4
6
|
|
|
5
7
|
const model = defineModel<string>();
|
|
6
8
|
const local = ref(model.value);
|
|
@@ -13,18 +15,17 @@ function commit() {
|
|
|
13
15
|
model.value = local.value;
|
|
14
16
|
}
|
|
15
17
|
|
|
16
|
-
|
|
17
|
-
label?: string;
|
|
18
|
-
hideLabel?: boolean;
|
|
18
|
+
interface Props extends FieldProps {
|
|
19
19
|
placeholder?: string;
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const props = defineProps<Props>();
|
|
22
23
|
</script>
|
|
23
24
|
|
|
24
25
|
<template>
|
|
25
|
-
<label v-if="props.label" class="input-label">
|
|
26
|
+
<label v-if="props.label" class="cfasim-input-label">
|
|
26
27
|
<span
|
|
27
|
-
class="input-label-row"
|
|
28
|
+
class="cfasim-input-label-row"
|
|
28
29
|
:class="{ 'visually-hidden': props.hideLabel }"
|
|
29
30
|
>
|
|
30
31
|
{{ props.label }}
|
|
@@ -32,6 +33,7 @@ const props = defineProps<{
|
|
|
32
33
|
</span>
|
|
33
34
|
<input
|
|
34
35
|
type="text"
|
|
36
|
+
class="cfasim-input"
|
|
35
37
|
v-model="local"
|
|
36
38
|
:placeholder="props.placeholder"
|
|
37
39
|
@blur="commit"
|
|
@@ -41,54 +43,12 @@ const props = defineProps<{
|
|
|
41
43
|
<div v-else>
|
|
42
44
|
<input
|
|
43
45
|
type="text"
|
|
46
|
+
class="cfasim-input"
|
|
44
47
|
v-model="local"
|
|
45
48
|
:placeholder="props.placeholder"
|
|
49
|
+
:aria-label="props.ariaLabel"
|
|
46
50
|
@blur="commit"
|
|
47
51
|
@keydown.enter="commit"
|
|
48
52
|
/>
|
|
49
53
|
</div>
|
|
50
54
|
</template>
|
|
51
|
-
|
|
52
|
-
<style scoped>
|
|
53
|
-
.input-label {
|
|
54
|
-
display: flex;
|
|
55
|
-
flex-direction: column;
|
|
56
|
-
gap: 0.25em;
|
|
57
|
-
font-size: var(--font-size-sm);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
.input-label-row {
|
|
61
|
-
display: flex;
|
|
62
|
-
align-items: center;
|
|
63
|
-
justify-content: space-between;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
input {
|
|
67
|
-
display: block;
|
|
68
|
-
width: 100%;
|
|
69
|
-
height: 2.5em;
|
|
70
|
-
padding: 0 0.75em;
|
|
71
|
-
font-size: inherit;
|
|
72
|
-
background-color: var(--color-bg-0);
|
|
73
|
-
color: var(--color-text);
|
|
74
|
-
border: 1px solid var(--color-border);
|
|
75
|
-
border-radius: 0.375em;
|
|
76
|
-
transition:
|
|
77
|
-
border-color var(--transition-fast),
|
|
78
|
-
box-shadow var(--transition-fast);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
input:hover {
|
|
82
|
-
border-color: var(--color-border-hover);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
input:focus {
|
|
86
|
-
outline: none;
|
|
87
|
-
border-color: var(--color-border-focus);
|
|
88
|
-
box-shadow: var(--shadow-focus);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
input::placeholder {
|
|
92
|
-
color: var(--color-text-tertiary);
|
|
93
|
-
}
|
|
94
|
-
</style>
|
|
@@ -142,12 +142,12 @@ Set `orientation="vertical"` to stack the items, and mark individual options
|
|
|
142
142
|
|
|
143
143
|
| Prop | Type | Required | Default |
|
|
144
144
|
|------|------|----------|---------|
|
|
145
|
-
| `options` | `ToggleGroupOption[]` | Yes | — |
|
|
146
|
-
| `multiple` | `boolean` | No | — |
|
|
147
145
|
| `label` | `string` | No | — |
|
|
148
146
|
| `hideLabel` | `boolean` | No | — |
|
|
149
147
|
| `ariaLabel` | `string` | No | — |
|
|
150
148
|
| `hint` | `string` | No | — |
|
|
149
|
+
| `options` | `ToggleGroupOption[]` | Yes | — |
|
|
150
|
+
| `multiple` | `boolean` | No | — |
|
|
151
151
|
| `disabled` | `boolean` | No | — |
|
|
152
152
|
| `orientation` | `"horizontal" \| "vertical"` | No | `"horizontal"` |
|
|
153
153
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { ToggleGroupItem, ToggleGroupRoot
|
|
2
|
+
import { ToggleGroupItem, ToggleGroupRoot } from "reka-ui";
|
|
3
3
|
import { computed } from "vue";
|
|
4
|
-
import
|
|
4
|
+
import FieldLabel from "../_internal/FieldLabel.vue";
|
|
5
|
+
import { useField, type FieldProps } from "../_internal/field";
|
|
5
6
|
|
|
6
7
|
export interface ToggleGroupOption {
|
|
7
8
|
value: string;
|
|
@@ -12,21 +13,18 @@ export interface ToggleGroupOption {
|
|
|
12
13
|
// `string` in single mode, `string[]` in multiple mode.
|
|
13
14
|
const model = defineModel<string | string[]>();
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
ariaLabel?: string;
|
|
22
|
-
hint?: string;
|
|
23
|
-
disabled?: boolean;
|
|
24
|
-
orientation?: "horizontal" | "vertical";
|
|
25
|
-
}>(),
|
|
26
|
-
{ orientation: "horizontal" },
|
|
27
|
-
);
|
|
16
|
+
interface Props extends FieldProps {
|
|
17
|
+
options: ToggleGroupOption[];
|
|
18
|
+
multiple?: boolean;
|
|
19
|
+
disabled?: boolean;
|
|
20
|
+
orientation?: "horizontal" | "vertical";
|
|
21
|
+
}
|
|
28
22
|
|
|
29
|
-
const
|
|
23
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
24
|
+
orientation: "horizontal",
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const { labelId, ariaProps } = useField(props);
|
|
30
28
|
const type = computed<"single" | "multiple">(() =>
|
|
31
29
|
props.multiple ? "multiple" : "single",
|
|
32
30
|
);
|
|
@@ -34,23 +32,20 @@ const type = computed<"single" | "multiple">(() =>
|
|
|
34
32
|
|
|
35
33
|
<template>
|
|
36
34
|
<div class="toggle-group-field">
|
|
37
|
-
<
|
|
38
|
-
v-if="label"
|
|
39
|
-
:id="`${id}-label`"
|
|
35
|
+
<FieldLabel
|
|
40
36
|
class="toggle-group-label"
|
|
41
|
-
:
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
37
|
+
:label="label"
|
|
38
|
+
:label-id="labelId"
|
|
39
|
+
:hide-label="hideLabel"
|
|
40
|
+
:hint="hint"
|
|
41
|
+
/>
|
|
46
42
|
<ToggleGroupRoot
|
|
47
43
|
v-model="model"
|
|
48
44
|
:type="type"
|
|
49
45
|
:disabled="disabled"
|
|
50
46
|
:orientation="orientation"
|
|
51
47
|
:class="['toggle-group', `toggle-group--${orientation}`]"
|
|
52
|
-
|
|
53
|
-
:aria-label="!label ? ariaLabel : undefined"
|
|
48
|
+
v-bind="ariaProps"
|
|
54
49
|
>
|
|
55
50
|
<ToggleGroupItem
|
|
56
51
|
v-for="opt in options"
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import Hint from "../Hint/Hint.vue";
|
|
3
|
+
|
|
4
|
+
// The visible label for a form field, with the shared hideLabel/hint
|
|
5
|
+
// behavior. Renders nothing without a `label`; the parent supplies its
|
|
6
|
+
// own class via fallthrough attrs so existing scoped styles keep working.
|
|
7
|
+
defineProps<{
|
|
8
|
+
label?: string;
|
|
9
|
+
labelId?: string;
|
|
10
|
+
hideLabel?: boolean;
|
|
11
|
+
hint?: string;
|
|
12
|
+
/** `for` attribute, for fields whose control is a real labelable element. */
|
|
13
|
+
htmlFor?: string;
|
|
14
|
+
}>();
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<template>
|
|
18
|
+
<label
|
|
19
|
+
v-if="label"
|
|
20
|
+
:id="labelId"
|
|
21
|
+
:for="htmlFor"
|
|
22
|
+
:class="{ 'visually-hidden': hideLabel }"
|
|
23
|
+
>
|
|
24
|
+
{{ label }}
|
|
25
|
+
<Hint v-if="hint && !hideLabel" :text="hint" />
|
|
26
|
+
</label>
|
|
27
|
+
</template>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { computed } from "vue";
|
|
2
|
+
import { useId } from "reka-ui";
|
|
3
|
+
|
|
4
|
+
/** Props shared by every labeled form field. */
|
|
5
|
+
export interface FieldProps {
|
|
6
|
+
label?: string;
|
|
7
|
+
/** Visually hide the label while keeping it for assistive tech. */
|
|
8
|
+
hideLabel?: boolean;
|
|
9
|
+
/** Accessible name used when no visible `label` is provided. */
|
|
10
|
+
ariaLabel?: string;
|
|
11
|
+
hint?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Generated field/label ids plus the aria attributes that point a control
|
|
16
|
+
* at its label: `aria-labelledby` when a visible `label` exists, else
|
|
17
|
+
* `aria-label`. Spread onto the control with `v-bind="ariaProps.value"`.
|
|
18
|
+
*/
|
|
19
|
+
export function useField(props: FieldProps) {
|
|
20
|
+
const id = useId();
|
|
21
|
+
const labelId = `${id}-label`;
|
|
22
|
+
const ariaProps = computed(() => ({
|
|
23
|
+
"aria-labelledby": props.label ? labelId : undefined,
|
|
24
|
+
"aria-label": !props.label ? props.ariaLabel : undefined,
|
|
25
|
+
}));
|
|
26
|
+
return { id, labelId, ariaProps };
|
|
27
|
+
}
|