Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(tag): new decorator prop #18077

Merged
Merged
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
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 @@ -2879,6 +2879,9 @@ Map {
"className": Object {
"type": "string",
},
"decorator": Object {
"type": "node",
},
"disabled": Object {
"type": "bool",
},
Expand Down Expand Up @@ -2911,9 +2914,7 @@ Map {
],
"type": "oneOf",
},
"slug": Object {
"type": "node",
},
"slug": [Function],
"tagTitle": Object {
"type": "string",
},
Expand Down Expand Up @@ -8442,6 +8443,9 @@ Map {
"className": Object {
"type": "string",
},
"decorator": Object {
"type": "node",
},
"disabled": Object {
"type": "bool",
},
Expand Down Expand Up @@ -8473,9 +8477,7 @@ Map {
],
"type": "oneOf",
},
"slug": Object {
"type": "node",
},
"slug": [Function],
"title": [Function],
"type": Object {
"args": Array [
Expand Down
48 changes: 40 additions & 8 deletions packages/react/src/components/Tag/DismissibleTag.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import classNames from 'classnames';
import { useId } from '../../internal/useId';
import { usePrefix } from '../../internal/usePrefix';
import { PolymorphicProps } from '../../types/common';
import deprecate from '../../prop-types/deprecate';
import Tag, { SIZES, TYPES } from './Tag';
import { Close } from '@carbon/icons-react';
import { Tooltip } from '../Tooltip';
Expand All @@ -23,6 +24,11 @@ export interface DismissibleTagBaseProps {
*/
className?: string;

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

/**
* Specify if the `DismissibleTag` is disabled
*/
Expand Down Expand Up @@ -51,6 +57,7 @@ export interface DismissibleTagBaseProps {
size?: keyof typeof SIZES;

/**
* @deprecated please use `decorator` instead.
* **Experimental:** Provide a `Slug` component to be rendered inside the `DismissibleTag` component
*/
slug?: ReactNode;
Expand Down Expand Up @@ -83,6 +90,7 @@ export type DismissibleTagProps<T extends React.ElementType> = PolymorphicProps<

const DismissibleTag = <T extends React.ElementType>({
className,
decorator,
disabled,
id,
renderIcon,
Expand Down Expand Up @@ -114,12 +122,20 @@ const DismissibleTag = <T extends React.ElementType>({
}
};

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

const tooltipClasses = classNames(
Expand Down Expand Up @@ -149,7 +165,15 @@ const DismissibleTag = <T extends React.ElementType>({
className={`${prefix}--tag__label`}>
{text}
</Text>
{normalizedSlug}
{slug ? (
normalizedDecorator
) : decorator ? (
<div className={`${prefix}--tag__decorator`}>
{normalizedDecorator}
</div>
) : (
''
)}
<Tooltip
label={isEllipsisApplied ? dismissLabel : title}
align="bottom"
Expand All @@ -175,6 +199,11 @@ DismissibleTag.propTypes = {
*/
className: PropTypes.string,

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

/**
* Specify if the `DismissibleTag` is disabled
*/
Expand Down Expand Up @@ -205,7 +234,10 @@ DismissibleTag.propTypes = {
/**
* **Experimental:** Provide a `Slug` component to be rendered inside the `DismissibleTag` 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.'
),

/**
* Provide text to be rendered inside of a the tag.
Expand Down
44 changes: 43 additions & 1 deletion packages/react/src/components/Tag/Tag-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,38 @@ describe('Tag', () => {
// requirement
expect(accessibilityLabel).toEqual(expect.stringContaining('Close tag'));
});

it('should respect decorator prop', () => {
render(
<DismissibleTag
type="red"
title="Close tag"
text="Tag content"
decorator={<AILabel />}
/>
);

expect(
screen.getByRole('button', { name: 'AI - Show information' })
).toBeInTheDocument();
});

it('should respect deprecated slug prop', () => {
const spy = jest.spyOn(console, 'warn').mockImplementation(() => {});
render(
<DismissibleTag
type="red"
title="Close tag"
text="Tag content"
slug={<AILabel />}
/>
);

expect(
screen.getByRole('button', { name: 'AI - Show information' })
).toBeInTheDocument();
spy.mockRestore();
});
});

it('should allow for a custom label', () => {
Expand All @@ -90,12 +122,22 @@ describe('Tag', () => {
expect(screen.getByTestId('test')).toBeInTheDocument();
});

it('should respect slug prop', () => {
it('should respect decorator prop', () => {
render(<Tag type="red" decorator={<AILabel />} />);

expect(
screen.getByRole('button', { name: 'AI - Show information' })
).toBeInTheDocument();
});

it('should respect deprecated slug prop', () => {
const spy = jest.spyOn(console, 'warn').mockImplementation(() => {});
render(<Tag type="red" slug={<AILabel />} />);

expect(
screen.getByRole('button', { name: 'AI - Show information' })
).toBeInTheDocument();
spy.mockRestore();
});

describe('Selectable Tag', () => {
Expand Down
29 changes: 15 additions & 14 deletions packages/react/src/components/Tag/Tag.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import React from 'react';
import { default as Tag } from '../Tag';
import TagSkeleton from '../Tag/Tag.Skeleton';
import DismissibleTag from '../Tag/DismissibleTag';
import { Asleep, View, FolderOpen, Folders } from '@carbon/icons-react';
import Button from '../Button';
import { AILabel, AILabelContent, AILabelActions } from '../AILabel';
Expand Down Expand Up @@ -217,36 +218,36 @@ const aiLabel = (

export const withAILabel = () => (
<div style={{ marginBottom: '4rem' }}>
<Tag slug={aiLabel} className="some-class" type="red" title="Clear Filter">
{'Tag'}
</Tag>

<Tag
filter
slug={aiLabel}
decorator={aiLabel}
className="some-class"
type="purple"
type="red"
title="Clear Filter">
{'Tag'}
</Tag>

<DismissibleTag
decorator={aiLabel}
className="some-class"
type="purple"
title="Clear Filter"
text="Tag"></DismissibleTag>

<Tag
renderIcon={Asleep}
slug={aiLabel}
decorator={aiLabel}
className="some-class"
type="blue"
title="Clear Filter">
{'Tag'}
</Tag>

<Tag
filter
<DismissibleTag
renderIcon={Asleep}
slug={aiLabel}
decorator={aiLabel}
className="some-class"
type="green"
title="Clear Filter">
{'Tag'}
</Tag>
title="Clear Filter"
text="Tag"></DismissibleTag>
</div>
);
51 changes: 40 additions & 11 deletions packages/react/src/components/Tag/Tag.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ export interface TagBaseProps {
*/
className?: string;

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

/**
* Specify if the `Tag` is disabled
*/
Expand Down Expand Up @@ -89,6 +94,7 @@ export interface TagBaseProps {
size?: keyof typeof SIZES;

/**
* @deprecated please use `decorator` instead.
* **Experimental:** Provide a `Slug` component to be rendered inside the `Tag` component
*/
slug?: ReactNode;
Expand All @@ -113,6 +119,7 @@ const Tag = React.forwardRef(function Tag<T extends React.ElementType>(
{
children,
className,
decorator,
id,
type,
filter, // remove filter in next major release - V12
Expand Down Expand Up @@ -168,13 +175,22 @@ const Tag = React.forwardRef(function Tag<T extends React.ElementType>(
}
};

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

if (filter) {
Expand All @@ -194,7 +210,7 @@ const Tag = React.forwardRef(function Tag<T extends React.ElementType>(
className={`${prefix}--tag__label`}>
{children !== null && children !== undefined ? children : typeText}
</Text>
{normalizedSlug}
{normalizedDecorator}
<button
type="button"
className={`${prefix}--tag__close-icon`}
Expand Down Expand Up @@ -265,8 +281,13 @@ const Tag = React.forwardRef(function Tag<T extends React.ElementType>(
{children !== null && children !== undefined ? children : typeText}
</Text>
)}

{normalizedSlug}
{slug ? (
normalizedDecorator
) : decorator ? (
<div className={`${prefix}--tag__decorator`}>{normalizedDecorator}</div>
) : (
''
)}
</ComponentTag>
);
});
Expand All @@ -288,6 +309,11 @@ Tag.propTypes = {
*/
className: PropTypes.string,

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

/**
* Specify if the `Tag` is disabled
*/
Expand Down Expand Up @@ -329,7 +355,10 @@ Tag.propTypes = {
/**
* **Experimental:** Provide a `Slug` component to be rendered inside the `Tag` 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.'
),

/**
* Text to show on clear filters
Expand Down
8 changes: 8 additions & 0 deletions packages/styles/scss/components/tag/_tag.scss
Original file line number Diff line number Diff line change
Expand Up @@ -413,11 +413,19 @@
border-color: currentColor;
}

.#{$prefix}--tag--filter .#{$prefix}--tag__decorator > *,
.#{$prefix}--tag--filter .#{$prefix}--ai-label,
.#{$prefix}--tag--filter .#{$prefix}--slug {
min-inline-size: convert.to-rem(32.14px);
}

// Decorator styles
.#{$prefix}--tag
.#{$prefix}--tag__decorator:not(:has(.#{$prefix}--ai-label)) {
block-size: 1rem;
text-align: center;
}

// Windows HCM fix

.#{$prefix}--tag {
Expand Down
Loading