Skip to content

Commit

Permalink
feat(tag): new decorator prop (#18077)
Browse files Browse the repository at this point in the history
* feat(tag): new decorator prop

* feat(tag-dismissible): add decorator

* fix(tag): format and remove filter updates only add to dissmissible

* fix(tag): format

* chore(tag): remove extra stories
  • Loading branch information
ariellalgilmore authored Nov 20, 2024
1 parent b53f566 commit 633ab3c
Show file tree
Hide file tree
Showing 6 changed files with 154 additions and 40 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 @@ -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

0 comments on commit 633ab3c

Please sign in to comment.