@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.
Files changed (44) hide show
  1. package/.eslintrc.cjs +19 -0
  2. package/.prettierrc.js +6 -0
  3. package/README.md +514 -0
  4. package/coverage/clover.xml +789 -215
  5. package/coverage/coverage-final.json +6 -6
  6. package/coverage/index.html +32 -32
  7. package/coverage/src/FzCheckbox.vue.html +1099 -151
  8. package/coverage/src/FzCheckboxGroup.vue.html +444 -81
  9. package/coverage/src/common.ts.html +79 -19
  10. package/coverage/src/components/ErrorAlert.vue.html +268 -0
  11. package/coverage/src/components/FzCheckboxGroupOption.vue.html +343 -70
  12. package/coverage/src/components/index.html +22 -22
  13. package/coverage/src/index.html +45 -45
  14. package/coverage/src/utils.ts.html +265 -0
  15. package/package.json +13 -11
  16. package/src/FzCheckbox.vue +411 -95
  17. package/src/FzCheckboxGroup.vue +179 -58
  18. package/src/__test__/FzCheckbox.test.ts +91 -19
  19. package/src/__test__/FzCheckboxGroup.test.ts +167 -4
  20. package/src/common.ts +20 -0
  21. package/src/components/ErrorAlert.vue +61 -0
  22. package/src/components/FzCheckboxGroupOption.vue +137 -46
  23. package/src/index.ts +1 -0
  24. package/src/types.ts +166 -23
  25. package/src/utils.ts +60 -0
  26. package/tsconfig.json +1 -1
  27. package/coverage/.tmp/coverage-0.json +0 -1
  28. package/coverage/.tmp/coverage-1.json +0 -1
  29. package/coverage/src/components/FzCheckboxErrorText.vue.html +0 -145
  30. package/coverage/src/types.ts.html +0 -310
  31. package/dist/checkbox.js +0 -301
  32. package/dist/checkbox.umd.cjs +0 -1
  33. package/dist/index.d.ts +0 -1
  34. package/dist/src/FzCheckbox.vue.d.ts +0 -95
  35. package/dist/src/FzCheckboxGroup.vue.d.ts +0 -70
  36. package/dist/src/__test__/FzCheckbox.test.d.ts +0 -1
  37. package/dist/src/__test__/FzCheckboxGroup.test.d.ts +0 -1
  38. package/dist/src/common.d.ts +0 -4
  39. package/dist/src/components/FzCheckboxErrorText.vue.d.ts +0 -24
  40. package/dist/src/components/FzCheckboxGroupOption.vue.d.ts +0 -82
  41. package/dist/src/index.d.ts +0 -3
  42. package/dist/src/types.d.ts +0 -76
  43. package/dist/style.css +0 -1
  44. package/src/components/FzCheckboxErrorText.vue +0 -20
@@ -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
- import { computed } from "vue";
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 FzCheckboxErrorText from "./components/FzCheckboxErrorText.vue";
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 id = `fz-checkbox-group-${generateRandomId()}`;
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
- const computedHelpTextClass = computed(() => [
42
- props.size === "sm" ? "text-xs" : "text-sm",
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
- const model = defineModel<string[]>({
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
- const staticLabeldClass = "flex flex-col";
52
- const staticContainerClass = "flex flex-col";
53
- const staticSlotContainerClass = "flex flex-col";
83
+ /** Base layout for the label element */
84
+ const staticLabeldClass: string = "flex flex-col";
54
85
 
55
- const computedLabelClass = computed(() => [
56
- mapSizeToClasses[props.size],
57
- props.size === "sm" ? "gap-4" : "",
58
- props.size === "md" ? "gap-6" : "",
59
- props.disabled ? "text-grey-400" : "",
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
- const computedContainerClass = computed(() => [
64
- mapSizeToClasses[props.size],
65
- props.size === "sm" ? "gap-10" : "",
66
- props.size === "md" ? "gap-12" : "",
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
- const computedSlotContainerClass = computed(() => [
70
- mapSizeToClasses[props.size],
71
- props.size === "sm" ? "gap-6" : "",
72
- props.size === "md" ? "gap-8" : "",
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
- function generateRandomId() {
76
- return Math.random().toString(36).slice(2, 9);
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.spec.ts
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
  };