Skip to content

Commit

Permalink
feat(radio-button): add new decorator prop (#18029)
Browse files Browse the repository at this point in the history
* feat(radio-button): add new decorator prop

* test(radio-button): increase coverage

* fix(radio-button): minify and stories

* fix(format): update

* fix(stories): remove tooltip

* fix(radio-button): remove tooltip
  • Loading branch information
ariellalgilmore authored Nov 18, 2024
1 parent d3bd715 commit e11a4bd
Show file tree
Hide file tree
Showing 7 changed files with 165 additions and 31 deletions.
14 changes: 8 additions & 6 deletions packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -6433,6 +6433,9 @@ Map {
"className": Object {
"type": "string",
},
"decorator": Object {
"type": "node",
},
"defaultChecked": Object {
"type": "bool",
},
Expand Down Expand Up @@ -6470,9 +6473,7 @@ Map {
"required": Object {
"type": "bool",
},
"slug": Object {
"type": "node",
},
"slug": [Function],
"value": Object {
"args": Array [
Array [
Expand All @@ -6498,6 +6499,9 @@ Map {
"className": Object {
"type": "string",
},
"decorator": Object {
"type": "node",
},
"defaultSelected": Object {
"args": Array [
Array [
Expand Down Expand Up @@ -6557,9 +6561,7 @@ Map {
"required": Object {
"type": "bool",
},
"slug": Object {
"type": "node",
},
"slug": [Function],
"valueSelected": Object {
"args": Array [
Array [
Expand Down
10 changes: 5 additions & 5 deletions packages/react/src/components/RadioButton/RadioButton.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ export const withAILabel = {
render: () => (
<div className="ai-label-check-radio-container">
<RadioButtonGroup
slug={AILabelFunc('default')}
decorator={AILabelFunc('default')}
orientation="vertical"
legendText="Group label"
name="radio-button-group"
Expand Down Expand Up @@ -163,13 +163,13 @@ export const withAILabel = {
labelText="Radio button label"
value="radio-4"
id="radio-4"
slug={AILabelFunc()}
decorator={AILabelFunc()}
/>
<RadioButton
labelText="Radio button label"
value="radio-5"
id="radio-5"
slug={AILabelFunc()}
decorator={AILabelFunc()}
/>
<RadioButton
labelText="Radio button label"
Expand All @@ -187,13 +187,13 @@ export const withAILabel = {
labelText="Radio button label"
value="radio-7"
id="radio-7"
slug={AILabelFunc('inline')}
decorator={AILabelFunc('inline')}
/>
<RadioButton
labelText="Radio button label"
value="radio-8"
id="radio-8"
slug={AILabelFunc('inline')}
decorator={AILabelFunc('inline')}
/>
<RadioButton
labelText="Radio button label"
Expand Down
55 changes: 46 additions & 9 deletions packages/react/src/components/RadioButton/RadioButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@
*/

import PropTypes from 'prop-types';
import React, { ReactNode, useRef } from 'react';
import React, { ReactElement, ReactNode, useRef } from 'react';
import classNames from 'classnames';
import { Text } from '../Text';
import deprecate from '../../prop-types/deprecate';
import { usePrefix } from '../../internal/usePrefix';
import { useId } from '../../internal/useId';
import mergeRefs from '../../tools/mergeRefs';
Expand All @@ -30,6 +31,11 @@ export interface RadioButtonProps
*/
className?: string;

/**
* **Experimental**: Provide a `decorator` component to be rendered inside the `RadioButton` component
*/
decorator?: ReactNode;

/**
* Specify whether the `<RadioButton>` should be checked by default
*/
Expand Down Expand Up @@ -83,6 +89,7 @@ export interface RadioButtonProps
onClick?: (evt: React.MouseEvent<HTMLInputElement>) => void;

/**
* @deprecated please use decorator instead.
* **Experimental**: Provide a `Slug` component to be rendered inside the `RadioButton` component
*/
slug?: ReactNode;
Expand All @@ -102,6 +109,7 @@ const RadioButton = React.forwardRef<HTMLInputElement, RadioButtonProps>(
(props, ref) => {
const {
className,
decorator,
disabled,
hideLabel,
id,
Expand Down Expand Up @@ -137,17 +145,29 @@ const RadioButton = React.forwardRef<HTMLInputElement, RadioButtonProps>(
[`${prefix}--radio-button-wrapper--label-${labelPosition}`]:
labelPosition !== 'right',
[`${prefix}--radio-button-wrapper--slug`]: slug,
[`${prefix}--radio-button-wrapper--decorator`]: decorator,
}
);

const inputRef = useRef<HTMLInputElement>(null);

let normalizedSlug: React.ReactElement | undefined;
if (slug && React.isValidElement(slug)) {
const size = slug.props?.['kind'] === 'inline' ? 'md' : 'mini';
normalizedSlug = React.cloneElement(slug as React.ReactElement<any>, {
size,
});
let normalizedDecorator = React.isValidElement(slug ?? decorator)
? (slug ?? decorator)
: null;
if (
normalizedDecorator &&
normalizedDecorator['type']?.displayName === 'AILabel'
) {
const size =
(normalizedDecorator as ReactElement).props?.['kind'] === 'inline'
? 'md'
: 'mini';
normalizedDecorator = React.cloneElement(
normalizedDecorator as React.ReactElement<any>,
{
size,
}
);
}

return (
Expand All @@ -169,7 +189,16 @@ const RadioButton = React.forwardRef<HTMLInputElement, RadioButtonProps>(
{labelText && (
<Text className={innerLabelClasses}>
{labelText}
{normalizedSlug}
{slug ? (
normalizedDecorator
) : decorator ? (
<div
className={`${prefix}--radio-button-wrapper-inner--decorator`}>
{normalizedDecorator}
</div>
) : (
''
)}
</Text>
)}
</label>
Expand All @@ -191,6 +220,11 @@ RadioButton.propTypes = {
*/
className: PropTypes.string,

/**
* **Experimental**: Provide a decorator component to be rendered inside the `RadioButton` component
*/
decorator: PropTypes.node,

/**
* Specify whether the `<RadioButton>` should be checked by default
*/
Expand Down Expand Up @@ -247,7 +281,10 @@ RadioButton.propTypes = {
/**
* **Experimental**: Provide a `Slug` component to be rendered inside the `RadioButton` component
*/
slug: PropTypes.node,
slug: deprecate(
PropTypes.node,
'The `slug` prop has been deprecated and will be removed in the next major version. Use the decorator prop instead.'
),

/**
* Specify the value of the `<RadioButton>`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,38 @@ describe('RadioButton', () => {
expect(ref).toHaveBeenCalledWith(screen.getByRole('radio'));
});

it('should respect slug prop', () => {
it('should respect decorator prop', () => {
const { container } = render(
<RadioButton
name="test-name"
value="test-value"
labelText="test-label"
decorator={<AILabel />}
/>
);

expect(container.firstChild).toHaveClass(
`${prefix}--radio-button-wrapper--decorator`
);
});

it('should update AILabel size', () => {
const { container } = render(
<RadioButton
name="test-name"
value="test-value"
labelText="test-label"
decorator={<AILabel kind="inline" />}
/>
);

expect(container.querySelector(`.${prefix}--ai-label__button`)).toHaveClass(
`${prefix}--ai-label__button--md`
);
});

it('should respect the deprecated slug prop', () => {
const spy = jest.spyOn(console, 'warn').mockImplementation(() => {});
const { container } = render(
<RadioButton
name="test-name"
Expand All @@ -149,6 +180,7 @@ describe('RadioButton', () => {
expect(container.firstChild).toHaveClass(
`${prefix}--radio-button-wrapper--slug`
);
spy.mockRestore();
});

it('should set the "required" attribute on the <input> by default', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,21 @@ describe('RadioButtonGroup', () => {
);
});

it('should respect slug prop', () => {
it('should respect decorator prop', () => {
const { container } = render(
<RadioButtonGroup decorator={<AILabel />} name="test" legendText="test">
<RadioButton labelText="test-1" value={1} />
<RadioButton labelText="test-0" value={0} />
</RadioButtonGroup>
);

expect(container.firstChild.firstChild).toHaveClass(
`${prefix}--radio-button-group--decorator`
);
});

it('should respect deprecated slug prop', () => {
const spy = jest.spyOn(console, 'warn').mockImplementation(() => {});
const { container } = render(
<RadioButtonGroup slug={<AILabel />} name="test" legendText="test">
<RadioButton labelText="test-1" value={1} />
Expand All @@ -244,6 +258,7 @@ describe('RadioButtonGroup', () => {
expect(container.firstChild.firstChild).toHaveClass(
`${prefix}--radio-button-group--slug`
);
spy.mockRestore();
});

it('should call `onChange` when the value of the group changes', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import type { RadioButtonProps } from '../RadioButton';
import { Legend } from '../Text';
import { usePrefix } from '../../internal/usePrefix';
import { WarningFilled, WarningAltFilled } from '@carbon/icons-react';
import deprecate from '../../prop-types/deprecate';
import mergeRefs from '../../tools/mergeRefs';
import { useId } from '../../internal/useId';

Expand All @@ -40,6 +41,11 @@ export interface RadioButtonGroupProps
*/
className?: string;

/**
* **Experimental**: Provide a decorator component to be rendered inside the `RadioButtonGroup` component
*/
decorator?: ReactNode;

/**
* Specify the `<RadioButton>` to be selected by default
*/
Expand Down Expand Up @@ -102,6 +108,7 @@ export interface RadioButtonGroupProps
readOnly?: boolean;

/**
* @deprecated please use decorator instead.
* **Experimental**: Provide a `Slug` component to be rendered inside the `RadioButtonGroup` component
*/
slug?: ReactNode;
Expand Down Expand Up @@ -132,6 +139,7 @@ const RadioButtonGroup = React.forwardRef(
const {
children,
className,
decorator,
defaultSelected,
disabled,
helperText,
Expand Down Expand Up @@ -220,6 +228,7 @@ const RadioButtonGroup = React.forwardRef(
[`${prefix}--radio-button-group--invalid`]: !readOnly && invalid,
[`${prefix}--radio-button-group--warning`]: showWarning,
[`${prefix}--radio-button-group--slug`]: slug,
[`${prefix}--radio-button-group--decorator`]: decorator,
});

const helperClasses = classNames(`${prefix}--form__helper-text`, {
Expand All @@ -238,13 +247,21 @@ const RadioButtonGroup = React.forwardRef(

const divRef = useRef<HTMLDivElement>(null);

// Slug is always size `mini`
let normalizedSlug: ReactElement | undefined;
if (slug && slug['type']?.displayName === 'AILabel') {
normalizedSlug = React.cloneElement(slug as React.ReactElement<any>, {
size: 'mini',
kind: 'default',
});
// AILabel is always size `mini`
let normalizedDecorator = React.isValidElement(slug ?? decorator)
? (slug ?? decorator)
: null;
if (
normalizedDecorator &&
normalizedDecorator['type']?.displayName === 'AILabel'
) {
normalizedDecorator = React.cloneElement(
normalizedDecorator as React.ReactElement<any>,
{
size: 'mini',
kind: 'default',
}
);
}

return (
Expand All @@ -258,7 +275,16 @@ const RadioButtonGroup = React.forwardRef(
{legendText && (
<Legend className={`${prefix}--label`}>
{legendText}
{normalizedSlug}
{slug ? (
normalizedDecorator
) : decorator ? (
<div
className={`${prefix}--radio-button-group-inner--decorator`}>
{normalizedDecorator}
</div>
) : (
''
)}
</Legend>
)}
{getRadioButtons()}
Expand Down Expand Up @@ -298,6 +324,11 @@ RadioButtonGroup.propTypes = {
*/
className: PropTypes.string,

/**
* **Experimental**: Provide a decorator component to be rendered inside the `RadioButtonGroup` component
*/
decorator: PropTypes.node,

/**
* Specify the `<RadioButton>` to be selected by default
*/
Expand Down Expand Up @@ -363,7 +394,10 @@ RadioButtonGroup.propTypes = {
/**
* **Experimental**: Provide a `Slug` component to be rendered inside the `RadioButtonGroup` component
*/
slug: PropTypes.node,
slug: deprecate(
PropTypes.node,
'The `slug` prop has been deprecated and will be removed in the next major version. Use the decorator prop instead.'
),

/**
* Specify the value that is currently selected in the group
Expand Down
Loading

0 comments on commit e11a4bd

Please sign in to comment.