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,4 +1,5 @@
|
|
|
1
1
|
// https://nuxt.com/docs/getting-started/testing#unit-testing
|
|
2
|
+
import { defineComponent } from "vue";
|
|
2
3
|
import type { VueWrapper } from "@vue/test-utils";
|
|
3
4
|
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
4
5
|
import type { FormUiTheme, OptionsLayout } from "~/types/forms/types.forms";
|
|
@@ -25,6 +26,11 @@ const initialSlots = {
|
|
|
25
26
|
};
|
|
26
27
|
|
|
27
28
|
let wrapper: VueWrapper<InstanceType<typeof ComponentUnderTest>>;
|
|
29
|
+
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
wrapper?.unmount();
|
|
32
|
+
vi.restoreAllMocks();
|
|
33
|
+
});
|
|
28
34
|
const wrapperFactory = (propsData = {}, slotsData = {}) => {
|
|
29
35
|
const mockPropsData = { ...initialPropsData, ...propsData };
|
|
30
36
|
const mockSlotsData = { ...initialSlots, ...slotsData };
|
|
@@ -84,10 +90,38 @@ describe("MultipleCheckboxes Component", () => {
|
|
|
84
90
|
});
|
|
85
91
|
});
|
|
86
92
|
|
|
87
|
-
it("gives each checkbox
|
|
88
|
-
wrapper = await wrapperFactory();
|
|
89
|
-
const
|
|
90
|
-
expect(
|
|
93
|
+
it("gives each button checkbox a unique, space-free id linked to its label", async () => {
|
|
94
|
+
wrapper = await wrapperFactory({ isButton: true });
|
|
95
|
+
const ids = wrapper.findAll('input[type="checkbox"]').map((checkbox) => checkbox.attributes("id")!);
|
|
96
|
+
expect(new Set(ids).size).toBe(tagsData.data.length);
|
|
97
|
+
ids.forEach((id) => expect(id).not.toMatch(/\s/));
|
|
98
|
+
ids.forEach((id) => expect(wrapper.find(`label[for="${id}"]`).exists()).toBe(true));
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("keeps option ids unique across two groups sharing a name and values", async () => {
|
|
102
|
+
const Host = defineComponent({
|
|
103
|
+
components: { ComponentUnderTest },
|
|
104
|
+
setup: () => ({ props: initialPropsData }),
|
|
105
|
+
template: `
|
|
106
|
+
<div>
|
|
107
|
+
<ComponentUnderTest v-bind="props" :model-value="[]" />
|
|
108
|
+
<ComponentUnderTest v-bind="props" :model-value="[]" />
|
|
109
|
+
</div>
|
|
110
|
+
`,
|
|
111
|
+
});
|
|
112
|
+
const host = await mountSuspended(Host);
|
|
113
|
+
const ids = host.findAll('input[type="checkbox"]').map((checkbox) => checkbox.attributes("id"));
|
|
114
|
+
expect(ids.length).toBe(tagsData.data.length * 2);
|
|
115
|
+
expect(new Set(ids).size).toBe(ids.length);
|
|
116
|
+
host.unmount();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("builds ids that don't depend on option values containing spaces", async () => {
|
|
120
|
+
wrapper = await wrapperFactory({
|
|
121
|
+
isButton: true,
|
|
122
|
+
fieldData: { ...tagsData, data: [{ id: "x", name: "tags", value: "has spaces <b>", label: "Spaces" }] },
|
|
123
|
+
});
|
|
124
|
+
expect(wrapper.find('input[type="checkbox"]').attributes("id")).not.toMatch(/\s/);
|
|
91
125
|
});
|
|
92
126
|
|
|
93
127
|
it("adds each clicked value to the model", async () => {
|
|
@@ -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 = {}, slotsData = {}) =>
|
|
16
21
|
mountSuspended(ComponentUnderTest, {
|
|
17
22
|
attachTo: document.body,
|
|
@@ -67,8 +67,7 @@ Also reads `--input-checkbox-decorator-icon-size` (decorator icon `font-size`, `
|
|
|
67
67
|
`--input-font-size` (label).
|
|
68
68
|
|
|
69
69
|
Private (not public API): `--_transition-duration` (the control's resolved duration, shared by two
|
|
70
|
-
transitions)
|
|
71
|
-
`--_white-space` (the button label's wrapping, swapped by `data-options-layout="inline"`).
|
|
70
|
+
transitions) and `--_icon-size` (the button shrinks the inner control's checked icon through it).
|
|
72
71
|
|
|
73
72
|
> **Changed 2026-09-27**:
|
|
74
73
|
>
|
|
@@ -80,6 +79,14 @@ transitions), `--_icon-size` (the button shrinks the inner control's checked ico
|
|
|
80
79
|
> so both tokens take effect for the first time: the `+` renders at `2rem` and the checked icon
|
|
81
80
|
> inside a button at `--input-checked-icon-size` minus `0.8rem`.
|
|
82
81
|
> - `--input-checkbox-label-color` is new (was unstyled, so `inherit`).
|
|
82
|
+
>
|
|
83
|
+
> **Changed 2026-10-16**: long labels wrap instead of overflowing. Both labels
|
|
84
|
+
> (`.input-checkbox-radio-field-label`, `.input-checkbox-radio-button-label`) and their roots set
|
|
85
|
+
> `min-inline-size: 0`, the labels set `overflow-wrap: anywhere`, and the control and decorator
|
|
86
|
+
> icon no longer shrink (`flex-shrink: 0`). The button label used to be `nowrap` under
|
|
87
|
+
> `data-options-layout="inline"` (private `--_white-space`, now removed), so an unbroken label
|
|
88
|
+
> overflowed the row. In a wrapping row a short label still sits on one line; if you relied on
|
|
89
|
+
> `nowrap`, set `white-space: nowrap` on `.input-checkbox-radio-button-label` yourself.
|
|
83
90
|
|
|
84
91
|
---
|
|
85
92
|
|
|
@@ -95,7 +102,7 @@ transitions), `--_icon-size` (the button shrinks the inner control's checked ico
|
|
|
95
102
|
| `data-invalid` | all three roots | The field has an error |
|
|
96
103
|
| `data-pill` | `.input-checkbox-radio-button` | `is-pill` |
|
|
97
104
|
| `data-direction="row" \| "row-reverse"` | `.input-checkbox-radio-button` | Always, mirrors `direction` |
|
|
98
|
-
| `data-options-layout` | `.input-checkbox-radio-button` | Always, mirrors `options-layout` (
|
|
105
|
+
| `data-options-layout` | `.input-checkbox-radio-button` | Always, mirrors `options-layout` (a hook only: it no longer changes wrapping) |
|
|
99
106
|
|
|
100
107
|
Inner elements: `.input-checkbox-radio-icon-slot` (holds the `checkedIcon` slot or the default
|
|
101
108
|
`.input-checkbox-radio-icon`), `.input-checkbox-radio-input` (the hidden native input),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<label
|
|
3
|
-
:for="
|
|
3
|
+
:for="inputId"
|
|
4
4
|
class="input-checkbox-radio-button"
|
|
5
5
|
:class="elementClasses"
|
|
6
6
|
:data-theme="theme"
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
:data-pill="isPill ? '' : undefined"
|
|
11
11
|
>
|
|
12
12
|
<InputCheckboxRadio
|
|
13
|
-
:id
|
|
13
|
+
:id="inputId"
|
|
14
14
|
v-model="modelValue"
|
|
15
15
|
:is-button="true"
|
|
16
16
|
:type
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
import type { FormUiTheme, InputUiVariant, OptionsLayout } from "~/types/forms/types.forms";
|
|
45
45
|
|
|
46
46
|
interface Props {
|
|
47
|
-
id
|
|
47
|
+
id?: string;
|
|
48
48
|
type: "checkbox" | "radio";
|
|
49
49
|
name: string;
|
|
50
50
|
label?: string;
|
|
@@ -65,6 +65,7 @@ interface Props {
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
const props = withDefaults(defineProps<Props>(), {
|
|
68
|
+
id: undefined,
|
|
68
69
|
label: "",
|
|
69
70
|
required: false,
|
|
70
71
|
theme: "default",
|
|
@@ -84,6 +85,9 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
84
85
|
|
|
85
86
|
const modelValue = defineModel<(string | number | boolean)[] | string | number | boolean | undefined>({ required: true });
|
|
86
87
|
|
|
88
|
+
const generatedId = useId();
|
|
89
|
+
const inputId = computed(() => props.id || generatedId);
|
|
90
|
+
|
|
87
91
|
const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
88
92
|
|
|
89
93
|
watch(
|
|
@@ -97,9 +101,8 @@ watch(
|
|
|
97
101
|
<style lang="css">
|
|
98
102
|
@layer components {
|
|
99
103
|
.input-checkbox-radio-button {
|
|
100
|
-
--_white-space: wrap;
|
|
101
|
-
|
|
102
104
|
display: flex;
|
|
105
|
+
min-inline-size: 0;
|
|
103
106
|
align-items: center;
|
|
104
107
|
justify-content: space-between;
|
|
105
108
|
gap: var(--input-checkbox-button-gap, 1rem);
|
|
@@ -122,10 +125,6 @@ watch(
|
|
|
122
125
|
border-radius: var(--input-checkbox-button-border-radius-pill, 100vw);
|
|
123
126
|
}
|
|
124
127
|
|
|
125
|
-
&[data-options-layout="inline"] {
|
|
126
|
-
--_white-space: nowrap;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
128
|
&:hover {
|
|
130
129
|
background-color: var(--input-checkbox-button-surface-hover, var(--theme-input-surface-hover));
|
|
131
130
|
outline-color: var(--input-checkbox-button-ring-hover, var(--theme-ring));
|
|
@@ -155,12 +154,14 @@ watch(
|
|
|
155
154
|
text-align: center;
|
|
156
155
|
padding-block: var(--input-checkbox-button-label-padding-block, 0.8rem);
|
|
157
156
|
padding-inline: var(--input-checkbox-button-label-padding-inline, 0.8rem);
|
|
158
|
-
|
|
157
|
+
min-inline-size: 0;
|
|
158
|
+
overflow-wrap: anywhere;
|
|
159
159
|
cursor: pointer;
|
|
160
160
|
}
|
|
161
161
|
|
|
162
162
|
.input-checkbox-radio-button-icon {
|
|
163
163
|
display: flex;
|
|
164
|
+
flex-shrink: 0;
|
|
164
165
|
align-items: center;
|
|
165
166
|
justify-content: center;
|
|
166
167
|
color: var(--input-checkbox-button-icon-color, var(--theme-text));
|
|
@@ -85,11 +85,14 @@ watch(
|
|
|
85
85
|
gap: var(--input-checked-icon-gap);
|
|
86
86
|
min-height: var(--input-min-height);
|
|
87
87
|
user-select: none;
|
|
88
|
+
min-inline-size: 0;
|
|
88
89
|
|
|
89
90
|
.input-checkbox-radio-field-label {
|
|
90
91
|
display: flex;
|
|
91
92
|
flex: 1;
|
|
92
93
|
align-items: center;
|
|
94
|
+
min-inline-size: 0;
|
|
95
|
+
overflow-wrap: anywhere;
|
|
93
96
|
color: var(--input-checkbox-label-color, inherit);
|
|
94
97
|
padding-block: var(--input-checkbox-label-padding-block);
|
|
95
98
|
padding-inline: var(--input-checkbox-label-padding-inline);
|
|
@@ -1,9 +1,9 @@
|
|
|
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 InputCheckboxRadio from "../InputCheckboxRadio.vue";
|
|
4
4
|
import type { FormUiTheme, InputUiVariant } from "~/types/forms/types.forms.d";
|
|
5
5
|
|
|
6
|
-
interface
|
|
6
|
+
interface StoryArgs {
|
|
7
7
|
type: "checkbox" | "radio";
|
|
8
8
|
id: string;
|
|
9
9
|
name: string;
|
|
@@ -15,9 +15,20 @@ interface InputCheckboxRadioStoryArgs {
|
|
|
15
15
|
inputVariant: InputUiVariant;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
// Links in the canvas open in the Storybook manager (target="_top"), not inside the preview iframe.
|
|
19
|
+
const storyNoteStyle = "margin: 0 0 1.6rem; font-size: 1.4rem;";
|
|
20
|
+
const parentNote = `
|
|
21
|
+
<p style="${storyNoteStyle}">
|
|
22
|
+
This is the bare control with no label. With a text label it's the
|
|
23
|
+
<a href="/?path=/story/components-forms-input-checkbox-radio-inputcheckboxradiofield--checkbox" target="_top">InputCheckboxRadioField</a>;
|
|
24
|
+
drawn as a button or pill it's the
|
|
25
|
+
<a href="/?path=/story/components-forms-input-checkbox-radio-inputcheckboxradiobutton--option-group" target="_top">InputCheckboxRadioButton</a>.
|
|
26
|
+
</p>
|
|
27
|
+
`;
|
|
28
|
+
|
|
29
|
+
const meta: Meta<StoryArgs> = {
|
|
19
30
|
title: "Components/Forms/Input Checkbox Radio/InputCheckboxRadio",
|
|
20
|
-
component:
|
|
31
|
+
component: InputCheckboxRadio,
|
|
21
32
|
argTypes: {
|
|
22
33
|
type: {
|
|
23
34
|
control: { type: "select" },
|
|
@@ -62,29 +73,45 @@ export default {
|
|
|
62
73
|
isButton: false,
|
|
63
74
|
inputVariant: "normal",
|
|
64
75
|
},
|
|
65
|
-
|
|
76
|
+
parameters: {
|
|
77
|
+
docs: {
|
|
78
|
+
description: {
|
|
79
|
+
component:
|
|
80
|
+
"The checkbox or radio control every form checkbox and radio is built from: a styled square or circle stacked over a hidden native input. It has no label, so pair its `id` with your own `label[for]`, or use `InputCheckboxRadioField` / `InputCheckboxRadioButton`. Slot: `checkedIcon`. No stress story here: the control renders no text, and long-label behaviour is covered by the Field and Button stress stories.",
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export default meta;
|
|
87
|
+
type Story = StoryObj<StoryArgs>;
|
|
66
88
|
|
|
67
|
-
const
|
|
68
|
-
components: {
|
|
89
|
+
const render = (args: StoryArgs) => ({
|
|
90
|
+
components: { InputCheckboxRadio },
|
|
69
91
|
setup() {
|
|
70
92
|
const modelValue = ref(false);
|
|
71
93
|
return { args, modelValue };
|
|
72
94
|
},
|
|
73
95
|
template: `
|
|
74
96
|
<div style="margin: 36px;">
|
|
75
|
-
|
|
97
|
+
${parentNote}
|
|
98
|
+
<InputCheckboxRadio v-bind="args" v-model="modelValue" />
|
|
76
99
|
</div>
|
|
77
100
|
`,
|
|
78
101
|
});
|
|
79
102
|
|
|
80
|
-
export const Checkbox =
|
|
81
|
-
Checkbox.args = { type: "checkbox" };
|
|
103
|
+
export const Checkbox: Story = { args: { type: "checkbox" }, render };
|
|
82
104
|
|
|
83
|
-
export const Radio =
|
|
84
|
-
Radio.args = { type: "radio" };
|
|
105
|
+
export const Radio: Story = { args: { type: "radio" }, render };
|
|
85
106
|
|
|
86
|
-
export const CheckboxAsDisc =
|
|
87
|
-
|
|
107
|
+
export const CheckboxAsDisc: Story = {
|
|
108
|
+
args: { type: "checkbox", isButton: true, displayAsDisc: true },
|
|
109
|
+
render,
|
|
110
|
+
parameters: { docs: { description: { story: "`displayAsDisc` rounds a checkbox, but only in button mode (`isButton`)." } } },
|
|
111
|
+
};
|
|
88
112
|
|
|
89
|
-
export const ErrorState =
|
|
90
|
-
|
|
113
|
+
export const ErrorState: Story = {
|
|
114
|
+
args: { type: "checkbox", fieldHasError: true, theme: "error" },
|
|
115
|
+
render,
|
|
116
|
+
parameters: { docs: { description: { story: "`fieldHasError` sets `aria-invalid` on the input and `data-invalid` on the root." } } },
|
|
117
|
+
};
|
package/app/components/05.forms/input-checkbox-radio/stories/InputCheckboxRadioButton.stories.ts
CHANGED
|
@@ -1,26 +1,53 @@
|
|
|
1
|
-
import type { Meta, StoryFn } from "@nuxtjs/storybook";
|
|
2
1
|
import { defineComponent, ref } from "vue";
|
|
3
|
-
import
|
|
2
|
+
import type { PropType } from "vue";
|
|
3
|
+
import type { Meta, StoryObj } from "@nuxtjs/storybook";
|
|
4
|
+
import InputCheckboxRadioButton from "../InputCheckboxRadioButton.vue";
|
|
5
|
+
import CanvasSwitcher from "../../../01.atoms/canvas-switcher/CanvasSwitcher.vue";
|
|
6
|
+
import type { OptionsLayout } from "~/types/forms/types.forms.d";
|
|
7
|
+
import type { MediaCanvas } from "~/types/components";
|
|
4
8
|
|
|
5
|
-
interface
|
|
9
|
+
interface StoryArgs {
|
|
6
10
|
type: "checkbox" | "radio";
|
|
7
11
|
label: string;
|
|
8
12
|
isPill: boolean;
|
|
9
13
|
fieldHasError: boolean;
|
|
10
14
|
displayAsDisc: boolean;
|
|
11
15
|
direction: "row" | "row-reverse";
|
|
16
|
+
optionsLayout: OptionsLayout;
|
|
17
|
+
options: string[];
|
|
12
18
|
}
|
|
13
19
|
|
|
14
|
-
|
|
20
|
+
// Links in the canvas open in the Storybook manager (target="_top"), not inside the preview iframe.
|
|
21
|
+
const storyNoteStyle = "margin: 0 0 1.6rem; font-size: 1.4rem;";
|
|
22
|
+
const parentNote = `
|
|
23
|
+
<p style="${storyNoteStyle}">
|
|
24
|
+
The group components draw each option with this when <code>isButton</code> is on, and add the
|
|
25
|
+
fieldset, legend, description and error: see the
|
|
26
|
+
<a href="/?path=/story/components-forms-input-checkbox-multiplecheckboxes--buttons" target="_top">MultipleCheckboxes</a>
|
|
27
|
+
and
|
|
28
|
+
<a href="/?path=/story/components-forms-input-radio-multipleradiobuttons--buttons" target="_top">MultipleRadiobuttons</a>
|
|
29
|
+
Buttons stories.
|
|
30
|
+
</p>
|
|
31
|
+
`;
|
|
32
|
+
|
|
33
|
+
const serviceOptions = ["Balayage", "Highlights", "Half Head Highlights", "Full Colour", "Lowlights", "Toner & Gloss"];
|
|
34
|
+
|
|
35
|
+
const layoutStyles: Record<OptionsLayout, string> = {
|
|
36
|
+
"equal-widths": "display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr)); gap: 0.8rem;",
|
|
37
|
+
inline: "display: flex; flex-wrap: wrap; gap: 0.8rem;",
|
|
38
|
+
block: "display: flex; flex-direction: column; gap: 0.8rem;",
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const meta: Meta<StoryArgs> = {
|
|
15
42
|
title: "Components/Forms/Input Checkbox Radio/InputCheckboxRadioButton",
|
|
16
|
-
component:
|
|
43
|
+
component: InputCheckboxRadioButton,
|
|
17
44
|
argTypes: {
|
|
18
45
|
type: {
|
|
19
46
|
control: { type: "select" },
|
|
20
47
|
options: ["checkbox", "radio"],
|
|
21
48
|
table: { category: "Basic" },
|
|
22
49
|
},
|
|
23
|
-
label: { control: "text", table: { category: "Basic" } },
|
|
50
|
+
label: { control: "text", description: "Single-option stories only", table: { category: "Basic" } },
|
|
24
51
|
isPill: { control: "boolean", table: { category: "Styling" } },
|
|
25
52
|
fieldHasError: { control: "boolean", table: { category: "States" } },
|
|
26
53
|
displayAsDisc: { control: "boolean", table: { category: "Styling" } },
|
|
@@ -30,6 +57,18 @@ export default {
|
|
|
30
57
|
description: "row-reverse puts the control on the right",
|
|
31
58
|
table: { category: "Styling" },
|
|
32
59
|
},
|
|
60
|
+
optionsLayout: {
|
|
61
|
+
control: { type: "select" },
|
|
62
|
+
options: ["equal-widths", "inline", "block"],
|
|
63
|
+
description:
|
|
64
|
+
"Rendered as `data-options-layout`. In the group stories it also lays out the story's wrapper the way `MultipleCheckboxes` does",
|
|
65
|
+
table: { category: "Styling" },
|
|
66
|
+
},
|
|
67
|
+
options: {
|
|
68
|
+
control: "object",
|
|
69
|
+
description: "Story-only: the option labels in the group stories",
|
|
70
|
+
table: { category: "Story data" },
|
|
71
|
+
},
|
|
33
72
|
},
|
|
34
73
|
args: {
|
|
35
74
|
type: "checkbox",
|
|
@@ -38,93 +77,121 @@ export default {
|
|
|
38
77
|
fieldHasError: false,
|
|
39
78
|
displayAsDisc: false,
|
|
40
79
|
direction: "row",
|
|
80
|
+
optionsLayout: "equal-widths",
|
|
81
|
+
options: serviceOptions,
|
|
41
82
|
},
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
83
|
+
decorators: [
|
|
84
|
+
(story, context) => ({
|
|
85
|
+
components: { story, CanvasSwitcher },
|
|
86
|
+
setup() {
|
|
87
|
+
const canvasName = ref<MediaCanvas>(context.parameters.initialCanvas ?? "fullWidthCanvas");
|
|
88
|
+
return { canvasName };
|
|
89
|
+
},
|
|
90
|
+
template: `
|
|
91
|
+
<div style="padding: 1.2rem 1.6rem; border-block-end: 1px solid currentColor;">
|
|
92
|
+
<CanvasSwitcher v-model:canvas-name="canvasName" />
|
|
93
|
+
</div>
|
|
94
|
+
<div :class="canvasName" style="margin-inline: auto; padding: 2rem; outline: 1px dashed currentColor;">
|
|
95
|
+
<story />
|
|
96
|
+
</div>
|
|
97
|
+
`,
|
|
98
|
+
}),
|
|
99
|
+
],
|
|
100
|
+
parameters: {
|
|
101
|
+
layout: "fullscreen",
|
|
102
|
+
docs: {
|
|
103
|
+
description: {
|
|
104
|
+
component:
|
|
105
|
+
"One checkbox or radio option drawn as a button or pill: a `<label>` holding the control, a centred label and a decorator icon. `id` is optional and generated with `useId()` when omitted. Props: `type`, `name`, `label`, `trueValue`, `multipleOptions`, `isPill`, `direction`, `displayAsDisc`, `optionsLayout`, `itemIcon`. Slots: `labelContent`, `checkedIcon`, `itemIcon`. Usually used through the `isButton` mode of `MultipleCheckboxes` and `MultipleRadiobuttons`: see the [MultipleCheckboxes Buttons](?path=/story/components-forms-input-checkbox-multiplecheckboxes--buttons) story.",
|
|
106
|
+
},
|
|
107
|
+
},
|
|
49
108
|
},
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
name="single"
|
|
55
|
-
:type="args.type"
|
|
56
|
-
:label="args.label"
|
|
57
|
-
:is-pill="args.isPill"
|
|
58
|
-
:field-has-error="args.fieldHasError"
|
|
59
|
-
:display-as-disc="args.displayAsDisc"
|
|
60
|
-
:direction="args.direction"
|
|
61
|
-
v-model="modelValue"
|
|
62
|
-
/>
|
|
63
|
-
</div>
|
|
64
|
-
`,
|
|
65
|
-
});
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
export default meta;
|
|
112
|
+
type Story = StoryObj<StoryArgs>;
|
|
66
113
|
|
|
67
|
-
export const Default =
|
|
114
|
+
export const Default: Story = {
|
|
115
|
+
render: (args: StoryArgs) => ({
|
|
116
|
+
components: { InputCheckboxRadioButton },
|
|
117
|
+
setup() {
|
|
118
|
+
const modelValue = ref(false);
|
|
119
|
+
return { args, modelValue };
|
|
120
|
+
},
|
|
121
|
+
template: `
|
|
122
|
+
${parentNote}
|
|
123
|
+
<div style="max-width: 260px;">
|
|
124
|
+
<InputCheckboxRadioButton
|
|
125
|
+
name="single"
|
|
126
|
+
:type="args.type"
|
|
127
|
+
:label="args.label"
|
|
128
|
+
:is-pill="args.isPill"
|
|
129
|
+
:field-has-error="args.fieldHasError"
|
|
130
|
+
:display-as-disc="args.displayAsDisc"
|
|
131
|
+
:direction="args.direction"
|
|
132
|
+
:options-layout="args.optionsLayout"
|
|
133
|
+
v-model="modelValue"
|
|
134
|
+
/>
|
|
135
|
+
</div>
|
|
136
|
+
`,
|
|
137
|
+
}),
|
|
138
|
+
argTypes: { options: { table: { disable: true } } },
|
|
139
|
+
};
|
|
68
140
|
|
|
69
|
-
// "Services of interest" pattern — the actual multi-option checkbox-pill layout this component
|
|
70
|
-
// is normally used in (as opposed to Default's single isolated control).
|
|
71
141
|
// Selection state lives in a child keyed on type, so switching checkbox/radio remounts it with
|
|
72
142
|
// the right empty value (an array for checkboxes, a string for radios). A watch on args.type
|
|
73
143
|
// isn't reliable here: the template sees the new arg, but the watcher doesn't fire.
|
|
74
|
-
const
|
|
75
|
-
components: {
|
|
144
|
+
const OptionGroupDemo = defineComponent({
|
|
145
|
+
components: { InputCheckboxRadioButton },
|
|
76
146
|
props: {
|
|
77
|
-
type: { type: String as
|
|
147
|
+
type: { type: String as PropType<"checkbox" | "radio">, required: true },
|
|
148
|
+
options: { type: Array as PropType<string[]>, required: true },
|
|
149
|
+
optionsLayout: { type: String as PropType<OptionsLayout>, required: true },
|
|
78
150
|
isPill: Boolean,
|
|
79
151
|
fieldHasError: Boolean,
|
|
80
152
|
displayAsDisc: Boolean,
|
|
81
|
-
direction: { type: String as
|
|
153
|
+
direction: { type: String as PropType<"row" | "row-reverse">, default: "row" },
|
|
82
154
|
},
|
|
83
155
|
setup(props) {
|
|
84
|
-
const services = [
|
|
85
|
-
"Balayage",
|
|
86
|
-
"Highlights",
|
|
87
|
-
"Half Head Highlights",
|
|
88
|
-
"Full Colour",
|
|
89
|
-
"Lowlights",
|
|
90
|
-
"Toner & Gloss",
|
|
91
|
-
];
|
|
92
156
|
const selected = ref<string[] | string>(props.type === "checkbox" ? [] : "");
|
|
93
|
-
return {
|
|
157
|
+
return { selected, layoutStyles };
|
|
94
158
|
},
|
|
95
159
|
template: `
|
|
96
160
|
<div>
|
|
97
|
-
<div style="
|
|
98
|
-
<
|
|
99
|
-
v-for="
|
|
100
|
-
:key="
|
|
101
|
-
:id="'service-' + service"
|
|
161
|
+
<div :style="layoutStyles[optionsLayout]">
|
|
162
|
+
<InputCheckboxRadioButton
|
|
163
|
+
v-for="(option, index) in options"
|
|
164
|
+
:key="index"
|
|
102
165
|
name="services"
|
|
103
166
|
:type="type"
|
|
104
|
-
:label="
|
|
105
|
-
:true-value="
|
|
167
|
+
:label="option"
|
|
168
|
+
:true-value="option"
|
|
106
169
|
:multiple-options="type === 'checkbox'"
|
|
107
170
|
:is-pill="isPill"
|
|
108
171
|
:field-has-error="fieldHasError"
|
|
109
172
|
:display-as-disc="displayAsDisc"
|
|
110
173
|
:direction="direction"
|
|
174
|
+
:options-layout="optionsLayout"
|
|
111
175
|
v-model="selected"
|
|
112
176
|
/>
|
|
113
177
|
</div>
|
|
114
|
-
<p style="margin:
|
|
178
|
+
<p style="margin-block-start: 1.6rem; font-family: monospace; overflow-wrap: anywhere;">selected: {{ JSON.stringify(selected) }}</p>
|
|
115
179
|
</div>
|
|
116
180
|
`,
|
|
117
181
|
});
|
|
118
182
|
|
|
119
|
-
const
|
|
120
|
-
components: {
|
|
183
|
+
const renderGroup = (args: StoryArgs) => ({
|
|
184
|
+
components: { OptionGroupDemo },
|
|
121
185
|
setup() {
|
|
122
186
|
return { args };
|
|
123
187
|
},
|
|
124
188
|
template: `
|
|
125
|
-
|
|
189
|
+
${parentNote}
|
|
190
|
+
<OptionGroupDemo
|
|
126
191
|
:key="args.type"
|
|
127
192
|
:type="args.type"
|
|
193
|
+
:options="args.options"
|
|
194
|
+
:options-layout="args.optionsLayout"
|
|
128
195
|
:is-pill="args.isPill"
|
|
129
196
|
:field-has-error="args.fieldHasError"
|
|
130
197
|
:display-as-disc="args.displayAsDisc"
|
|
@@ -133,5 +200,44 @@ const OptionGroupTemplate: StoryFn<InputCheckboxRadioButtonStoryArgs> = (args) =
|
|
|
133
200
|
`,
|
|
134
201
|
});
|
|
135
202
|
|
|
136
|
-
export const OptionGroup =
|
|
137
|
-
|
|
203
|
+
export const OptionGroup: Story = {
|
|
204
|
+
render: renderGroup,
|
|
205
|
+
argTypes: { label: { table: { disable: true } } },
|
|
206
|
+
parameters: {
|
|
207
|
+
docs: {
|
|
208
|
+
description: {
|
|
209
|
+
story: "A group of options sharing one `name` and model, the way the group components render them. Switch `type` and `optionsLayout`.",
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
export const StressTest: Story = {
|
|
216
|
+
name: "Stress Test (Worst-Case Data)",
|
|
217
|
+
args: {
|
|
218
|
+
optionsLayout: "inline",
|
|
219
|
+
isPill: true,
|
|
220
|
+
options: [
|
|
221
|
+
"A",
|
|
222
|
+
"",
|
|
223
|
+
"Haarverlängerungsberatungstermindienstleistungspaket",
|
|
224
|
+
"https://example.com/a/very/long/unbroken/url/that/should/wrap/somewhere",
|
|
225
|
+
"<b>Not bold</b> & <i>not italic</i>",
|
|
226
|
+
"💇♀️ Colour + cut 🌈",
|
|
227
|
+
"قص الشعر وتصفيفه",
|
|
228
|
+
"A conditioning treatment with a scalp massage, a hot towel and a blow-dry finish",
|
|
229
|
+
...Array.from({ length: 16 }, (_, index) => `Option ${index + 9}`),
|
|
230
|
+
],
|
|
231
|
+
},
|
|
232
|
+
render: renderGroup,
|
|
233
|
+
argTypes: { label: { table: { disable: true } } },
|
|
234
|
+
parameters: {
|
|
235
|
+
initialCanvas: "mobileCanvas",
|
|
236
|
+
docs: {
|
|
237
|
+
description: {
|
|
238
|
+
story:
|
|
239
|
+
"24 pill options with an empty label, a single character, a long unbroken German word and URL, HTML-like text, emoji, right-to-left text and a long sentence, laid out inline. Check at every canvas width: no option pokes out of the dashed outline (long labels wrap inside their pill instead), the control and decorator icon keep their size, short labels stay on one line, and markup renders as text. Switch `optionsLayout`, `direction` and `type`.",
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
};
|