@dumiflex/wp-kit 0.3.0 → 0.5.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/CHANGELOG.md +92 -51
- package/LICENSE +674 -674
- package/README.md +73 -76
- package/package.json +78 -77
- package/src/Avatar.vue +83 -83
- package/src/Badge.vue +97 -97
- package/src/Banner.vue +118 -118
- package/src/BookmarkButton.vue +27 -27
- package/src/BundleMeta.vue +51 -51
- package/src/Button.vue +103 -103
- package/src/Card.vue +68 -68
- package/src/Checkbox.vue +109 -107
- package/src/Chip.vue +163 -97
- package/src/ChoiceCards.vue +133 -133
- package/src/CloseButton.vue +69 -69
- package/src/CodeBlock.vue +146 -119
- package/src/ColorField.vue +188 -206
- package/src/ColorPicker.vue +201 -201
- package/src/Combobox.vue +331 -334
- package/src/ConfirmDialog.vue +165 -163
- package/src/ConfirmModal.vue +147 -147
- package/src/ConstraintMatrixPreview.vue +708 -708
- package/src/ContextMenu.vue +386 -379
- package/src/CopyButton.vue +156 -156
- package/src/DataTable.vue +293 -292
- package/src/DatePicker.vue +234 -234
- package/src/DescriptionList.vue +73 -73
- package/src/DiffView.vue +68 -68
- package/src/Disclosure.vue +166 -166
- package/src/DragHandle.vue +49 -49
- package/src/Drawer.vue +131 -131
- package/src/DropZone.vue +196 -196
- package/src/DropdownMenu.vue +72 -72
- package/src/EmptyState.vue +111 -111
- package/src/Field.vue +28 -28
- package/src/HsvPicker.vue +208 -208
- package/src/Icon.vue +39 -39
- package/src/IconPicker.vue +209 -209
- package/src/ImageGrid.vue +127 -125
- package/src/InlineEdit.vue +183 -183
- package/src/Input.vue +99 -99
- package/src/Kbd.vue +56 -56
- package/src/Lightbox.vue +176 -173
- package/src/ListFilter.vue +85 -85
- package/src/ListRow.vue +204 -204
- package/src/LoadingMark.vue +217 -161
- package/src/Logo.vue +60 -60
- package/src/Modal.vue +78 -76
- package/src/ModalShell.vue +109 -107
- package/src/Pagination.vue +106 -106
- package/src/Popover.vue +180 -171
- package/src/PostCard.vue +210 -211
- package/src/ProgressBar.vue +90 -90
- package/src/RadioGroup.vue +177 -177
- package/src/RangeSlider.vue +183 -183
- package/src/RelativeDate.vue +35 -35
- package/src/ResizeHandle.vue +143 -143
- package/src/SearchInput.vue +192 -199
- package/src/Select.vue +644 -641
- package/src/Separator.vue +54 -54
- package/src/Skeleton.vue +98 -82
- package/src/Slider.vue +198 -198
- package/src/SortableList.vue +260 -260
- package/src/SplitPane.vue +68 -68
- package/src/StarButton.vue +40 -40
- package/src/Stepper.vue +141 -141
- package/src/SupportLinks.vue +93 -93
- package/src/TagInput.vue +297 -300
- package/src/Textarea.vue +57 -57
- package/src/Timeline.vue +92 -92
- package/src/ToastCard.vue +119 -119
- package/src/ToastHost.vue +40 -40
- package/src/Toggle.vue +36 -36
- package/src/TokenizedValue.vue +310 -310
- package/src/Toolbar.vue +110 -110
- package/src/VirtualList.vue +87 -87
- package/src/VisuallyHidden.vue +29 -29
- package/src/WpButton.vue +75 -75
- package/src/WpCheck.vue +104 -102
- package/src/WpIconButton.vue +107 -107
- package/src/WpImageZoom.vue +544 -544
- package/src/WpNumber.vue +178 -183
- package/src/WpSegmented.vue +175 -175
- package/src/WpSelect.vue +93 -89
- package/src/WpSwitch.vue +163 -163
- package/src/WpTabs.vue +188 -188
- package/src/_modal-motion.css +33 -33
- package/src/_skeleton.css +50 -50
- package/src/axis-color.ts +42 -42
- package/src/button.css +169 -169
- package/src/calendar.ts +59 -59
- package/src/clipboard.ts +50 -50
- package/src/colour-presets.ts +12 -12
- package/src/diff.ts +51 -51
- package/src/drop-zone.ts +41 -41
- package/src/field.css +50 -0
- package/src/flip.ts +237 -237
- package/src/floating-layer.ts +48 -0
- package/src/floating.ts +99 -99
- package/src/highlight.ts +199 -0
- package/src/hsv.ts +100 -100
- package/src/icon-catalog.ts +52 -52
- package/src/image-zoom.ts +93 -93
- package/src/kind-colour.ts +28 -0
- package/src/loading-mark-motion.ts +249 -0
- package/src/logo-paths.ts +21 -21
- package/src/matrix-axis.ts +91 -91
- package/src/pagination.ts +27 -27
- package/src/platform-keys.ts +29 -29
- package/src/rich-tokenize.ts +357 -357
- package/src/select-types.ts +32 -32
- package/src/slider-math.ts +45 -45
- package/src/sortable.ts +22 -22
- package/src/theme/fonts/OFL-Fraunces.txt +93 -93
- package/src/theme/fonts/OFL-JetBrainsMono.txt +93 -93
- package/src/theme/fonts/OFL-Manrope.txt +93 -93
- package/src/theme/looks.css +428 -428
- package/src/theme/palettes.css +254 -254
- package/src/theme/tokens.css +2896 -2894
- package/src/tokens.ts +56 -56
- package/src/tooltip.css +43 -43
- package/src/tooltip.ts +146 -146
- package/src/useDialogFocus.ts +73 -73
- package/src/useGrowableField.ts +357 -357
- package/src/useModalKeyShield.ts +116 -116
- package/src/useToast.ts +48 -48
- package/src/wp-logo.svg +14 -14
- package/src/ChipToggle.vue +0 -92
- package/src/KindChip.vue +0 -53
- package/src/RatingBadge.vue +0 -42
package/src/RadioGroup.vue
CHANGED
|
@@ -1,177 +1,177 @@
|
|
|
1
|
-
<script setup lang="ts" generic="T extends string">
|
|
2
|
-
/**
|
|
3
|
-
* Radio group: one of a few options, each with room for a line saying what
|
|
4
|
-
* it does. Use it when the options need explaining; for two to five short
|
|
5
|
-
* words that fit on one row use the Segmented control. Works on the canvas
|
|
6
|
-
* (`size="sm"`) and in the manager (`md`).
|
|
7
|
-
*
|
|
8
|
-
* <RadioGroup v-model="mode" :options="[{ value: 'tags', label: 'Tags', hint: 'Danbooru-style, comma separated' }]" aria-label="Prompt style" />
|
|
9
|
-
*
|
|
10
|
-
* APG radio group: one Tab stop (the chosen option), the arrow keys move and
|
|
11
|
-
* choose, disabled options are skipped. `orientation="horizontal"` lays the
|
|
12
|
-
* options out in a row. The `label` slot draws an option's label itself
|
|
13
|
-
* (a name plus a badge), and `test-prefix` gives each option a `data-test`.
|
|
14
|
-
*
|
|
15
|
-
* <RadioGroup v-model="provider" :options="opts" test-prefix="provider">
|
|
16
|
-
* <template #label="{ option }">{{ option.label }} <Badge text="Local" /></template>
|
|
17
|
-
* </RadioGroup>
|
|
18
|
-
*
|
|
19
|
-
* `variant="swatch"` draws each option as a round colour swatch (its
|
|
20
|
-
* `color`), in a row, with the label as its name and tooltip: an accent or
|
|
21
|
-
* frame colour picked from a few.
|
|
22
|
-
*/
|
|
23
|
-
import { nextTick, ref } from "vue";
|
|
24
|
-
|
|
25
|
-
export interface RadioOption<V extends string = string> {
|
|
26
|
-
value: V;
|
|
27
|
-
label: string;
|
|
28
|
-
hint?: string | undefined;
|
|
29
|
-
disabled?: boolean | undefined;
|
|
30
|
-
/** The swatch colour, for `variant="swatch"`. */
|
|
31
|
-
color?: string | undefined;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const props = withDefaults(defineProps<{
|
|
35
|
-
modelValue: T | null;
|
|
36
|
-
options: RadioOption<T>[];
|
|
37
|
-
orientation?: "vertical" | "horizontal" | undefined;
|
|
38
|
-
size?: "sm" | "md" | undefined;
|
|
39
|
-
disabled?: boolean | undefined;
|
|
40
|
-
invalid?: boolean | undefined;
|
|
41
|
-
ariaLabel?: string | undefined;
|
|
42
|
-
/** Draw each option as a bordered card (for a few big choices). */
|
|
43
|
-
cards?: boolean | undefined;
|
|
44
|
-
/** `data-test` of each option is `<testPrefix>-<value>`. */
|
|
45
|
-
testPrefix?: string | undefined;
|
|
46
|
-
/** `swatch`: round colour swatches in a row, named by their label. */
|
|
47
|
-
variant?: "default" | "swatch" | undefined;
|
|
48
|
-
}>(), { orientation: "vertical", size: "md", ariaLabel: undefined, testPrefix: undefined, variant: "default" });
|
|
49
|
-
|
|
50
|
-
defineSlots<{ label?: (p: { option: RadioOption<T> }) => unknown }>();
|
|
51
|
-
|
|
52
|
-
const emit = defineEmits<{ (e: "update:modelValue", v: T): void }>();
|
|
53
|
-
const rootEl = ref<HTMLElement | null>(null);
|
|
54
|
-
|
|
55
|
-
function pick(v: T): void {
|
|
56
|
-
if (!props.disabled && v !== props.modelValue) emit("update:modelValue", v);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function tabStop(): T | undefined {
|
|
60
|
-
const live = props.options.filter((o) => !o.disabled);
|
|
61
|
-
return (live.find((o) => o.value === props.modelValue) ?? live[0])?.value;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function onKeydown(ev: KeyboardEvent, at: T): void {
|
|
65
|
-
if (props.disabled) return;
|
|
66
|
-
const live = props.options.filter((o) => !o.disabled);
|
|
67
|
-
const i = live.findIndex((o) => o.value === at);
|
|
68
|
-
let next: number;
|
|
69
|
-
switch (ev.key) {
|
|
70
|
-
case "ArrowDown": case "ArrowRight": next = (i + 1) % live.length; break;
|
|
71
|
-
case "ArrowUp": case "ArrowLeft": next = (i - 1 + live.length) % live.length; break;
|
|
72
|
-
case " ": ev.preventDefault(); pick(at); return;
|
|
73
|
-
default: return;
|
|
74
|
-
}
|
|
75
|
-
ev.preventDefault();
|
|
76
|
-
const v = live[next].value;
|
|
77
|
-
pick(v);
|
|
78
|
-
void nextTick(() => {
|
|
79
|
-
rootEl.value?.querySelectorAll<HTMLElement>("[role=radio]")[props.options.findIndex((o) => o.value === v)]?.focus();
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
</script>
|
|
83
|
-
|
|
84
|
-
<template>
|
|
85
|
-
<div
|
|
86
|
-
ref="rootEl"
|
|
87
|
-
class="wp-radio-group"
|
|
88
|
-
role="radiogroup"
|
|
89
|
-
:aria-label="ariaLabel"
|
|
90
|
-
:aria-disabled="disabled || undefined"
|
|
91
|
-
:aria-invalid="invalid || undefined"
|
|
92
|
-
:data-orientation="orientation"
|
|
93
|
-
:data-size="size"
|
|
94
|
-
:data-cards="cards || undefined"
|
|
95
|
-
:data-variant="variant"
|
|
96
|
-
>
|
|
97
|
-
<!-- Each radio has a tabindex (roving: only the checked one is a Tab stop). -->
|
|
98
|
-
<!-- eslint-disable-next-line vuejs-accessibility/interactive-supports-focus -->
|
|
99
|
-
<div
|
|
100
|
-
v-for="o in options"
|
|
101
|
-
:key="o.value"
|
|
102
|
-
class="wp-radio-group__opt"
|
|
103
|
-
role="radio"
|
|
104
|
-
:aria-checked="modelValue === o.value"
|
|
105
|
-
:aria-disabled="disabled || o.disabled || undefined"
|
|
106
|
-
:tabindex="!disabled && o.value === tabStop() ? 0 : -1"
|
|
107
|
-
:data-checked="modelValue === o.value || undefined"
|
|
108
|
-
:data-test="testPrefix ? `${testPrefix}-${o.value}` : undefined"
|
|
109
|
-
:aria-label="variant === 'swatch' ? o.label : undefined"
|
|
110
|
-
:title="variant === 'swatch' ? o.label : undefined"
|
|
111
|
-
@click="!o.disabled && pick(o.value)"
|
|
112
|
-
@keydown="onKeydown($event, o.value)"
|
|
113
|
-
>
|
|
114
|
-
<span
|
|
115
|
-
v-if="variant === 'swatch'"
|
|
116
|
-
class="wp-radio-group__swatch"
|
|
117
|
-
:style="{ background: o.color }"
|
|
118
|
-
aria-hidden="true"
|
|
119
|
-
/>
|
|
120
|
-
<span v-else class="wp-radio-group__dot" aria-hidden="true" />
|
|
121
|
-
<span v-if="variant !== 'swatch'" class="wp-radio-group__text">
|
|
122
|
-
<span class="wp-radio-group__label"><slot name="label" :option="o">{{ o.label }}</slot></span>
|
|
123
|
-
<span v-if="o.hint" class="wp-radio-group__hint">{{ o.hint }}</span>
|
|
124
|
-
</span>
|
|
125
|
-
</div>
|
|
126
|
-
</div>
|
|
127
|
-
</template>
|
|
128
|
-
|
|
129
|
-
<style scoped>
|
|
130
|
-
.wp-radio-group { display: flex; flex-direction: column; gap: var(--wp-space-3); min-width: 0; font-family: var(--wp-font-sans); }
|
|
131
|
-
.wp-radio-group[data-orientation="horizontal"] { flex-direction: row; flex-wrap: wrap; gap: var(--wp-space-5); }
|
|
132
|
-
.wp-radio-group[data-size="sm"] { gap: var(--wp-space-2); }
|
|
133
|
-
.wp-radio-group__opt {
|
|
134
|
-
display: flex; align-items: flex-start; gap: var(--wp-space-3); min-width: 0;
|
|
135
|
-
padding: 2px; border-radius: var(--wp-radius-sm); color: var(--wp-text); cursor: pointer; outline: none;
|
|
136
|
-
}
|
|
137
|
-
.wp-radio-group[data-cards] .wp-radio-group__opt {
|
|
138
|
-
flex: 1 1 0; padding: var(--wp-space-4); border: 1px solid var(--wp-border); border-radius: var(--wp-radius); background: var(--wp-bg-1);
|
|
139
|
-
transition: border-color var(--wp-motion-hover), background var(--wp-motion-hover);
|
|
140
|
-
}
|
|
141
|
-
.wp-radio-group[data-cards] .wp-radio-group__opt[data-checked] { border-color: var(--wp-accent); background: color-mix(in srgb, var(--wp-accent) 8%, var(--wp-bg-1)); }
|
|
142
|
-
.wp-radio-group__opt:focus-visible { box-shadow: 0 0 0 2px var(--wp-border-focus); }
|
|
143
|
-
.wp-radio-group__opt[aria-disabled="true"] { opacity: 0.45; cursor: not-allowed; }
|
|
144
|
-
.wp-radio-group__dot {
|
|
145
|
-
position: relative; flex: none; box-sizing: border-box; width: 16px; height: 16px; margin-top: 1px;
|
|
146
|
-
border: 1.5px solid var(--wp-border-strong); border-radius: 50%; /* round: a radio is round in every look */ background: var(--wp-bg-1);
|
|
147
|
-
transition: border-color var(--wp-motion-hover);
|
|
148
|
-
}
|
|
149
|
-
.wp-radio-group[data-size="sm"] .wp-radio-group__dot { width: 13px; height: 13px; }
|
|
150
|
-
.wp-radio-group__opt:hover:not([aria-disabled="true"]) .wp-radio-group__dot { border-color: var(--wp-accent); }
|
|
151
|
-
.wp-radio-group__opt[data-checked] .wp-radio-group__dot { border-color: var(--wp-accent); }
|
|
152
|
-
.wp-radio-group__opt[data-checked] .wp-radio-group__dot::after {
|
|
153
|
-
content: ""; position: absolute; inset: 3px; border-radius: 50%; /* round: radio dot */ background: var(--wp-accent);
|
|
154
|
-
}
|
|
155
|
-
.wp-radio-group[data-size="sm"] .wp-radio-group__opt[data-checked] .wp-radio-group__dot::after { inset: 2.5px; }
|
|
156
|
-
/* A picked option that is disabled turns grey, so it can't pass for a live one. */
|
|
157
|
-
.wp-radio-group__opt[aria-disabled="true"][data-checked] .wp-radio-group__dot { border-color: var(--wp-text-dim); }
|
|
158
|
-
.wp-radio-group__opt[aria-disabled="true"][data-checked] .wp-radio-group__dot::after { background: var(--wp-text-dim); }
|
|
159
|
-
.wp-radio-group[aria-invalid="true"] .wp-radio-group__dot { border-color: var(--wp-error-border); }
|
|
160
|
-
.wp-radio-group__text { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
|
|
161
|
-
.wp-radio-group__label { display: inline-flex; flex-wrap: wrap; align-items: center; gap: var(--wp-space-3); font-size: var(--wp-text-sm); line-height: 18px; }
|
|
162
|
-
.wp-radio-group[data-size="sm"] .wp-radio-group__label { font-size: var(--wp-text-xs); line-height: 15px; }
|
|
163
|
-
.wp-radio-group__hint { color: var(--wp-text-muted); font-size: var(--wp-text-xs); line-height: 1.4; }
|
|
164
|
-
.wp-radio-group[data-variant="swatch"] { flex-direction: row; flex-wrap: wrap; gap: var(--wp-space-3); }
|
|
165
|
-
.wp-radio-group[data-variant="swatch"] .wp-radio-group__opt { padding: 2px; border-radius: min(50%, var(--wp-radius-cap, 999px)); }
|
|
166
|
-
.wp-radio-group__swatch {
|
|
167
|
-
display: block; width: 20px; height: 20px; border-radius: min(50%, var(--wp-radius-cap, 999px));
|
|
168
|
-
box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--wp-text) 18%, transparent);
|
|
169
|
-
}
|
|
170
|
-
.wp-radio-group[data-size="sm"] .wp-radio-group__swatch { width: 16px; height: 16px; }
|
|
171
|
-
.wp-radio-group[data-variant="swatch"] .wp-radio-group__opt[data-checked] { box-shadow: 0 0 0 2px var(--wp-accent); }
|
|
172
|
-
.wp-radio-group[data-variant="swatch"] .wp-radio-group__opt:focus-visible { box-shadow: 0 0 0 2px var(--wp-border-focus); }
|
|
173
|
-
.wp-radio-group[data-variant="swatch"] .wp-radio-group__opt[data-checked]:focus-visible { box-shadow: 0 0 0 2px var(--wp-accent), 0 0 0 4px var(--wp-border-focus); }
|
|
174
|
-
@media (prefers-reduced-motion: reduce) {
|
|
175
|
-
.wp-radio-group__dot, .wp-radio-group[data-cards] .wp-radio-group__opt { transition: none; }
|
|
176
|
-
}
|
|
177
|
-
</style>
|
|
1
|
+
<script setup lang="ts" generic="T extends string">
|
|
2
|
+
/**
|
|
3
|
+
* Radio group: one of a few options, each with room for a line saying what
|
|
4
|
+
* it does. Use it when the options need explaining; for two to five short
|
|
5
|
+
* words that fit on one row use the Segmented control. Works on the canvas
|
|
6
|
+
* (`size="sm"`) and in the manager (`md`).
|
|
7
|
+
*
|
|
8
|
+
* <RadioGroup v-model="mode" :options="[{ value: 'tags', label: 'Tags', hint: 'Danbooru-style, comma separated' }]" aria-label="Prompt style" />
|
|
9
|
+
*
|
|
10
|
+
* APG radio group: one Tab stop (the chosen option), the arrow keys move and
|
|
11
|
+
* choose, disabled options are skipped. `orientation="horizontal"` lays the
|
|
12
|
+
* options out in a row. The `label` slot draws an option's label itself
|
|
13
|
+
* (a name plus a badge), and `test-prefix` gives each option a `data-test`.
|
|
14
|
+
*
|
|
15
|
+
* <RadioGroup v-model="provider" :options="opts" test-prefix="provider">
|
|
16
|
+
* <template #label="{ option }">{{ option.label }} <Badge text="Local" /></template>
|
|
17
|
+
* </RadioGroup>
|
|
18
|
+
*
|
|
19
|
+
* `variant="swatch"` draws each option as a round colour swatch (its
|
|
20
|
+
* `color`), in a row, with the label as its name and tooltip: an accent or
|
|
21
|
+
* frame colour picked from a few.
|
|
22
|
+
*/
|
|
23
|
+
import { nextTick, ref } from "vue";
|
|
24
|
+
|
|
25
|
+
export interface RadioOption<V extends string = string> {
|
|
26
|
+
value: V;
|
|
27
|
+
label: string;
|
|
28
|
+
hint?: string | undefined;
|
|
29
|
+
disabled?: boolean | undefined;
|
|
30
|
+
/** The swatch colour, for `variant="swatch"`. */
|
|
31
|
+
color?: string | undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const props = withDefaults(defineProps<{
|
|
35
|
+
modelValue: T | null;
|
|
36
|
+
options: RadioOption<T>[];
|
|
37
|
+
orientation?: "vertical" | "horizontal" | undefined;
|
|
38
|
+
size?: "sm" | "md" | undefined;
|
|
39
|
+
disabled?: boolean | undefined;
|
|
40
|
+
invalid?: boolean | undefined;
|
|
41
|
+
ariaLabel?: string | undefined;
|
|
42
|
+
/** Draw each option as a bordered card (for a few big choices). */
|
|
43
|
+
cards?: boolean | undefined;
|
|
44
|
+
/** `data-test` of each option is `<testPrefix>-<value>`. */
|
|
45
|
+
testPrefix?: string | undefined;
|
|
46
|
+
/** `swatch`: round colour swatches in a row, named by their label. */
|
|
47
|
+
variant?: "default" | "swatch" | undefined;
|
|
48
|
+
}>(), { orientation: "vertical", size: "md", ariaLabel: undefined, testPrefix: undefined, variant: "default" });
|
|
49
|
+
|
|
50
|
+
defineSlots<{ label?: (p: { option: RadioOption<T> }) => unknown }>();
|
|
51
|
+
|
|
52
|
+
const emit = defineEmits<{ (e: "update:modelValue", v: T): void }>();
|
|
53
|
+
const rootEl = ref<HTMLElement | null>(null);
|
|
54
|
+
|
|
55
|
+
function pick(v: T): void {
|
|
56
|
+
if (!props.disabled && v !== props.modelValue) emit("update:modelValue", v);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function tabStop(): T | undefined {
|
|
60
|
+
const live = props.options.filter((o) => !o.disabled);
|
|
61
|
+
return (live.find((o) => o.value === props.modelValue) ?? live[0])?.value;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function onKeydown(ev: KeyboardEvent, at: T): void {
|
|
65
|
+
if (props.disabled) return;
|
|
66
|
+
const live = props.options.filter((o) => !o.disabled);
|
|
67
|
+
const i = live.findIndex((o) => o.value === at);
|
|
68
|
+
let next: number;
|
|
69
|
+
switch (ev.key) {
|
|
70
|
+
case "ArrowDown": case "ArrowRight": next = (i + 1) % live.length; break;
|
|
71
|
+
case "ArrowUp": case "ArrowLeft": next = (i - 1 + live.length) % live.length; break;
|
|
72
|
+
case " ": ev.preventDefault(); pick(at); return;
|
|
73
|
+
default: return;
|
|
74
|
+
}
|
|
75
|
+
ev.preventDefault();
|
|
76
|
+
const v = live[next].value;
|
|
77
|
+
pick(v);
|
|
78
|
+
void nextTick(() => {
|
|
79
|
+
rootEl.value?.querySelectorAll<HTMLElement>("[role=radio]")[props.options.findIndex((o) => o.value === v)]?.focus();
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
</script>
|
|
83
|
+
|
|
84
|
+
<template>
|
|
85
|
+
<div
|
|
86
|
+
ref="rootEl"
|
|
87
|
+
class="wp-radio-group"
|
|
88
|
+
role="radiogroup"
|
|
89
|
+
:aria-label="ariaLabel"
|
|
90
|
+
:aria-disabled="disabled || undefined"
|
|
91
|
+
:aria-invalid="invalid || undefined"
|
|
92
|
+
:data-orientation="orientation"
|
|
93
|
+
:data-size="size"
|
|
94
|
+
:data-cards="cards || undefined"
|
|
95
|
+
:data-variant="variant"
|
|
96
|
+
>
|
|
97
|
+
<!-- Each radio has a tabindex (roving: only the checked one is a Tab stop). -->
|
|
98
|
+
<!-- eslint-disable-next-line vuejs-accessibility/interactive-supports-focus -->
|
|
99
|
+
<div
|
|
100
|
+
v-for="o in options"
|
|
101
|
+
:key="o.value"
|
|
102
|
+
class="wp-radio-group__opt"
|
|
103
|
+
role="radio"
|
|
104
|
+
:aria-checked="modelValue === o.value"
|
|
105
|
+
:aria-disabled="disabled || o.disabled || undefined"
|
|
106
|
+
:tabindex="!disabled && o.value === tabStop() ? 0 : -1"
|
|
107
|
+
:data-checked="modelValue === o.value || undefined"
|
|
108
|
+
:data-test="testPrefix ? `${testPrefix}-${o.value}` : undefined"
|
|
109
|
+
:aria-label="variant === 'swatch' ? o.label : undefined"
|
|
110
|
+
:title="variant === 'swatch' ? o.label : undefined"
|
|
111
|
+
@click="!o.disabled && pick(o.value)"
|
|
112
|
+
@keydown="onKeydown($event, o.value)"
|
|
113
|
+
>
|
|
114
|
+
<span
|
|
115
|
+
v-if="variant === 'swatch'"
|
|
116
|
+
class="wp-radio-group__swatch"
|
|
117
|
+
:style="{ background: o.color }"
|
|
118
|
+
aria-hidden="true"
|
|
119
|
+
/>
|
|
120
|
+
<span v-else class="wp-radio-group__dot" aria-hidden="true" />
|
|
121
|
+
<span v-if="variant !== 'swatch'" class="wp-radio-group__text">
|
|
122
|
+
<span class="wp-radio-group__label"><slot name="label" :option="o">{{ o.label }}</slot></span>
|
|
123
|
+
<span v-if="o.hint" class="wp-radio-group__hint">{{ o.hint }}</span>
|
|
124
|
+
</span>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
</template>
|
|
128
|
+
|
|
129
|
+
<style scoped>
|
|
130
|
+
.wp-radio-group { display: flex; flex-direction: column; gap: var(--wp-space-3); min-width: 0; font-family: var(--wp-font-sans); }
|
|
131
|
+
.wp-radio-group[data-orientation="horizontal"] { flex-direction: row; flex-wrap: wrap; gap: var(--wp-space-5); }
|
|
132
|
+
.wp-radio-group[data-size="sm"] { gap: var(--wp-space-2); }
|
|
133
|
+
.wp-radio-group__opt {
|
|
134
|
+
display: flex; align-items: flex-start; gap: var(--wp-space-3); min-width: 0;
|
|
135
|
+
padding: 2px; border-radius: var(--wp-radius-sm); color: var(--wp-text); cursor: pointer; outline: none;
|
|
136
|
+
}
|
|
137
|
+
.wp-radio-group[data-cards] .wp-radio-group__opt {
|
|
138
|
+
flex: 1 1 0; padding: var(--wp-space-4); border: 1px solid var(--wp-border); border-radius: var(--wp-radius); background: var(--wp-bg-1);
|
|
139
|
+
transition: border-color var(--wp-motion-hover), background var(--wp-motion-hover);
|
|
140
|
+
}
|
|
141
|
+
.wp-radio-group[data-cards] .wp-radio-group__opt[data-checked] { border-color: var(--wp-accent); background: color-mix(in srgb, var(--wp-accent) 8%, var(--wp-bg-1)); }
|
|
142
|
+
.wp-radio-group__opt:focus-visible { box-shadow: 0 0 0 2px var(--wp-border-focus); }
|
|
143
|
+
.wp-radio-group__opt[aria-disabled="true"] { opacity: 0.45; cursor: not-allowed; }
|
|
144
|
+
.wp-radio-group__dot {
|
|
145
|
+
position: relative; flex: none; box-sizing: border-box; width: 16px; height: 16px; margin-top: 1px;
|
|
146
|
+
border: 1.5px solid var(--wp-border-strong); border-radius: 50%; /* round: a radio is round in every look */ background: var(--wp-bg-1);
|
|
147
|
+
transition: border-color var(--wp-motion-hover);
|
|
148
|
+
}
|
|
149
|
+
.wp-radio-group[data-size="sm"] .wp-radio-group__dot { width: 13px; height: 13px; }
|
|
150
|
+
.wp-radio-group__opt:hover:not([aria-disabled="true"]) .wp-radio-group__dot { border-color: var(--wp-accent); }
|
|
151
|
+
.wp-radio-group__opt[data-checked] .wp-radio-group__dot { border-color: var(--wp-accent); }
|
|
152
|
+
.wp-radio-group__opt[data-checked] .wp-radio-group__dot::after {
|
|
153
|
+
content: ""; position: absolute; inset: 3px; border-radius: 50%; /* round: radio dot */ background: var(--wp-accent);
|
|
154
|
+
}
|
|
155
|
+
.wp-radio-group[data-size="sm"] .wp-radio-group__opt[data-checked] .wp-radio-group__dot::after { inset: 2.5px; }
|
|
156
|
+
/* A picked option that is disabled turns grey, so it can't pass for a live one. */
|
|
157
|
+
.wp-radio-group__opt[aria-disabled="true"][data-checked] .wp-radio-group__dot { border-color: var(--wp-text-dim); }
|
|
158
|
+
.wp-radio-group__opt[aria-disabled="true"][data-checked] .wp-radio-group__dot::after { background: var(--wp-text-dim); }
|
|
159
|
+
.wp-radio-group[aria-invalid="true"] .wp-radio-group__dot { border-color: var(--wp-error-border); }
|
|
160
|
+
.wp-radio-group__text { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
|
|
161
|
+
.wp-radio-group__label { display: inline-flex; flex-wrap: wrap; align-items: center; gap: var(--wp-space-3); font-size: var(--wp-text-sm); line-height: 18px; }
|
|
162
|
+
.wp-radio-group[data-size="sm"] .wp-radio-group__label { font-size: var(--wp-text-xs); line-height: 15px; }
|
|
163
|
+
.wp-radio-group__hint { color: var(--wp-text-muted); font-size: var(--wp-text-xs); line-height: 1.4; }
|
|
164
|
+
.wp-radio-group[data-variant="swatch"] { flex-direction: row; flex-wrap: wrap; gap: var(--wp-space-3); }
|
|
165
|
+
.wp-radio-group[data-variant="swatch"] .wp-radio-group__opt { padding: 2px; border-radius: min(50%, var(--wp-radius-cap, 999px)); }
|
|
166
|
+
.wp-radio-group__swatch {
|
|
167
|
+
display: block; width: 20px; height: 20px; border-radius: min(50%, var(--wp-radius-cap, 999px));
|
|
168
|
+
box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--wp-text) 18%, transparent);
|
|
169
|
+
}
|
|
170
|
+
.wp-radio-group[data-size="sm"] .wp-radio-group__swatch { width: 16px; height: 16px; }
|
|
171
|
+
.wp-radio-group[data-variant="swatch"] .wp-radio-group__opt[data-checked] { box-shadow: 0 0 0 2px var(--wp-accent); }
|
|
172
|
+
.wp-radio-group[data-variant="swatch"] .wp-radio-group__opt:focus-visible { box-shadow: 0 0 0 2px var(--wp-border-focus); }
|
|
173
|
+
.wp-radio-group[data-variant="swatch"] .wp-radio-group__opt[data-checked]:focus-visible { box-shadow: 0 0 0 2px var(--wp-accent), 0 0 0 4px var(--wp-border-focus); }
|
|
174
|
+
@media (prefers-reduced-motion: reduce) {
|
|
175
|
+
.wp-radio-group__dot, .wp-radio-group[data-cards] .wp-radio-group__opt { transition: none; }
|
|
176
|
+
}
|
|
177
|
+
</style>
|