@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
@@ -23,30 +23,30 @@
23
23
  <div class='clearfix'>
24
24
 
25
25
  <div class='fl pad1y space-right2'>
26
- <span class="strong">58.16% </span>
26
+ <span class="strong">79.89% </span>
27
27
  <span class="quiet">Statements</span>
28
- <span class='fraction'>57/98</span>
28
+ <span class='fraction'>151/189</span>
29
29
  </div>
30
30
 
31
31
 
32
32
  <div class='fl pad1y space-right2'>
33
- <span class="strong">33.33% </span>
33
+ <span class="strong">86.66% </span>
34
34
  <span class="quiet">Branches</span>
35
- <span class='fraction'>1/3</span>
35
+ <span class='fraction'>13/15</span>
36
36
  </div>
37
37
 
38
38
 
39
39
  <div class='fl pad1y space-right2'>
40
40
  <span class="strong">0% </span>
41
41
  <span class="quiet">Functions</span>
42
- <span class='fraction'>0/3</span>
42
+ <span class='fraction'>0/4</span>
43
43
  </div>
44
44
 
45
45
 
46
46
  <div class='fl pad1y space-right2'>
47
- <span class="strong">58.16% </span>
47
+ <span class="strong">79.89% </span>
48
48
  <span class="quiet">Lines</span>
49
- <span class='fraction'>57/98</span>
49
+ <span class='fraction'>151/189</span>
50
50
  </div>
51
51
 
52
52
 
@@ -57,7 +57,7 @@
57
57
  <template id="filterTemplate">
58
58
  <div class="quiet">
59
59
  Filter:
60
- <input oninput="onInput()" type="search" id="fileSearch">
60
+ <input type="search" id="fileSearch">
61
61
  </div>
62
62
  </template>
63
63
  </div>
@@ -161,7 +161,98 @@
161
161
  <a name='L96'></a><a href='#L96'>96</a>
162
162
  <a name='L97'></a><a href='#L97'>97</a>
163
163
  <a name='L98'></a><a href='#L98'>98</a>
164
- <a name='L99'></a><a href='#L99'>99</a></td><td class="line-coverage quiet"><span class="cline-any cline-yes">1x</span>
164
+ <a name='L99'></a><a href='#L99'>99</a>
165
+ <a name='L100'></a><a href='#L100'>100</a>
166
+ <a name='L101'></a><a href='#L101'>101</a>
167
+ <a name='L102'></a><a href='#L102'>102</a>
168
+ <a name='L103'></a><a href='#L103'>103</a>
169
+ <a name='L104'></a><a href='#L104'>104</a>
170
+ <a name='L105'></a><a href='#L105'>105</a>
171
+ <a name='L106'></a><a href='#L106'>106</a>
172
+ <a name='L107'></a><a href='#L107'>107</a>
173
+ <a name='L108'></a><a href='#L108'>108</a>
174
+ <a name='L109'></a><a href='#L109'>109</a>
175
+ <a name='L110'></a><a href='#L110'>110</a>
176
+ <a name='L111'></a><a href='#L111'>111</a>
177
+ <a name='L112'></a><a href='#L112'>112</a>
178
+ <a name='L113'></a><a href='#L113'>113</a>
179
+ <a name='L114'></a><a href='#L114'>114</a>
180
+ <a name='L115'></a><a href='#L115'>115</a>
181
+ <a name='L116'></a><a href='#L116'>116</a>
182
+ <a name='L117'></a><a href='#L117'>117</a>
183
+ <a name='L118'></a><a href='#L118'>118</a>
184
+ <a name='L119'></a><a href='#L119'>119</a>
185
+ <a name='L120'></a><a href='#L120'>120</a>
186
+ <a name='L121'></a><a href='#L121'>121</a>
187
+ <a name='L122'></a><a href='#L122'>122</a>
188
+ <a name='L123'></a><a href='#L123'>123</a>
189
+ <a name='L124'></a><a href='#L124'>124</a>
190
+ <a name='L125'></a><a href='#L125'>125</a>
191
+ <a name='L126'></a><a href='#L126'>126</a>
192
+ <a name='L127'></a><a href='#L127'>127</a>
193
+ <a name='L128'></a><a href='#L128'>128</a>
194
+ <a name='L129'></a><a href='#L129'>129</a>
195
+ <a name='L130'></a><a href='#L130'>130</a>
196
+ <a name='L131'></a><a href='#L131'>131</a>
197
+ <a name='L132'></a><a href='#L132'>132</a>
198
+ <a name='L133'></a><a href='#L133'>133</a>
199
+ <a name='L134'></a><a href='#L134'>134</a>
200
+ <a name='L135'></a><a href='#L135'>135</a>
201
+ <a name='L136'></a><a href='#L136'>136</a>
202
+ <a name='L137'></a><a href='#L137'>137</a>
203
+ <a name='L138'></a><a href='#L138'>138</a>
204
+ <a name='L139'></a><a href='#L139'>139</a>
205
+ <a name='L140'></a><a href='#L140'>140</a>
206
+ <a name='L141'></a><a href='#L141'>141</a>
207
+ <a name='L142'></a><a href='#L142'>142</a>
208
+ <a name='L143'></a><a href='#L143'>143</a>
209
+ <a name='L144'></a><a href='#L144'>144</a>
210
+ <a name='L145'></a><a href='#L145'>145</a>
211
+ <a name='L146'></a><a href='#L146'>146</a>
212
+ <a name='L147'></a><a href='#L147'>147</a>
213
+ <a name='L148'></a><a href='#L148'>148</a>
214
+ <a name='L149'></a><a href='#L149'>149</a>
215
+ <a name='L150'></a><a href='#L150'>150</a>
216
+ <a name='L151'></a><a href='#L151'>151</a>
217
+ <a name='L152'></a><a href='#L152'>152</a>
218
+ <a name='L153'></a><a href='#L153'>153</a>
219
+ <a name='L154'></a><a href='#L154'>154</a>
220
+ <a name='L155'></a><a href='#L155'>155</a>
221
+ <a name='L156'></a><a href='#L156'>156</a>
222
+ <a name='L157'></a><a href='#L157'>157</a>
223
+ <a name='L158'></a><a href='#L158'>158</a>
224
+ <a name='L159'></a><a href='#L159'>159</a>
225
+ <a name='L160'></a><a href='#L160'>160</a>
226
+ <a name='L161'></a><a href='#L161'>161</a>
227
+ <a name='L162'></a><a href='#L162'>162</a>
228
+ <a name='L163'></a><a href='#L163'>163</a>
229
+ <a name='L164'></a><a href='#L164'>164</a>
230
+ <a name='L165'></a><a href='#L165'>165</a>
231
+ <a name='L166'></a><a href='#L166'>166</a>
232
+ <a name='L167'></a><a href='#L167'>167</a>
233
+ <a name='L168'></a><a href='#L168'>168</a>
234
+ <a name='L169'></a><a href='#L169'>169</a>
235
+ <a name='L170'></a><a href='#L170'>170</a>
236
+ <a name='L171'></a><a href='#L171'>171</a>
237
+ <a name='L172'></a><a href='#L172'>172</a>
238
+ <a name='L173'></a><a href='#L173'>173</a>
239
+ <a name='L174'></a><a href='#L174'>174</a>
240
+ <a name='L175'></a><a href='#L175'>175</a>
241
+ <a name='L176'></a><a href='#L176'>176</a>
242
+ <a name='L177'></a><a href='#L177'>177</a>
243
+ <a name='L178'></a><a href='#L178'>178</a>
244
+ <a name='L179'></a><a href='#L179'>179</a>
245
+ <a name='L180'></a><a href='#L180'>180</a>
246
+ <a name='L181'></a><a href='#L181'>181</a>
247
+ <a name='L182'></a><a href='#L182'>182</a>
248
+ <a name='L183'></a><a href='#L183'>183</a>
249
+ <a name='L184'></a><a href='#L184'>184</a>
250
+ <a name='L185'></a><a href='#L185'>185</a>
251
+ <a name='L186'></a><a href='#L186'>186</a>
252
+ <a name='L187'></a><a href='#L187'>187</a>
253
+ <a name='L188'></a><a href='#L188'>188</a>
254
+ <a name='L189'></a><a href='#L189'>189</a>
255
+ <a name='L190'></a><a href='#L190'>190</a></td><td class="line-coverage quiet"><span class="cline-any cline-yes">18x</span>
165
256
  <span class="cline-any cline-yes">1x</span>
166
257
  <span class="cline-any cline-yes">1x</span>
167
258
  <span class="cline-any cline-yes">1x</span>
@@ -209,19 +300,53 @@
209
300
  <span class="cline-any cline-yes">1x</span>
210
301
  <span class="cline-any cline-yes">1x</span>
211
302
  <span class="cline-any cline-yes">1x</span>
212
- <span class="cline-any cline-no">&nbsp;</span>
213
- <span class="cline-any cline-no">&nbsp;</span>
214
303
  <span class="cline-any cline-yes">1x</span>
215
304
  <span class="cline-any cline-yes">1x</span>
216
305
  <span class="cline-any cline-yes">1x</span>
217
- <span class="cline-any cline-yes">8x</span>
218
- <span class="cline-any cline-no">&nbsp;</span>
219
- <span class="cline-any cline-no">&nbsp;</span>
306
+ <span class="cline-any cline-yes">1x</span>
307
+ <span class="cline-any cline-yes">1x</span>
308
+ <span class="cline-any cline-yes">1x</span>
309
+ <span class="cline-any cline-yes">1x</span>
310
+ <span class="cline-any cline-yes">1x</span>
311
+ <span class="cline-any cline-yes">1x</span>
312
+ <span class="cline-any cline-yes">1x</span>
313
+ <span class="cline-any cline-yes">1x</span>
314
+ <span class="cline-any cline-yes">1x</span>
315
+ <span class="cline-any cline-yes">1x</span>
316
+ <span class="cline-any cline-yes">1x</span>
317
+ <span class="cline-any cline-yes">1x</span>
318
+ <span class="cline-any cline-yes">1x</span>
319
+ <span class="cline-any cline-yes">1x</span>
320
+ <span class="cline-any cline-yes">1x</span>
321
+ <span class="cline-any cline-yes">1x</span>
322
+ <span class="cline-any cline-yes">1x</span>
323
+ <span class="cline-any cline-yes">1x</span>
324
+ <span class="cline-any cline-yes">1x</span>
325
+ <span class="cline-any cline-yes">1x</span>
326
+ <span class="cline-any cline-yes">1x</span>
327
+ <span class="cline-any cline-yes">1x</span>
328
+ <span class="cline-any cline-yes">1x</span>
329
+ <span class="cline-any cline-yes">19x</span>
330
+ <span class="cline-any cline-yes">1x</span>
331
+ <span class="cline-any cline-yes">1x</span>
332
+ <span class="cline-any cline-yes">2x</span>
333
+ <span class="cline-any cline-yes">1x</span>
334
+ <span class="cline-any cline-yes">19x</span>
335
+ <span class="cline-any cline-yes">1x</span>
336
+ <span class="cline-any cline-yes">1x</span>
337
+ <span class="cline-any cline-yes">1x</span>
338
+ <span class="cline-any cline-yes">1x</span>
339
+ <span class="cline-any cline-yes">1x</span>
340
+ <span class="cline-any cline-yes">1x</span>
341
+ <span class="cline-any cline-yes">1x</span>
342
+ <span class="cline-any cline-yes">1x</span>
343
+ <span class="cline-any cline-yes">1x</span>
344
+ <span class="cline-any cline-yes">1x</span>
345
+ <span class="cline-any cline-yes">1x</span>
346
+ <span class="cline-any cline-yes">1x</span>
220
347
  <span class="cline-any cline-no">&nbsp;</span>
221
348
  <span class="cline-any cline-no">&nbsp;</span>
222
349
  <span class="cline-any cline-no">&nbsp;</span>
223
- <span class="cline-any cline-yes">1x</span>
224
- <span class="cline-any cline-yes">1x</span>
225
350
  <span class="cline-any cline-no">&nbsp;</span>
226
351
  <span class="cline-any cline-no">&nbsp;</span>
227
352
  <span class="cline-any cline-no">&nbsp;</span>
@@ -238,6 +363,17 @@
238
363
  <span class="cline-any cline-no">&nbsp;</span>
239
364
  <span class="cline-any cline-no">&nbsp;</span>
240
365
  <span class="cline-any cline-yes">1x</span>
366
+ <span class="cline-any cline-yes">1x</span>
367
+ <span class="cline-any cline-yes">1x</span>
368
+ <span class="cline-any cline-yes">1x</span>
369
+ <span class="cline-any cline-yes">1x</span>
370
+ <span class="cline-any cline-yes">1x</span>
371
+ <span class="cline-any cline-yes">1x</span>
372
+ <span class="cline-any cline-yes">1x</span>
373
+ <span class="cline-any cline-yes">1x</span>
374
+ <span class="cline-any cline-yes">1x</span>
375
+ <span class="cline-any cline-yes">1x</span>
376
+ <span class="cline-any cline-no">&nbsp;</span>
241
377
  <span class="cline-any cline-no">&nbsp;</span>
242
378
  <span class="cline-any cline-no">&nbsp;</span>
243
379
  <span class="cline-any cline-no">&nbsp;</span>
@@ -259,94 +395,185 @@
259
395
  <span class="cline-any cline-no">&nbsp;</span>
260
396
  <span class="cline-any cline-yes">1x</span>
261
397
  <span class="cline-any cline-yes">1x</span>
262
- <span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">&lt;template&gt;
263
- &lt;FzCheckbox
264
- v-model="<span class="fstat-no" title="function not covered" >model"</span>
265
- :value="props.value"
266
- :label="props.label"
267
- :disabled="disabled"
268
- :emphasis="emphasis"
269
- :error="error"
270
- :size="size"
271
- :indeterminate="isIndeterminate"
272
- @change="onCheckboxParentChange"
273
- &gt;
274
- &lt;template #children v-if="<span class="branch-0 cbranch-no" title="branch not covered" >children?.length"&gt;</span>
275
- &lt;div :class="[staticChildContainerClass, computedChildContainerClasses]"&gt;
276
- &lt;FzCheckbox
277
- v-for="child in children"
278
- :key="child.value ? child.value.toString() : child.label"
279
- v-model="model"
280
- :disabled="disabled"
281
- v-bind="child"
282
- :emphasis="emphasis"
283
- :error="error"
284
- :size="size"
285
- @change="handleCheckboxParentChange"
286
- /&gt;
287
- &lt;/div&gt;
288
- &lt;/template&gt;
289
- &lt;/FzCheckbox&gt;
290
- &lt;/template&gt;
291
- &lt;script setup lang="ts"&gt;
398
+ <span class="cline-any cline-yes">1x</span>
399
+ <span class="cline-any cline-yes">1x</span>
400
+ <span class="cline-any cline-yes">1x</span>
401
+ <span class="cline-any cline-yes">1x</span>
402
+ <span class="cline-any cline-yes">1x</span>
403
+ <span class="cline-any cline-yes">1x</span>
404
+ <span class="cline-any cline-yes">1x</span>
405
+ <span class="cline-any cline-yes">1x</span>
406
+ <span class="cline-any cline-yes">1x</span>
407
+ <span class="cline-any cline-yes">1x</span>
408
+ <span class="cline-any cline-yes">1x</span>
409
+ <span class="cline-any cline-yes">1x</span>
410
+ <span class="cline-any cline-yes">1x</span>
411
+ <span class="cline-any cline-yes">1x</span>
412
+ <span class="cline-any cline-yes">1x</span>
413
+ <span class="cline-any cline-yes">1x</span>
414
+ <span class="cline-any cline-yes">1x</span>
415
+ <span class="cline-any cline-yes">1x</span>
416
+ <span class="cline-any cline-yes">1x</span>
417
+ <span class="cline-any cline-yes">1x</span>
418
+ <span class="cline-any cline-yes">1x</span>
419
+ <span class="cline-any cline-yes">1x</span>
420
+ <span class="cline-any cline-yes">1x</span>
421
+ <span class="cline-any cline-yes">1x</span>
422
+ <span class="cline-any cline-yes">1x</span>
423
+ <span class="cline-any cline-yes">1x</span>
424
+ <span class="cline-any cline-yes">1x</span>
425
+ <span class="cline-any cline-yes">1x</span>
426
+ <span class="cline-any cline-yes">1x</span>
427
+ <span class="cline-any cline-yes">1x</span>
428
+ <span class="cline-any cline-yes">1x</span>
429
+ <span class="cline-any cline-yes">1x</span>
430
+ <span class="cline-any cline-yes">1x</span>
431
+ <span class="cline-any cline-yes">1x</span>
432
+ <span class="cline-any cline-yes">1x</span>
433
+ <span class="cline-any cline-yes">1x</span>
434
+ <span class="cline-any cline-yes">1x</span>
435
+ <span class="cline-any cline-yes">1x</span>
436
+ <span class="cline-any cline-yes">1x</span>
437
+ <span class="cline-any cline-yes">1x</span>
438
+ <span class="cline-any cline-yes">1x</span>
439
+ <span class="cline-any cline-yes">1x</span>
440
+ <span class="cline-any cline-yes">1x</span>
441
+ <span class="cline-any cline-yes">1x</span>
442
+ <span class="cline-any cline-yes">1x</span>
443
+ <span class="cline-any cline-yes">1x</span>
444
+ <span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">&lt;script setup lang="ts"&gt;
445
+ /**
446
+ * FzCheckboxGroupOption Component
447
+ *
448
+ * Renders a single checkbox option within a checkbox group, with support for
449
+ * hierarchical parent-child relationships. When children are present, the parent
450
+ * checkbox displays an indeterminate state when partially selected.
451
+ *
452
+ * Key features:
453
+ * - Automatic indeterminate state management for parent checkboxes
454
+ * - Bi-directional sync: parent controls children, children update parent
455
+ * - ARIA relationships via aria-owns for accessibility
456
+ * - Deterministic IDs for proper ARIA associations
457
+ *
458
+ * @component
459
+ * @internal Used internally by FzCheckboxGroup, not intended for direct use
460
+ */
292
461
  import { computed } from "vue";
293
462
  import FzCheckbox from "../FzCheckbox.vue";
294
463
  import { ParentCheckbox } from "../types";
464
+ import { generateCheckboxId } from "../utils";
295
465
  &nbsp;
296
- FzCheckbox.compatConfig = {
297
- MODE: 3,
298
- };
466
+ const props = defineProps&lt;ParentCheckbox&gt;();
299
467
  &nbsp;
300
- const props = defineProps&lt;ParentCheckbox &amp; { size: "sm" | "md" }&gt;();
301
- const currentValue = computed(() =&gt; props.value ?? props.label);
468
+ /** The actual value used for this checkbox (falls back to label if no value provided) */
469
+ const currentValue = computed&lt;string | number | boolean&gt;(
470
+ () =&gt; props.value ?? props.label,
471
+ );
472
+ &nbsp;
473
+ /**
474
+ * Two-way binding for selected values.
475
+ * Shared with parent component and all sibling checkboxes.
476
+ */
302
477
  const model = defineModel&lt;(string | number | boolean)[]&gt;({
303
478
  required: true,
304
479
  default: [],
305
480
  });
306
481
  &nbsp;
307
- const staticChildContainerClass = "flex flex-col justify-center pl-24";
482
+ /**
483
+ * Unique identifier for this parent checkbox.
484
+ * Used as a prefix for child checkbox IDs to establish ARIA relationships.
485
+ */
486
+ const parentId: string = generateCheckboxId();
308
487
  &nbsp;
309
- const computedChildContainerClasses = computed(() =&gt; [
310
- <span class="cstat-no" title="statement not covered" > props.size === "sm" ? "gap-6 mt-6" : "",</span>
311
- <span class="cstat-no" title="statement not covered" > props.size === "md" ? "gap-8 mt-8" : "",</span>
312
- ]);
488
+ /**
489
+ * Computes space-separated list of child checkbox IDs.
490
+ * Used for aria-owns attribute to establish semantic parent-child relationship.
491
+ *
492
+ * @returns Space-separated string of child IDs, or undefined if no children
493
+ */
494
+ const childrenIds = computed&lt;string | undefined&gt;(() =&gt;
495
+ props.children?.map((child, index) =&gt; `${parentId}-child-${index}`).join(" "),
496
+ );
313
497
  &nbsp;
314
- const isIndeterminate = computed(() =&gt; {
315
- if (!props.children) return <span class="branch-0 cbranch-no" title="branch not covered" >false;</span>
316
- <span class="cstat-no" title="statement not covered" ></span>
317
- <span class="cstat-no" title="statement not covered" > const numChecked = props.children.filter((child) =&gt;</span>
318
- <span class="cstat-no" title="statement not covered" > model.value.includes(child.value ?? child.label),</span>
319
- <span class="cstat-no" title="statement not covered" > ).length;</span>
320
- <span class="cstat-no" title="statement not covered" > return numChecked &gt; 0 &amp;&amp; numChecked &lt; props.children.length;</span>
498
+ /** Base layout classes for the children container (indented and vertical) */
499
+ const staticChildContainerClass: string =
500
+ "flex flex-col justify-center gap-8 pl-24";
501
+ &nbsp;
502
+ /** Size-specific spacing for child checkboxes */
503
+ const computedChildContainerClasses = computed&lt;string[]&gt;(() =&gt; []);
504
+ &nbsp;
505
+ /**
506
+ * Determines if parent checkbox should be in indeterminate state.
507
+ * Indeterminate means some, but not all, children are checked.
508
+ *
509
+ * States:
510
+ * - No children: false (not indeterminate)
511
+ * - No children checked: false (unchecked)
512
+ * - All children checked: false (fully checked)
513
+ * - Some children checked: true (indeterminate/partial)
514
+ *
515
+ * @returns true if parent should display indeterminate state
516
+ */
517
+ const isIndeterminate = computed&lt;boolean&gt;(() =&gt; {
518
+ if (!props.children) return false;
519
+ &nbsp;
520
+ const numChecked = props.children.filter((child) =&gt;
521
+ model.value.includes(child.<span class="branch-0 cbranch-no" title="branch not covered" >value ?? child.label)</span>,
522
+ ).length;
523
+ return numChecked &gt; <span class="branch-0 cbranch-no" title="branch not covered" >0 &amp;&amp; numChecked &lt; props.children.length;</span>
321
524
  });
322
525
  &nbsp;
526
+ /**
527
+ * Handles child checkbox changes to update parent state.
528
+ * Called when a child checkbox is toggled.
529
+ *
530
+ * Logic:
531
+ * - If ALL children are now checked → add parent to selection
532
+ * - If ANY child is unchecked → remove parent from selection
533
+ *
534
+ * Note: Uses concat() instead of push() to ensure Vue reactivity triggers.
535
+ */
323
536
  <span class="cstat-no" title="statement not covered" ><span class="fstat-no" title="function not covered" >function handleCheckboxParentChange() {</span></span>
324
537
  <span class="cstat-no" title="statement not covered" > if (!props.children) return;</span>
538
+ <span class="cstat-no" title="statement not covered" ></span>
325
539
  <span class="cstat-no" title="statement not covered" > const numChecked = props.children.filter((child) =&gt;</span>
326
540
  <span class="cstat-no" title="statement not covered" > model.value.includes(child.value ?? child.label),</span>
327
541
  <span class="cstat-no" title="statement not covered" > ).length;</span>
328
542
  <span class="cstat-no" title="statement not covered" ></span>
329
543
  <span class="cstat-no" title="statement not covered" > if (numChecked === props.children.length) {</span>
330
- <span class="cstat-no" title="statement not covered" > // push parent value to model (using concat to force reactivity, push seems to not work)</span>
331
- <span class="cstat-no" title="statement not covered" > model.value = model.value.concat(currentValue.value);</span>
544
+ <span class="cstat-no" title="statement not covered" > // All children checked: add parent value to model if not already present</span>
545
+ <span class="cstat-no" title="statement not covered" > if (!model.value.includes(currentValue.value)) {</span>
546
+ <span class="cstat-no" title="statement not covered" > model.value = model.value.concat(currentValue.value);</span>
547
+ <span class="cstat-no" title="statement not covered" > }</span>
332
548
  <span class="cstat-no" title="statement not covered" > } else {</span>
333
- <span class="cstat-no" title="statement not covered" > // remove parent value from model if it exists</span>
549
+ <span class="cstat-no" title="statement not covered" > // Not all children checked: remove parent value if present</span>
334
550
  <span class="cstat-no" title="statement not covered" > if (model.value.includes(currentValue.value))</span>
335
551
  <span class="cstat-no" title="statement not covered" > model.value = model.value.filter((value) =&gt; value !== currentValue.value);</span>
336
552
  <span class="cstat-no" title="statement not covered" > }</span>
337
553
  <span class="cstat-no" title="statement not covered" >}</span>
338
554
  &nbsp;
555
+ /**
556
+ * Handles parent checkbox changes to cascade to children.
557
+ * Called when the parent checkbox is clicked.
558
+ *
559
+ * Cascade behavior:
560
+ * - Parent checked → check all children
561
+ * - Parent unchecked → uncheck all children
562
+ *
563
+ * Only modifies children that need changes (doesn't re-add already selected children).
564
+ */
339
565
  <span class="cstat-no" title="statement not covered" ><span class="fstat-no" title="function not covered" >function onCheckboxParentChange() {</span></span>
340
566
  <span class="cstat-no" title="statement not covered" > if (!props.children) return;</span>
567
+ <span class="cstat-no" title="statement not covered" ></span>
341
568
  <span class="cstat-no" title="statement not covered" > if (model.value.includes(currentValue.value)) {</span>
342
- <span class="cstat-no" title="statement not covered" > // push all children values to model that are not already in model</span>
569
+ <span class="cstat-no" title="statement not covered" > // Parent is checked: add all unchecked children to model</span>
343
570
  <span class="cstat-no" title="statement not covered" > model.value = model.value.concat(</span>
344
571
  <span class="cstat-no" title="statement not covered" > props.children</span>
345
572
  <span class="cstat-no" title="statement not covered" > ?.map((child) =&gt; child.value ?? child.label)</span>
346
573
  <span class="cstat-no" title="statement not covered" > .filter((value) =&gt; !model.value.includes(value)),</span>
347
574
  <span class="cstat-no" title="statement not covered" > );</span>
348
575
  <span class="cstat-no" title="statement not covered" > } else {</span>
349
- <span class="cstat-no" title="statement not covered" > // remove all children values from model</span>
576
+ <span class="cstat-no" title="statement not covered" > // Parent is unchecked: remove all children from model</span>
350
577
  <span class="cstat-no" title="statement not covered" > model.value = model.value.filter(</span>
351
578
  <span class="cstat-no" title="statement not covered" > (value) =&gt;</span>
352
579
  <span class="cstat-no" title="statement not covered" > !props.children</span>
@@ -356,7 +583,53 @@ const isIndeterminate = computed(() =&gt; {
356
583
  <span class="cstat-no" title="statement not covered" > }</span>
357
584
  <span class="cstat-no" title="statement not covered" >}</span>
358
585
  &lt;/script&gt;
359
- &lt;style scoped&gt;&lt;/style&gt;
586
+ &nbsp;
587
+ &lt;template&gt;
588
+ &lt;!--
589
+ Parent checkbox
590
+ - Displays indeterminate state when children are partially selected
591
+ - aria-owns links to child checkboxes for screen reader navigation
592
+ - Change event cascades selection to all children
593
+ --&gt;
594
+ &lt;FzCheckbox
595
+ v-model="<span class="fstat-no" title="function not covered" >model"</span>
596
+ :value="props.value"
597
+ :label="props.label"
598
+ :disabled="disabled"
599
+ :emphasis="emphasis"
600
+ :error="error"
601
+ :indeterminate="isIndeterminate"
602
+ :aria-owns="children?.length ? childrenIds : undefined"
603
+ @change="onCheckboxParentChange"
604
+ &gt;
605
+ &lt;!--
606
+ Children slot: renders nested child checkboxes
607
+ Only rendered if children array has items
608
+ --&gt;
609
+ &lt;template #children v-if="children?.length"&gt;
610
+ &lt;!-- Indented container for visual hierarchy --&gt;
611
+ &lt;div :class="[staticChildContainerClass, computedChildContainerClasses]"&gt;
612
+ &lt;!--
613
+ Child checkboxes
614
+ - Assigned deterministic IDs matching those in parent's aria-owns
615
+ - Change events update parent's indeterminate state
616
+ - Share same v-model array with parent and siblings
617
+ --&gt;
618
+ &lt;FzCheckbox
619
+ v-for="(child, index) in children"
620
+ :key="child.value ? child.value.toString() : child.label"
621
+ v-model="<span class="fstat-no" title="function not covered" >model"</span>
622
+ :disabled="disabled"
623
+ v-bind="child"
624
+ :emphasis="emphasis"
625
+ :error="error"
626
+ :checkbox-id="`${parentId}-child-${index}`"
627
+ @change="handleCheckboxParentChange"
628
+ /&gt;
629
+ &lt;/div&gt;
630
+ &lt;/template&gt;
631
+ &lt;/FzCheckbox&gt;
632
+ &lt;/template&gt;
360
633
  &nbsp;</pre></td></tr></table></pre>
361
634
 
362
635
  <div class='push'></div><!-- for sticky footer -->
@@ -364,7 +637,7 @@ const isIndeterminate = computed(() =&gt; {
364
637
  <div class='footer quiet pad2 space-top1 center small'>
365
638
  Code coverage generated by
366
639
  <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
367
- at 2024-11-18T13:59:50.683Z
640
+ at 2025-10-30T09:58:41.772Z
368
641
  </div>
369
642
  <script src="../../prettify.js"></script>
370
643
  <script>