Skip to content

Commit 324adaf

Browse files
authored
fix(CheckboxGroup): propagate props to Checkbox children (#20267)
* fix(CheckboxGroup): propagate props to Checkbox children * refactor: shrink diff
1 parent 51511e6 commit 324adaf

File tree

2 files changed

+136
-3
lines changed

2 files changed

+136
-3
lines changed

packages/react/src/components/CheckboxGroup/CheckboxGroup-test.js

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Copyright IBM Corp. 2016, 2023
2+
* Copyright IBM Corp. 2016, 2025
33
*
44
* This source code is licensed under the Apache-2.0 license found in the
55
* LICENSE file in the root directory of this source tree.
@@ -13,6 +13,16 @@ import { AILabel } from '../AILabel';
1313

1414
const prefix = 'cds';
1515

16+
/**
17+
* @param element {Element}
18+
* @returns {Record<string, string>}
19+
*/
20+
const getElementAttributes = ({ attributes }) =>
21+
Array.from(attributes).reduce(
22+
(acc, { name, value }) => ({ ...acc, [name]: value }),
23+
{}
24+
);
25+
1626
describe('CheckboxGroup', () => {
1727
it('should support a custom `className` prop on the outermost element', () => {
1828
const { container } = render(
@@ -191,4 +201,89 @@ describe('CheckboxGroup', () => {
191201
`${prefix}--checkbox-group--horizontal`
192202
);
193203
});
204+
205+
describe('prop inheritance', () => {
206+
it('should pass props to child `Checkbox` components', () => {
207+
render(
208+
<CheckboxGroup legendText="Checkbox heading" invalid readOnly warn>
209+
<Checkbox labelText="Checkbox 1" id="checkbox-1" />
210+
<Checkbox labelText="Checkbox 2" id="checkbox-2" />
211+
</CheckboxGroup>
212+
);
213+
214+
const checkbox1 = screen.getByLabelText('Checkbox 1');
215+
const checkbox2 = screen.getByLabelText('Checkbox 2');
216+
const attributes1 = getElementAttributes(checkbox1);
217+
const attributes2 = getElementAttributes(checkbox2);
218+
219+
expect(attributes1).toEqual({
220+
class: `${prefix}--checkbox`,
221+
id: 'checkbox-1',
222+
'data-invalid': 'true',
223+
'aria-readonly': 'true',
224+
type: 'checkbox',
225+
});
226+
expect(attributes2).toEqual({
227+
class: `${prefix}--checkbox`,
228+
id: 'checkbox-2',
229+
'data-invalid': 'true',
230+
'aria-readonly': 'true',
231+
type: 'checkbox',
232+
});
233+
});
234+
235+
it('should not override individual `Checkbox` props', () => {
236+
render(
237+
<CheckboxGroup legendText="Checkbox heading" readOnly>
238+
<Checkbox labelText="Checkbox 1" id="checkbox-1" readOnly={false} />
239+
<Checkbox labelText="Checkbox 2" id="checkbox-2" />
240+
</CheckboxGroup>
241+
);
242+
243+
const checkbox1 = screen.getByLabelText('Checkbox 1');
244+
const checkbox2 = screen.getByLabelText('Checkbox 2');
245+
const attributes1 = getElementAttributes(checkbox1);
246+
const attributes2 = getElementAttributes(checkbox2);
247+
248+
expect(attributes1).toEqual({
249+
class: `${prefix}--checkbox`,
250+
id: 'checkbox-1',
251+
// Should not be read-only because it explicitly sets `readOnly` to
252+
// `false`.
253+
'aria-readonly': 'false',
254+
type: 'checkbox',
255+
});
256+
expect(attributes2).toEqual({
257+
class: `${prefix}--checkbox`,
258+
id: 'checkbox-2',
259+
// Should be read-only because it inherits from the group.
260+
'aria-readonly': 'true',
261+
type: 'checkbox',
262+
});
263+
});
264+
265+
it('should not affect non-`Checkbox` children', () => {
266+
render(
267+
<CheckboxGroup legendText="Checkbox heading" readOnly>
268+
<Checkbox labelText="Checkbox label" id="checkbox-1" />
269+
<div data-testid="non-checkbox">Not a checkbox</div>
270+
</CheckboxGroup>
271+
);
272+
273+
const checkbox = screen.getByLabelText('Checkbox label');
274+
const nonCheckbox = screen.getByTestId('non-checkbox');
275+
const checkboxAttributes = getElementAttributes(checkbox);
276+
const nonCheckboxAttributes = getElementAttributes(nonCheckbox);
277+
278+
expect(checkboxAttributes).toEqual({
279+
class: `${prefix}--checkbox`,
280+
id: 'checkbox-1',
281+
'aria-readonly': 'true',
282+
type: 'checkbox',
283+
});
284+
expect(nonCheckboxAttributes).toEqual({
285+
'data-testid': 'non-checkbox',
286+
});
287+
});
288+
});
194289
});

packages/react/src/components/CheckboxGroup/CheckboxGroup.tsx

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,21 @@
66
*/
77

88
import PropTypes from 'prop-types';
9-
import React, { cloneElement, type ReactNode } from 'react';
9+
import React, {
10+
Children,
11+
cloneElement,
12+
isValidElement,
13+
type ComponentProps,
14+
type ReactNode,
15+
} from 'react';
1016
import cx from 'classnames';
1117
import { deprecate } from '../../prop-types/deprecate';
1218
import { usePrefix } from '../../internal/usePrefix';
1319
import { WarningFilled, WarningAltFilled } from '@carbon/icons-react';
1420
import { useId } from '../../internal/useId';
1521
import { AILabel } from '../AILabel';
1622
import { isComponentElement } from '../../internal';
23+
import { Checkbox } from '../Checkbox';
1724

1825
export interface CheckboxGroupProps {
1926
children?: ReactNode;
@@ -89,6 +96,37 @@ const CheckboxGroup: React.FC<CheckboxGroupProps> = ({
8996
? cloneElement(candidate, { size: 'mini', kind: 'default' })
9097
: null;
9198

99+
const clonedChildren = Children.map(children, (child) => {
100+
if (
101+
isValidElement<ComponentProps<typeof Checkbox>>(child) &&
102+
child.type === Checkbox
103+
) {
104+
const childProps: Pick<
105+
ComponentProps<typeof Checkbox>,
106+
'invalid' | 'readOnly' | 'warn'
107+
> = {
108+
...(typeof invalid !== 'undefined' &&
109+
typeof child.props.invalid === 'undefined'
110+
? { invalid }
111+
: {}),
112+
...(typeof readOnly !== 'undefined' &&
113+
typeof child.props.readOnly === 'undefined'
114+
? { readOnly }
115+
: {}),
116+
...(typeof warn !== 'undefined' &&
117+
typeof child.props.warn === 'undefined'
118+
? { warn }
119+
: {}),
120+
};
121+
122+
return Object.keys(childProps).length
123+
? cloneElement(child, childProps)
124+
: child;
125+
}
126+
127+
return child;
128+
});
129+
92130
return (
93131
<fieldset
94132
className={fieldsetClasses}
@@ -111,7 +149,7 @@ const CheckboxGroup: React.FC<CheckboxGroupProps> = ({
111149
''
112150
)}
113151
</legend>
114-
{children}
152+
{clonedChildren}
115153
<div className={`${prefix}--checkbox-group__validation-msg`}>
116154
{!readOnly && invalid && (
117155
<>

0 commit comments

Comments
 (0)