@fiscozen/checkbox 0.1.6 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.cjs +19 -0
- package/.prettierrc.js +6 -0
- package/README.md +514 -0
- package/coverage/clover.xml +789 -215
- package/coverage/coverage-final.json +6 -6
- package/coverage/index.html +32 -32
- package/coverage/src/FzCheckbox.vue.html +1099 -151
- package/coverage/src/FzCheckboxGroup.vue.html +444 -81
- package/coverage/src/common.ts.html +79 -19
- package/coverage/src/components/ErrorAlert.vue.html +268 -0
- package/coverage/src/components/FzCheckboxGroupOption.vue.html +343 -70
- package/coverage/src/components/index.html +22 -22
- package/coverage/src/index.html +45 -45
- package/coverage/src/utils.ts.html +265 -0
- package/package.json +13 -11
- package/src/FzCheckbox.vue +411 -95
- package/src/FzCheckboxGroup.vue +179 -58
- package/src/__test__/FzCheckbox.test.ts +91 -19
- package/src/__test__/FzCheckboxGroup.test.ts +167 -4
- package/src/common.ts +20 -0
- package/src/components/ErrorAlert.vue +61 -0
- package/src/components/FzCheckboxGroupOption.vue +137 -46
- package/src/index.ts +1 -0
- package/src/types.ts +166 -23
- package/src/utils.ts +60 -0
- package/tsconfig.json +1 -1
- package/coverage/.tmp/coverage-0.json +0 -1
- package/coverage/.tmp/coverage-1.json +0 -1
- package/coverage/src/components/FzCheckboxErrorText.vue.html +0 -145
- package/coverage/src/types.ts.html +0 -310
- package/dist/checkbox.js +0 -301
- package/dist/checkbox.umd.cjs +0 -1
- package/dist/index.d.ts +0 -1
- package/dist/src/FzCheckbox.vue.d.ts +0 -95
- package/dist/src/FzCheckboxGroup.vue.d.ts +0 -70
- package/dist/src/__test__/FzCheckbox.test.d.ts +0 -1
- package/dist/src/__test__/FzCheckboxGroup.test.d.ts +0 -1
- package/dist/src/common.d.ts +0 -4
- package/dist/src/components/FzCheckboxErrorText.vue.d.ts +0 -24
- package/dist/src/components/FzCheckboxGroupOption.vue.d.ts +0 -82
- package/dist/src/index.d.ts +0 -3
- package/dist/src/types.d.ts +0 -76
- package/dist/style.css +0 -1
- package/src/components/FzCheckboxErrorText.vue +0 -20
package/src/FzCheckboxGroup.vue
CHANGED
|
@@ -1,78 +1,199 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div :class="[staticContainerClass, computedContainerClass]" :id="id">
|
|
3
|
-
<label :for="id" :class="[staticLabeldClass, computedLabelClass]">
|
|
4
|
-
<span>{{ label }}<span v-if="required"> *</span></span>
|
|
5
|
-
<p :class="computedHelpTextClass" v-if="$slots.help">
|
|
6
|
-
<slot name="help" />
|
|
7
|
-
</p>
|
|
8
|
-
</label>
|
|
9
|
-
<div :class="[staticSlotContainerClass, computedSlotContainerClass]">
|
|
10
|
-
<FzCheckboxGroupOption
|
|
11
|
-
v-for="option in options"
|
|
12
|
-
:key="option.value ? option.value.toString() : option.label"
|
|
13
|
-
v-model="model"
|
|
14
|
-
:disabled="disabled"
|
|
15
|
-
v-bind="option"
|
|
16
|
-
:emphasis="emphasis"
|
|
17
|
-
:error="error"
|
|
18
|
-
:size="size"
|
|
19
|
-
/>
|
|
20
|
-
</div>
|
|
21
|
-
<FzCheckboxErrorText :size="size" v-if="error && $slots.error">
|
|
22
|
-
<slot name="error" />
|
|
23
|
-
</FzCheckboxErrorText>
|
|
24
|
-
</div>
|
|
25
|
-
</template>
|
|
26
|
-
|
|
27
1
|
<script setup lang="ts">
|
|
28
|
-
|
|
2
|
+
/**
|
|
3
|
+
* FzCheckboxGroup Component
|
|
4
|
+
*
|
|
5
|
+
* A container component for managing multiple related checkboxes as a group.
|
|
6
|
+
* Provides group-level labeling, help text, error handling, and accessibility features.
|
|
7
|
+
* Supports both flat checkbox lists and hierarchical parent-child structures.
|
|
8
|
+
*
|
|
9
|
+
* @component
|
|
10
|
+
* @example
|
|
11
|
+
* // Basic checkbox group
|
|
12
|
+
* <FzCheckboxGroup
|
|
13
|
+
* v-model="selectedOptions"
|
|
14
|
+
* label="Choose options"
|
|
15
|
+
* :options="[
|
|
16
|
+
* { label: 'Option 1', value: 'opt1' },
|
|
17
|
+
* { label: 'Option 2', value: 'opt2' }
|
|
18
|
+
* ]"
|
|
19
|
+
* />
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* // Hierarchical checkbox group with children
|
|
23
|
+
* <FzCheckboxGroup
|
|
24
|
+
* v-model="selection"
|
|
25
|
+
* label="Select features"
|
|
26
|
+
* :options="[
|
|
27
|
+
* {
|
|
28
|
+
* label: 'All Features',
|
|
29
|
+
* value: 'all',
|
|
30
|
+
* children: [
|
|
31
|
+
* { label: 'Feature A', value: 'a' },
|
|
32
|
+
* { label: 'Feature B', value: 'b' }
|
|
33
|
+
* ]
|
|
34
|
+
* }
|
|
35
|
+
* ]"
|
|
36
|
+
* />
|
|
37
|
+
*/
|
|
38
|
+
import { computed, useSlots, watch } from "vue";
|
|
29
39
|
import { FzCheckboxGroupProps } from "./types";
|
|
30
|
-
import
|
|
31
|
-
import { mapSizeToClasses } from "./common";
|
|
40
|
+
import { generateGroupId } from "./utils";
|
|
32
41
|
import FzCheckboxGroupOption from "./components/FzCheckboxGroupOption.vue";
|
|
33
|
-
|
|
34
|
-
FzCheckboxGroupOption.compatConfig = {
|
|
35
|
-
MODE: 3,
|
|
36
|
-
};
|
|
42
|
+
import ErrorAlert from "./components/ErrorAlert.vue";
|
|
37
43
|
|
|
38
44
|
const props = defineProps<FzCheckboxGroupProps>();
|
|
39
|
-
const
|
|
45
|
+
const slots = useSlots();
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Deprecation warning for size prop.
|
|
49
|
+
* Watches the size prop and warns once on mount if it's provided.
|
|
50
|
+
* Using watch with immediate:true ensures the warning only fires once per component instance.
|
|
51
|
+
*/
|
|
52
|
+
watch(
|
|
53
|
+
() => props.size,
|
|
54
|
+
(size) => {
|
|
55
|
+
if (size !== undefined) {
|
|
56
|
+
console.warn(
|
|
57
|
+
'[FzCheckboxGroup] The "size" prop is deprecated and will be removed in a future version. Checkboxes now have a fixed size.'
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
{ immediate: true }
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
/** Unique identifier for the checkbox group, used for ARIA relationships */
|
|
65
|
+
const id: string = generateGroupId();
|
|
40
66
|
|
|
41
|
-
|
|
42
|
-
|
|
67
|
+
/** Dynamic classes for help text based on disabled state */
|
|
68
|
+
const computedHelpTextClass = computed<string[]>(() => [
|
|
69
|
+
"text-sm",
|
|
43
70
|
props.disabled ? "text-grey-400" : "text-grey-500",
|
|
44
71
|
]);
|
|
45
72
|
|
|
46
|
-
|
|
73
|
+
/**
|
|
74
|
+
* Two-way binding for selected checkbox values.
|
|
75
|
+
* Supports string, number, and boolean values.
|
|
76
|
+
* Always an array, even when empty.
|
|
77
|
+
*/
|
|
78
|
+
const model = defineModel<(string | number | boolean)[]>({
|
|
47
79
|
required: true,
|
|
48
80
|
default: [],
|
|
49
81
|
});
|
|
50
82
|
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
const staticSlotContainerClass = "flex flex-col";
|
|
83
|
+
/** Base layout for the label element */
|
|
84
|
+
const staticLabeldClass: string = "flex flex-col";
|
|
54
85
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
!props.disabled ? "text-grey-500" : "",
|
|
61
|
-
]);
|
|
86
|
+
/** Base layout for the root container */
|
|
87
|
+
const staticContainerClass: string = "flex flex-col gap-10";
|
|
88
|
+
|
|
89
|
+
/** Base layout for the checkboxes container */
|
|
90
|
+
const staticSlotContainerClass: string = "flex items-start";
|
|
62
91
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
92
|
+
/** Dynamic label classes based on spacing and disabled state */
|
|
93
|
+
const computedLabelClass = computed<string[]>(() => [
|
|
94
|
+
"text-base",
|
|
95
|
+
"gap-6",
|
|
96
|
+
props.disabled ? "text-grey-400" : "text-core-black",
|
|
67
97
|
]);
|
|
68
98
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
99
|
+
/** Dynamic container classes */
|
|
100
|
+
const computedContainerClass = computed<string[]>(() => ["text-base"]);
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Dynamic classes for the checkbox container.
|
|
104
|
+
* Handles both horizontal and vertical layouts with appropriate spacing.
|
|
105
|
+
*/
|
|
106
|
+
const computedSlotContainerClass = computed<string[]>(() => [
|
|
107
|
+
"text-base",
|
|
108
|
+
props.horizontal ? "gap-16" : "gap-8",
|
|
109
|
+
props.horizontal ? "flex-row" : "flex-col",
|
|
73
110
|
]);
|
|
74
111
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
112
|
+
/**
|
|
113
|
+
* Computes the aria-describedby attribute value for the checkbox group.
|
|
114
|
+
* Combines help text and error message IDs when present.
|
|
115
|
+
*
|
|
116
|
+
* @returns Space-separated string of IDs, or undefined if no descriptions
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* // Only help text
|
|
120
|
+
* "fz-checkbox-group-123-help"
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* // Only error
|
|
124
|
+
* "fz-checkbox-group-123-error"
|
|
125
|
+
*
|
|
126
|
+
* @example
|
|
127
|
+
* // Both help and error
|
|
128
|
+
* "fz-checkbox-group-123-help fz-checkbox-group-123-error"
|
|
129
|
+
*/
|
|
130
|
+
const computedAriaDescribedby = computed<string | undefined>(() => {
|
|
131
|
+
const descriptions: string[] = [];
|
|
132
|
+
|
|
133
|
+
if (slots.help) {
|
|
134
|
+
descriptions.push(`${id}-help`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (props.error && slots.error) {
|
|
138
|
+
descriptions.push(`${id}-error`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return descriptions.length > 0 ? descriptions.join(" ") : undefined;
|
|
142
|
+
});
|
|
78
143
|
</script>
|
|
144
|
+
|
|
145
|
+
<template>
|
|
146
|
+
<!-- Root container for the entire checkbox group -->
|
|
147
|
+
<div :class="[staticContainerClass, computedContainerClass]">
|
|
148
|
+
<!--
|
|
149
|
+
Group label with optional required indicator and help text
|
|
150
|
+
Connected to checkbox group via aria-labelledby
|
|
151
|
+
-->
|
|
152
|
+
<label :id="id + '-label'" :class="[staticLabeldClass, computedLabelClass]">
|
|
153
|
+
<!-- Main label text with required asterisk if applicable -->
|
|
154
|
+
<span>{{ label }}<span v-if="required"> *</span></span>
|
|
155
|
+
|
|
156
|
+
<!-- Optional help text slot for additional context -->
|
|
157
|
+
<p :id="id + '-help'" :class="computedHelpTextClass" v-if="$slots.help">
|
|
158
|
+
<slot name="help" />
|
|
159
|
+
</p>
|
|
160
|
+
</label>
|
|
161
|
+
|
|
162
|
+
<!--
|
|
163
|
+
Checkbox group container with ARIA group role
|
|
164
|
+
- role="group": Identifies this as a group of related form controls
|
|
165
|
+
- aria-labelledby: Links to the label element
|
|
166
|
+
- aria-describedby: Links to help text and/or error message for screen readers
|
|
167
|
+
- aria-required: Indicates if selection is mandatory
|
|
168
|
+
- aria-invalid: Indicates validation state
|
|
169
|
+
-->
|
|
170
|
+
<div
|
|
171
|
+
:class="[staticSlotContainerClass, computedSlotContainerClass]"
|
|
172
|
+
:id="id"
|
|
173
|
+
role="group"
|
|
174
|
+
:aria-labelledby="id + '-label'"
|
|
175
|
+
:aria-describedby="computedAriaDescribedby"
|
|
176
|
+
:aria-required="required ? 'true' : 'false'"
|
|
177
|
+
:aria-invalid="error ? 'true' : 'false'"
|
|
178
|
+
>
|
|
179
|
+
<!--
|
|
180
|
+
Render each checkbox option
|
|
181
|
+
Supports both simple checkboxes and parent-child hierarchies
|
|
182
|
+
Key uses value if available, falls back to label for uniqueness
|
|
183
|
+
-->
|
|
184
|
+
<FzCheckboxGroupOption
|
|
185
|
+
v-for="option in options"
|
|
186
|
+
:key="option.value ? option.value.toString() : option.label"
|
|
187
|
+
v-model="model"
|
|
188
|
+
:disabled="disabled"
|
|
189
|
+
v-bind="option"
|
|
190
|
+
:emphasis="emphasis"
|
|
191
|
+
:error="error"
|
|
192
|
+
/>
|
|
193
|
+
</div>
|
|
194
|
+
<!-- Error message display with accessible ARIA live region -->
|
|
195
|
+
<ErrorAlert v-if="error && $slots.error" :id="id + '-error'">
|
|
196
|
+
<slot name="error" />
|
|
197
|
+
</ErrorAlert>
|
|
198
|
+
</div>
|
|
199
|
+
</template>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// FzCheckbox.
|
|
1
|
+
// FzCheckbox.test.ts
|
|
2
2
|
import { mount } from "@vue/test-utils";
|
|
3
3
|
import { describe, it, expect } from "vitest";
|
|
4
4
|
import FzCheckbox from "../FzCheckbox.vue";
|
|
@@ -11,7 +11,6 @@ describe("FzCheckbox", () => {
|
|
|
11
11
|
props: {
|
|
12
12
|
label: "Test Checkbox",
|
|
13
13
|
value: "test",
|
|
14
|
-
size: "md",
|
|
15
14
|
modelValue: false,
|
|
16
15
|
},
|
|
17
16
|
});
|
|
@@ -20,18 +19,6 @@ describe("FzCheckbox", () => {
|
|
|
20
19
|
expect(wrapper.html()).toContain("Test Checkbox");
|
|
21
20
|
});
|
|
22
21
|
|
|
23
|
-
it("emits an update event when clicked", async () => {
|
|
24
|
-
const wrapper = mount(FzCheckbox, {
|
|
25
|
-
props: {
|
|
26
|
-
label: "Test Checkbox",
|
|
27
|
-
value: "test",
|
|
28
|
-
modelValue: false,
|
|
29
|
-
},
|
|
30
|
-
});
|
|
31
|
-
await wrapper.find("input").trigger("click");
|
|
32
|
-
expect(wrapper.emitted()).toHaveProperty("update:modelValue");
|
|
33
|
-
});
|
|
34
|
-
|
|
35
22
|
it("is checked when v-model is true", async () => {
|
|
36
23
|
const wrapper = mount(FzCheckbox, {
|
|
37
24
|
props: {
|
|
@@ -62,7 +49,6 @@ describe("FzCheckbox", () => {
|
|
|
62
49
|
props: {
|
|
63
50
|
label: "Test Checkbox",
|
|
64
51
|
value: "test",
|
|
65
|
-
size: "md",
|
|
66
52
|
modelValue: false,
|
|
67
53
|
emphasis: true,
|
|
68
54
|
},
|
|
@@ -78,7 +64,6 @@ describe("FzCheckbox", () => {
|
|
|
78
64
|
props: {
|
|
79
65
|
label: "Test Checkbox",
|
|
80
66
|
value: "test",
|
|
81
|
-
size: "md",
|
|
82
67
|
modelValue: false,
|
|
83
68
|
disabled: true,
|
|
84
69
|
},
|
|
@@ -93,7 +78,6 @@ describe("FzCheckbox", () => {
|
|
|
93
78
|
props: {
|
|
94
79
|
label: "Test Checkbox",
|
|
95
80
|
value: "test",
|
|
96
|
-
size: "md",
|
|
97
81
|
modelValue: false,
|
|
98
82
|
disabled: true,
|
|
99
83
|
},
|
|
@@ -111,7 +95,6 @@ describe("FzCheckbox", () => {
|
|
|
111
95
|
props: {
|
|
112
96
|
label: "Test Checkbox",
|
|
113
97
|
value: "test",
|
|
114
|
-
size: "md",
|
|
115
98
|
modelValue: undefined,
|
|
116
99
|
},
|
|
117
100
|
});
|
|
@@ -124,11 +107,100 @@ describe("FzCheckbox", () => {
|
|
|
124
107
|
props: {
|
|
125
108
|
label: "Test Checkbox",
|
|
126
109
|
value: "test",
|
|
127
|
-
size: "md",
|
|
128
110
|
modelValue: null,
|
|
129
111
|
},
|
|
130
112
|
});
|
|
131
113
|
await wrapper.vm.$nextTick();
|
|
132
114
|
expect(wrapper.find("input").element.checked).toBe(false);
|
|
133
115
|
});
|
|
116
|
+
|
|
117
|
+
it("has correct ARIA attributes (not standalone, no error)", async () => {
|
|
118
|
+
const wrapper = mount(FzCheckbox, {
|
|
119
|
+
props: {
|
|
120
|
+
label: "Test Checkbox",
|
|
121
|
+
value: "test",
|
|
122
|
+
modelValue: false,
|
|
123
|
+
required: true,
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
await wrapper.vm.$nextTick();
|
|
127
|
+
const input = wrapper.find("input[type='checkbox']");
|
|
128
|
+
const id = input.attributes("id");
|
|
129
|
+
expect(input.attributes("aria-checked")).toBe("false");
|
|
130
|
+
expect(input.attributes("aria-label")).toBeUndefined();
|
|
131
|
+
expect(input.attributes("aria-required")).toBe("true");
|
|
132
|
+
expect(input.attributes("aria-invalid")).toBe("false");
|
|
133
|
+
expect(input.attributes("aria-describedby")).toBeUndefined();
|
|
134
|
+
expect(input.attributes("aria-labelledby")).toBe(`${id}-label`);
|
|
135
|
+
expect(wrapper.find(`#${id}-label`).exists()).toBe(true);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("has correct ARIA attributes (standalone)", async () => {
|
|
139
|
+
const wrapper = mount(FzCheckbox, {
|
|
140
|
+
props: {
|
|
141
|
+
label: "Test Checkbox",
|
|
142
|
+
value: "test",
|
|
143
|
+
modelValue: false,
|
|
144
|
+
standalone: true,
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
await wrapper.vm.$nextTick();
|
|
148
|
+
const input = wrapper.find("input[type='checkbox']");
|
|
149
|
+
expect(input.attributes("aria-labelledby")).toBeUndefined();
|
|
150
|
+
expect(input.attributes("aria-label")).toBe("Test Checkbox");
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("has correct ARIA attributes when error is present", async () => {
|
|
154
|
+
const wrapper = mount(FzCheckbox, {
|
|
155
|
+
props: {
|
|
156
|
+
label: "Test Checkbox",
|
|
157
|
+
value: "test",
|
|
158
|
+
modelValue: false,
|
|
159
|
+
error: true,
|
|
160
|
+
},
|
|
161
|
+
slots: {
|
|
162
|
+
error: "Test error message",
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
await wrapper.vm.$nextTick();
|
|
166
|
+
const input = wrapper.find("input[type='checkbox']");
|
|
167
|
+
const id = input.attributes("id");
|
|
168
|
+
expect(input.attributes("aria-invalid")).toBe("true");
|
|
169
|
+
expect(input.attributes("aria-describedby")).toBe(`${id}-error`);
|
|
170
|
+
expect(wrapper.find(`#${id}-error`).exists()).toBe(true);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("has correct ARIA attributes when error is present but no error message slot", async () => {
|
|
174
|
+
const wrapper = mount(FzCheckbox, {
|
|
175
|
+
props: {
|
|
176
|
+
label: "Test Checkbox",
|
|
177
|
+
value: "test",
|
|
178
|
+
modelValue: false,
|
|
179
|
+
error: true,
|
|
180
|
+
},
|
|
181
|
+
// No error slot provided
|
|
182
|
+
});
|
|
183
|
+
await wrapper.vm.$nextTick();
|
|
184
|
+
const input = wrapper.find("input[type='checkbox']");
|
|
185
|
+
// Error state must still be indicated via aria-invalid
|
|
186
|
+
expect(input.attributes("aria-invalid")).toBe("true");
|
|
187
|
+
// But aria-describedby should not reference an error element when no error slot is present
|
|
188
|
+
expect(input.attributes("aria-describedby")).toBeUndefined();
|
|
189
|
+
// ErrorAlert should not be rendered when no error slot is provided
|
|
190
|
+
expect(wrapper.find("[role='alert']").exists()).toBe(false);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("has aria-checked='mixed' when indeterminate", async () => {
|
|
194
|
+
const wrapper = mount(FzCheckbox, {
|
|
195
|
+
props: {
|
|
196
|
+
label: "Test Checkbox",
|
|
197
|
+
value: "test",
|
|
198
|
+
modelValue: false,
|
|
199
|
+
indeterminate: true,
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
await wrapper.vm.$nextTick();
|
|
203
|
+
const input = wrapper.find("input[type='checkbox']");
|
|
204
|
+
expect(input.attributes("aria-checked")).toBe("mixed");
|
|
205
|
+
});
|
|
134
206
|
});
|
|
@@ -8,7 +8,6 @@ describe("FzCheckboxGroup", () => {
|
|
|
8
8
|
const wrapper = mount(FzCheckboxGroup, {
|
|
9
9
|
props: {
|
|
10
10
|
label: "Test Checkbox Group",
|
|
11
|
-
size: "md",
|
|
12
11
|
modelValue: [],
|
|
13
12
|
options: [
|
|
14
13
|
{ label: "Option 1", value: "option1" },
|
|
@@ -26,7 +25,6 @@ describe("FzCheckboxGroup", () => {
|
|
|
26
25
|
const wrapper = mount(FzCheckboxGroup, {
|
|
27
26
|
props: {
|
|
28
27
|
label: "Test Checkbox Group",
|
|
29
|
-
size: "md",
|
|
30
28
|
modelValue: [],
|
|
31
29
|
options: [
|
|
32
30
|
{ label: "Option 1", value: "option1" },
|
|
@@ -42,6 +40,9 @@ describe("FzCheckboxGroup", () => {
|
|
|
42
40
|
await wrapper.vm.$nextTick();
|
|
43
41
|
expect(wrapper.html()).toContain("Test error message");
|
|
44
42
|
expect(wrapper.findAllComponents(FzCheckbox).length).toBe(2);
|
|
43
|
+
expect(wrapper.props("error")).toBe(true);
|
|
44
|
+
|
|
45
|
+
// Verify error prop is propagated to individual checkboxes
|
|
45
46
|
expect(wrapper.findAllComponents(FzCheckbox).at(0)!.props("error")).toBe(
|
|
46
47
|
true,
|
|
47
48
|
);
|
|
@@ -54,7 +55,6 @@ describe("FzCheckboxGroup", () => {
|
|
|
54
55
|
const wrapper = mount(FzCheckboxGroup, {
|
|
55
56
|
props: {
|
|
56
57
|
label: "Test Checkbox Group",
|
|
57
|
-
size: "md",
|
|
58
58
|
modelValue: [],
|
|
59
59
|
options: [
|
|
60
60
|
{ label: "Option 1", value: "option1" },
|
|
@@ -78,7 +78,6 @@ describe("FzCheckboxGroup", () => {
|
|
|
78
78
|
const wrapper = mount(FzCheckboxGroup, {
|
|
79
79
|
props: {
|
|
80
80
|
label: "Test Checkbox Group",
|
|
81
|
-
size: "md",
|
|
82
81
|
modelValue: [],
|
|
83
82
|
options: [
|
|
84
83
|
{ label: "Option 1", value: "option1" },
|
|
@@ -97,4 +96,168 @@ describe("FzCheckboxGroup", () => {
|
|
|
97
96
|
true,
|
|
98
97
|
);
|
|
99
98
|
});
|
|
99
|
+
|
|
100
|
+
it("has correct ARIA attributes for accessibility", async () => {
|
|
101
|
+
const wrapper = mount(FzCheckboxGroup, {
|
|
102
|
+
props: {
|
|
103
|
+
label: "Test Checkbox Group",
|
|
104
|
+
modelValue: [],
|
|
105
|
+
options: [
|
|
106
|
+
{ label: "Option 1", value: "option1" },
|
|
107
|
+
{ label: "Option 2", value: "option2" },
|
|
108
|
+
],
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
await wrapper.vm.$nextTick();
|
|
112
|
+
const groupId = wrapper.find("[role='group']").attributes("id");
|
|
113
|
+
const labelId = groupId + "-label";
|
|
114
|
+
expect(wrapper.find("[role='group']").exists()).toBe(true);
|
|
115
|
+
expect(wrapper.find("[role='group']").attributes("aria-labelledby")).toBe(
|
|
116
|
+
labelId,
|
|
117
|
+
);
|
|
118
|
+
expect(wrapper.find(`#${labelId}`).exists()).toBe(true);
|
|
119
|
+
expect(
|
|
120
|
+
wrapper.find("[role='group']").attributes("aria-describedby"),
|
|
121
|
+
).toBeUndefined();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("has aria-describedby when error is present", async () => {
|
|
125
|
+
const wrapper = mount(FzCheckboxGroup, {
|
|
126
|
+
props: {
|
|
127
|
+
label: "Test Checkbox Group",
|
|
128
|
+
modelValue: [],
|
|
129
|
+
options: [
|
|
130
|
+
{ label: "Option 1", value: "option1" },
|
|
131
|
+
{ label: "Option 2", value: "option2" },
|
|
132
|
+
],
|
|
133
|
+
error: true,
|
|
134
|
+
},
|
|
135
|
+
slots: {
|
|
136
|
+
error: "Test error message",
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
await wrapper.vm.$nextTick();
|
|
140
|
+
const groupId = wrapper.find("[role='group']").attributes("id");
|
|
141
|
+
const errorId = groupId + "-error";
|
|
142
|
+
expect(wrapper.find("[role='group']").attributes("aria-describedby")).toBe(
|
|
143
|
+
errorId,
|
|
144
|
+
);
|
|
145
|
+
expect(wrapper.find(`#${errorId}`).exists()).toBe(true);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("has aria-invalid but no aria-describedby when error is present but no error message slot", async () => {
|
|
149
|
+
const wrapper = mount(FzCheckboxGroup, {
|
|
150
|
+
props: {
|
|
151
|
+
label: "Test Checkbox Group",
|
|
152
|
+
modelValue: [],
|
|
153
|
+
options: [
|
|
154
|
+
{ label: "Option 1", value: "option1" },
|
|
155
|
+
{ label: "Option 2", value: "option2" },
|
|
156
|
+
],
|
|
157
|
+
error: true,
|
|
158
|
+
},
|
|
159
|
+
// No error slot provided
|
|
160
|
+
});
|
|
161
|
+
await wrapper.vm.$nextTick();
|
|
162
|
+
const group = wrapper.find("[role='group']");
|
|
163
|
+
// Error state should still be indicated via aria-invalid
|
|
164
|
+
expect(group.attributes("aria-invalid")).toBe("true");
|
|
165
|
+
// But aria-describedby should not reference an error element when no error slot is present
|
|
166
|
+
expect(group.attributes("aria-describedby")).toBeUndefined();
|
|
167
|
+
// ErrorAlert should not be rendered when no error slot is provided
|
|
168
|
+
expect(wrapper.find("[role='alert']").exists()).toBe(false);
|
|
169
|
+
// Verify error prop is still propagated to individual checkboxes
|
|
170
|
+
const checkboxes = wrapper.findAllComponents(FzCheckbox);
|
|
171
|
+
checkboxes.forEach((checkbox) => {
|
|
172
|
+
expect(checkbox.props("error")).toBe(true);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("has aria-describedby when help text is present", async () => {
|
|
177
|
+
const wrapper = mount(FzCheckboxGroup, {
|
|
178
|
+
props: {
|
|
179
|
+
label: "Test Checkbox Group",
|
|
180
|
+
modelValue: [],
|
|
181
|
+
options: [
|
|
182
|
+
{ label: "Option 1", value: "option1" },
|
|
183
|
+
{ label: "Option 2", value: "option2" },
|
|
184
|
+
],
|
|
185
|
+
},
|
|
186
|
+
slots: {
|
|
187
|
+
help: "This is help text",
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
await wrapper.vm.$nextTick();
|
|
191
|
+
const groupId = wrapper.find("[role='group']").attributes("id");
|
|
192
|
+
const helpId = groupId + "-help";
|
|
193
|
+
expect(wrapper.find("[role='group']").attributes("aria-describedby")).toBe(
|
|
194
|
+
helpId,
|
|
195
|
+
);
|
|
196
|
+
expect(wrapper.find(`#${helpId}`).exists()).toBe(true);
|
|
197
|
+
expect(wrapper.find(`#${helpId}`).text()).toBe("This is help text");
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("has aria-describedby with both help and error", async () => {
|
|
201
|
+
const wrapper = mount(FzCheckboxGroup, {
|
|
202
|
+
props: {
|
|
203
|
+
label: "Test Checkbox Group",
|
|
204
|
+
modelValue: [],
|
|
205
|
+
options: [
|
|
206
|
+
{ label: "Option 1", value: "option1" },
|
|
207
|
+
{ label: "Option 2", value: "option2" },
|
|
208
|
+
],
|
|
209
|
+
error: true,
|
|
210
|
+
},
|
|
211
|
+
slots: {
|
|
212
|
+
help: "This is help text",
|
|
213
|
+
error: "This is an error",
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
await wrapper.vm.$nextTick();
|
|
217
|
+
const groupId = wrapper.find("[role='group']").attributes("id");
|
|
218
|
+
const helpId = groupId + "-help";
|
|
219
|
+
const errorId = groupId + "-error";
|
|
220
|
+
const describedby = wrapper
|
|
221
|
+
.find("[role='group']")
|
|
222
|
+
.attributes("aria-describedby");
|
|
223
|
+
|
|
224
|
+
// Should contain both IDs separated by space
|
|
225
|
+
expect(describedby).toContain(helpId);
|
|
226
|
+
expect(describedby).toContain(errorId);
|
|
227
|
+
expect(describedby).toBe(`${helpId} ${errorId}`);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("propagates error prop to child checkboxes in hierarchical structure", async () => {
|
|
231
|
+
const wrapper = mount(FzCheckboxGroup, {
|
|
232
|
+
props: {
|
|
233
|
+
label: "Test Checkbox Group",
|
|
234
|
+
modelValue: [],
|
|
235
|
+
options: [
|
|
236
|
+
{
|
|
237
|
+
label: "Parent Option",
|
|
238
|
+
value: "parent",
|
|
239
|
+
children: [
|
|
240
|
+
{ label: "Child 1", value: "child1" },
|
|
241
|
+
{ label: "Child 2", value: "child2" },
|
|
242
|
+
],
|
|
243
|
+
},
|
|
244
|
+
],
|
|
245
|
+
error: true,
|
|
246
|
+
},
|
|
247
|
+
slots: {
|
|
248
|
+
error: "Test error message",
|
|
249
|
+
},
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
await wrapper.vm.$nextTick();
|
|
253
|
+
const allCheckboxes = wrapper.findAllComponents(FzCheckbox);
|
|
254
|
+
|
|
255
|
+
// Should have parent + 2 children = 3 checkboxes
|
|
256
|
+
expect(allCheckboxes.length).toBe(3);
|
|
257
|
+
|
|
258
|
+
// All checkboxes should have error prop set to true
|
|
259
|
+
allCheckboxes.forEach((checkbox) => {
|
|
260
|
+
expect(checkbox.props("error")).toBe(true);
|
|
261
|
+
});
|
|
262
|
+
});
|
|
100
263
|
});
|
package/src/common.ts
CHANGED
|
@@ -1,4 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utilities for the Fiscozen Checkbox component library.
|
|
3
|
+
*
|
|
4
|
+
* @module @fiscozen/checkbox/common
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Maps checkbox size variants to corresponding Tailwind CSS text size classes.
|
|
9
|
+
*
|
|
10
|
+
* Used to maintain consistent typography across checkbox labels and helper text.
|
|
11
|
+
* Applied to both FzCheckbox and FzCheckboxGroup components.
|
|
12
|
+
*
|
|
13
|
+
* @constant
|
|
14
|
+
* @type {Record<"sm" | "md", string>}
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* const labelClass = mapSizeToClasses[props.size]; // "text-sm" or "text-base"
|
|
18
|
+
*/
|
|
1
19
|
export const mapSizeToClasses = {
|
|
20
|
+
/** Small size: 14px font size (0.875rem) */
|
|
2
21
|
sm: "text-sm",
|
|
22
|
+
/** Medium size: 16px font size (1rem) - default */
|
|
3
23
|
md: "text-base",
|
|
4
24
|
};
|