Skip to content

Commit

Permalink
feat(checkbox): replace slug prop with decorator (#18040)
Browse files Browse the repository at this point in the history
* feat(checkbox): replace slug prop with decorator

* chore: update tests

* chore: add test

* chore: typo

* Update packages/react/src/components/Checkbox/Checkbox.tsx

Co-authored-by: Ariella Gilmore <ariellalgilmore@gmail.com>

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

Co-authored-by: Ariella Gilmore <ariellalgilmore@gmail.com>

* chore: cleanup styles

* feat: update checkbox decorator styles

* chore: typo

* chore: cleanup styles

* chore: format

* chore: revert storybook test changes

* chore: typo

---------

Co-authored-by: Ariella Gilmore <ariellalgilmore@gmail.com>
Co-authored-by: Guilherme Datilio Ribeiro <guilhermedatilio@gmail.com>
  • Loading branch information
3 people authored Nov 20, 2024
1 parent 0582ce3 commit cf5dc88
Show file tree
Hide file tree
Showing 7 changed files with 179 additions and 35 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 @@ -613,6 +613,9 @@ Map {
"className": Object {
"type": "string",
},
"decorator": Object {
"type": "node",
},
"defaultChecked": Object {
"type": "bool",
},
Expand Down Expand Up @@ -648,9 +651,7 @@ Map {
"readOnly": Object {
"type": "bool",
},
"slug": Object {
"type": "node",
},
"slug": [Function],
"title": Object {
"type": "string",
},
Expand All @@ -671,6 +672,9 @@ Map {
"className": Object {
"type": "string",
},
"decorator": Object {
"type": "node",
},
"helperText": Object {
"type": "node",
},
Expand Down Expand Up @@ -699,9 +703,7 @@ Map {
"readOnly": Object {
"type": "bool",
},
"slug": Object {
"type": "node",
},
"slug": [Function],
"warn": Object {
"type": "bool",
},
Expand Down
10 changes: 5 additions & 5 deletions packages/react/src/components/Checkbox/Checkbox.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ const AILabelFunc = (kind) => (

export const withAILabel = () => (
<div className="ai-label-check-radio-container">
<CheckboxGroup legendText="Group Label" slug={AILabelFunc()}>
<CheckboxGroup legendText="Group Label" decorator={AILabelFunc()}>
<Checkbox labelText={`Checkbox label`} id="checkbox-label-1" />
<Checkbox labelText={`Checkbox label`} id="checkbox-label-2" />
<Checkbox labelText={`Checkbox label`} id="checkbox-label-3" />
Expand All @@ -134,12 +134,12 @@ export const withAILabel = () => (
<Checkbox
labelText={`Checkbox label`}
id="checkbox-label-4"
slug={AILabelFunc()}
decorator={AILabelFunc()}
/>
<Checkbox
labelText={`Checkbox label`}
id="checkbox-label-5"
slug={AILabelFunc()}
decorator={AILabelFunc()}
/>
<Checkbox labelText={`Checkbox label`} id="checkbox-label-6" />
</CheckboxGroup>
Expand All @@ -148,12 +148,12 @@ export const withAILabel = () => (
<Checkbox
labelText={`Checkbox label`}
id="checkbox-label-7"
slug={AILabelFunc('inline')}
decorator={AILabelFunc('inline')}
/>
<Checkbox
labelText={`Checkbox label`}
id="checkbox-label-8"
slug={AILabelFunc('inline')}
decorator={AILabelFunc('inline')}
/>
<Checkbox labelText={`Checkbox label`} id="checkbox-label-9" />
</CheckboxGroup>
Expand Down
54 changes: 45 additions & 9 deletions packages/react/src/components/Checkbox/Checkbox.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 } from 'react';
import React, { ReactElement, ReactNode } from 'react';
import classNames from 'classnames';
import { Text } from '../Text';
import deprecate from '../../prop-types/deprecate';
import { usePrefix } from '../../internal/usePrefix';
import { WarningFilled, WarningAltFilled } from '@carbon/icons-react';
import { useId } from '../../internal/useId';
Expand All @@ -32,6 +33,11 @@ export interface CheckboxProps
*/
labelText: NonNullable<ReactNode>;

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

/**
* Specify whether the underlying input should be checked by default
*/
Expand Down Expand Up @@ -68,6 +74,7 @@ export interface CheckboxProps
invalidText?: ReactNode;

/**
* @deprecated please use decorator instead.
* **Experimental**: Provide a `Slug` component to be rendered inside the `Checkbox` component
*/
slug?: ReactNode;
Expand Down Expand Up @@ -102,6 +109,7 @@ const Checkbox = React.forwardRef(
(
{
className,
decorator,
helperText,
id,
labelText,
Expand Down Expand Up @@ -146,18 +154,30 @@ const Checkbox = React.forwardRef(
[`${prefix}--checkbox-wrapper--invalid`]: !readOnly && invalid,
[`${prefix}--checkbox-wrapper--warning`]: showWarning,
[`${prefix}--checkbox-wrapper--slug`]: slug,
[`${prefix}--checkbox-wrapper--decorator`]: decorator,
}
);
const innerLabelClasses = classNames(`${prefix}--checkbox-label-text`, {
[`${prefix}--visually-hidden`]: hideLabel,
});

let normalizedSlug;
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 Down Expand Up @@ -203,7 +223,15 @@ const Checkbox = React.forwardRef(
title={title}>
<Text className={innerLabelClasses}>
{labelText}
{normalizedSlug}
{slug ? (
normalizedDecorator
) : decorator ? (
<div className={`${prefix}--checkbox-wrapper-inner--decorator`}>
{normalizedDecorator}
</div>
) : (
''
)}
</Text>
</label>
<div className={`${prefix}--checkbox__validation-msg`}>
Expand Down Expand Up @@ -239,6 +267,11 @@ Checkbox.propTypes = {
*/
className: PropTypes.string,

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

/**
* Specify whether the underlying input should be checked by default
*/
Expand Down Expand Up @@ -300,7 +333,10 @@ Checkbox.propTypes = {
/**
* **Experimental**: Provide a `Slug` component to be rendered inside the `Checkbox` 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 a title for the <label> node for the Checkbox
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,8 @@ describe('Checkbox', () => {
expect(onChange).not.toHaveBeenCalled();
});

it('should respect slug prop', () => {
it('should respect deprecated slug prop', () => {
const spy = jest.spyOn(console, 'warn').mockImplementation(() => {});
const { container } = render(
<Checkbox
defaultChecked
Expand All @@ -212,5 +213,36 @@ describe('Checkbox', () => {
expect(container.firstChild).toHaveClass(
`${prefix}--checkbox-wrapper--slug`
);
spy.mockRestore();
});

it('should respect decorator prop', () => {
const { container } = render(
<Checkbox
defaultChecked
labelText="Checkbox label"
id="checkbox-label-1"
decorator={<AILabel />}
/>
);

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

it('should set size to "md" when decorator kind is "inline"', () => {
const { container } = render(
<Checkbox
defaultChecked
labelText="Checkbox label"
id="checkbox-label-2"
decorator={<AILabel kind="inline" />}
/>
);

expect(
container.querySelector('.cds--ai-label__button--md')
).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,8 @@ describe('CheckboxGroup', () => {
);
});

it('should respect slug prop', () => {
it('should respect deprecated slug prop', () => {
const spy = jest.spyOn(console, 'warn').mockImplementation(() => {});
const { container } = render(
<CheckboxGroup
className="some-class"
Expand All @@ -160,7 +161,23 @@ describe('CheckboxGroup', () => {
);

expect(container.firstChild).toHaveClass(`${prefix}--checkbox-group--slug`);
spy.mockRestore();
});

it('should respect decorator prop', () => {
const { container } = render(
<CheckboxGroup
className="some-class"
legendText="Checkbox heading"
decorator={<AILabel />}
/>
);

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

it('should render checkboxes horizontally', () => {
const { container } = render(
<CheckboxGroup orientation="horizontal" legendText="test-horizental-prop">
Expand Down
52 changes: 41 additions & 11 deletions packages/react/src/components/CheckboxGroup/CheckboxGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,28 @@
*/

import PropTypes from 'prop-types';
import React, { ReactNode } from 'react';
import React, { ReactElement, ReactNode } from 'react';
import cx from 'classnames';
import deprecate from '../../prop-types/deprecate';
import { usePrefix } from '../../internal/usePrefix';
import { WarningFilled, WarningAltFilled } from '@carbon/icons-react';
import { useId } from '../../internal/useId';

export interface CheckboxGroupProps {
children?: ReactNode;
className?: string;
decorator?: ReactNode;
helperText?: ReactNode;
invalid?: boolean;
invalidText?: ReactNode;
legendId?: ReactNode;
orientation?: 'horizontal' | 'vertical';
legendText: ReactNode;
readOnly?: boolean;
/**
* * @deprecated please use decorator instead.
* **Experimental**: Provide a `Slug` component to be rendered inside the `Checkbox` component
*/
slug?: ReactNode;
warn?: boolean;
warnText?: ReactNode;
Expand All @@ -35,6 +41,7 @@ export interface CustomType {
const CheckboxGroup: React.FC<CheckboxGroupProps> = ({
children,
className,
decorator,
helperText,
invalid,
invalidText,
Expand Down Expand Up @@ -70,19 +77,26 @@ const CheckboxGroup: React.FC<CheckboxGroupProps> = ({
[`${prefix}--checkbox-group--invalid`]: !readOnly && invalid,
[`${prefix}--checkbox-group--warning`]: showWarning,
[`${prefix}--checkbox-group--slug`]: slug,
[`${prefix}--checkbox-group--decorator`]: decorator,
});

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

return (
<fieldset
className={fieldsetClasses}
Expand All @@ -95,7 +109,15 @@ const CheckboxGroup: React.FC<CheckboxGroupProps> = ({
className={`${prefix}--label`}
id={legendId || rest['aria-labelledby']}>
{legendText}
{normalizedSlug}
{slug ? (
normalizedDecorator
) : decorator ? (
<div className={`${prefix}--checkbox-group-inner--decorator`}>
{normalizedDecorator}
</div>
) : (
''
)}
</legend>
{children}
<div className={`${prefix}--checkbox-group__validation-msg`}>
Expand Down Expand Up @@ -130,6 +152,11 @@ CheckboxGroup.propTypes = {
*/
className: PropTypes.string,

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

/**
* Provide text for the form group for additional help
*/
Expand Down Expand Up @@ -168,7 +195,10 @@ CheckboxGroup.propTypes = {
/**
* **Experimental**: Provide a `Slug` component to be rendered inside the `CheckboxGroup` 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 whether the form group is currently in warning state
Expand Down
Loading

0 comments on commit cf5dc88

Please sign in to comment.