Skip to content

Commit b1d5ecd

Browse files
anuanto966heloiseluiriddhybansaltay1orjones
authored
fix(radio-button): Prevent invalid/warn states when disabled/readonly (#20810)
* fix(radio-button): prevent invalid/warn states when disabled/readonly Fixed RadioButton components in both React and Web Components to not show invalid/warn states when in disabled or readonly state, as users cannot interact with these components in those states. * fix(time-picker): prevent invalid/warn states when disabled/readonly Fixed Timepicker components in both React and Web Components to not show invalid/warn states when in disabled or readonly state, as users cannot interact with these components in those states. * chore: update snapshots --------- Co-authored-by: “heloise-lui” <heloise.lui@ibm.com> Co-authored-by: Riddhi Bansal <41935566+riddhybansal@users.noreply.github.com> Co-authored-by: Taylor Jones <tay1orjones@users.noreply.github.com>
1 parent 9980c23 commit b1d5ecd

File tree

8 files changed

+399
-12
lines changed

8 files changed

+399
-12
lines changed

packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8281,6 +8281,12 @@ Map {
82818281
"id": {
82828282
"type": "string",
82838283
},
8284+
"invalid": {
8285+
"type": "bool",
8286+
},
8287+
"invalidText": {
8288+
"type": "node",
8289+
},
82848290
"labelPosition": {
82858291
"args": [
82868292
[
@@ -8303,6 +8309,9 @@ Map {
83038309
"onClick": {
83048310
"type": "func",
83058311
},
8312+
"readOnly": {
8313+
"type": "bool",
8314+
},
83068315
"required": {
83078316
"type": "bool",
83088317
},
@@ -8320,6 +8329,12 @@ Map {
83208329
],
83218330
"type": "oneOfType",
83228331
},
8332+
"warn": {
8333+
"type": "bool",
8334+
},
8335+
"warnText": {
8336+
"type": "node",
8337+
},
83238338
},
83248339
"render": [Function],
83258340
},

packages/react/src/components/RadioButton/RadioButton.tsx

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { useId } from '../../internal/useId';
1515
import { mergeRefs } from '../../tools/mergeRefs';
1616
import { AILabel } from '../AILabel';
1717
import { isComponentElement } from '../../internal';
18+
import { useNormalizedInputProps } from '../../internal/useNormalizedInputProps';
1819

1920
type ExcludedAttributes = 'onChange';
2021

@@ -105,14 +106,39 @@ export interface RadioButtonProps
105106
* `true` to specify if the input is required.
106107
*/
107108
required?: boolean;
109+
110+
/**
111+
* Specify whether the control is currently invalid
112+
*/
113+
invalid?: boolean;
114+
115+
/**
116+
* Provide the text that is displayed when the control is in an invalid state
117+
*/
118+
invalidText?: ReactNode;
119+
120+
/**
121+
* Specify whether the control is currently in warning state
122+
*/
123+
warn?: boolean;
124+
125+
/**
126+
* Provide the text that is displayed when the control is in warning state
127+
*/
128+
warnText?: ReactNode;
129+
130+
/**
131+
* Specify whether the RadioButton should be read-only
132+
*/
133+
readOnly?: boolean;
108134
}
109135

110136
const RadioButton = React.forwardRef<HTMLInputElement, RadioButtonProps>(
111137
(props, ref) => {
112138
const {
113139
className,
114140
decorator,
115-
disabled,
141+
disabled = false,
116142
hideLabel,
117143
id,
118144
labelPosition = 'right',
@@ -122,13 +148,28 @@ const RadioButton = React.forwardRef<HTMLInputElement, RadioButtonProps>(
122148
value = '',
123149
slug,
124150
required,
151+
invalid = false,
152+
invalidText,
153+
warn = false,
154+
warnText,
155+
readOnly,
125156
...rest
126157
} = props;
127158

128159
const prefix = usePrefix();
129160
const uid = useId('radio-button');
130161
const uniqueId = id || uid;
131162

163+
const normalizedProps = useNormalizedInputProps({
164+
id: uniqueId,
165+
readOnly,
166+
disabled,
167+
invalid,
168+
invalidText,
169+
warn,
170+
warnText,
171+
});
172+
132173
function handleOnChange(event: React.ChangeEvent<HTMLInputElement>) {
133174
onChange(value, name, event);
134175
}
@@ -148,6 +189,8 @@ const RadioButton = React.forwardRef<HTMLInputElement, RadioButtonProps>(
148189
labelPosition !== 'right',
149190
[`${prefix}--radio-button-wrapper--slug`]: slug,
150191
[`${prefix}--radio-button-wrapper--decorator`]: decorator,
192+
[`${prefix}--radio-button-wrapper--invalid`]: normalizedProps.invalid,
193+
[`${prefix}--radio-button-wrapper--warning`]: normalizedProps.warn,
151194
}
152195
);
153196

@@ -170,10 +213,11 @@ const RadioButton = React.forwardRef<HTMLInputElement, RadioButtonProps>(
170213
onChange={handleOnChange}
171214
id={uniqueId}
172215
ref={mergeRefs(inputRef, ref)}
173-
disabled={disabled}
216+
disabled={normalizedProps.disabled}
174217
value={value}
175218
name={name}
176219
required={required}
220+
readOnly={readOnly}
177221
/>
178222
<label htmlFor={uniqueId} className={`${prefix}--radio-button__label`}>
179223
<span className={`${prefix}--radio-button__appearance`} />
@@ -193,6 +237,7 @@ const RadioButton = React.forwardRef<HTMLInputElement, RadioButtonProps>(
193237
</Text>
194238
)}
195239
</label>
240+
{normalizedProps.validation}
196241
</div>
197242
);
198243
}
@@ -269,6 +314,31 @@ RadioButton.propTypes = {
269314
*/
270315
required: PropTypes.bool,
271316

317+
/**
318+
* Specify whether the control is currently invalid
319+
*/
320+
invalid: PropTypes.bool,
321+
322+
/**
323+
* Provide the text that is displayed when the control is in an invalid state
324+
*/
325+
invalidText: PropTypes.node,
326+
327+
/**
328+
* Specify whether the control is currently in warning state
329+
*/
330+
warn: PropTypes.bool,
331+
332+
/**
333+
* Provide the text that is displayed when the control is in warning state
334+
*/
335+
warnText: PropTypes.node,
336+
337+
/**
338+
* Specify whether the RadioButton should be read-only
339+
*/
340+
readOnly: PropTypes.bool,
341+
272342
/**
273343
* **Experimental**: Provide a `Slug` component to be rendered inside the `RadioButton` component
274344
*/

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

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,5 +327,121 @@ describe('RadioButtonGroup', () => {
327327
expect(screen.getByDisplayValue('option-1')).toBeRequired();
328328
expect(screen.getByDisplayValue('option-2')).toBeRequired();
329329
});
330+
331+
describe('Invalid and Warning States', () => {
332+
it('should apply invalid class when invalid prop is true', () => {
333+
const { container } = render(
334+
<RadioButtonGroup
335+
invalid={true}
336+
invalidText="Invalid selection"
337+
name="test"
338+
legendText="test">
339+
<RadioButton labelText="test-1" value="test-1" />
340+
<RadioButton labelText="test-2" value="test-2" />
341+
</RadioButtonGroup>
342+
);
343+
344+
const fieldset = container.querySelector('fieldset');
345+
expect(fieldset).toHaveClass(`${prefix}--radio-button-group--invalid`);
346+
expect(screen.getByText('Invalid selection')).toBeInTheDocument();
347+
});
348+
349+
it('should apply warning class when warn prop is true', () => {
350+
const { container } = render(
351+
<RadioButtonGroup
352+
warn={true}
353+
warnText="Warning message"
354+
name="test"
355+
legendText="test">
356+
<RadioButton labelText="test-1" value="test-1" />
357+
<RadioButton labelText="test-2" value="test-2" />
358+
</RadioButtonGroup>
359+
);
360+
361+
const fieldset = container.querySelector('fieldset');
362+
expect(fieldset).toHaveClass(`${prefix}--radio-button-group--warning`);
363+
expect(screen.getByText('Warning message')).toBeInTheDocument();
364+
});
365+
366+
it('should not apply invalid class or show invalid text when disabled and invalid', () => {
367+
const { container } = render(
368+
<RadioButtonGroup
369+
disabled={true}
370+
invalid={true}
371+
invalidText="Invalid selection"
372+
name="test"
373+
legendText="test">
374+
<RadioButton labelText="test-1" value="test-1" />
375+
<RadioButton labelText="test-2" value="test-2" />
376+
</RadioButtonGroup>
377+
);
378+
379+
const fieldset = container.querySelector('fieldset');
380+
expect(fieldset).not.toHaveClass(
381+
`${prefix}--radio-button-group--invalid`
382+
);
383+
expect(screen.queryByText('Invalid selection')).not.toBeInTheDocument();
384+
});
385+
386+
it('should not apply warning class or show warning text when disabled and warn', () => {
387+
const { container } = render(
388+
<RadioButtonGroup
389+
disabled={true}
390+
warn={true}
391+
warnText="Warning message"
392+
name="test"
393+
legendText="test">
394+
<RadioButton labelText="test-1" value="test-1" />
395+
<RadioButton labelText="test-2" value="test-2" />
396+
</RadioButtonGroup>
397+
);
398+
399+
const fieldset = container.querySelector('fieldset');
400+
expect(fieldset).not.toHaveClass(
401+
`${prefix}--radio-button-group--warning`
402+
);
403+
expect(screen.queryByText('Warning message')).not.toBeInTheDocument();
404+
});
405+
406+
it('should not apply invalid class or show invalid text when readOnly and invalid', () => {
407+
const { container } = render(
408+
<RadioButtonGroup
409+
readOnly={true}
410+
invalid={true}
411+
invalidText="Invalid selection"
412+
name="test"
413+
legendText="test">
414+
<RadioButton labelText="test-1" value="test-1" />
415+
<RadioButton labelText="test-2" value="test-2" />
416+
</RadioButtonGroup>
417+
);
418+
419+
const fieldset = container.querySelector('fieldset');
420+
expect(fieldset).not.toHaveClass(
421+
`${prefix}--radio-button-group--invalid`
422+
);
423+
expect(screen.queryByText('Invalid selection')).not.toBeInTheDocument();
424+
});
425+
426+
it('should not apply warning class or show warning text when readOnly and warn', () => {
427+
const { container } = render(
428+
<RadioButtonGroup
429+
readOnly={true}
430+
warn={true}
431+
warnText="Warning message"
432+
name="test"
433+
legendText="test">
434+
<RadioButton labelText="test-1" value="test-1" />
435+
<RadioButton labelText="test-2" value="test-2" />
436+
</RadioButtonGroup>
437+
);
438+
439+
const fieldset = container.querySelector('fieldset');
440+
expect(fieldset).not.toHaveClass(
441+
`${prefix}--radio-button-group--warning`
442+
);
443+
expect(screen.queryByText('Warning message')).not.toBeInTheDocument();
444+
});
445+
});
330446
});
331447
});

packages/react/src/components/RadioButtonGroup/RadioButtonGroup.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ const RadioButtonGroup = React.forwardRef(
218218
}
219219
}
220220

221-
const showWarning = !readOnly && !invalid && warn;
221+
const showWarning = !readOnly && !disabled && !invalid && warn;
222222
const showHelper = !invalid && !disabled && !warn;
223223

224224
const wrapperClasses = classNames(`${prefix}--form-item`, className);
@@ -228,7 +228,8 @@ const RadioButtonGroup = React.forwardRef(
228228
orientation === 'vertical',
229229
[`${prefix}--radio-button-group--label-${labelPosition}`]: labelPosition,
230230
[`${prefix}--radio-button-group--readonly`]: readOnly,
231-
[`${prefix}--radio-button-group--invalid`]: !readOnly && invalid,
231+
[`${prefix}--radio-button-group--invalid`]:
232+
!readOnly && !disabled && invalid,
232233
[`${prefix}--radio-button-group--warning`]: showWarning,
233234
[`${prefix}--radio-button-group--slug`]: slug,
234235
[`${prefix}--radio-button-group--decorator`]: decorator,
@@ -283,7 +284,7 @@ const RadioButtonGroup = React.forwardRef(
283284
{getRadioButtons()}
284285
</fieldset>
285286
<div className={`${prefix}--radio-button__validation-msg`}>
286-
{!readOnly && invalid && (
287+
{!readOnly && !disabled && invalid && (
287288
<>
288289
<WarningFilled
289290
className={`${prefix}--radio-button__invalid-icon`}

0 commit comments

Comments
 (0)