Skip to content

Commit 5522ce2

Browse files
authored
Merge branch 'main' into kl-gmt-1525-icons-naming
2 parents a08d733 + 577bbbb commit 5522ce2

File tree

8 files changed

+141
-18
lines changed

8 files changed

+141
-18
lines changed

packages/gamut-kit/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
All notable changes to this project will be documented in this file.
44
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
55

6+
### [0.6.573](https://github.com/Codecademy/gamut/compare/@codecademy/[email protected]...@codecademy/[email protected]) (2025-12-10)
7+
8+
**Note:** Version bump only for package @codecademy/gamut-kit
9+
610
### [0.6.572](https://github.com/Codecademy/gamut/compare/@codecademy/[email protected]...@codecademy/[email protected]) (2025-12-08)
711

812
**Note:** Version bump only for package @codecademy/gamut-kit

packages/gamut-kit/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
{
22
"name": "@codecademy/gamut-kit",
33
"description": "Styleguide & Component library for Codecademy",
4-
"version": "0.6.572",
4+
"version": "0.6.573",
55
"author": "Codecademy Engineering <[email protected]>",
66
"dependencies": {
7-
"@codecademy/gamut": "67.5.4",
7+
"@codecademy/gamut": "67.6.0",
88
"@codecademy/gamut-icons": "9.53.0",
99
"@codecademy/gamut-illustrations": "0.57.0",
1010
"@codecademy/gamut-patterns": "0.10.19",

packages/gamut/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@
33
All notable changes to this project will be documented in this file.
44
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
55

6+
## [67.6.0](https://github.com/Codecademy/gamut/compare/@codecademy/[email protected]...@codecademy/[email protected]) (2025-12-10)
7+
8+
### Features
9+
10+
- **ConnectedForm, GridForm:** Add aria-controls to nested checkboxes ([2a7a7ad](https://github.com/Codecademy/gamut/commit/2a7a7ade029972c2e7f990cc140d270b17123d9f))
11+
612
### [67.5.4](https://github.com/Codecademy/gamut/compare/@codecademy/[email protected]...@codecademy/[email protected]) (2025-12-08)
713

814
**Note:** Version bump only for package @codecademy/gamut

packages/gamut/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@codecademy/gamut",
33
"description": "Styleguide & Component library for Codecademy",
4-
"version": "67.5.4",
4+
"version": "67.6.0",
55
"author": "Codecademy Engineering <[email protected]>",
66
"dependencies": {
77
"@codecademy/gamut-icons": "9.53.0",

packages/gamut/src/ConnectedForm/ConnectedInputs/ConnectedNestedCheckboxes/__tests__/utils.test.tsx

Lines changed: 107 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -480,12 +480,13 @@ describe('ConnectedNestedCheckboxes utils', () => {
480480
const result = renderCheckbox({
481481
option: mockOption,
482482
state,
483-
checkboxId: 'test-id',
483+
name: 'test',
484484
isRequired: true,
485485
isDisabled: false,
486486
onBlur: mockOnBlur,
487487
onChange: mockOnChange,
488488
ref: mockRef,
489+
flatOptions: [mockOption],
489490
});
490491

491492
const { container } = render(result);
@@ -503,12 +504,13 @@ describe('ConnectedNestedCheckboxes utils', () => {
503504
const result = renderCheckbox({
504505
option: mockOption,
505506
state,
506-
checkboxId: 'test-id',
507+
name: 'test',
507508
isRequired: false,
508509
isDisabled: false,
509510
onBlur: mockOnBlur,
510511
onChange: mockOnChange,
511512
ref: mockRef,
513+
flatOptions: [mockOption],
512514
});
513515

514516
const { container } = render(result);
@@ -525,12 +527,13 @@ describe('ConnectedNestedCheckboxes utils', () => {
525527
const result = renderCheckbox({
526528
option: mockOption,
527529
state,
528-
checkboxId: 'test-id',
530+
name: 'test',
529531
isRequired: false,
530532
isDisabled: false,
531533
onBlur: mockOnBlur,
532534
onChange: mockOnChange,
533535
ref: mockRef,
536+
flatOptions: [mockOption],
534537
});
535538

536539
const { container } = render(result);
@@ -547,12 +550,13 @@ describe('ConnectedNestedCheckboxes utils', () => {
547550
const result = renderCheckbox({
548551
option: { ...mockOption, level: 2 },
549552
state,
550-
checkboxId: 'test-id',
553+
name: 'test',
551554
isRequired: false,
552555
isDisabled: false,
553556
onBlur: mockOnBlur,
554557
onChange: mockOnChange,
555558
ref: mockRef,
559+
flatOptions: [{ ...mockOption, level: 2 }],
556560
});
557561

558562
const { container } = render(result);
@@ -567,12 +571,13 @@ describe('ConnectedNestedCheckboxes utils', () => {
567571
const result = renderCheckbox({
568572
option: { ...mockOption, disabled: true },
569573
state,
570-
checkboxId: 'test-id',
574+
name: 'test',
571575
isRequired: false,
572576
isDisabled: true,
573577
onBlur: mockOnBlur,
574578
onChange: mockOnChange,
575579
ref: mockRef,
580+
flatOptions: [{ ...mockOption, disabled: true }],
576581
});
577582

578583
const { container } = render(result);
@@ -587,13 +592,14 @@ describe('ConnectedNestedCheckboxes utils', () => {
587592
const result = renderCheckbox({
588593
option: mockOption,
589594
state,
590-
checkboxId: 'test-id',
595+
name: 'test',
591596
isRequired: false,
592597
isDisabled: false,
593598
onBlur: mockOnBlur,
594599
onChange: mockOnChange,
595600
ref: mockRef,
596601
error: true,
602+
flatOptions: [mockOption],
597603
});
598604

599605
const { container } = render(result);
@@ -612,12 +618,13 @@ describe('ConnectedNestedCheckboxes utils', () => {
612618
const result = renderCheckbox({
613619
option: optionWithAriaLabel,
614620
state,
615-
checkboxId: 'test-id',
621+
name: 'test',
616622
isRequired: false,
617623
isDisabled: false,
618624
onBlur: mockOnBlur,
619625
onChange: mockOnChange,
620626
ref: mockRef,
627+
flatOptions: [optionWithAriaLabel],
621628
});
622629

623630
const { container } = render(result);
@@ -632,12 +639,13 @@ describe('ConnectedNestedCheckboxes utils', () => {
632639
const result = renderCheckbox({
633640
option: mockOption,
634641
state,
635-
checkboxId: 'test-id',
642+
name: 'test',
636643
isRequired: false,
637644
isDisabled: false,
638645
onBlur: mockOnBlur,
639646
onChange: mockOnChange,
640647
ref: mockRef,
648+
flatOptions: [mockOption],
641649
});
642650

643651
const { container } = render(result);
@@ -656,18 +664,108 @@ describe('ConnectedNestedCheckboxes utils', () => {
656664
const result = renderCheckbox({
657665
option: optionWithElementLabel as any, // ts should prevent this from ever happening but we have a default just in case
658666
state,
659-
checkboxId: 'test-id',
667+
name: 'test',
660668
isRequired: false,
661669
isDisabled: false,
662670
onBlur: mockOnBlur,
663671
onChange: mockOnChange,
664672
ref: mockRef,
673+
flatOptions: [optionWithElementLabel as any],
665674
});
666675

667676
const { container } = render(result);
668677
const checkbox = container.querySelector('input[type="checkbox"]');
669678

670679
expect(checkbox).toHaveAttribute('aria-label', 'checkbox');
671680
});
681+
682+
it('should generate aria-controls with all nested descendants', () => {
683+
const state = { checked: false };
684+
const flatOptions = [
685+
{
686+
value: 'parent',
687+
level: 0,
688+
parentValue: undefined,
689+
options: ['child1', 'child2'],
690+
label: 'Parent',
691+
},
692+
{
693+
value: 'child1',
694+
level: 1,
695+
parentValue: 'parent',
696+
options: ['grandchild1'],
697+
label: 'Child 1',
698+
},
699+
{
700+
value: 'child2',
701+
level: 1,
702+
parentValue: 'parent',
703+
options: [],
704+
label: 'Child 2',
705+
},
706+
{
707+
value: 'grandchild1',
708+
level: 2,
709+
parentValue: 'child1',
710+
options: [],
711+
label: 'Grandchild 1',
712+
},
713+
];
714+
715+
const parentOption = flatOptions[0];
716+
717+
const result = renderCheckbox({
718+
option: parentOption,
719+
state,
720+
name: 'test-parent',
721+
isRequired: false,
722+
isDisabled: false,
723+
onBlur: mockOnBlur,
724+
onChange: mockOnChange,
725+
ref: mockRef,
726+
flatOptions,
727+
});
728+
729+
const { container } = render(result);
730+
const checkbox = container.querySelector('input[type="checkbox"]');
731+
732+
// Should include all descendants (child1, grandchild1, child2), not just immediate children
733+
expect(checkbox).toHaveAttribute(
734+
'aria-controls',
735+
'test-parent-child1 test-parent-grandchild1 test-parent-child2'
736+
);
737+
});
738+
739+
it('should not have aria-controls for leaf nodes', () => {
740+
const state = { checked: false };
741+
const flatOptions = [
742+
{
743+
value: 'leaf',
744+
level: 0,
745+
parentValue: undefined,
746+
options: [],
747+
label: 'Leaf',
748+
},
749+
];
750+
751+
const leafOption = flatOptions[0];
752+
753+
const result = renderCheckbox({
754+
option: leafOption,
755+
state,
756+
name: 'test-leaf',
757+
isRequired: false,
758+
isDisabled: false,
759+
onBlur: mockOnBlur,
760+
onChange: mockOnChange,
761+
ref: mockRef,
762+
flatOptions,
763+
});
764+
765+
const { container } = render(result);
766+
const checkbox = container.querySelector('input[type="checkbox"]');
767+
768+
expect(checkbox).not.toHaveAttribute('aria-controls');
769+
});
672770
});
673771
});

packages/gamut/src/ConnectedForm/ConnectedInputs/ConnectedNestedCheckboxes/index.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ export const ConnectedNestedCheckboxes: React.FC<
6161
return renderCheckbox({
6262
option: { ...option, spacing },
6363
state,
64-
checkboxId: `${name}-${option.value}`,
64+
name,
6565
isRequired,
6666
isDisabled,
6767
onBlur,
@@ -76,6 +76,7 @@ export const ConnectedNestedCheckboxes: React.FC<
7676
});
7777
},
7878
ref,
79+
flatOptions,
7980
});
8081
})}
8182
</Box>

packages/gamut/src/ConnectedForm/ConnectedInputs/ConnectedNestedCheckboxes/utils.tsx

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -154,25 +154,27 @@ export const handleCheckboxChange = ({
154154
interface RenderCheckboxParams {
155155
option: FlatCheckbox;
156156
state: FlatCheckboxState;
157-
checkboxId: string;
157+
name: string;
158158
isRequired: boolean;
159159
isDisabled: boolean;
160160
onBlur: () => void;
161161
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
162162
ref: React.RefCallback<HTMLInputElement>;
163163
error?: boolean;
164+
flatOptions: FlatCheckbox[];
164165
}
165166

166167
export const renderCheckbox = ({
167168
option,
168169
state,
169-
checkboxId,
170+
name,
170171
isRequired,
171172
isDisabled,
172173
onBlur,
173174
onChange,
174175
ref,
175176
error,
177+
flatOptions,
176178
}: RenderCheckboxParams) => {
177179
let checkedProps = {};
178180
if (state.checked) {
@@ -193,6 +195,16 @@ export const renderCheckbox = ({
193195
};
194196
}
195197

198+
const checkboxId = `${name}-${option.value}`;
199+
200+
// Generate aria-controls for parent checkboxes with all nested descendants
201+
const ariaControls =
202+
option.options.length > 0
203+
? getAllDescendants(option.value, flatOptions)
204+
.map((childValue) => `${name}-${childValue}`)
205+
.join(' ')
206+
: undefined;
207+
196208
return (
197209
<Box
198210
as="li"
@@ -201,6 +213,7 @@ export const renderCheckbox = ({
201213
ml={(option.level * 24) as any}
202214
>
203215
<Checkbox
216+
aria-controls={ariaControls}
204217
aria-invalid={error}
205218
aria-label={
206219
option['aria-label'] === undefined
@@ -211,11 +224,11 @@ export const renderCheckbox = ({
211224
}
212225
aria-required={isRequired}
213226
disabled={isDisabled || option.disabled}
214-
htmlFor={checkboxId}
227+
htmlFor={name}
215228
id={checkboxId}
216229
label={option.label}
217230
multiline={option.multiline}
218-
name={checkboxId}
231+
name={name}
219232
spacing={option.spacing}
220233
onBlur={onBlur}
221234
onChange={onChange}

packages/gamut/src/GridForm/GridFormInputGroup/GridFormNestedCheckboxInput/index.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export const GridFormNestedCheckboxInput: React.FC<
5959
return renderCheckbox({
6060
option: { ...option, spacing: field.spacing },
6161
state,
62-
checkboxId: `${field.name}-${option.value}`,
62+
name: field.name,
6363
isRequired: !!required,
6464
isDisabled: !!isDisabled,
6565
onBlur,
@@ -75,6 +75,7 @@ export const GridFormNestedCheckboxInput: React.FC<
7575
},
7676
ref,
7777
error,
78+
flatOptions,
7879
});
7980
})}
8081
</Box>

0 commit comments

Comments
 (0)