srcdev-nuxt-components 9.6.7 → 9.6.9
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/.claude/rules/consumer/ids-from-useid.md +14 -0
- package/.claude/skills/component-compliance-checklist.md +18 -1
- package/.claude/skills/component-inline-action-button.md +1 -1
- package/.claude/skills/component-local-style-override.md +1 -1
- package/.claude/skills/component-naming.md +13 -13
- package/.claude/skills/components/container-glow.md +2 -2
- package/.claude/skills/components/form-field.md +2 -2
- package/.claude/skills/components/form-wrapper.md +1 -1
- package/.claude/skills/components/glowing-border.md +1 -1
- package/.claude/skills/components/input-checkbox-radio.md +19 -3
- package/.claude/skills/components/input-checkbox.md +14 -3
- package/.claude/skills/components/input-description.md +7 -7
- package/.claude/skills/components/input-error.md +3 -3
- package/.claude/skills/components/input-label.md +7 -8
- package/.claude/skills/components/input-number.md +60 -29
- package/.claude/skills/components/input-radio.md +15 -1
- package/.claude/skills/components/{input-range-core.md → input-range.md} +53 -18
- package/.claude/skills/components/input-select.md +153 -0
- package/.claude/skills/components/input-text.md +220 -0
- package/.claude/skills/components/input-textarea.md +147 -0
- package/.claude/skills/components/select-menu.md +4 -4
- package/.claude/skills/components/toggle-switch.md +143 -0
- package/.claude/skills/composable-zod-validation.md +1 -1
- package/.claude/skills/icon-sets.md +1 -1
- package/.claude/skills/icon-sizing.md +1 -1
- package/.claude/skills/index.md +10 -10
- package/.claude/skills/storybook-add-story.md +1 -1
- package/.claude/skills/testing-add-unit-test.md +17 -0
- package/.claude/skills/theming-component-token-pattern.md +8 -8
- package/.claude/skills/theming-form-geometry-tokens.md +1 -1
- package/.vscode/srcdev-component-input-checkbox-radio.code-snippets +0 -1
- package/.vscode/srcdev-component-input-range.code-snippets +38 -18
- package/.vscode/srcdev-component-input-select.code-snippets +20 -14
- package/.vscode/srcdev-component-input-text.code-snippets +46 -40
- package/.vscode/srcdev-component-input-textarea.code-snippets +19 -14
- package/.vscode/srcdev-component-toggle-switch.code-snippets +19 -29
- package/app/assets/styles/setup/04.elements/forms/00.element-defaults.css +2 -2
- package/app/assets/styles/setup/04.elements/forms/06.button-geometry.css +1 -1
- package/app/components/01.atoms/animations/glowing-border/CONSUMER-STYLING.md +2 -2
- package/app/components/01.atoms/animations/glowing-border/stories/GlowingBorder.stories.ts +4 -4
- package/app/components/02.molecules/progress-tracker/stories/ProgressTracker.stories.ts +4 -4
- package/app/components/05.forms/form-errors/CONSUMER-STYLING.md +1 -1
- package/app/components/05.forms/form-field/CONSUMER-STYLING.md +2 -2
- package/app/components/05.forms/input-button/stories/InputButton.stories.ts +3 -3
- package/app/components/05.forms/input-checkbox/CONSUMER-STYLING.md +10 -0
- package/app/components/05.forms/input-checkbox/MultipleCheckboxes.vue +18 -4
- package/app/components/05.forms/input-checkbox/SingleCheckbox.vue +14 -2
- package/app/components/05.forms/input-checkbox/stories/MultipleCheckboxes.stories.ts +146 -37
- package/app/components/05.forms/input-checkbox/stories/SingleCheckbox.stories.ts +107 -30
- package/app/components/05.forms/input-checkbox/tests/MultipleCheckboxes.spec.ts +38 -4
- package/app/components/05.forms/input-checkbox/tests/SingleCheckbox.spec.ts +5 -0
- package/app/components/05.forms/input-checkbox-radio/CONSUMER-STYLING.md +10 -3
- package/app/components/05.forms/input-checkbox-radio/InputCheckboxRadio.vue +1 -0
- package/app/components/05.forms/input-checkbox-radio/InputCheckboxRadioButton.vue +11 -10
- package/app/components/05.forms/input-checkbox-radio/InputCheckboxRadioField.vue +3 -0
- package/app/components/05.forms/input-checkbox-radio/stories/InputCheckboxRadio.stories.ts +44 -17
- package/app/components/05.forms/input-checkbox-radio/stories/InputCheckboxRadioButton.stories.ts +164 -58
- package/app/components/05.forms/input-checkbox-radio/stories/InputCheckboxRadioField.stories.ts +94 -21
- package/app/components/05.forms/input-checkbox-radio/tests/InputCheckboxRadio.spec.ts +14 -3
- package/app/components/05.forms/input-checkbox-radio/tests/InputCheckboxRadioButton.spec.ts +38 -3
- package/app/components/05.forms/input-checkbox-radio/tests/InputCheckboxRadioField.spec.ts +14 -3
- package/app/components/05.forms/input-description/CONSUMER-STYLING.md +7 -7
- package/app/components/05.forms/input-label/CONSUMER-STYLING.md +6 -6
- package/app/components/05.forms/input-number/CONSUMER-STYLING.md +50 -6
- package/app/components/05.forms/input-number/InputNumber.vue +39 -27
- package/app/components/05.forms/input-number/InputNumberField.vue +29 -16
- package/app/components/05.forms/input-number/stories/InputNumber.stories.ts +76 -25
- package/app/components/05.forms/input-number/stories/InputNumberField.stories.ts +116 -50
- package/app/components/05.forms/input-number/tests/InputNumber.spec.ts +49 -13
- package/app/components/05.forms/input-number/tests/InputNumberField.spec.ts +59 -5
- package/app/components/05.forms/input-radio/CONSUMER-STYLING.md +11 -0
- package/app/components/05.forms/input-radio/MultipleRadiobuttons.vue +14 -3
- package/app/components/05.forms/input-radio/stories/MultipleRadiobuttons.stories.ts +138 -38
- package/app/components/05.forms/input-radio/tests/MultipleRadioButtons.spec.ts +50 -3
- package/app/components/05.forms/input-range/CONSUMER-STYLING.md +68 -26
- package/app/components/05.forms/input-range/InputRange.vue +247 -0
- package/app/components/05.forms/input-range/{InputRangeDefault.vue → InputRangeField.vue} +41 -18
- package/app/components/05.forms/input-range/stories/InputRange.stories.ts +187 -0
- package/app/components/05.forms/input-range/stories/InputRangeField.stories.ts +196 -0
- package/app/components/05.forms/input-range/tests/{InputRangeCore.spec.ts → InputRange.spec.ts} +39 -15
- package/app/components/05.forms/input-range/tests/{InputRangeDefault.spec.ts → InputRangeField.spec.ts} +71 -7
- package/app/components/05.forms/input-select/CONSUMER-STYLING.md +80 -10
- package/app/components/05.forms/input-select/InputSelect.vue +198 -0
- package/app/components/05.forms/input-select/InputSelectField.vue +130 -0
- package/app/components/05.forms/input-select/stories/InputSelect.stories.ts +184 -0
- package/app/components/05.forms/input-select/stories/InputSelectField.stories.ts +187 -0
- package/app/components/05.forms/input-select/tests/{InputSelectCore.spec.ts → InputSelect.spec.ts} +75 -12
- package/app/components/05.forms/input-select/tests/InputSelectField.spec.ts +121 -0
- package/app/components/05.forms/input-text/CONSUMER-STYLING.md +85 -15
- package/app/components/05.forms/input-text/{InputPasswordWithLabel.vue → InputPasswordField.vue} +16 -19
- package/app/components/05.forms/input-text/{InputTextCore.vue → InputText.vue} +42 -78
- package/app/components/05.forms/input-text/{InputTextAsNumberWithLabel.vue → InputTextAsNumberField.vue} +66 -62
- package/app/components/05.forms/input-text/InputTextField.vue +168 -0
- package/app/components/05.forms/input-text/stories/InputPasswordField.stories.ts +163 -0
- package/app/components/05.forms/input-text/stories/InputText.stories.ts +194 -0
- package/app/components/05.forms/input-text/stories/InputTextAsNumberField.stories.ts +182 -0
- package/app/components/05.forms/input-text/stories/InputTextField.stories.ts +206 -0
- package/app/components/05.forms/input-text/tests/{InputPasswordWithLabel.spec.ts → InputPasswordField.spec.ts} +42 -3
- package/app/components/05.forms/input-text/tests/InputText.spec.ts +166 -0
- package/app/components/05.forms/input-text/tests/InputTextAsNumberField.spec.ts +135 -0
- package/app/components/05.forms/input-text/tests/InputTextField.spec.ts +130 -0
- package/app/components/05.forms/input-textarea/CONSUMER-STYLING.md +94 -12
- package/app/components/05.forms/input-textarea/InputTextarea.vue +165 -0
- package/app/components/05.forms/input-textarea/InputTextareaField.vue +159 -0
- package/app/components/05.forms/input-textarea/stories/InputTextarea.stories.ts +198 -0
- package/app/components/05.forms/input-textarea/stories/InputTextareaField.stories.ts +161 -0
- package/app/components/05.forms/input-textarea/tests/InputTextarea.spec.ts +149 -0
- package/app/components/05.forms/input-textarea/tests/InputTextareaField.spec.ts +122 -0
- package/app/components/05.forms/patterns/stories/ContactForm.stories.ts +13 -13
- package/app/components/05.forms/patterns/stories/LoginForm.stories.ts +6 -6
- package/app/components/05.forms/patterns/stories/MigratedFieldsForm.stories.ts +34 -36
- package/app/components/05.forms/toggle-switch/CONSUMER-STYLING.md +71 -50
- package/app/components/05.forms/toggle-switch/{ToggleSwitchCore.vue → ToggleSwitch.vue} +58 -73
- package/app/components/05.forms/toggle-switch/{ToggleSwitchWithLabel.vue → ToggleSwitchField.vue} +65 -19
- package/app/components/05.forms/toggle-switch/stories/ToggleSwitch.stories.ts +148 -0
- package/app/components/05.forms/toggle-switch/stories/ToggleSwitchField.stories.ts +176 -0
- package/app/components/05.forms/toggle-switch/tests/ToggleSwitch.spec.ts +145 -0
- package/app/components/05.forms/toggle-switch/tests/ToggleSwitchField.spec.ts +155 -0
- package/package.json +1 -1
- package/.claude/skills/components/input-select-core.md +0 -151
- package/.claude/skills/components/input-text-core.md +0 -204
- package/.claude/skills/components/input-textarea-core.md +0 -154
- package/.claude/skills/components/toggle-switch-core.md +0 -169
- package/app/components/05.forms/input-range/InputRangeCore.vue +0 -169
- package/app/components/05.forms/input-range/stories/InputRangeCore.stories.ts +0 -126
- package/app/components/05.forms/input-range/stories/InputRangeDefault.stories.ts +0 -108
- package/app/components/05.forms/input-select/InputSelectCore.vue +0 -188
- package/app/components/05.forms/input-select/InputSelectWithLabel.vue +0 -118
- package/app/components/05.forms/input-select/stories/InputSelectCore.stories.ts +0 -69
- package/app/components/05.forms/input-select/stories/InputSelectWithLabel.stories.ts +0 -115
- package/app/components/05.forms/input-select/tests/InputSelectWithLabel.spec.ts +0 -87
- package/app/components/05.forms/input-text/InputTextWithLabel.vue +0 -154
- package/app/components/05.forms/input-text/stories/InputPasswordWithLabel.stories.ts +0 -368
- package/app/components/05.forms/input-text/stories/InputTextAsNumberWithLabel.stories.ts +0 -480
- package/app/components/05.forms/input-text/stories/InputTextCore.stories.ts +0 -267
- package/app/components/05.forms/input-text/stories/InputTextWithLabel.stories.ts +0 -319
- package/app/components/05.forms/input-text/tests/InputTextAsNumberWithLabel.spec.ts +0 -75
- package/app/components/05.forms/input-text/tests/InputTextCore.spec.ts +0 -60
- package/app/components/05.forms/input-text/tests/InputTextWithLabel.spec.ts +0 -71
- package/app/components/05.forms/input-textarea/InputTextareaCore.vue +0 -159
- package/app/components/05.forms/input-textarea/InputTextareaWithLabel.vue +0 -136
- package/app/components/05.forms/input-textarea/stories/InputTextareaCore.stories.ts +0 -247
- package/app/components/05.forms/input-textarea/stories/InputTextareaWithLabel.stories.ts +0 -458
- package/app/components/05.forms/input-textarea/tests/InputTextareaCore.spec.ts +0 -119
- package/app/components/05.forms/input-textarea/tests/InputTextareaWithLabel.spec.ts +0 -93
- package/app/components/05.forms/toggle-switch/ToggleSwitchWithLabelInline.vue +0 -81
- package/app/components/05.forms/toggle-switch/stories/ToggleSwitchCore.stories.ts +0 -251
- package/app/components/05.forms/toggle-switch/stories/ToggleSwitchWithLabel.stories.ts +0 -251
- package/app/components/05.forms/toggle-switch/stories/ToggleSwitchWithLabelInline.stories.ts +0 -283
- package/app/components/05.forms/toggle-switch/tests/ToggleSwitchCore.spec.ts +0 -101
- package/app/components/05.forms/toggle-switch/tests/ToggleSwitchWithLabel.spec.ts +0 -133
- package/app/components/05.forms/toggle-switch/tests/ToggleSwitchWithLabelInline.spec.ts +0 -66
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div
|
|
3
|
-
class="input-range-
|
|
3
|
+
class="input-range-field"
|
|
4
|
+
:class="elementClasses"
|
|
4
5
|
:data-theme="theme"
|
|
5
|
-
:data-invalid="fieldHasError ? '' :
|
|
6
|
-
:class="[elementClasses, { error: fieldHasError }]"
|
|
6
|
+
:data-invalid="fieldHasError ? '' : undefined"
|
|
7
7
|
>
|
|
8
8
|
<InputLabel
|
|
9
9
|
:id
|
|
10
10
|
:required
|
|
11
11
|
input-variant="normal"
|
|
12
12
|
:field-has-error
|
|
13
|
-
:style-class-passthrough="['input-
|
|
13
|
+
:style-class-passthrough="['input-range-label']"
|
|
14
14
|
>
|
|
15
15
|
<template #textLabel>{{ label }}</template>
|
|
16
16
|
</InputLabel>
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
</template>
|
|
29
29
|
</InputDescription>
|
|
30
30
|
|
|
31
|
-
<
|
|
31
|
+
<InputRange
|
|
32
32
|
:id
|
|
33
33
|
v-model="modelValue"
|
|
34
34
|
:name
|
|
@@ -40,20 +40,22 @@
|
|
|
40
40
|
:weight
|
|
41
41
|
:field-has-error
|
|
42
42
|
:aria-describedby="ariaDescribedby()"
|
|
43
|
-
:style-class-passthrough="styleClassPassthrough"
|
|
44
43
|
>
|
|
45
|
-
<template v-if="slots.
|
|
46
|
-
<slot name="
|
|
44
|
+
<template v-if="slots.markers" #markers>
|
|
45
|
+
<slot name="markers"></slot>
|
|
46
|
+
</template>
|
|
47
|
+
<template v-if="slots.datalist" #datalist="{ datalistId }">
|
|
48
|
+
<slot name="datalist" :datalist-id="datalistId"></slot>
|
|
47
49
|
</template>
|
|
48
50
|
<template v-if="slots.left" #left>
|
|
49
51
|
<InputButton
|
|
50
52
|
type="button"
|
|
51
|
-
:readonly="
|
|
53
|
+
:readonly="modelValue <= min"
|
|
52
54
|
:is-pending="false"
|
|
53
55
|
:button-text="stepDownLabel"
|
|
54
56
|
:theme
|
|
55
57
|
variant="secondary"
|
|
56
|
-
@click.stop.prevent="
|
|
58
|
+
@click.stop.prevent="stepBy(-1)"
|
|
57
59
|
>
|
|
58
60
|
<template #iconOnly>
|
|
59
61
|
<slot name="left"></slot>
|
|
@@ -63,19 +65,19 @@
|
|
|
63
65
|
<template v-if="slots.right" #right>
|
|
64
66
|
<InputButton
|
|
65
67
|
type="button"
|
|
66
|
-
:readonly="
|
|
68
|
+
:readonly="modelValue >= max"
|
|
67
69
|
:is-pending="false"
|
|
68
70
|
:button-text="stepUpLabel"
|
|
69
71
|
:theme
|
|
70
72
|
variant="secondary"
|
|
71
|
-
@click.stop.prevent="
|
|
73
|
+
@click.stop.prevent="stepBy(1)"
|
|
72
74
|
>
|
|
73
75
|
<template #iconOnly>
|
|
74
76
|
<slot name="right"></slot>
|
|
75
77
|
</template>
|
|
76
78
|
</InputButton>
|
|
77
79
|
</template>
|
|
78
|
-
</
|
|
80
|
+
</InputRange>
|
|
79
81
|
<InputError
|
|
80
82
|
:id="errorId"
|
|
81
83
|
:error-message
|
|
@@ -119,7 +121,14 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
119
121
|
});
|
|
120
122
|
|
|
121
123
|
const slots = useSlots();
|
|
122
|
-
const { elementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
124
|
+
const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
125
|
+
|
|
126
|
+
watch(
|
|
127
|
+
() => props.styleClassPassthrough,
|
|
128
|
+
() => {
|
|
129
|
+
resetElementClasses(props.styleClassPassthrough);
|
|
130
|
+
}
|
|
131
|
+
);
|
|
123
132
|
|
|
124
133
|
const { id, errorId, descriptionId, ariaDescribedby } = useAriaDescribedById(
|
|
125
134
|
props.name,
|
|
@@ -131,9 +140,23 @@ const modelValue = defineModel<number>({
|
|
|
131
140
|
required: true,
|
|
132
141
|
});
|
|
133
142
|
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
143
|
+
const decimalPlaces = (value: number) => (String(value).split(".")[1] ?? "").length;
|
|
144
|
+
const clamp = (value: number) => Math.min(Math.max(value, props.min), props.max);
|
|
145
|
+
|
|
146
|
+
const stepBy = (direction: 1 | -1) => {
|
|
147
|
+
const current = Number(modelValue.value);
|
|
148
|
+
const stepSize = Math.abs(props.step) || 1;
|
|
149
|
+
const precision = Math.max(decimalPlaces(stepSize), decimalPlaces(props.min));
|
|
150
|
+
const next = clamp(Number(((Number.isFinite(current) ? current : props.min) + direction * stepSize).toFixed(precision)));
|
|
151
|
+
if (next !== current) modelValue.value = next;
|
|
138
152
|
};
|
|
139
153
|
</script>
|
|
154
|
+
|
|
155
|
+
<style lang="css">
|
|
156
|
+
@layer components {
|
|
157
|
+
.input-range-field {
|
|
158
|
+
min-inline-size: 0;
|
|
159
|
+
overflow-wrap: anywhere;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
</style>
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { ref } from "vue";
|
|
2
|
+
import type { Meta, StoryObj } from "@nuxtjs/storybook";
|
|
3
|
+
import InputRange from "../InputRange.vue";
|
|
4
|
+
import InputButton from "../../input-button/InputButton.vue";
|
|
5
|
+
import type { FormUiTheme } from "~/types/forms/types.forms.d";
|
|
6
|
+
|
|
7
|
+
interface StoryArgs {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
min: number;
|
|
11
|
+
max: number;
|
|
12
|
+
step: number;
|
|
13
|
+
required: boolean;
|
|
14
|
+
fieldHasError: boolean;
|
|
15
|
+
theme: FormUiTheme;
|
|
16
|
+
weight: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Links in the canvas open in the Storybook manager (target="_top"), not inside the preview iframe.
|
|
20
|
+
const storyNoteStyle = "margin: 0 0 1.6rem; font-size: 1.4rem;";
|
|
21
|
+
const parentNote = `
|
|
22
|
+
<p style="${storyNoteStyle}">
|
|
23
|
+
With a label, description, error message and clamped step buttons wired up, this is the
|
|
24
|
+
<a href="/?path=/story/components-forms-input-range-inputrangefield--with-step-buttons" target="_top">InputRangeField</a>.
|
|
25
|
+
</p>
|
|
26
|
+
`;
|
|
27
|
+
|
|
28
|
+
const meta: Meta<StoryArgs> = {
|
|
29
|
+
title: "Components/Forms/Input Range/InputRange",
|
|
30
|
+
component: InputRange,
|
|
31
|
+
argTypes: {
|
|
32
|
+
id: { control: "text", table: { category: "Basic" } },
|
|
33
|
+
name: { control: "text", table: { category: "Basic" } },
|
|
34
|
+
min: { control: "number", table: { category: "Basic" } },
|
|
35
|
+
max: { control: "number", table: { category: "Basic" } },
|
|
36
|
+
step: { control: "number", table: { category: "Basic" } },
|
|
37
|
+
required: { control: "boolean", table: { category: "Basic" } },
|
|
38
|
+
fieldHasError: { control: "boolean", description: "Sets data-invalid and aria-invalid", table: { category: "States" } },
|
|
39
|
+
theme: {
|
|
40
|
+
control: { type: "select" },
|
|
41
|
+
options: ["default", "success", "error", "warning"],
|
|
42
|
+
table: { category: "Styling" },
|
|
43
|
+
},
|
|
44
|
+
weight: { control: "text", description: "Adds an `input-range--{weight}` class; nothing styles it yet", table: { category: "Styling" } },
|
|
45
|
+
},
|
|
46
|
+
args: {
|
|
47
|
+
id: "range-default",
|
|
48
|
+
name: "rangeDefault",
|
|
49
|
+
min: 0,
|
|
50
|
+
max: 100,
|
|
51
|
+
step: 1,
|
|
52
|
+
required: false,
|
|
53
|
+
fieldHasError: false,
|
|
54
|
+
theme: "default",
|
|
55
|
+
weight: "normal",
|
|
56
|
+
},
|
|
57
|
+
parameters: {
|
|
58
|
+
docs: {
|
|
59
|
+
description: {
|
|
60
|
+
component:
|
|
61
|
+
"The bare native `<input type=\"range\">`. Slots: `left`/`right` (step buttons), `markers` (custom tick marks overlaid on the track; the native thumb is hidden), and `datalist`, which receives a generated `datalistId` slot prop to put on your `<datalist>`. No label, description or error: that's `InputRangeField`. Hooks: `data-theme`, `data-invalid`, `data-has-markers`.",
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export default meta;
|
|
68
|
+
type Story = StoryObj<StoryArgs>;
|
|
69
|
+
|
|
70
|
+
const render = (args: StoryArgs) => ({
|
|
71
|
+
components: { InputRange },
|
|
72
|
+
setup() {
|
|
73
|
+
const modelValue = ref(50);
|
|
74
|
+
return { args, modelValue };
|
|
75
|
+
},
|
|
76
|
+
template: `
|
|
77
|
+
<div style="margin: 36px; max-width: 320px;">
|
|
78
|
+
${parentNote}
|
|
79
|
+
<InputRange v-bind="args" v-model="modelValue" />
|
|
80
|
+
</div>
|
|
81
|
+
`,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
export const Default: Story = { render };
|
|
85
|
+
|
|
86
|
+
export const ErrorState: Story = {
|
|
87
|
+
args: { fieldHasError: true },
|
|
88
|
+
render,
|
|
89
|
+
parameters: { docs: { description: { story: "`fieldHasError` sets `data-invalid` on the wrapper and `aria-invalid` on the input." } } },
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export const WithStepButtons: Story = {
|
|
93
|
+
render: (args: StoryArgs) => ({
|
|
94
|
+
components: { InputRange, InputButton },
|
|
95
|
+
setup() {
|
|
96
|
+
const modelValue = ref(50);
|
|
97
|
+
return { args, modelValue };
|
|
98
|
+
},
|
|
99
|
+
template: `
|
|
100
|
+
<div style="margin: 36px; max-width: 320px;">
|
|
101
|
+
${parentNote}
|
|
102
|
+
<InputRange v-bind="args" v-model="modelValue">
|
|
103
|
+
<template #left>
|
|
104
|
+
<InputButton type="button" button-text="Step down" variant="secondary" @click="modelValue = Math.max(args.min, modelValue - args.step)">
|
|
105
|
+
<template #iconOnly><Icon name="mdi:minus" /></template>
|
|
106
|
+
</InputButton>
|
|
107
|
+
</template>
|
|
108
|
+
<template #right>
|
|
109
|
+
<InputButton type="button" button-text="Step up" variant="secondary" @click="modelValue = Math.min(args.max, modelValue + args.step)">
|
|
110
|
+
<template #iconOnly><Icon name="mdi:plus" /></template>
|
|
111
|
+
</InputButton>
|
|
112
|
+
</template>
|
|
113
|
+
</InputRange>
|
|
114
|
+
</div>
|
|
115
|
+
`,
|
|
116
|
+
}),
|
|
117
|
+
parameters: {
|
|
118
|
+
docs: {
|
|
119
|
+
description: {
|
|
120
|
+
story: "`InputButton`s in the `left`/`right` slots, sized by `--input-range-button-size`. The wiring here is minimal; `InputRangeField` clamps and rounds properly.",
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
export const WithMarkers: Story = {
|
|
127
|
+
args: { min: 0, max: 2, step: 1 },
|
|
128
|
+
render: (args: StoryArgs) => ({
|
|
129
|
+
components: { InputRange },
|
|
130
|
+
setup() {
|
|
131
|
+
const modelValue = ref(1);
|
|
132
|
+
return { args, modelValue };
|
|
133
|
+
},
|
|
134
|
+
template: `
|
|
135
|
+
<div style="margin: 36px; max-width: 320px;">
|
|
136
|
+
<InputRange v-bind="args" v-model="modelValue">
|
|
137
|
+
<template #markers>
|
|
138
|
+
<div class="input-range-markers">
|
|
139
|
+
<div class="input-range-marker"><span class="input-range-marker-icon">😀</span></div>
|
|
140
|
+
<div class="input-range-marker"><span class="input-range-marker-icon">🙂</span></div>
|
|
141
|
+
<div class="input-range-marker"><span class="input-range-marker-icon">😐</span></div>
|
|
142
|
+
</div>
|
|
143
|
+
</template>
|
|
144
|
+
</InputRange>
|
|
145
|
+
</div>
|
|
146
|
+
`,
|
|
147
|
+
}),
|
|
148
|
+
parameters: {
|
|
149
|
+
docs: {
|
|
150
|
+
description: {
|
|
151
|
+
story:
|
|
152
|
+
"The `markers` slot with the pre-styled `.input-range-markers` / `.input-range-marker` / `.input-range-marker-icon` classes. The line joining them sits behind the markers, the whole row is the slider (drag, click or use the arrow keys), and a ring circles the selected marker. Use one marker per step.",
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
export const WithDatalist: Story = {
|
|
159
|
+
render: (args: StoryArgs) => ({
|
|
160
|
+
components: { InputRange },
|
|
161
|
+
setup() {
|
|
162
|
+
const modelValue = ref(50);
|
|
163
|
+
return { args, modelValue };
|
|
164
|
+
},
|
|
165
|
+
template: `
|
|
166
|
+
<div style="margin: 36px; max-width: 320px;">
|
|
167
|
+
<InputRange v-bind="args" v-model="modelValue">
|
|
168
|
+
<template #datalist="{ datalistId }">
|
|
169
|
+
<datalist :id="datalistId" class="input-range-datalist">
|
|
170
|
+
<option value="0" label="Low"></option>
|
|
171
|
+
<option value="50" label="Medium"></option>
|
|
172
|
+
<option value="100" label="High"></option>
|
|
173
|
+
</datalist>
|
|
174
|
+
</template>
|
|
175
|
+
</InputRange>
|
|
176
|
+
</div>
|
|
177
|
+
`,
|
|
178
|
+
}),
|
|
179
|
+
parameters: {
|
|
180
|
+
docs: {
|
|
181
|
+
description: {
|
|
182
|
+
story:
|
|
183
|
+
"The `datalist` slot receives a generated `datalistId`; put it on your `<datalist>` and the input's `list` points at it. The thumb snaps to the options in browsers that support it.",
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
};
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { computed, ref } from "vue";
|
|
2
|
+
import type { Meta, StoryObj } from "@nuxtjs/storybook";
|
|
3
|
+
import InputRangeField from "../InputRangeField.vue";
|
|
4
|
+
import CanvasSwitcher from "../../../01.atoms/canvas-switcher/CanvasSwitcher.vue";
|
|
5
|
+
import type { FormUiTheme } from "~/types/forms/types.forms.d";
|
|
6
|
+
import type { MediaCanvas } from "~/types/components";
|
|
7
|
+
|
|
8
|
+
interface StoryArgs {
|
|
9
|
+
modelValue: number;
|
|
10
|
+
name: string;
|
|
11
|
+
label: string;
|
|
12
|
+
min: number;
|
|
13
|
+
max: number;
|
|
14
|
+
step: number;
|
|
15
|
+
errorMessage: string;
|
|
16
|
+
fieldHasError: boolean;
|
|
17
|
+
required: boolean;
|
|
18
|
+
theme: FormUiTheme;
|
|
19
|
+
stepDownLabel: string;
|
|
20
|
+
stepUpLabel: string;
|
|
21
|
+
useStepButtons: boolean;
|
|
22
|
+
descriptionText: string;
|
|
23
|
+
datalistOptions: string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const meta: Meta<StoryArgs> = {
|
|
27
|
+
title: "Components/Forms/Input Range/InputRangeField",
|
|
28
|
+
component: InputRangeField,
|
|
29
|
+
argTypes: {
|
|
30
|
+
modelValue: { control: "number", table: { category: "Model" } },
|
|
31
|
+
name: { control: "text", table: { category: "Basic" } },
|
|
32
|
+
label: { control: "text", table: { category: "Basic" } },
|
|
33
|
+
min: { control: "number", table: { category: "Basic" } },
|
|
34
|
+
max: { control: "number", table: { category: "Basic" } },
|
|
35
|
+
step: { control: "number", table: { category: "Basic" } },
|
|
36
|
+
errorMessage: { control: "text", table: { category: "Validation" } },
|
|
37
|
+
fieldHasError: { control: "boolean", table: { category: "Validation" } },
|
|
38
|
+
required: { control: "boolean", table: { category: "Validation" } },
|
|
39
|
+
theme: {
|
|
40
|
+
control: { type: "select" },
|
|
41
|
+
options: ["default", "success", "error", "warning"],
|
|
42
|
+
table: { category: "Styling" },
|
|
43
|
+
},
|
|
44
|
+
stepDownLabel: { control: "text", description: "Localisation override for the step-down button", table: { category: "Accessibility" } },
|
|
45
|
+
stepUpLabel: { control: "text", description: "Localisation override for the step-up button", table: { category: "Accessibility" } },
|
|
46
|
+
useStepButtons: {
|
|
47
|
+
control: "boolean",
|
|
48
|
+
description: "Story-only: render the +/- step buttons by filling the left/right slots",
|
|
49
|
+
table: { category: "Slots" },
|
|
50
|
+
},
|
|
51
|
+
descriptionText: { control: "text", description: "Story-only: fills the descriptionText slot", table: { category: "Slots" } },
|
|
52
|
+
datalistOptions: {
|
|
53
|
+
control: "object",
|
|
54
|
+
description: "Story-only: option labels for a datalist spread evenly from min to max (empty for none)",
|
|
55
|
+
table: { category: "Slots" },
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
args: {
|
|
59
|
+
modelValue: 50,
|
|
60
|
+
name: "priceRange",
|
|
61
|
+
label: "Price range",
|
|
62
|
+
min: 0,
|
|
63
|
+
max: 100,
|
|
64
|
+
step: 1,
|
|
65
|
+
errorMessage: "",
|
|
66
|
+
fieldHasError: false,
|
|
67
|
+
required: false,
|
|
68
|
+
theme: "default",
|
|
69
|
+
stepDownLabel: "Step down",
|
|
70
|
+
stepUpLabel: "Step up",
|
|
71
|
+
useStepButtons: false,
|
|
72
|
+
descriptionText: "",
|
|
73
|
+
datalistOptions: [],
|
|
74
|
+
},
|
|
75
|
+
decorators: [
|
|
76
|
+
(story, context) => ({
|
|
77
|
+
components: { story, CanvasSwitcher },
|
|
78
|
+
setup() {
|
|
79
|
+
const canvasName = ref<MediaCanvas>(context.parameters.initialCanvas ?? "fullWidthCanvas");
|
|
80
|
+
return { canvasName };
|
|
81
|
+
},
|
|
82
|
+
template: `
|
|
83
|
+
<div style="padding: 1.2rem 1.6rem; border-block-end: 1px solid currentColor;">
|
|
84
|
+
<CanvasSwitcher v-model:canvas-name="canvasName" />
|
|
85
|
+
</div>
|
|
86
|
+
<div :class="canvasName" style="margin-inline: auto; padding: 2rem; outline: 1px dashed currentColor;">
|
|
87
|
+
<story />
|
|
88
|
+
</div>
|
|
89
|
+
`,
|
|
90
|
+
}),
|
|
91
|
+
],
|
|
92
|
+
parameters: {
|
|
93
|
+
layout: "fullscreen",
|
|
94
|
+
docs: {
|
|
95
|
+
description: {
|
|
96
|
+
component:
|
|
97
|
+
"Labelled range slider: `InputLabel`, an optional description, `InputRange` and an error strip. Filling the `left`/`right` slots with an icon adds step buttons: they step by `step`, round away floating-point drift and clamp to `min`/`max`. `markers` and `datalist` are forwarded to `InputRange` (the datalist slot passes its generated `datalistId` through). `stepDownLabel`/`stepUpLabel` localise the buttons' accessible names.",
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export default meta;
|
|
104
|
+
type Story = StoryObj<StoryArgs>;
|
|
105
|
+
|
|
106
|
+
const render = (args: StoryArgs) => ({
|
|
107
|
+
components: { InputRangeField },
|
|
108
|
+
setup() {
|
|
109
|
+
const componentArgs = computed(() => {
|
|
110
|
+
const {
|
|
111
|
+
modelValue: _modelValue,
|
|
112
|
+
useStepButtons: _useStepButtons,
|
|
113
|
+
descriptionText: _descriptionText,
|
|
114
|
+
datalistOptions: _datalistOptions,
|
|
115
|
+
...rest
|
|
116
|
+
} = args;
|
|
117
|
+
return rest;
|
|
118
|
+
});
|
|
119
|
+
const datalist = computed(() => {
|
|
120
|
+
const count = args.datalistOptions.length;
|
|
121
|
+
return args.datalistOptions.map((label, index) => ({
|
|
122
|
+
label,
|
|
123
|
+
value: count > 1 ? args.min + ((args.max - args.min) * index) / (count - 1) : args.min,
|
|
124
|
+
}));
|
|
125
|
+
});
|
|
126
|
+
return { args, componentArgs, datalist };
|
|
127
|
+
},
|
|
128
|
+
template: `
|
|
129
|
+
<InputRangeField v-model="args.modelValue" v-bind="componentArgs">
|
|
130
|
+
<template v-if="args.descriptionText" #descriptionText>{{ args.descriptionText }}</template>
|
|
131
|
+
<template v-if="args.useStepButtons" #left><Icon name="mdi:minus" /></template>
|
|
132
|
+
<template v-if="args.useStepButtons" #right><Icon name="mdi:plus" /></template>
|
|
133
|
+
<template v-if="datalist.length" #datalist="{ datalistId }">
|
|
134
|
+
<datalist :id="datalistId" class="input-range-datalist">
|
|
135
|
+
<option v-for="option in datalist" :key="option.value" :value="option.value" :label="option.label"></option>
|
|
136
|
+
</datalist>
|
|
137
|
+
</template>
|
|
138
|
+
</InputRangeField>
|
|
139
|
+
<p style="margin-block-start: 1.6rem; font-family: monospace;">modelValue: {{ JSON.stringify(args.modelValue) }}</p>
|
|
140
|
+
`,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
export const Default: Story = { render };
|
|
144
|
+
|
|
145
|
+
export const WithStepButtons: Story = {
|
|
146
|
+
args: { useStepButtons: true },
|
|
147
|
+
render,
|
|
148
|
+
parameters: { docs: { description: { story: "Step buttons from the `left`/`right` slots. Step-down goes read-only at `min`, step-up at `max`." } } },
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
export const DecimalSteps: Story = {
|
|
152
|
+
args: { useStepButtons: true, label: "Opacity", modelValue: 0.2, min: 0, max: 1, step: 0.1 },
|
|
153
|
+
render,
|
|
154
|
+
parameters: { docs: { description: { story: "A fractional `step`: stepping up from 0.2 gives 0.3, not 0.30000000000000004." } } },
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
export const WithError: Story = {
|
|
158
|
+
args: { fieldHasError: true, errorMessage: "Please choose a value within the allowed range", theme: "error" },
|
|
159
|
+
render,
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
export const Required: Story = { args: { required: true }, render };
|
|
163
|
+
|
|
164
|
+
export const LocalisedLabels: Story = {
|
|
165
|
+
args: { useStepButtons: true, stepDownLabel: "Diminuer", stepUpLabel: "Augmenter" },
|
|
166
|
+
render,
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
export const StressTest: Story = {
|
|
170
|
+
name: "Stress Test (Worst-Case Data)",
|
|
171
|
+
args: {
|
|
172
|
+
useStepButtons: true,
|
|
173
|
+
modelValue: 123456789,
|
|
174
|
+
min: -999999999999,
|
|
175
|
+
max: 999999999999,
|
|
176
|
+
step: 0.0001,
|
|
177
|
+
label:
|
|
178
|
+
"🎚️ Maximalerpreisbereichfürdiegewünschtebehandlunginklusivezusatzleistungen https://example.com/a/very/long/unbroken/url <b>not bold</b> نطاق",
|
|
179
|
+
descriptionText: "يرجى اختيار نطاق Ziehen Sie den Regler, um den gewünschten Preisbereich festzulegen.",
|
|
180
|
+
errorMessage: "DieserWertliegtleideraußerhalbdeszulässigenBereichsbitteversuchenSieeserneut",
|
|
181
|
+
fieldHasError: true,
|
|
182
|
+
stepDownLabel: "Verringern 🔽",
|
|
183
|
+
stepUpLabel: "Erhöhen 🔼",
|
|
184
|
+
datalistOptions: ["Minimum", "Sehrniedrigerpreisbereich", "😀", "<b>Mitte</b>", "متوسط", "Hoch", "Maximum"],
|
|
185
|
+
},
|
|
186
|
+
render,
|
|
187
|
+
parameters: {
|
|
188
|
+
initialCanvas: "mobileCanvas",
|
|
189
|
+
docs: {
|
|
190
|
+
description: {
|
|
191
|
+
story:
|
|
192
|
+
"A range of minus to plus a trillion with a 0.0001 step, long unbroken German/URL/emoji/RTL/HTML-like copy, and a 7-option datalist with long and odd labels. Check at every canvas width: the label, description and error wrap inside the dashed outline, the slider shrinks to fit between step buttons that keep their size, and markup renders as text. Set `step` to 0 or a negative number and press the step buttons.",
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
};
|
package/app/components/05.forms/input-range/tests/{InputRangeCore.spec.ts → InputRange.spec.ts}
RENAMED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// https://nuxt.com/docs/getting-started/testing#unit-testing
|
|
2
2
|
import type { VueWrapper } from "@vue/test-utils";
|
|
3
3
|
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
4
|
-
import ComponentUnderTest from "../
|
|
4
|
+
import ComponentUnderTest from "../InputRange.vue";
|
|
5
5
|
|
|
6
6
|
const initialPropsData = {
|
|
7
7
|
id: "test-range",
|
|
@@ -12,6 +12,11 @@ const initialPropsData = {
|
|
|
12
12
|
};
|
|
13
13
|
|
|
14
14
|
let wrapper: VueWrapper<InstanceType<typeof ComponentUnderTest>>;
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
wrapper?.unmount();
|
|
18
|
+
vi.restoreAllMocks();
|
|
19
|
+
});
|
|
15
20
|
const wrapperFactory = (propsData = {}, slots = {}) => {
|
|
16
21
|
const mockPropsData = { ...initialPropsData, ...propsData };
|
|
17
22
|
|
|
@@ -22,7 +27,7 @@ const wrapperFactory = (propsData = {}, slots = {}) => {
|
|
|
22
27
|
});
|
|
23
28
|
};
|
|
24
29
|
|
|
25
|
-
describe("
|
|
30
|
+
describe("InputRange", () => {
|
|
26
31
|
it("mounts without error", async () => {
|
|
27
32
|
wrapper = await wrapperFactory();
|
|
28
33
|
expect(wrapper.vm).toBeTruthy();
|
|
@@ -60,8 +65,8 @@ describe("InputRangeCore", () => {
|
|
|
60
65
|
|
|
61
66
|
it("does not render the left/right slot wrappers when the slots are not used", async () => {
|
|
62
67
|
wrapper = await wrapperFactory();
|
|
63
|
-
expect(wrapper.find(".slot
|
|
64
|
-
expect(wrapper.find(".slot
|
|
68
|
+
expect(wrapper.find(".input-range-slot-left").exists()).toBe(false);
|
|
69
|
+
expect(wrapper.find(".input-range-slot-right").exists()).toBe(false);
|
|
65
70
|
});
|
|
66
71
|
|
|
67
72
|
it("renders the left/right slots when provided", async () => {
|
|
@@ -73,11 +78,11 @@ describe("InputRangeCore", () => {
|
|
|
73
78
|
}
|
|
74
79
|
);
|
|
75
80
|
|
|
76
|
-
expect(wrapper.find(".slot
|
|
77
|
-
expect(wrapper.find(".slot
|
|
81
|
+
expect(wrapper.find(".input-range-slot-left [data-testid='left-slot']").exists()).toBe(true);
|
|
82
|
+
expect(wrapper.find(".input-range-slot-right [data-testid='right-slot']").exists()).toBe(true);
|
|
78
83
|
});
|
|
79
84
|
|
|
80
|
-
it("renders the markers slot and
|
|
85
|
+
it("renders the markers slot and sets data-has-markers on the wrapper", async () => {
|
|
81
86
|
wrapper = await wrapperFactory(
|
|
82
87
|
{},
|
|
83
88
|
{
|
|
@@ -86,25 +91,44 @@ describe("InputRangeCore", () => {
|
|
|
86
91
|
);
|
|
87
92
|
|
|
88
93
|
expect(wrapper.find('[data-testid="markers"]').exists()).toBe(true);
|
|
89
|
-
expect(wrapper.find("input").
|
|
94
|
+
expect(wrapper.find(".input-range-wrapper").attributes("data-has-markers")).toBe("");
|
|
90
95
|
});
|
|
91
96
|
|
|
92
|
-
it("does not
|
|
97
|
+
it("does not set data-has-markers when the markers slot is not used", async () => {
|
|
93
98
|
wrapper = await wrapperFactory();
|
|
94
|
-
expect(wrapper.find("input").
|
|
99
|
+
expect(wrapper.find(".input-range-wrapper").attributes("data-has-markers")).toBeUndefined();
|
|
95
100
|
});
|
|
96
101
|
|
|
97
|
-
it("
|
|
102
|
+
it("links a generated datalist id through the datalistId slot prop", async () => {
|
|
98
103
|
wrapper = await wrapperFactory(
|
|
99
104
|
{ name: "price" },
|
|
100
|
-
{ datalist:
|
|
105
|
+
{ datalist: `<template #datalist="{ datalistId }"><datalist :id="datalistId"></datalist></template>` }
|
|
101
106
|
);
|
|
102
|
-
|
|
107
|
+
const list = wrapper.find("input").attributes("list");
|
|
108
|
+
|
|
109
|
+
expect(list).toBeTruthy();
|
|
110
|
+
expect(list).not.toBe("price-datalist");
|
|
111
|
+
expect(wrapper.find("datalist").attributes("id")).toBe(list);
|
|
103
112
|
});
|
|
104
113
|
|
|
105
|
-
it("
|
|
114
|
+
it("omits the list attribute when the datalist slot is not used", async () => {
|
|
106
115
|
wrapper = await wrapperFactory();
|
|
107
|
-
expect(wrapper.find("input").attributes("list")).
|
|
116
|
+
expect(wrapper.find("input").attributes("list")).toBeUndefined();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("marks the wrapper and input invalid when fieldHasError is true", async () => {
|
|
120
|
+
wrapper = await wrapperFactory({ fieldHasError: true });
|
|
121
|
+
expect(wrapper.find(".input-range-wrapper").attributes("data-invalid")).toBe("");
|
|
122
|
+
expect(wrapper.find("input").attributes("aria-invalid")).toBe("true");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("updates passthrough classes when the prop changes", async () => {
|
|
126
|
+
wrapper = await wrapperFactory({ styleClassPassthrough: ["first"] });
|
|
127
|
+
await wrapper.setProps({ styleClassPassthrough: ["second"] });
|
|
128
|
+
const classes = wrapper.find("input").classes();
|
|
129
|
+
|
|
130
|
+
expect(classes).toContain("second");
|
|
131
|
+
expect(classes).not.toContain("first");
|
|
108
132
|
});
|
|
109
133
|
|
|
110
134
|
it("applies the weight class from the weight prop", async () => {
|