srcdev-nuxt-components 9.6.7 → 9.6.8
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 +6 -0
- package/.claude/skills/component-naming.md +2 -2
- 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 +1 -1
- package/.claude/skills/components/input-error.md +1 -1
- package/.claude/skills/components/input-label.md +2 -2
- package/.claude/skills/components/input-number.md +44 -13
- 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-textarea-core.md +4 -4
- package/.claude/skills/index.md +5 -5
- package/.claude/skills/testing-add-unit-test.md +17 -0
- package/.vscode/srcdev-component-input-checkbox-radio.code-snippets +0 -1
- package/.vscode/srcdev-component-input-range.code-snippets +38 -18
- 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 +2 -2
- package/app/components/05.forms/input-label/CONSUMER-STYLING.md +2 -2
- package/app/components/05.forms/input-number/CONSUMER-STYLING.md +42 -2
- package/app/components/05.forms/input-number/InputNumber.vue +37 -25
- 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/patterns/stories/MigratedFieldsForm.stories.ts +5 -5
- package/package.json +1 -1
- 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
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import type { Meta, StoryFn } from "@nuxtjs/storybook";
|
|
2
1
|
import { ref } from "vue";
|
|
3
|
-
import
|
|
2
|
+
import type { Meta, StoryObj } from "@nuxtjs/storybook";
|
|
3
|
+
import InputNumber from "../InputNumber.vue";
|
|
4
|
+
import InputButton from "../../input-button/InputButton.vue";
|
|
5
|
+
import type { FormUiTheme, InputUiVariant } from "~/types/forms/types.forms.d";
|
|
4
6
|
|
|
5
|
-
interface
|
|
7
|
+
interface StoryArgs {
|
|
6
8
|
id: string;
|
|
7
9
|
name: string;
|
|
8
10
|
min: number;
|
|
@@ -11,13 +13,23 @@ interface InputNumberStoryArgs {
|
|
|
11
13
|
placeholder: string;
|
|
12
14
|
required: boolean;
|
|
13
15
|
fieldHasError: boolean;
|
|
16
|
+
theme: FormUiTheme;
|
|
14
17
|
weight: string;
|
|
15
|
-
inputVariant:
|
|
18
|
+
inputVariant: InputUiVariant;
|
|
16
19
|
}
|
|
17
20
|
|
|
18
|
-
|
|
21
|
+
// Links in the canvas open in the Storybook manager (target="_top"), not inside the preview iframe.
|
|
22
|
+
const storyNoteStyle = "margin: 0 0 1.6rem; font-size: 1.4rem;";
|
|
23
|
+
const parentNote = `
|
|
24
|
+
<p style="${storyNoteStyle}">
|
|
25
|
+
With a label, description, error message and clamped step buttons wired up, this is the
|
|
26
|
+
<a href="/?path=/story/components-forms-input-number-inputnumberfield--with-step-buttons" target="_top">InputNumberField</a>.
|
|
27
|
+
</p>
|
|
28
|
+
`;
|
|
29
|
+
|
|
30
|
+
const meta: Meta<StoryArgs> = {
|
|
19
31
|
title: "Components/Forms/Input Number/InputNumber",
|
|
20
|
-
component:
|
|
32
|
+
component: InputNumber,
|
|
21
33
|
argTypes: {
|
|
22
34
|
id: { control: "text", table: { category: "Basic" } },
|
|
23
35
|
name: { control: "text", table: { category: "Basic" } },
|
|
@@ -27,7 +39,12 @@ export default {
|
|
|
27
39
|
placeholder: { control: "text", table: { category: "Basic" } },
|
|
28
40
|
required: { control: "boolean", table: { category: "Basic" } },
|
|
29
41
|
fieldHasError: { control: "boolean", table: { category: "States" } },
|
|
30
|
-
|
|
42
|
+
theme: {
|
|
43
|
+
control: { type: "select" },
|
|
44
|
+
options: ["default", "success", "error", "warning"],
|
|
45
|
+
table: { category: "Styling" },
|
|
46
|
+
},
|
|
47
|
+
weight: { control: "text", description: "Adds an `input-number--{weight}` class; nothing styles it yet", table: { category: "Styling" } },
|
|
31
48
|
inputVariant: {
|
|
32
49
|
control: { type: "select" },
|
|
33
50
|
options: ["normal", "outlined", "underlined"],
|
|
@@ -43,50 +60,84 @@ export default {
|
|
|
43
60
|
placeholder: "",
|
|
44
61
|
required: false,
|
|
45
62
|
fieldHasError: false,
|
|
63
|
+
theme: "default",
|
|
46
64
|
weight: "normal",
|
|
47
65
|
inputVariant: "normal",
|
|
48
66
|
},
|
|
49
|
-
|
|
67
|
+
parameters: {
|
|
68
|
+
docs: {
|
|
69
|
+
description: {
|
|
70
|
+
component:
|
|
71
|
+
"The bare native `<input type=\"number\">` with `left`/`right` slots for embedded step buttons. No label, description or error: that's `InputNumberField`. `inputmode` follows `min` and `step` (`numeric`, `decimal`, or none when negatives are allowed). Hooks: `data-input-variant`, `data-theme`, `data-invalid`, `data-has-left-slot`, `data-has-right-slot`.",
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export default meta;
|
|
78
|
+
type Story = StoryObj<StoryArgs>;
|
|
50
79
|
|
|
51
|
-
const
|
|
52
|
-
components: {
|
|
80
|
+
const render = (args: StoryArgs) => ({
|
|
81
|
+
components: { InputNumber },
|
|
53
82
|
setup() {
|
|
54
83
|
const modelValue = ref(50);
|
|
55
84
|
return { args, modelValue };
|
|
56
85
|
},
|
|
57
86
|
template: `
|
|
58
87
|
<div style="margin: 36px; max-width: 320px;">
|
|
59
|
-
|
|
88
|
+
${parentNote}
|
|
89
|
+
<InputNumber v-bind="args" v-model="modelValue" />
|
|
60
90
|
</div>
|
|
61
91
|
`,
|
|
62
92
|
});
|
|
63
93
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
export const ErrorState = Template.bind({});
|
|
67
|
-
ErrorState.args = { fieldHasError: true };
|
|
68
|
-
|
|
69
|
-
const WithStepButtonsTemplate: StoryFn<InputNumberStoryArgs> = (args) => ({
|
|
70
|
-
components: { StorybookComponent },
|
|
94
|
+
const renderWithSlots = (args: StoryArgs) => ({
|
|
95
|
+
components: { InputNumber, InputButton },
|
|
71
96
|
setup() {
|
|
72
97
|
const modelValue = ref(50);
|
|
73
98
|
return { args, modelValue };
|
|
74
99
|
},
|
|
75
100
|
template: `
|
|
76
101
|
<div style="margin: 36px; max-width: 320px;">
|
|
77
|
-
|
|
102
|
+
${parentNote}
|
|
103
|
+
<InputNumber v-bind="args" v-model="modelValue">
|
|
78
104
|
<template #left>
|
|
79
|
-
<
|
|
105
|
+
<InputButton type="button" button-text="Step down" variant="inline" @click="modelValue = Math.max(args.min, modelValue - args.step)">
|
|
106
|
+
<template #iconOnly><Icon name="mdi:minus" /></template>
|
|
107
|
+
</InputButton>
|
|
80
108
|
</template>
|
|
81
109
|
<template #right>
|
|
82
|
-
<
|
|
110
|
+
<InputButton type="button" button-text="Step up" variant="inline" @click="modelValue = Math.min(args.max, modelValue + args.step)">
|
|
111
|
+
<template #iconOnly><Icon name="mdi:plus" /></template>
|
|
112
|
+
</InputButton>
|
|
83
113
|
</template>
|
|
84
|
-
</
|
|
114
|
+
</InputNumber>
|
|
85
115
|
</div>
|
|
86
116
|
`,
|
|
87
117
|
});
|
|
88
118
|
|
|
89
|
-
export const
|
|
119
|
+
export const Default: Story = { render };
|
|
120
|
+
|
|
121
|
+
export const ErrorState: Story = {
|
|
122
|
+
args: { fieldHasError: true },
|
|
123
|
+
render,
|
|
124
|
+
parameters: { docs: { description: { story: "`fieldHasError` sets `data-invalid` on the wrapper." } } },
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
export const WithStepButtons: Story = {
|
|
128
|
+
render: renderWithSlots,
|
|
129
|
+
parameters: {
|
|
130
|
+
docs: {
|
|
131
|
+
description: {
|
|
132
|
+
story:
|
|
133
|
+
"Inline `InputButton`s in the `left`/`right` slots become flush segments of the control. The wiring here is deliberately minimal; `InputNumberField` does it properly (clamping, rounding, empty field).",
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
};
|
|
90
138
|
|
|
91
|
-
export const Underlined =
|
|
92
|
-
|
|
139
|
+
export const Underlined: Story = {
|
|
140
|
+
args: { inputVariant: "underlined" },
|
|
141
|
+
render: renderWithSlots,
|
|
142
|
+
parameters: { docs: { description: { story: "`inputVariant=\"underlined\"` keeps only a bottom border." } } },
|
|
143
|
+
};
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import
|
|
1
|
+
import { computed, ref } from "vue";
|
|
2
|
+
import type { Meta, StoryObj } from "@nuxtjs/storybook";
|
|
3
|
+
import InputNumberField from "../InputNumberField.vue";
|
|
4
|
+
import CanvasSwitcher from "../../../01.atoms/canvas-switcher/CanvasSwitcher.vue";
|
|
5
|
+
import type { FormUiTheme, InputUiVariant } from "~/types/forms/types.forms.d";
|
|
6
|
+
import type { MediaCanvas } from "~/types/components";
|
|
4
7
|
|
|
5
|
-
interface
|
|
8
|
+
interface StoryArgs {
|
|
6
9
|
modelValue: number;
|
|
7
10
|
name: string;
|
|
8
11
|
label: string;
|
|
@@ -14,15 +17,16 @@ interface InputNumberFieldStoryArgs {
|
|
|
14
17
|
fieldHasError: boolean;
|
|
15
18
|
required: boolean;
|
|
16
19
|
theme: FormUiTheme;
|
|
17
|
-
inputVariant:
|
|
20
|
+
inputVariant: InputUiVariant;
|
|
18
21
|
stepDownLabel: string;
|
|
19
22
|
stepUpLabel: string;
|
|
20
23
|
useStepButtons: boolean;
|
|
24
|
+
descriptionText: string;
|
|
21
25
|
}
|
|
22
26
|
|
|
23
|
-
|
|
27
|
+
const meta: Meta<StoryArgs> = {
|
|
24
28
|
title: "Components/Forms/Input Number/InputNumberField",
|
|
25
|
-
component:
|
|
29
|
+
component: InputNumberField,
|
|
26
30
|
argTypes: {
|
|
27
31
|
modelValue: { control: "number", table: { category: "Model" } },
|
|
28
32
|
name: { control: "text", table: { category: "Basic" } },
|
|
@@ -46,7 +50,16 @@ export default {
|
|
|
46
50
|
},
|
|
47
51
|
stepDownLabel: { control: "text", description: "Localisation override for the step-down button", table: { category: "Accessibility" } },
|
|
48
52
|
stepUpLabel: { control: "text", description: "Localisation override for the step-up button", table: { category: "Accessibility" } },
|
|
49
|
-
useStepButtons: {
|
|
53
|
+
useStepButtons: {
|
|
54
|
+
control: "boolean",
|
|
55
|
+
description: "Story-only: render the +/- step buttons by filling the left/right slots",
|
|
56
|
+
table: { category: "Slots" },
|
|
57
|
+
},
|
|
58
|
+
descriptionText: {
|
|
59
|
+
control: "text",
|
|
60
|
+
description: "Story-only: fills the descriptionText slot",
|
|
61
|
+
table: { category: "Slots" },
|
|
62
|
+
},
|
|
50
63
|
},
|
|
51
64
|
args: {
|
|
52
65
|
modelValue: 5,
|
|
@@ -64,60 +77,113 @@ export default {
|
|
|
64
77
|
stepDownLabel: "Step down",
|
|
65
78
|
stepUpLabel: "Step up",
|
|
66
79
|
useStepButtons: false,
|
|
80
|
+
descriptionText: "",
|
|
81
|
+
},
|
|
82
|
+
decorators: [
|
|
83
|
+
(story, context) => ({
|
|
84
|
+
components: { story, CanvasSwitcher },
|
|
85
|
+
setup() {
|
|
86
|
+
const canvasName = ref<MediaCanvas>(context.parameters.initialCanvas ?? "fullWidthCanvas");
|
|
87
|
+
return { canvasName };
|
|
88
|
+
},
|
|
89
|
+
template: `
|
|
90
|
+
<div style="padding: 1.2rem 1.6rem; border-block-end: 1px solid currentColor;">
|
|
91
|
+
<CanvasSwitcher v-model:canvas-name="canvasName" />
|
|
92
|
+
</div>
|
|
93
|
+
<div :class="canvasName" style="margin-inline: auto; padding: 2rem; outline: 1px dashed currentColor;">
|
|
94
|
+
<story />
|
|
95
|
+
</div>
|
|
96
|
+
`,
|
|
97
|
+
}),
|
|
98
|
+
],
|
|
99
|
+
parameters: {
|
|
100
|
+
layout: "fullscreen",
|
|
101
|
+
docs: {
|
|
102
|
+
description: {
|
|
103
|
+
component:
|
|
104
|
+
"Labelled number field: `InputLabel`, an optional description, `InputNumber` and an error strip. Filling the `left`/`right` slots with an icon adds step buttons: they step by `step`, round away floating-point drift, clamp to `min`/`max` (pulling a typed out-of-range value back in) and start from 0, clamped, when the field is empty. `stepDownLabel`/`stepUpLabel` localise their accessible names.",
|
|
105
|
+
},
|
|
106
|
+
},
|
|
67
107
|
},
|
|
68
|
-
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
export default meta;
|
|
111
|
+
type Story = StoryObj<StoryArgs>;
|
|
69
112
|
|
|
70
|
-
const
|
|
71
|
-
components: {
|
|
113
|
+
const render = (args: StoryArgs) => ({
|
|
114
|
+
components: { InputNumberField },
|
|
72
115
|
setup() {
|
|
73
|
-
|
|
116
|
+
const componentArgs = computed(() => {
|
|
117
|
+
const { modelValue: _modelValue, useStepButtons: _useStepButtons, descriptionText: _descriptionText, ...rest } = args;
|
|
118
|
+
return rest;
|
|
119
|
+
});
|
|
120
|
+
return { args, componentArgs };
|
|
74
121
|
},
|
|
75
122
|
template: `
|
|
76
|
-
<
|
|
77
|
-
<
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
:max="args.max"
|
|
83
|
-
:step="args.step"
|
|
84
|
-
:placeholder="args.placeholder"
|
|
85
|
-
:error-message="args.errorMessage"
|
|
86
|
-
:field-has-error="args.fieldHasError"
|
|
87
|
-
:required="args.required"
|
|
88
|
-
:theme="args.theme"
|
|
89
|
-
:input-variant="args.inputVariant"
|
|
90
|
-
:step-down-label="args.stepDownLabel"
|
|
91
|
-
:step-up-label="args.stepUpLabel"
|
|
92
|
-
>
|
|
93
|
-
<template v-if="args.useStepButtons" #left><span>−</span></template>
|
|
94
|
-
<template v-if="args.useStepButtons" #right><span>+</span></template>
|
|
95
|
-
</StorybookComponent>
|
|
96
|
-
</div>
|
|
123
|
+
<InputNumberField v-model="args.modelValue" v-bind="componentArgs">
|
|
124
|
+
<template v-if="args.descriptionText" #descriptionText>{{ args.descriptionText }}</template>
|
|
125
|
+
<template v-if="args.useStepButtons" #left><Icon name="mdi:minus" /></template>
|
|
126
|
+
<template v-if="args.useStepButtons" #right><Icon name="mdi:plus" /></template>
|
|
127
|
+
</InputNumberField>
|
|
128
|
+
<p style="margin-block-start: 1.6rem; font-family: monospace;">modelValue: {{ JSON.stringify(args.modelValue) }}</p>
|
|
97
129
|
`,
|
|
98
130
|
});
|
|
99
131
|
|
|
100
|
-
export const Default =
|
|
132
|
+
export const Default: Story = { render };
|
|
133
|
+
|
|
134
|
+
export const WithStepButtons: Story = {
|
|
135
|
+
args: { useStepButtons: true },
|
|
136
|
+
render,
|
|
137
|
+
parameters: { docs: { description: { story: "Step buttons from the `left`/`right` slots. Step-down goes read-only at `min`, step-up at `max`." } } },
|
|
138
|
+
};
|
|
101
139
|
|
|
102
|
-
export const
|
|
103
|
-
|
|
140
|
+
export const DecimalSteps: Story = {
|
|
141
|
+
args: { useStepButtons: true, label: "Weight (kg)", modelValue: 0.2, min: 0, max: 2, step: 0.1 },
|
|
142
|
+
render,
|
|
143
|
+
parameters: {
|
|
144
|
+
docs: { description: { story: "A fractional `step`: stepping up from 0.2 gives 0.3, not 0.30000000000000004, and the mobile keyboard is `decimal`." } },
|
|
145
|
+
},
|
|
146
|
+
};
|
|
104
147
|
|
|
105
|
-
export const WithError =
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
errorMessage: "Please enter a valid quantity",
|
|
109
|
-
theme: "error",
|
|
148
|
+
export const WithError: Story = {
|
|
149
|
+
args: { fieldHasError: true, errorMessage: "Please enter a valid quantity", theme: "error" },
|
|
150
|
+
render,
|
|
110
151
|
};
|
|
111
152
|
|
|
112
|
-
export const Required =
|
|
113
|
-
Required.args = { required: true };
|
|
153
|
+
export const Required: Story = { args: { required: true }, render };
|
|
114
154
|
|
|
115
|
-
export const LocalisedLabels =
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
stepDownLabel: "Diminuer",
|
|
119
|
-
stepUpLabel: "Augmenter",
|
|
155
|
+
export const LocalisedLabels: Story = {
|
|
156
|
+
args: { useStepButtons: true, stepDownLabel: "Diminuer", stepUpLabel: "Augmenter" },
|
|
157
|
+
render,
|
|
120
158
|
};
|
|
121
159
|
|
|
122
|
-
export const Underlined =
|
|
123
|
-
|
|
160
|
+
export const Underlined: Story = { args: { inputVariant: "underlined", useStepButtons: true }, render };
|
|
161
|
+
|
|
162
|
+
export const StressTest: Story = {
|
|
163
|
+
name: "Stress Test (Worst-Case Data)",
|
|
164
|
+
args: {
|
|
165
|
+
useStepButtons: true,
|
|
166
|
+
modelValue: 123456789012,
|
|
167
|
+
min: -999999999999,
|
|
168
|
+
max: 999999999999999,
|
|
169
|
+
step: 0.0001,
|
|
170
|
+
label:
|
|
171
|
+
"🔢 Gewünschteanzahlderteilnehmendenpersonenfürdiegruppenbuchung https://example.com/a/very/long/unbroken/url <b>not bold</b> عدد",
|
|
172
|
+
descriptionText: "يرجى إدخال رقم Geben Sie eine Zahl zwischen minus einer Billion und einer Billiarde ein.",
|
|
173
|
+
placeholder: "Bitte eine sehr lange Platzhalterzahl eingeben",
|
|
174
|
+
errorMessage: "DiesezahlliegtleideraußerhalbdeszulässigenBereichsbitteversuchenSieeserneut",
|
|
175
|
+
fieldHasError: true,
|
|
176
|
+
stepDownLabel: "Verringern 🔽",
|
|
177
|
+
stepUpLabel: "Erhöhen 🔼",
|
|
178
|
+
},
|
|
179
|
+
render,
|
|
180
|
+
parameters: {
|
|
181
|
+
initialCanvas: "mobileCanvas",
|
|
182
|
+
docs: {
|
|
183
|
+
description: {
|
|
184
|
+
story:
|
|
185
|
+
"A 12-digit value, a min of minus a trillion, a 15-digit max and a 0.0001 step, with long unbroken German/URL/emoji/RTL/HTML-like copy. Check at every canvas width: the label, description and error wrap inside the dashed outline, the control's minimum width is capped (12em) so it fits a phone, the step buttons keep their size, and markup renders as text. Clear the field and press step-up (starts from 0), type 9999999999999999 and press step-down (clamps to max), and set `step` to 0 or a negative number.",
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
};
|
|
@@ -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
|
|
|
@@ -70,10 +75,10 @@ describe("InputNumber", () => {
|
|
|
70
75
|
|
|
71
76
|
it("does not render the left/right slot wrappers when the slots are not used", async () => {
|
|
72
77
|
wrapper = await wrapperFactory();
|
|
73
|
-
expect(wrapper.find(".slot
|
|
74
|
-
expect(wrapper.find(".slot
|
|
75
|
-
expect(wrapper.find(".input-number-wrapper").
|
|
76
|
-
expect(wrapper.find(".input-number-wrapper").
|
|
78
|
+
expect(wrapper.find(".input-number-slot-left").exists()).toBe(false);
|
|
79
|
+
expect(wrapper.find(".input-number-slot-right").exists()).toBe(false);
|
|
80
|
+
expect(wrapper.find(".input-number-wrapper").attributes("data-has-left-slot")).toBeUndefined();
|
|
81
|
+
expect(wrapper.find(".input-number-wrapper").attributes("data-has-right-slot")).toBeUndefined();
|
|
77
82
|
});
|
|
78
83
|
|
|
79
84
|
it("renders the left/right slots when provided", async () => {
|
|
@@ -85,10 +90,10 @@ describe("InputNumber", () => {
|
|
|
85
90
|
}
|
|
86
91
|
);
|
|
87
92
|
|
|
88
|
-
expect(wrapper.find(".slot
|
|
89
|
-
expect(wrapper.find(".slot
|
|
90
|
-
expect(wrapper.find(".input-number-wrapper").
|
|
91
|
-
expect(wrapper.find(".input-number-wrapper").
|
|
93
|
+
expect(wrapper.find(".input-number-slot-left [data-testid='left-slot']").exists()).toBe(true);
|
|
94
|
+
expect(wrapper.find(".input-number-slot-right [data-testid='right-slot']").exists()).toBe(true);
|
|
95
|
+
expect(wrapper.find(".input-number-wrapper").attributes("data-has-left-slot")).toBe("");
|
|
96
|
+
expect(wrapper.find(".input-number-wrapper").attributes("data-has-right-slot")).toBe("");
|
|
92
97
|
});
|
|
93
98
|
|
|
94
99
|
it("applies the weight class from the weight prop", async () => {
|
|
@@ -106,14 +111,45 @@ describe("InputNumber", () => {
|
|
|
106
111
|
expect(wrapper.find("input").attributes("required")).toBeDefined();
|
|
107
112
|
});
|
|
108
113
|
|
|
109
|
-
it("defaults
|
|
114
|
+
it("defaults data-input-variant to normal", async () => {
|
|
110
115
|
wrapper = await wrapperFactory();
|
|
111
|
-
expect(wrapper.find(".input-number-wrapper").
|
|
116
|
+
expect(wrapper.find(".input-number-wrapper").attributes("data-input-variant")).toBe("normal");
|
|
112
117
|
});
|
|
113
118
|
|
|
114
|
-
it("
|
|
119
|
+
it("renders inputVariant as data-input-variant, not a bare class", async () => {
|
|
115
120
|
wrapper = await wrapperFactory({ inputVariant: "underlined" });
|
|
116
|
-
|
|
117
|
-
expect(
|
|
121
|
+
const root = wrapper.find(".input-number-wrapper");
|
|
122
|
+
expect(root.attributes("data-input-variant")).toBe("underlined");
|
|
123
|
+
expect(root.classes()).not.toContain("underlined");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("doesn't set a pattern, which number inputs ignore", async () => {
|
|
127
|
+
wrapper = await wrapperFactory();
|
|
128
|
+
expect(wrapper.find("input").attributes("pattern")).toBeUndefined();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("picks inputmode from min and step", async () => {
|
|
132
|
+
wrapper = await wrapperFactory();
|
|
133
|
+
expect(wrapper.find("input").attributes("inputmode")).toBe("numeric");
|
|
134
|
+
|
|
135
|
+
await wrapper.setProps({ step: 0.5 });
|
|
136
|
+
expect(wrapper.find("input").attributes("inputmode")).toBe("decimal");
|
|
137
|
+
|
|
138
|
+
await wrapper.setProps({ min: -10 });
|
|
139
|
+
expect(wrapper.find("input").attributes("inputmode")).toBeUndefined();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("caps the minimum width for very long min and max values", async () => {
|
|
143
|
+
wrapper = await wrapperFactory({ min: -999999999999, max: 999999999999999, step: 0.0001 });
|
|
144
|
+
const vm = wrapper.vm as unknown as { minLength: string };
|
|
145
|
+
expect(vm.minLength).toBe("12em");
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("updates passthrough classes when the prop changes", async () => {
|
|
149
|
+
wrapper = await wrapperFactory({ styleClassPassthrough: ["first"] });
|
|
150
|
+
await wrapper.setProps({ styleClassPassthrough: ["second"] });
|
|
151
|
+
const classes = wrapper.find("input").classes();
|
|
152
|
+
expect(classes).toContain("second");
|
|
153
|
+
expect(classes).not.toContain("first");
|
|
118
154
|
});
|
|
119
155
|
});
|
|
@@ -13,6 +13,11 @@ const initialPropsData = {
|
|
|
13
13
|
};
|
|
14
14
|
|
|
15
15
|
let wrapper: VueWrapper<InstanceType<typeof ComponentUnderTest>>;
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
wrapper?.unmount();
|
|
19
|
+
vi.restoreAllMocks();
|
|
20
|
+
});
|
|
16
21
|
const wrapperFactory = (propsData = {}, slots = {}) => {
|
|
17
22
|
const mockPropsData = { ...initialPropsData, ...propsData };
|
|
18
23
|
|
|
@@ -44,10 +49,10 @@ describe("InputNumberField", () => {
|
|
|
44
49
|
expect(wrapper.find("input").attributes("placeholder")).toBe("Enter a quantity");
|
|
45
50
|
});
|
|
46
51
|
|
|
47
|
-
it("sets data-invalid
|
|
52
|
+
it("sets data-invalid, not a bare error class, when fieldHasError is true", async () => {
|
|
48
53
|
wrapper = await wrapperFactory({ fieldHasError: true });
|
|
49
54
|
expect(wrapper.find(".input-number-field").attributes("data-invalid")).toBe("");
|
|
50
|
-
expect(wrapper.find(".input-number-field").classes()).toContain("error");
|
|
55
|
+
expect(wrapper.find(".input-number-field").classes()).not.toContain("error");
|
|
51
56
|
});
|
|
52
57
|
|
|
53
58
|
it("does not render step buttons when the left/right slots are not used", async () => {
|
|
@@ -130,14 +135,63 @@ describe("InputNumberField", () => {
|
|
|
130
135
|
expect(wrapper.text()).toContain("This field is required");
|
|
131
136
|
});
|
|
132
137
|
|
|
133
|
-
it("applies styleClassPassthrough", async () => {
|
|
138
|
+
it("applies styleClassPassthrough to the root only, not the inner input", async () => {
|
|
134
139
|
wrapper = await wrapperFactory({ styleClassPassthrough: "custom-number" });
|
|
135
140
|
expect(wrapper.find(".input-number-field").classes()).toContain("custom-number");
|
|
141
|
+
expect(wrapper.find("input").classes()).not.toContain("custom-number");
|
|
136
142
|
});
|
|
137
143
|
|
|
138
144
|
it("forwards inputVariant to the wrapper, InputLabel, and InputNumber", async () => {
|
|
139
145
|
wrapper = await wrapperFactory({ inputVariant: "underlined" });
|
|
140
|
-
expect(wrapper.find(".input-number-field").
|
|
141
|
-
expect(wrapper.find(".input-number-wrapper").
|
|
146
|
+
expect(wrapper.find(".input-number-field").attributes("data-input-variant")).toBe("underlined");
|
|
147
|
+
expect(wrapper.find(".input-number-wrapper").attributes("data-input-variant")).toBe("underlined");
|
|
148
|
+
expect(wrapper.find(".input-error-message").attributes("data-input-variant")).toBe("underlined");
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe("step buttons", () => {
|
|
152
|
+
const stepSlots = { left: "<span>-</span>", right: "<span>+</span>" };
|
|
153
|
+
const stepDown = () => wrapper.findAll("button").find((button) => button.text().includes("-"))!;
|
|
154
|
+
const stepUp = () => wrapper.findAll("button").find((button) => button.text().includes("+"))!;
|
|
155
|
+
|
|
156
|
+
it("rounds away floating-point drift on fractional steps", async () => {
|
|
157
|
+
wrapper = await wrapperFactory({ modelValue: 0.2, min: 0, max: 1, step: 0.1 }, stepSlots);
|
|
158
|
+
await stepUp().trigger("click");
|
|
159
|
+
expect(wrapper.emitted("update:modelValue")?.[0]).toEqual([0.3]);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("clamps to max when a step would overshoot it", async () => {
|
|
163
|
+
wrapper = await wrapperFactory({ modelValue: 8, min: 0, max: 10, step: 5 }, stepSlots);
|
|
164
|
+
await stepUp().trigger("click");
|
|
165
|
+
expect(wrapper.emitted("update:modelValue")?.[0]).toEqual([10]);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("clamps to min when a step would undershoot it", async () => {
|
|
169
|
+
wrapper = await wrapperFactory({ modelValue: 2, min: 0, max: 10, step: 5 }, stepSlots);
|
|
170
|
+
await stepDown().trigger("click");
|
|
171
|
+
expect(wrapper.emitted("update:modelValue")?.[0]).toEqual([0]);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("brings a typed out-of-range value back into range", async () => {
|
|
175
|
+
wrapper = await wrapperFactory({ modelValue: 500, min: 0, max: 100 }, stepSlots);
|
|
176
|
+
await stepDown().trigger("click");
|
|
177
|
+
expect(wrapper.emitted("update:modelValue")?.[0]).toEqual([100]);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("starts from 0, clamped into range, when the field is empty", async () => {
|
|
181
|
+
wrapper = await wrapperFactory({ modelValue: "", min: 5, max: 10 }, stepSlots);
|
|
182
|
+
await stepUp().trigger("click");
|
|
183
|
+
expect(wrapper.emitted("update:modelValue")?.[0]).toEqual([5]);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("treats a negative or zero step as its size, or 1", async () => {
|
|
187
|
+
wrapper = await wrapperFactory({ modelValue: 5, min: 0, max: 10, step: -2 }, stepSlots);
|
|
188
|
+
await stepUp().trigger("click");
|
|
189
|
+
expect(wrapper.emitted("update:modelValue")?.[0]).toEqual([7]);
|
|
190
|
+
|
|
191
|
+
wrapper.unmount();
|
|
192
|
+
wrapper = await wrapperFactory({ modelValue: 5, min: 0, max: 10, step: 0 }, stepSlots);
|
|
193
|
+
await stepUp().trigger("click");
|
|
194
|
+
expect(wrapper.emitted("update:modelValue")?.[0]).toEqual([6]);
|
|
195
|
+
});
|
|
142
196
|
});
|
|
143
197
|
});
|
|
@@ -26,6 +26,17 @@ No private tokens.
|
|
|
26
26
|
|
|
27
27
|
> **Changed 2026-09-27**: the gap and top margin were hardcoded; they're now tokens with the same
|
|
28
28
|
> defaults, matching `MultipleCheckboxes`.
|
|
29
|
+
>
|
|
30
|
+
> **Changed 2026-10-16**: the equal-width column minimum is capped at `min(100%, ...)` so one long
|
|
31
|
+
> label can no longer push the grid wider than its container.
|
|
32
|
+
|
|
33
|
+
### Root fieldset
|
|
34
|
+
|
|
35
|
+
`.multiple-radiobuttons-fieldset` sets `min-inline-size: 0` and `overflow-wrap: anywhere` (added
|
|
36
|
+
2026-10-16). A native `<fieldset>` otherwise grows to its min-content width and overflows a narrow
|
|
37
|
+
column; the wrap setting inherits into the legend, description, option labels and error, so long
|
|
38
|
+
unbroken words and URLs wrap. Neither is a token; override `overflow-wrap` on your passthrough
|
|
39
|
+
class if you need the old behaviour.
|
|
29
40
|
|
|
30
41
|
---
|
|
31
42
|
|
|
@@ -28,7 +28,6 @@
|
|
|
28
28
|
<template v-for="item in fieldData.data" :key="item.id">
|
|
29
29
|
<InputCheckboxRadioButton
|
|
30
30
|
v-if="isButton"
|
|
31
|
-
:id="`${name}-${item.value}`"
|
|
32
31
|
v-model="modelValue"
|
|
33
32
|
type="radio"
|
|
34
33
|
:name
|
|
@@ -110,7 +109,14 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
110
109
|
|
|
111
110
|
const slots = useSlots();
|
|
112
111
|
|
|
113
|
-
const { elementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
112
|
+
const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
113
|
+
|
|
114
|
+
watch(
|
|
115
|
+
() => props.styleClassPassthrough,
|
|
116
|
+
() => {
|
|
117
|
+
resetElementClasses(props.styleClassPassthrough);
|
|
118
|
+
}
|
|
119
|
+
);
|
|
114
120
|
|
|
115
121
|
const { id, errorId, descriptionId, ariaDescribedby } = useAriaDescribedById(
|
|
116
122
|
props.name,
|
|
@@ -140,6 +146,11 @@ watch(
|
|
|
140
146
|
|
|
141
147
|
<style lang="css">
|
|
142
148
|
@layer components {
|
|
149
|
+
.multiple-radiobuttons-fieldset {
|
|
150
|
+
min-inline-size: 0;
|
|
151
|
+
overflow-wrap: anywhere;
|
|
152
|
+
}
|
|
153
|
+
|
|
143
154
|
.multiple-radiobuttons-items {
|
|
144
155
|
display: flex;
|
|
145
156
|
gap: var(--multiple-radiobuttons-gap, 1.2rem);
|
|
@@ -159,7 +170,7 @@ watch(
|
|
|
159
170
|
grid-template-columns: repeat(
|
|
160
171
|
auto-fit,
|
|
161
172
|
minmax(
|
|
162
|
-
calc(var(--input-checked-icon-gap) + (2 * var(--input-checkbox-label-padding-inline)) + v-bind(maxChildWidth)),
|
|
173
|
+
min(100%, calc(var(--input-checked-icon-gap) + (2 * var(--input-checkbox-label-padding-inline)) + v-bind(maxChildWidth))),
|
|
163
174
|
1fr
|
|
164
175
|
)
|
|
165
176
|
);
|