Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -480,12 +480,13 @@ describe('ConnectedNestedCheckboxes utils', () => {
const result = renderCheckbox({
option: mockOption,
state,
checkboxId: 'test-id',
name: 'test-id',
isRequired: true,
isDisabled: false,
onBlur: mockOnBlur,
onChange: mockOnChange,
ref: mockRef,
flatOptions: [mockOption],
});

const { container } = render(result);
Expand All @@ -503,12 +504,13 @@ describe('ConnectedNestedCheckboxes utils', () => {
const result = renderCheckbox({
option: mockOption,
state,
checkboxId: 'test-id',
name: 'test-id',
isRequired: false,
isDisabled: false,
onBlur: mockOnBlur,
onChange: mockOnChange,
ref: mockRef,
flatOptions: [mockOption],
});

const { container } = render(result);
Expand All @@ -525,12 +527,13 @@ describe('ConnectedNestedCheckboxes utils', () => {
const result = renderCheckbox({
option: mockOption,
state,
checkboxId: 'test-id',
name: 'test-id',
isRequired: false,
isDisabled: false,
onBlur: mockOnBlur,
onChange: mockOnChange,
ref: mockRef,
flatOptions: [mockOption],
});

const { container } = render(result);
Expand All @@ -547,12 +550,13 @@ describe('ConnectedNestedCheckboxes utils', () => {
const result = renderCheckbox({
option: { ...mockOption, level: 2 },
state,
checkboxId: 'test-id',
name: 'test-id',
isRequired: false,
isDisabled: false,
onBlur: mockOnBlur,
onChange: mockOnChange,
ref: mockRef,
flatOptions: [{ ...mockOption, level: 2 }],
});

const { container } = render(result);
Expand All @@ -567,12 +571,13 @@ describe('ConnectedNestedCheckboxes utils', () => {
const result = renderCheckbox({
option: { ...mockOption, disabled: true },
state,
checkboxId: 'test-id',
name: 'test-id',
isRequired: false,
isDisabled: true,
onBlur: mockOnBlur,
onChange: mockOnChange,
ref: mockRef,
flatOptions: [{ ...mockOption, disabled: true }],
});

const { container } = render(result);
Expand All @@ -587,13 +592,14 @@ describe('ConnectedNestedCheckboxes utils', () => {
const result = renderCheckbox({
option: mockOption,
state,
checkboxId: 'test-id',
name: 'test-id',
isRequired: false,
isDisabled: false,
onBlur: mockOnBlur,
onChange: mockOnChange,
ref: mockRef,
error: true,
flatOptions: [mockOption],
});

const { container } = render(result);
Expand All @@ -612,12 +618,13 @@ describe('ConnectedNestedCheckboxes utils', () => {
const result = renderCheckbox({
option: optionWithAriaLabel,
state,
checkboxId: 'test-id',
name: 'test-id',
isRequired: false,
isDisabled: false,
onBlur: mockOnBlur,
onChange: mockOnChange,
ref: mockRef,
flatOptions: [optionWithAriaLabel],
});

const { container } = render(result);
Expand All @@ -632,12 +639,13 @@ describe('ConnectedNestedCheckboxes utils', () => {
const result = renderCheckbox({
option: mockOption,
state,
checkboxId: 'test-id',
name: 'test-id',
isRequired: false,
isDisabled: false,
onBlur: mockOnBlur,
onChange: mockOnChange,
ref: mockRef,
flatOptions: [mockOption],
});

const { container } = render(result);
Expand All @@ -656,18 +664,108 @@ describe('ConnectedNestedCheckboxes utils', () => {
const result = renderCheckbox({
option: optionWithElementLabel as any, // ts should prevent this from ever happening but we have a default just in case
state,
checkboxId: 'test-id',
name: 'test-id',
isRequired: false,
isDisabled: false,
onBlur: mockOnBlur,
onChange: mockOnChange,
ref: mockRef,
flatOptions: [optionWithElementLabel as any],
});

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

expect(checkbox).toHaveAttribute('aria-label', 'checkbox');
});

it('should generate aria-controls with all nested descendants', () => {
const state = { checked: false };
const flatOptions = [
{
value: 'parent',
level: 0,
parentValue: undefined,
options: ['child1', 'child2'],
label: 'Parent',
},
{
value: 'child1',
level: 1,
parentValue: 'parent',
options: ['grandchild1'],
label: 'Child 1',
},
{
value: 'child2',
level: 1,
parentValue: 'parent',
options: [],
label: 'Child 2',
},
{
value: 'grandchild1',
level: 2,
parentValue: 'child1',
options: [],
label: 'Grandchild 1',
},
];

const parentOption = flatOptions[0];

const result = renderCheckbox({
option: parentOption,
state,
name: 'test-parent',
isRequired: false,
isDisabled: false,
onBlur: mockOnBlur,
onChange: mockOnChange,
ref: mockRef,
flatOptions,
});

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

// Should include all descendants (child1, grandchild1, child2), not just immediate children
expect(checkbox).toHaveAttribute(
'aria-controls',
'test-parent-child1 test-parent-grandchild1 test-parent-child2'
);
});

it('should not have aria-controls for leaf nodes', () => {
const state = { checked: false };
const flatOptions = [
{
value: 'leaf',
level: 0,
parentValue: undefined,
options: [],
label: 'Leaf',
},
];

const leafOption = flatOptions[0];

const result = renderCheckbox({
option: leafOption,
state,
name: 'test-leaf',
isRequired: false,
isDisabled: false,
onBlur: mockOnBlur,
onChange: mockOnChange,
ref: mockRef,
flatOptions,
});

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

expect(checkbox).not.toHaveAttribute('aria-controls');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export const ConnectedNestedCheckboxes: React.FC<
return renderCheckbox({
option: { ...option, spacing },
state,
checkboxId: `${name}-${option.value}`,
name,
isRequired,
isDisabled,
onBlur,
Expand All @@ -76,6 +76,7 @@ export const ConnectedNestedCheckboxes: React.FC<
});
},
ref,
flatOptions,
});
})}
</Box>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,25 +154,27 @@ export const handleCheckboxChange = ({
interface RenderCheckboxParams {
option: FlatCheckbox;
state: FlatCheckboxState;
checkboxId: string;
name: string;
isRequired: boolean;
isDisabled: boolean;
onBlur: () => void;
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
ref: React.RefCallback<HTMLInputElement>;
error?: boolean;
flatOptions: FlatCheckbox[];
}

export const renderCheckbox = ({
option,
state,
checkboxId,
name,
isRequired,
isDisabled,
onBlur,
onChange,
ref,
error,
flatOptions,
}: RenderCheckboxParams) => {
let checkedProps = {};
if (state.checked) {
Expand All @@ -193,6 +195,16 @@ export const renderCheckbox = ({
};
}

const checkboxId = `${name}-${option.value}`;

// Generate aria-controls for parent checkboxes with all nested descendants
const ariaControls =
option.options.length > 0
? getAllDescendants(option.value, flatOptions)
.map((childValue) => `${name}-${childValue}`)
.join(' ')
: undefined;

return (
<Box
as="li"
Expand All @@ -201,6 +213,7 @@ export const renderCheckbox = ({
ml={(option.level * 24) as any}
>
<Checkbox
aria-controls={ariaControls}
aria-invalid={error}
aria-label={
option['aria-label'] === undefined
Expand All @@ -211,11 +224,11 @@ export const renderCheckbox = ({
}
aria-required={isRequired}
disabled={isDisabled || option.disabled}
htmlFor={checkboxId}
htmlFor={name}
id={checkboxId}
label={option.label}
multiline={option.multiline}
name={checkboxId}
name={name}
spacing={option.spacing}
onBlur={onBlur}
onChange={onChange}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export const GridFormNestedCheckboxInput: React.FC<
return renderCheckbox({
option: { ...option, spacing: field.spacing },
state,
checkboxId: `${field.name}-${option.value}`,
name: field.name,
isRequired: !!required,
isDisabled: !!isDisabled,
onBlur,
Expand All @@ -75,6 +75,7 @@ export const GridFormNestedCheckboxInput: React.FC<
},
ref,
error,
flatOptions,
});
})}
</Box>
Expand Down
Loading