From 536d3422de93f869db7703e3761b245d4b8a68c2 Mon Sep 17 00:00:00 2001 From: Guilherme Datilio Ribeiro Date: Wed, 6 Mar 2024 17:20:57 -0300 Subject: [PATCH] feat: added interactive tag (#15808) * feat: added interactive tag * fix: fixed changes after meeting with team * test: added tests * fix: fixed typescript error using any like Toggletip does * test: fixed test * test: fixed avt test * test: fixed playwright test * fix: fixed snapshot and style for operational tag * fix: fixed TJ comments * fix: fixed nested button with operational tag * fix: fixed Lauren comments * fix: fixed operational tag with popover * fix: fixed overview page code * fix: fixed Lauren comments * test: fixed avt test * fix: fixed percy errors * test: removed skip from test * docs: fixed docs * fix: removed onClick from selectable tag * fix: fixed issue 15820 from toggletip a11y * fix: fixed typescript * fix: fixed typescript error --- .../InteractiveTag-test.avt.e2e.js | 108 +++++ .../InteractiveTag/InteractiveTag-test.e2e.js | 42 ++ e2e/components/Tag/Tag-test.avt.e2e.js | 21 +- .../__snapshots__/PublicAPI-test.js.snap | 27 +- packages/react/src/__tests__/index-test.js | 1 - .../src/components/Tag/DismissibleTag.tsx | 194 ++++++++ .../src/components/Tag/InteractiveTag.mdx | 89 ++++ .../components/Tag/InteractiveTag.stories.js | 424 ++++++++++++++++++ .../src/components/Tag/OperationalTag.tsx | 166 +++++++ .../src/components/Tag/SelectableTag.tsx | 159 +++++++ packages/react/src/components/Tag/Tag-test.js | 5 +- .../react/src/components/Tag/Tag.stories.js | 37 +- packages/react/src/components/Tag/Tag.tsx | 73 ++- .../src/components/Tag/docs/overview.mdx | 8 +- packages/react/src/components/Tag/index.ts | 1 - .../components/Tag/storyInteractiveTag.scss | 5 + .../react/src/components/Toggletip/index.tsx | 28 +- .../scss/components/popover/_popover.scss | 12 + .../styles/scss/components/tag/_mixins.scss | 16 +- packages/styles/scss/components/tag/_tag.scss | 186 ++++++-- .../styles/scss/components/tag/_tokens.scss | 265 +++++++++++ .../themes/src/component-tokens/tag/tokens.js | 123 ++++- .../src/tokens/__tests__/metadata-test.js | 40 ++ packages/themes/src/tokens/components.js | 10 + 24 files changed, 1891 insertions(+), 149 deletions(-) create mode 100644 e2e/components/InteractiveTag/InteractiveTag-test.avt.e2e.js create mode 100644 e2e/components/InteractiveTag/InteractiveTag-test.e2e.js create mode 100644 packages/react/src/components/Tag/DismissibleTag.tsx create mode 100644 packages/react/src/components/Tag/InteractiveTag.mdx create mode 100644 packages/react/src/components/Tag/InteractiveTag.stories.js create mode 100644 packages/react/src/components/Tag/OperationalTag.tsx create mode 100644 packages/react/src/components/Tag/SelectableTag.tsx create mode 100644 packages/react/src/components/Tag/storyInteractiveTag.scss diff --git a/e2e/components/InteractiveTag/InteractiveTag-test.avt.e2e.js b/e2e/components/InteractiveTag/InteractiveTag-test.avt.e2e.js new file mode 100644 index 000000000000..0850ab819921 --- /dev/null +++ b/e2e/components/InteractiveTag/InteractiveTag-test.avt.e2e.js @@ -0,0 +1,108 @@ +/** + * Copyright IBM Corp. 2016, 2023 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +'use strict'; + +import { expect, test } from '@playwright/test'; +import { visitStory } from '../../test-utils/storybook'; + +test.describe('@avt InteractiveTag', () => { + test('@avt-advanced-states DismissibleTag', async ({ page }) => { + await visitStory(page, { + component: 'Tag', + id: 'experimental-unstable-interactivetag--dismissible', + globals: { + theme: 'white', + }, + }); + await expect(page).toHaveNoACViolations('DismissibleTag'); + }); + + // Testing being skipped because it is failing in the ToggleTip that operational it's using + test('@avt-advanced-states OperationalTag', async ({ page }) => { + await visitStory(page, { + component: 'Tag', + id: 'experimental-unstable-interactivetag--operational', + globals: { + theme: 'white', + }, + }); + await expect(page).toHaveNoACViolations('OperationalTag'); + }); + + test('@avt-advanced-states SelectableTag', async ({ page }) => { + await visitStory(page, { + component: 'Tag', + id: 'experimental-unstable-interactivetag--selectable', + globals: { + theme: 'white', + }, + }); + await expect(page).toHaveNoACViolations('SelectableTag'); + }); + + test('@avt-keyboard-nav DismissibleTag', async ({ page }) => { + await visitStory(page, { + component: 'Tag', + id: 'experimental-unstable-interactivetag--dismissible', + globals: { + theme: 'white', + }, + }); + const button = page.getByRole('button').first(); + await expect(button).toBeVisible(); + await page.keyboard.press('Tab'); + await expect(button).toBeFocused(); + }); + + test('@avt-keyboard-nav OperationalTag', async ({ page }) => { + await visitStory(page, { + component: 'Tag', + id: 'experimental-unstable-interactivetag--operational', + globals: { + theme: 'white', + }, + }); + const button = page.getByRole('button').first(); + await expect(button).toBeVisible(); + await page.keyboard.press('Tab'); + await expect(button).toBeFocused(); + await expect(button).toHaveClass(/cds--tag--red/); + + await page.keyboard.press('Tab'); + await page.keyboard.press('Tab'); + await page.keyboard.press('Tab'); + await page.keyboard.press('Tab'); + await page.keyboard.press('Tab'); + await page.keyboard.press('Tab'); + await page.keyboard.press('Tab'); + await page.keyboard.press('Tab'); + await page.keyboard.press('Tab'); + await page.keyboard.press('Tab'); + + // Expecte the OperationalTag with tooltip be focusable and visible + await expect(page.getByRole('button').nth(10)).toBeFocused(); + await page.keyboard.press('Enter'); + await expect(page.getByText('View More')).toBeVisible(); + }); + + test('@avt-keyboard-nav SelectableTag', async ({ page }) => { + await visitStory(page, { + component: 'Tag', + id: 'experimental-unstable-interactivetag--selectable', + globals: { + theme: 'white', + }, + }); + const tag = page.getByRole('button').first(); + await expect(tag).toBeVisible(); + await page.keyboard.press('Tab'); + await expect(tag).toBeFocused(); + await page.keyboard.press('Enter'); + await expect(tag).toHaveClass(/cds--tag--selectable-selected/); + }); +}); diff --git a/e2e/components/InteractiveTag/InteractiveTag-test.e2e.js b/e2e/components/InteractiveTag/InteractiveTag-test.e2e.js new file mode 100644 index 000000000000..fe9795fd3fa8 --- /dev/null +++ b/e2e/components/InteractiveTag/InteractiveTag-test.e2e.js @@ -0,0 +1,42 @@ +/** + * Copyright IBM Corp. 2016, 2023 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +'use strict'; + +import { test } from '@playwright/test'; +import { themes } from '../../test-utils/env'; +import { snapshotStory } from '../../test-utils/storybook'; + +test.describe('InteractiveTag', () => { + themes.forEach((theme) => { + test.describe(theme, () => { + test('DismissibleTag @vrt', async ({ page }) => { + await snapshotStory(page, { + component: 'DismissibleTag', + id: 'experimental-unstable-interactivetag--dismissible', + theme, + }); + }); + + test('OperationalTag @vrt', async ({ page }) => { + await snapshotStory(page, { + component: 'OperationalTag', + id: 'experimental-unstable-interactivetag--operational', + theme, + }); + }); + + test('SelectableTag @vrt', async ({ page }) => { + await snapshotStory(page, { + component: 'SelectableTag', + id: 'experimental-unstable-interactivetag--selectable', + theme, + }); + }); + }); + }); +}); diff --git a/e2e/components/Tag/Tag-test.avt.e2e.js b/e2e/components/Tag/Tag-test.avt.e2e.js index ec62bf644055..37debfcac4b4 100644 --- a/e2e/components/Tag/Tag-test.avt.e2e.js +++ b/e2e/components/Tag/Tag-test.avt.e2e.js @@ -14,7 +14,7 @@ test.describe('@avt Tag', () => { test('@avt-default-state Tag', async ({ page }) => { await visitStory(page, { component: 'Tag', - id: 'components-tag--default', + id: 'components-tag--read-only', globals: { theme: 'white', }, @@ -32,23 +32,4 @@ test.describe('@avt Tag', () => { }); await expect(page).toHaveNoACViolations('Tag-skeleton'); }); - - test('@avt-keyboard-nav Tag', async ({ page }) => { - await visitStory(page, { - component: 'Tag', - id: 'components-tag--playground', - globals: { - theme: 'white', - }, - args: { - filter: true, - }, - }); - - await expect(page.getByText('Tag content')).toBeVisible(); - await page.keyboard.press('Tab'); - await expect( - page.getByRole('button', { name: 'Clear filter' }) - ).toBeFocused(); - }); }); diff --git a/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap b/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap index c5be36655130..a3e198b61222 100644 --- a/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap +++ b/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap @@ -8179,15 +8179,11 @@ Map { "disabled": Object { "type": "bool", }, - "filter": Object { - "type": "bool", - }, + "filter": [Function], "id": Object { "type": "string", }, - "onClose": Object { - "type": "func", - }, + "onClose": [Function], "renderIcon": Object { "args": Array [ Array [ @@ -8206,6 +8202,7 @@ Map { Array [ "sm", "md", + "lg", ], ], "type": "oneOf", @@ -8213,9 +8210,7 @@ Map { "slug": Object { "type": "node", }, - "title": Object { - "type": "string", - }, + "title": [Function], "type": Object { "args": Array [ Array [ @@ -9426,20 +9421,6 @@ Map { "$$typeof": Symbol(react.forward_ref), "render": [Function], }, - "types" => Object { - "0": "red", - "1": "magenta", - "10": "high-contrast", - "11": "outline", - "2": "purple", - "3": "blue", - "4": "cyan", - "5": "teal", - "6": "green", - "7": "gray", - "8": "cool-gray", - "9": "warm-gray", - }, "unstable_FeatureFlags" => Object { "propTypes": Object { "children": Object { diff --git a/packages/react/src/__tests__/index-test.js b/packages/react/src/__tests__/index-test.js index 40c89429b1b9..4505669d263d 100644 --- a/packages/react/src/__tests__/index-test.js +++ b/packages/react/src/__tests__/index-test.js @@ -238,7 +238,6 @@ describe('Carbon Components React', () => { "TreeView", "UnorderedList", "VStack", - "types", "unstable_FeatureFlags", "unstable_Layout", "unstable_LayoutDirection", diff --git a/packages/react/src/components/Tag/DismissibleTag.tsx b/packages/react/src/components/Tag/DismissibleTag.tsx new file mode 100644 index 000000000000..b4095fd5824a --- /dev/null +++ b/packages/react/src/components/Tag/DismissibleTag.tsx @@ -0,0 +1,194 @@ +/** + * Copyright IBM Corp. 2016, 2023 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import PropTypes, { ReactNodeLike } from 'prop-types'; +import React from 'react'; +import classNames from 'classnames'; +import setupGetInstanceId from '../../tools/setupGetInstanceId'; +import { usePrefix } from '../../internal/usePrefix'; +import { PolymorphicProps } from '../../types/common'; +import Tag, { SIZES, TYPES } from './Tag'; +import { Close } from '@carbon/icons-react'; + +const getInstanceId = setupGetInstanceId(); + +export interface DismissibleTagBaseProps { + /** + * Provide content to be rendered inside of a `DismissibleTag` + */ + children?: React.ReactNode; + + /** + * Provide a custom className that is applied to the containing + */ + className?: string; + + /** + * Specify if the `DismissibleTag` is disabled + */ + disabled?: boolean; + + /** + * Specify the id for the selectabletag. + */ + id?: string; + + /** + * Click handler for filter tag close button. + */ + onClose?: (event: React.MouseEvent) => void; + + /** + * Optional prop to render a custom icon. + * Can be a React component class + */ + renderIcon?: React.ElementType; + + /** + * Specify the size of the Tag. Currently supports either `sm`, + * `md` (default) or `lg` sizes. + */ + size?: keyof typeof SIZES; + + /** + * **Experimental:** Provide a `Slug` component to be rendered inside the `DismissibleTag` component + */ + slug?: ReactNodeLike; + + /** + * Text to show on clear filters + */ + title?: string; + + /** + * Specify the type of the `Tag` + */ + type?: keyof typeof TYPES; +} + +export type DismissibleTagProps = PolymorphicProps< + T, + DismissibleTagBaseProps +>; + +const DismissibleTag = ({ + children, + className, + disabled, + id, + renderIcon, + title = 'Clear filter', + onClose, + slug, + size, + type, + ...other +}: DismissibleTagProps) => { + const prefix = usePrefix(); + const tagId = id || `tag-${getInstanceId()}`; + const tagClasses = classNames(`${prefix}--tag--filter`, className); + + const handleClose = (event: React.MouseEvent) => { + if (onClose) { + event.stopPropagation(); + onClose(event); + } + }; + + let normalizedSlug; + if (slug && slug['type']?.displayName === 'Slug') { + normalizedSlug = React.cloneElement(slug as React.ReactElement, { + size: 'sm', + kind: 'inline', + }); + } + + // Removing onClick from the spread operator + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { onClick, ...otherProps } = other; + + return ( + + type={type} + size={size} + renderIcon={renderIcon} + disabled={disabled} + className={tagClasses} + id={tagId} + {...otherProps}> +
+ {children} + {normalizedSlug} + +
+ + ); +}; +DismissibleTag.propTypes = { + /** + * Provide content to be rendered inside of a `DismissibleTag` + */ + children: PropTypes.node, + + /** + * Provide a custom className that is applied to the containing + */ + className: PropTypes.string, + + /** + * Specify if the `DismissibleTag` is disabled + */ + disabled: PropTypes.bool, + + /** + * Specify the id for the tag. + */ + id: PropTypes.string, + + /** + * Click handler for filter tag close button. + */ + onClose: PropTypes.func, + + /** + * Optional prop to render a custom icon. + * Can be a React component class + */ + renderIcon: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), + + /** + * Specify the size of the Tag. Currently supports either `sm`, + * `md` (default) or `lg` sizes. + */ + size: PropTypes.oneOf(Object.keys(SIZES)), + + /** + * **Experimental:** Provide a `Slug` component to be rendered inside the `DismissibleTag` component + */ + slug: PropTypes.node, + + /** + * Text to show on clear filters + */ + title: PropTypes.string, + + /** + * Specify the type of the `Tag` + */ + type: PropTypes.oneOf(Object.keys(TYPES)), +}; + +export const types = Object.keys(TYPES); +export default DismissibleTag; diff --git a/packages/react/src/components/Tag/InteractiveTag.mdx b/packages/react/src/components/Tag/InteractiveTag.mdx new file mode 100644 index 000000000000..0ff2b9f481b2 --- /dev/null +++ b/packages/react/src/components/Tag/InteractiveTag.mdx @@ -0,0 +1,89 @@ +import { Canvas, Story } from '@storybook/blocks'; +import * as InteractiveTagStories from './InteractiveTag.stories'; + +# Interactive Tag + +[Source code](https://github.com/carbon-design-system/carbon/tree/main/packages/react/src/components/Tag) + |  +[Usage guidelines](https://www.carbondesignsystem.com/components/tag/usage) + |  +[Accessibility](https://www.carbondesignsystem.com/components/tag/accessibility) + +## Overview + +Tags can be used to categorize items. Use short labels for easy scanning. Use +two words only if necessary to describe the status and differentiate it from +other tags. + +## Dismissible + +Dismissible tags are used to remove tags that can be filtered out by the user. + +When to use: + +- Use DismissibleTag when you want to allow users to easily clear or dismiss + specific filters applied to items. + + + + + +```jsx + + {'Tag content'} + +``` + +## Selectable + +Selectable tags are used to select or unselect single or multiple items. +Selectable tags can also be used in filtering by label scenarios when they do +not need to be dismissed. + +When to use: + +- Use selectable tags to toggle between a selected or unselected state. +- Use when needing to filter without an explicit dismissable functionality. (We + need to be a little more specific around filtering for selectable versus + dismissable tags). + + + + + +```jsx + + {'Tag content'} + +``` + +## Operational + +Operational tags enable the user to view a list of all items associated with a +given tag in different ways. + +When to use: + +- Use to view a list of items with the same tag in a toggletip, popover, or + breadcrumb detail view. + +When not to use: + +- Do not use operational tags as a replacement for links that direct you to an + entirely different page or launch you out to another tab. +- Do not use in combination with Dismissable tags. Instead, consider letting the + user enter an "edit mode" to dismiss tags. + + + + + +```jsx + + {'Tag content'} + +``` diff --git a/packages/react/src/components/Tag/InteractiveTag.stories.js b/packages/react/src/components/Tag/InteractiveTag.stories.js new file mode 100644 index 000000000000..b8384d6bef09 --- /dev/null +++ b/packages/react/src/components/Tag/InteractiveTag.stories.js @@ -0,0 +1,424 @@ +/** + * Copyright IBM Corp. 2016, 2023 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React, { useState } from 'react'; +import { default as Tag } from '.'; +import { default as SelectableTag } from './SelectableTag'; +import { default as OperationalTag } from './OperationalTag'; +import { default as DismissibleTag } from './DismissibleTag'; +import { Asleep } from '@carbon/icons-react'; +import { Tooltip } from '../Tooltip'; +import { Toggletip, ToggletipButton, ToggletipContent } from '../Toggletip'; +import { Popover, PopoverContent } from '../Popover'; +import mdx from './InteractiveTag.mdx'; +import { usePrefix } from '../../internal/usePrefix'; +import './storyInteractiveTag.scss'; + +export default { + title: 'Experimental/unstable__InteractiveTag', + component: SelectableTag, + parameters: { + docs: { + page: mdx, + }, + }, +}; + +export const Selectable = (args) => { + return ( + + {'Tag content'} + + ); +}; + +Selectable.args = { + disabled: false, +}; + +Selectable.argTypes = { + as: { + table: { + disable: true, + }, + }, + type: { + table: { + disable: true, + }, + }, + filter: { + table: { + disable: true, + }, + }, + onClose: { + table: { + disable: true, + }, + }, + selected: { + control: 'false', + description: 'Specify the state of the selectable tag.', + }, + title: { + table: { + disable: true, + }, + }, + size: { + options: ['sm', 'md', 'lg'], + control: { + type: 'select', + }, + }, +}; + +export const Operational = (args) => { + const prefix = usePrefix(); + const [open, setOpen] = useState(false); + + return ( + <> +
+ + {'Tag content'} + + + {'Tag content'} + + + {'Tag content'} + + + {'Tag content'} + + + {'Tag content'} + + + {'Tag content'} + + + {'Tag content'} + + + {'Tag content'} + + + {'Tag content'} + + + {'Tag content'} + +
+ +

Interactive examples

+
+ + + {'Tag content'} + + + + + + + {'Tag content'} + + + +
+

Tag 1 name

+

Tag 2 name

+

Tag 3 name

+

Tag 4 name

+

Tag 5 name

+
+
+
+ + + { + setOpen(!open); + }} + renderIcon={Asleep} + className="some-class" + {...args}> + {'Tag content'} + + +
+ + {'Tag 1 name'} + + + {'Tag 2 name'} + + + {'Tag 3 name'} + + + {'Tag 4 name'} + + + {'Tag 5 name'} + +
+
+
+
+ + ); +}; + +Operational.args = { + disabled: false, + size: 'md', +}; + +Operational.argTypes = { + id: { + control: false, + }, + children: { + control: false, + }, + className: { + control: false, + }, + as: { + table: { + disable: true, + }, + }, + filter: { + table: { + disable: true, + }, + }, + onClose: { + table: { + disable: true, + }, + }, + title: { + table: { + disable: true, + }, + }, + selected: { + table: { + disable: true, + }, + }, + type: { + control: false, + }, + size: { + options: ['sm', 'md', 'lg'], + control: { + type: 'select', + }, + }, + // type: { + // options: ['red', 'magenta', 'blue'], + // control: { + // type: 'select', + // }, + // }, +}; + +export const Dismissible = (args) => { + return ( + <> + + {'Tag content'} + + + {'Tag content'} + + + {'Tag content'} + + + {'Tag content'} + + + {'Tag content'} + + + {'Tag content'} + + + {'Tag content'} + + + {'Tag content'} + + + {'Tag content'} + + + {'Tag content'} + + + {'Tag content'} + + + {'Tag content'} + + + ); +}; + +Dismissible.args = { + disabled: false, + size: 'md', +}; + +Dismissible.argTypes = { + as: { + table: { + disable: true, + }, + }, + filter: { + table: { + disable: true, + }, + }, + selected: { + table: { + disable: true, + }, + }, + size: { + options: ['sm', 'md', 'lg'], + control: { + type: 'select', + }, + }, +}; diff --git a/packages/react/src/components/Tag/OperationalTag.tsx b/packages/react/src/components/Tag/OperationalTag.tsx new file mode 100644 index 000000000000..7feb9189dc18 --- /dev/null +++ b/packages/react/src/components/Tag/OperationalTag.tsx @@ -0,0 +1,166 @@ +/** + * Copyright IBM Corp. 2016, 2023 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import PropTypes, { ReactNodeLike } from 'prop-types'; +import React, { MouseEventHandler } from 'react'; +import classNames from 'classnames'; +import setupGetInstanceId from '../../tools/setupGetInstanceId'; +import { usePrefix } from '../../internal/usePrefix'; +import { PolymorphicProps } from '../../types/common'; +import Tag, { SIZES } from './Tag'; + +const getInstanceId = setupGetInstanceId(); + +const TYPES = { + red: 'Red', + magenta: 'Magenta', + purple: 'Purple', + blue: 'Blue', + cyan: 'Cyan', + teal: 'Teal', + green: 'Green', + gray: 'Gray', + 'cool-gray': 'Cool-Gray', + 'warm-gray': 'Warm-Gray', +}; + +export interface OperationalTagBaseProps { + /** + * Provide content to be rendered inside of a `OperationalTag` + */ + children?: React.ReactNode; + + /** + * Provide a custom className that is applied to the containing + */ + className?: string; + + /** + * Specify if the `OperationalTag` is disabled + */ + disabled?: boolean; + + /** + * Specify the id for the OperationalTag. + */ + id?: string; + + /** + * Optional prop to render a custom icon. + * Can be a React component class + */ + renderIcon?: React.ElementType; + onClick?: MouseEventHandler; + + /** + * Specify the size of the Tag. Currently supports either `sm`, + * `md` (default) or `lg` sizes. + */ + size?: keyof typeof SIZES; + + /** + * **Experimental:** Provide a `Slug` component to be rendered inside the `OperationalTag` component + */ + slug?: ReactNodeLike; + + /** + * Specify the type of the `Tag` + */ + type?: keyof typeof TYPES; +} + +export type OperationalTagProps = PolymorphicProps< + T, + OperationalTagBaseProps +>; + +const OperationalTag = ({ + children, + className, + disabled, + id, + renderIcon, + slug, + size, + type = 'gray', + ...other +}: OperationalTagProps) => { + const prefix = usePrefix(); + const tagId = id || `tag-${getInstanceId()}`; + const tagClasses = classNames(`${prefix}--tag--operational`, className); + + let normalizedSlug; + if (slug && slug['type']?.displayName === 'Slug') { + normalizedSlug = React.cloneElement(slug as React.ReactElement, { + size: 'sm', + kind: 'inline', + }); + } + + return ( + + type={type} + size={size} + renderIcon={renderIcon} + disabled={disabled} + className={tagClasses} + id={tagId} + {...other}> +
+ {children} + {normalizedSlug} +
+ + ); +}; + +OperationalTag.propTypes = { + /** + * Provide content to be rendered inside of a `OperationalTag` + */ + children: PropTypes.node, + + /** + * Provide a custom className that is applied to the containing + */ + className: PropTypes.string, + + /** + * Specify if the `OperationalTag` is disabled + */ + disabled: PropTypes.bool, + + /** + * Specify the id for the tag. + */ + id: PropTypes.string, + + /** + * Optional prop to render a custom icon. + * Can be a React component class + */ + renderIcon: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), + + /** + * Specify the size of the Tag. Currently supports either `sm`, + * `md` (default) or `lg` sizes. + */ + size: PropTypes.oneOf(Object.keys(SIZES)), + + /** + * **Experimental:** Provide a `Slug` component to be rendered inside the `OperationalTag` component + */ + slug: PropTypes.node, + + /** + * Specify the type of the `Tag` + */ + type: PropTypes.oneOf(Object.keys(TYPES)), +}; + +export const types = Object.keys(TYPES); +export default OperationalTag; diff --git a/packages/react/src/components/Tag/SelectableTag.tsx b/packages/react/src/components/Tag/SelectableTag.tsx new file mode 100644 index 000000000000..31bc74fb456d --- /dev/null +++ b/packages/react/src/components/Tag/SelectableTag.tsx @@ -0,0 +1,159 @@ +/** + * Copyright IBM Corp. 2016, 2023 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import PropTypes, { ReactNodeLike } from 'prop-types'; +import React, { useState } from 'react'; +import classNames from 'classnames'; +import setupGetInstanceId from '../../tools/setupGetInstanceId'; +import { usePrefix } from '../../internal/usePrefix'; +import { PolymorphicProps } from '../../types/common'; +import Tag, { SIZES } from './Tag'; + +const getInstanceId = setupGetInstanceId(); + +export interface SelectableTagBaseProps { + /** + * Provide content to be rendered inside of a `SelectableTag` + */ + children?: React.ReactNode; + + /** + * Provide a custom className that is applied to the containing + */ + className?: string; + + /** + * Specify if the `SelectableTag` is disabled + */ + disabled?: boolean; + + /** + * Specify the id for the selectabletag. + */ + id?: string; + + /** + * Optional prop to render a custom icon. + * Can be a React component class + */ + renderIcon?: React.ElementType; + + /** + * Specify the state of the selectable tag. + */ + selected?: boolean; + + /** + * Specify the size of the Tag. Currently supports either `sm`, + * `md` (default) or `lg` sizes. + */ + size?: keyof typeof SIZES; + + /** + * **Experimental:** Provide a `Slug` component to be rendered inside the `SelectableTag` component + */ + slug?: ReactNodeLike; +} + +export type SelectableTagProps = PolymorphicProps< + T, + SelectableTagBaseProps +>; + +const SelectableTag = ({ + children, + className, + disabled, + id, + renderIcon, + selected = false, + slug, + size, + ...other +}: SelectableTagProps) => { + const prefix = usePrefix(); + const tagId = id || `tag-${getInstanceId()}`; + const [selectedTag, setSelectedTag] = useState(selected); + const tagClasses = classNames(`${prefix}--tag--selectable`, className, { + [`${prefix}--tag--selectable-selected`]: selectedTag, + }); + + let normalizedSlug; + if (slug && slug['type']?.displayName === 'Slug') { + normalizedSlug = React.cloneElement(slug as React.ReactElement, { + size: 'sm', + kind: 'inline', + }); + } + + // Removing onClick from the spread operator + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { onClick, ...otherProps } = other; + + return ( + + slug={slug} + size={size} + renderIcon={renderIcon} + disabled={disabled} + className={tagClasses} + id={tagId} + onClick={() => setSelectedTag(!selectedTag)} + {...otherProps}> +
+ {children} + {normalizedSlug} +
+ + ); +}; + +SelectableTag.propTypes = { + /** + * Provide content to be rendered inside of a `SelectableTag` + */ + children: PropTypes.node, + + /** + * Provide a custom className that is applied to the containing + */ + className: PropTypes.string, + + /** + * Specify if the `SelectableTag` is disabled + */ + disabled: PropTypes.bool, + + /** + * Specify the id for the tag. + */ + id: PropTypes.string, + + /** + * Optional prop to render a custom icon. + * Can be a React component class + */ + renderIcon: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), + + /** + * Specify the state of the selectable tag. + */ + selected: PropTypes.bool, + + /** + * Specify the size of the Tag. Currently supports either `sm`, + * `md` (default) or `lg` sizes. + */ + size: PropTypes.oneOf(Object.keys(SIZES)), + + /** + * **Experimental:** Provide a `Slug` component to be rendered inside the `SelectableTag` component + */ + slug: PropTypes.node, +}; + +export default SelectableTag; diff --git a/packages/react/src/components/Tag/Tag-test.js b/packages/react/src/components/Tag/Tag-test.js index 1ba02e600ee1..bd7715035e0c 100644 --- a/packages/react/src/components/Tag/Tag-test.js +++ b/packages/react/src/components/Tag/Tag-test.js @@ -9,6 +9,7 @@ import { Add } from '@carbon/icons-react'; import { render, screen } from '@testing-library/react'; import React from 'react'; import Tag from './'; +import DismissibleTag from './DismissibleTag'; import { Slug } from '../Slug'; describe('Tag', () => { @@ -31,9 +32,9 @@ describe('Tag', () => { it('should have an appropriate aria-label when (filterable)', () => { const children = 'tag-3'; const { container } = render( - + {children} - + ); // eslint-disable-next-line testing-library/no-container, testing-library/no-node-access const button = container.querySelector('[aria-label]'); diff --git a/packages/react/src/components/Tag/Tag.stories.js b/packages/react/src/components/Tag/Tag.stories.js index 20af2d55aaf1..b162934e0dd2 100644 --- a/packages/react/src/components/Tag/Tag.stories.js +++ b/packages/react/src/components/Tag/Tag.stories.js @@ -8,49 +8,50 @@ import React from 'react'; import { default as Tag } from '../Tag'; import TagSkeleton from '../Tag/Tag.Skeleton'; +import { Asleep } from '@carbon/icons-react'; export default { title: 'Components/Tag', component: Tag, }; -export const Default = () => { +export const ReadOnly = () => { return ( <> - + {'Tag content'} - + {'Tag content'} - + {'Tag content'} - + {'Tag content'} - + {'Tag content'} - + {'Tag content'} - + {'Tag content'} - + {'Tag content'} - + {'Tag content'} - + {'Tag content'} - + {'Tag content'} - + {'Tag content'} @@ -58,7 +59,11 @@ export const Default = () => { }; export const Playground = (args) => { - return {'Tag content'}; + return ( + + {'Tag content'} + + ); }; Playground.args = { @@ -92,7 +97,7 @@ Playground.argTypes = { control: false, }, size: { - options: ['sm', 'md'], + options: ['sm', 'md', 'lg'], control: { type: 'select', }, @@ -186,7 +191,7 @@ Skeleton.argTypes = { }, }, size: { - options: ['sm', 'md'], + options: ['sm', 'md', 'lg'], control: { type: 'select', }, diff --git a/packages/react/src/components/Tag/Tag.tsx b/packages/react/src/components/Tag/Tag.tsx index 148676978f3f..959c67210077 100644 --- a/packages/react/src/components/Tag/Tag.tsx +++ b/packages/react/src/components/Tag/Tag.tsx @@ -13,9 +13,10 @@ import setupGetInstanceId from '../../tools/setupGetInstanceId'; import { usePrefix } from '../../internal/usePrefix'; import { PolymorphicProps } from '../../types/common'; import { Text } from '../Text'; +import deprecate from '../../prop-types/deprecate'; const getInstanceId = setupGetInstanceId(); -const TYPES = { +export const TYPES = { red: 'Red', magenta: 'Magenta', purple: 'Purple', @@ -30,6 +31,12 @@ const TYPES = { outline: 'Outline', }; +export const SIZES = { + sm: 'sm', + md: 'md', + lg: 'lg', +}; + export interface TagBaseProps { /** * Provide content to be rendered inside of a `Tag` @@ -47,7 +54,7 @@ export interface TagBaseProps { disabled?: boolean; /** - * Determine if `Tag` is a filter/chip + * @deprecated This property is deprecated and will be removed in the next major version. Use DismissibleTag instead. */ filter?: boolean; @@ -57,7 +64,7 @@ export interface TagBaseProps { id?: string; /** - * Click handler for filter tag close button. + * @deprecated This property is deprecated and will be removed in the next major version. Use DismissibleTag instead. */ onClose?: (event: React.MouseEvent) => void; @@ -68,10 +75,10 @@ export interface TagBaseProps { renderIcon?: React.ElementType; /** - * Specify the size of the Tag. Currently supports either `sm` or - * 'md' (default) sizes. + * Specify the size of the Tag. Currently supports either `sm`, + * `md` (default) or `lg` sizes. */ - size?: 'sm' | 'md'; + size?: keyof typeof SIZES; /** * **Experimental:** Provide a `Slug` component to be rendered inside the `Tag` component @@ -79,7 +86,7 @@ export interface TagBaseProps { slug?: ReactNodeLike; /** - * Text to show on clear filters + * @deprecated This property is deprecated and will be removed in the next major version. Use DismissibleTag instead. */ title?: string; @@ -99,11 +106,11 @@ const Tag = ({ className, id, type, - filter, + filter, // remove filter in next major release - V12 renderIcon: CustomIconElement, - title = 'Clear filter', + title = 'Clear filter', // remove title in next major release - V12 disabled, - onClose, + onClose, // remove onClose in next major release - V12 size, as: BaseComponent, slug, @@ -111,13 +118,22 @@ const Tag = ({ }: TagProps) => { const prefix = usePrefix(); const tagId = id || `tag-${getInstanceId()}`; + + const conditions = [ + `${prefix}--tag--selectable`, + `${prefix}--tag--filter`, + `${prefix}--tag--operational`, + ]; + + const isInteractiveTag = conditions.some((el) => className?.includes(el)); + const tagClasses = classNames(`${prefix}--tag`, className, { [`${prefix}--tag--disabled`]: disabled, [`${prefix}--tag--filter`]: filter, [`${prefix}--tag--${size}`]: size, // TODO: V12 - Remove this class [`${prefix}--layout--size-${size}`]: size, [`${prefix}--tag--${type}`]: type, - [`${prefix}--tag--interactive`]: other.onClick && !filter, + [`${prefix}--tag--interactive`]: other.onClick && !isInteractiveTag, }); const typeText = @@ -132,7 +148,7 @@ const Tag = ({ // Slug is always size `md` and `inline` let normalizedSlug; - if (slug && slug['type']?.displayName === 'Slug') { + if (slug && slug['type']?.displayName === 'Slug' && !isInteractiveTag) { normalizedSlug = React.cloneElement(slug as React.ReactElement, { size: 'sm', kind: 'inline', @@ -143,7 +159,7 @@ const Tag = ({ const ComponentTag = BaseComponent ?? 'div'; return ( - {CustomIconElement ? ( + {CustomIconElement && size !== 'sm' ? (
@@ -169,15 +185,19 @@ const Tag = ({ ); } - const ComponentTag = BaseComponent ?? (other.onClick ? 'button' : 'div'); + const ComponentTag = + BaseComponent ?? + (other.onClick || className?.includes(`${prefix}--tag--operational`) + ? 'button' + : 'div'); return ( - {CustomIconElement ? ( + {CustomIconElement && size !== 'sm' ? (
@@ -217,7 +237,10 @@ Tag.propTypes = { /** * Determine if `Tag` is a filter/chip */ - filter: PropTypes.bool, + filter: deprecate( + PropTypes.bool, + 'This property is deprecated and will be removed in the next major version. Use DismissibleTag instead.' + ), /** * Specify the id for the tag. @@ -227,7 +250,10 @@ Tag.propTypes = { /** * Click handler for filter tag close button. */ - onClose: PropTypes.func, + onClose: deprecate( + PropTypes.func, + 'This property is deprecated and will be removed in the next major version. Use DismissibleTag instead.' + ), /** * Optional prop to render a custom icon. @@ -236,10 +262,10 @@ Tag.propTypes = { renderIcon: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), /** - * Specify the size of the Tag. Currently supports either `sm` or - * 'md' (default) sizes. + * Specify the size of the Tag. Currently supports either `sm`, + * `md` (default) or `lg` sizes. */ - size: PropTypes.oneOf(['sm', 'md']), + size: PropTypes.oneOf(Object.keys(SIZES)), /** * **Experimental:** Provide a `Slug` component to be rendered inside the `Tag` component @@ -249,7 +275,10 @@ Tag.propTypes = { /** * Text to show on clear filters */ - title: PropTypes.string, + title: deprecate( + PropTypes.string, + 'This property is deprecated and will be removed in the next major version. Use DismissibleTag instead.' + ), /** * Specify the type of the `Tag` diff --git a/packages/react/src/components/Tag/docs/overview.mdx b/packages/react/src/components/Tag/docs/overview.mdx index c6f8113947b7..b199adbd12b2 100644 --- a/packages/react/src/components/Tag/docs/overview.mdx +++ b/packages/react/src/components/Tag/docs/overview.mdx @@ -5,8 +5,8 @@ url="https://react.carbondesignsystem.com" variants={[ { - label: 'Default', - variant: 'components-tag--default' - } + label: 'Read Only', + variant: 'components-tag--read-only', + }, ]} -/> \ No newline at end of file +/> diff --git a/packages/react/src/components/Tag/index.ts b/packages/react/src/components/Tag/index.ts index f66b8019e4ab..6effbc14c371 100644 --- a/packages/react/src/components/Tag/index.ts +++ b/packages/react/src/components/Tag/index.ts @@ -8,6 +8,5 @@ import Tag from './Tag'; export * from './Tag.Skeleton'; -export * from './Tag'; export default Tag; export { Tag }; diff --git a/packages/react/src/components/Tag/storyInteractiveTag.scss b/packages/react/src/components/Tag/storyInteractiveTag.scss new file mode 100644 index 000000000000..faf601110188 --- /dev/null +++ b/packages/react/src/components/Tag/storyInteractiveTag.scss @@ -0,0 +1,5 @@ +// Style to match design spec + +#operational-tag > .cds--popover--caret { + --cds-popover-offset: 5px; +} diff --git a/packages/react/src/components/Toggletip/index.tsx b/packages/react/src/components/Toggletip/index.tsx index 306c0e0ab12a..a965c3d1dc4d 100644 --- a/packages/react/src/components/Toggletip/index.tsx +++ b/packages/react/src/components/Toggletip/index.tsx @@ -22,6 +22,7 @@ import { match, keys } from '../../internal/keyboard'; import { useWindowEvent } from '../../internal/useEvent'; import { useId } from '../../internal/useId'; import { usePrefix } from '../../internal/usePrefix'; +import { PolymorphicProps } from '../../types/common'; type ToggletipLabelProps = { as?: E | undefined; @@ -69,6 +70,7 @@ type ToggleTipContextType = | { buttonProps: ComponentProps<'button'>; contentProps: ComponentProps; + onClick: ComponentProps<'button'>; }; // Used to coordinate accessibility props between button and content along with @@ -126,6 +128,9 @@ export function Toggletip({ contentProps: { id, }, + onClick: { + onClick: actions.toggle, + }, }; const onKeyDown: KeyboardEventHandler = (event) => { @@ -235,30 +240,45 @@ Toggletip.propTypes = { defaultOpen: PropTypes.bool, }; -interface ToggletipButtonProps { +interface ToggletipButtonBaseProps { children?: ReactNode; className?: string | undefined; label?: string | undefined; } +export type ToggleTipButtonProps = + PolymorphicProps; + /** * `ToggletipButton` controls the visibility of the Toggletip through mouse * clicks and keyboard interactions. */ -export function ToggletipButton({ +export function ToggletipButton({ children, className: customClassName, label = 'Show information', -}: ToggletipButtonProps) { + as: BaseComponent, + ...rest +}: ToggleTipButtonProps) { const toggletip = useToggletip(); const prefix = usePrefix(); const className = cx(`${prefix}--toggletip-button`, customClassName); + const ComponentToggle: any = BaseComponent ?? 'button'; + + if (ComponentToggle !== 'button') { + return ( + + {children} + + ); + } return ( ); diff --git a/packages/styles/scss/components/popover/_popover.scss b/packages/styles/scss/components/popover/_popover.scss index 89bad035ba18..a0c509cead52 100644 --- a/packages/styles/scss/components/popover/_popover.scss +++ b/packages/styles/scss/components/popover/_popover.scss @@ -125,6 +125,18 @@ $popover-caret-height: custom-property.get-var( // Popover content .#{$prefix}--popover-content { + // The layout.redefine-tokens is been included here because it is been redifined in _tag.scss + @include layout.redefine-tokens( + ( + size: ( + height: ( + sm: convert.to-rem(32px), + md: convert.to-rem(40px), + lg: convert.to-rem(48px), + ), + ), + ) + ); @include component-reset.reset; position: absolute; diff --git a/packages/styles/scss/components/tag/_mixins.scss b/packages/styles/scss/components/tag/_mixins.scss index 0d6f409727d2..46d153592854 100644 --- a/packages/styles/scss/components/tag/_mixins.scss +++ b/packages/styles/scss/components/tag/_mixins.scss @@ -8,11 +8,23 @@ /// @access private /// @group tag -@mixin tag-theme($bg-color, $text-color, $filter-hover-color: $bg-color) { +@mixin tag-theme( + $bg-color, + $text-color, + $filter-hover-color: $bg-color, + $border-color: $bg-color +) { background-color: $bg-color; color: $text-color; - &.#{$prefix}--tag--interactive, + &.#{$prefix}--tag--operational { + border: 1px solid $border-color; + + &:hover { + background-color: $filter-hover-color; + } + } + .#{$prefix}--tag__close-icon { &:hover { background-color: $filter-hover-color; diff --git a/packages/styles/scss/components/tag/_tag.scss b/packages/styles/scss/components/tag/_tag.scss index 83dd24f4836d..591bb212adb7 100644 --- a/packages/styles/scss/components/tag/_tag.scss +++ b/packages/styles/scss/components/tag/_tag.scss @@ -31,20 +31,20 @@ xs: convert.to-rem(18px), sm: convert.to-rem(18px), md: convert.to-rem(24px), + lg: convert.to-rem(32px), ), ), ) ); - @include layout.use('size', $default: 'md', $min: 'sm', $max: 'md'); - + @include layout.use('size', $default: 'md', $min: 'sm', $max: 'lg'); @include type-style('label-01'); @include tag-theme($tag-background-gray, $tag-color-gray, $tag-hover-gray); display: inline-flex; align-items: center; justify-content: center; - border-radius: convert.to-rem(15px); + border-radius: convert.to-rem(16px); margin: $spacing-02; cursor: default; // restricts size of contained elements @@ -56,20 +56,118 @@ vertical-align: middle; word-break: break-word; + &.#{$prefix}--tag--lg { + padding-inline-start: $spacing-04; + } + + &:has(.#{$prefix}--tag__custom-icon) { + padding-inline-start: $spacing-02; + } + + &.#{$prefix}--tag--lg:not(.#{$prefix}--tag--filter) { + padding-inline: $spacing-04; + } + + &.#{$prefix}--tag--lg:has(.#{$prefix}--tag__custom-icon) { + padding-inline-start: $spacing-03; + } + &:not(:first-child) { margin-inline-start: 0; } } + .#{$prefix}--tag__label { + overflow: hidden; + max-inline-size: 100%; + text-overflow: ellipsis; + white-space: nowrap; + } + + .#{$prefix}--tag--interactive:focus { + box-shadow: inset 0 0 0 1px $focus; + outline: none; + } + + .#{$prefix}--tag--interactive:hover { + cursor: pointer; + } + + // tags used for filtering + .#{$prefix}--tag--filter { + cursor: pointer; + padding-block-end: 0; + padding-block-start: 0; + padding-inline-end: 0; + + &:hover { + outline: none; + } + } + + .#{$prefix}--interactive--tag-children { + display: inline-flex; + place-items: center; + } + + .#{$prefix}--tag--selectable { + border: 1px solid $border-inverse; + background-color: $layer; + color: $text-primary; + cursor: pointer; + + &:hover { + background-color: $layer-hover; + outline: none; + } + + &:focus { + outline: 2px solid $focus; + outline-offset: 1px; + } + } + + .#{$prefix}--tag--selectable-selected { + background-color: $layer-selected-inverse; + color: $text-inverse; + + &:hover { + background-color: $layer-selected-inverse; + } + } + + .#{$prefix}--tag--operational { + border: 1px solid $tag-border-gray; + background-color: $tag-background-gray; + color: $tag-color-gray; + cursor: pointer; + + &:hover { + background-color: $tag-hover-gray; + outline: none; + } + + &:focus { + outline: 2px solid $focus; + outline-offset: 1px; + } + } + .#{$prefix}--tag--red { - @include tag-theme($tag-background-red, $tag-color-red, $tag-hover-red); + @include tag-theme( + $tag-background-red, + $tag-color-red, + $tag-hover-red, + $tag-border-red + ); } .#{$prefix}--tag--magenta { @include tag-theme( $tag-background-magenta, $tag-color-magenta, - $tag-hover-magenta + $tag-hover-magenta, + $tag-border-magenta ); } @@ -77,39 +175,62 @@ @include tag-theme( $tag-background-purple, $tag-color-purple, - $tag-hover-purple + $tag-hover-purple, + $tag-border-purple ); } .#{$prefix}--tag--blue { - @include tag-theme($tag-background-blue, $tag-color-blue, $tag-hover-blue); + @include tag-theme( + $tag-background-blue, + $tag-color-blue, + $tag-hover-blue, + $tag-border-blue + ); } .#{$prefix}--tag--cyan { - @include tag-theme($tag-background-cyan, $tag-color-cyan, $tag-hover-cyan); + @include tag-theme( + $tag-background-cyan, + $tag-color-cyan, + $tag-hover-cyan, + $tag-border-cyan + ); } .#{$prefix}--tag--teal { - @include tag-theme($tag-background-teal, $tag-color-teal, $tag-hover-teal); + @include tag-theme( + $tag-background-teal, + $tag-color-teal, + $tag-hover-teal, + $tag-border-teal + ); } .#{$prefix}--tag--green { @include tag-theme( $tag-background-green, $tag-color-green, - $tag-hover-green + $tag-hover-green, + $tag-border-green ); } .#{$prefix}--tag--gray { - @include tag-theme($tag-background-gray, $tag-color-gray, $tag-hover-gray); + @include tag-theme( + $tag-background-gray, + $tag-color-gray, + $tag-hover-gray, + $tag-border-gray + ); } .#{$prefix}--tag--cool-gray { @include tag-theme( $tag-background-cool-gray, $tag-color-cool-gray, - $tag-hover-cool-gray + $tag-hover-cool-gray, + $tag-border-cool-gray ); } @@ -117,11 +238,12 @@ @include tag-theme( $tag-background-warm-gray, $tag-color-warm-gray, - $tag-hover-warm-gray + $tag-hover-warm-gray, + $tag-border-warm-gray ); } - .#{$prefix}--tag--high-contrast { + .#{$prefix}--tag--high-contrast:not(.#{$prefix}--tag--operational) { @include tag-theme( $background-inverse, $text-inverse, @@ -129,10 +251,11 @@ ); } - .#{$prefix}--tag--outline { + .#{$prefix}--tag--outline:not(.#{$prefix}--tag--operational) { @include tag-theme($background, $text-primary, $layer-hover); - box-shadow: 0 0 0 1px $background-inverse; + outline: 1px solid $background-inverse; + outline-offset: -1px; } .#{$prefix}--tag--disabled, @@ -147,31 +270,15 @@ } } - .#{$prefix}--tag__label { - overflow: hidden; - max-inline-size: 100%; - text-overflow: ellipsis; - white-space: nowrap; - } - - .#{$prefix}--tag--interactive:focus { - box-shadow: inset 0 0 0 1px $focus; - outline: none; - } - - .#{$prefix}--tag--interactive:hover { - cursor: pointer; - } - - // tags used for filtering - .#{$prefix}--tag--filter { - cursor: pointer; - padding-block-end: 0; - padding-block-start: 0; - padding-inline-end: 0; + .#{$prefix}--tag--selectable.#{$prefix}--tag--disabled, + .#{$prefix}--tag--operational.#{$prefix}--tag--disabled { + border: 1px solid $border-disabled; + background-color: $layer; + color: $text-disabled; &:hover { - outline: none; + background-color: $layer; + cursor: not-allowed; } } @@ -222,6 +329,7 @@ } .#{$prefix}--tag__close-icon:focus { + z-index: 99999; border-radius: 50%; box-shadow: inset 0 0 0 1px $focus; outline: none; diff --git a/packages/styles/scss/components/tag/_tokens.scss b/packages/styles/scss/components/tag/_tokens.scss index 77c6908ce510..dfc00561cfc4 100644 --- a/packages/styles/scss/components/tag/_tokens.scss +++ b/packages/styles/scss/components/tag/_tokens.scss @@ -617,6 +617,226 @@ $tag-hover-cool-gray: ( ), ) !default; +$tag-border-gray: ( + fallback: map.get(tag.$tag-border-gray, white-theme), + values: ( + ( + theme: themes.$white, + value: map.get(tag.$tag-border-gray, white-theme), + ), + ( + theme: themes.$g10, + value: map.get(tag.$tag-border-gray, g-10), + ), + ( + theme: themes.$g90, + value: map.get(tag.$tag-border-gray, g-90), + ), + ( + theme: themes.$g100, + value: map.get(tag.$tag-border-gray, g-100), + ), + ), +) !default; + +$tag-border-red: ( + fallback: map.get(tag.$tag-border-red, white-theme), + values: ( + ( + theme: themes.$white, + value: map.get(tag.$tag-border-red, white-theme), + ), + ( + theme: themes.$g10, + value: map.get(tag.$tag-border-red, g-10), + ), + ( + theme: themes.$g90, + value: map.get(tag.$tag-border-red, g-90), + ), + ( + theme: themes.$g100, + value: map.get(tag.$tag-border-red, g-100), + ), + ), +) !default; + +$tag-border-blue: ( + fallback: map.get(tag.$tag-border-blue, white-theme), + values: ( + ( + theme: themes.$white, + value: map.get(tag.$tag-border-blue, white-theme), + ), + ( + theme: themes.$g10, + value: map.get(tag.$tag-border-blue, g-10), + ), + ( + theme: themes.$g90, + value: map.get(tag.$tag-border-blue, g-90), + ), + ( + theme: themes.$g100, + value: map.get(tag.$tag-border-blue, g-100), + ), + ), +) !default; + +$tag-border-cyan: ( + fallback: map.get(tag.$tag-border-cyan, white-theme), + values: ( + ( + theme: themes.$white, + value: map.get(tag.$tag-border-cyan, white-theme), + ), + ( + theme: themes.$g10, + value: map.get(tag.$tag-border-cyan, g-10), + ), + ( + theme: themes.$g90, + value: map.get(tag.$tag-border-cyan, g-90), + ), + ( + theme: themes.$g100, + value: map.get(tag.$tag-border-cyan, g-100), + ), + ), +) !default; + +$tag-border-teal: ( + fallback: map.get(tag.$tag-border-teal, white-theme), + values: ( + ( + theme: themes.$white, + value: map.get(tag.$tag-border-teal, white-theme), + ), + ( + theme: themes.$g10, + value: map.get(tag.$tag-border-teal, g-10), + ), + ( + theme: themes.$g90, + value: map.get(tag.$tag-border-teal, g-90), + ), + ( + theme: themes.$g100, + value: map.get(tag.$tag-border-teal, g-100), + ), + ), +) !default; + +$tag-border-green: ( + fallback: map.get(tag.$tag-border-green, white-theme), + values: ( + ( + theme: themes.$white, + value: map.get(tag.$tag-border-green, white-theme), + ), + ( + theme: themes.$g10, + value: map.get(tag.$tag-border-green, g-10), + ), + ( + theme: themes.$g90, + value: map.get(tag.$tag-border-green, g-90), + ), + ( + theme: themes.$g100, + value: map.get(tag.$tag-border-green, g-100), + ), + ), +) !default; + +$tag-border-magenta: ( + fallback: map.get(tag.$tag-border-magenta, white-theme), + values: ( + ( + theme: themes.$white, + value: map.get(tag.$tag-border-magenta, white-theme), + ), + ( + theme: themes.$g10, + value: map.get(tag.$tag-border-magenta, g-10), + ), + ( + theme: themes.$g90, + value: map.get(tag.$tag-border-magenta, g-90), + ), + ( + theme: themes.$g100, + value: map.get(tag.$tag-border-magenta, g-100), + ), + ), +) !default; + +$tag-border-purple: ( + fallback: map.get(tag.$tag-border-purple, white-theme), + values: ( + ( + theme: themes.$white, + value: map.get(tag.$tag-border-purple, white-theme), + ), + ( + theme: themes.$g10, + value: map.get(tag.$tag-border-purple, g-10), + ), + ( + theme: themes.$g90, + value: map.get(tag.$tag-border-purple, g-90), + ), + ( + theme: themes.$g100, + value: map.get(tag.$tag-border-purple, g-100), + ), + ), +) !default; + +$tag-border-cool-gray: ( + fallback: map.get(tag.$tag-border-cool-gray, white-theme), + values: ( + ( + theme: themes.$white, + value: map.get(tag.$tag-border-cool-gray, white-theme), + ), + ( + theme: themes.$g10, + value: map.get(tag.$tag-border-cool-gray, g-10), + ), + ( + theme: themes.$g90, + value: map.get(tag.$tag-border-cool-gray, g-90), + ), + ( + theme: themes.$g100, + value: map.get(tag.$tag-border-cool-gray, g-100), + ), + ), +) !default; + +$tag-border-warm-gray: ( + fallback: map.get(tag.$tag-border-warm-gray, white-theme), + values: ( + ( + theme: themes.$white, + value: map.get(tag.$tag-border-warm-gray, white-theme), + ), + ( + theme: themes.$g10, + value: map.get(tag.$tag-border-warm-gray, g-10), + ), + ( + theme: themes.$g90, + value: map.get(tag.$tag-border-warm-gray, g-90), + ), + ( + theme: themes.$g100, + value: map.get(tag.$tag-border-warm-gray, g-100), + ), + ), +) !default; + // warm-gray $tag-background-warm-gray: ( fallback: map.get(tag.$tag-background-warm-gray, white-theme), @@ -709,6 +929,16 @@ $tag-tokens: ( tag-background-gray: $tag-background-gray, tag-color-gray: $tag-color-gray, tag-hover-gray: $tag-hover-gray, + tag-border-red: $tag-border-red, + tag-border-blue: $tag-border-blue, + tag-border-cyan: $tag-border-cyan, + tag-border-teal: $tag-border-teal, + tag-border-green: $tag-border-green, + tag-border-magenta: $tag-border-magenta, + tag-border-purple: $tag-border-purple, + tag-border-gray: $tag-border-gray, + tag-border-cool-gray: $tag-border-cool-gray, + tag-border-warm-gray: $tag-border-warm-gray, tag-background-cool-gray: $tag-background-cool-gray, tag-color-cool-gray: $tag-color-cool-gray, tag-hover-cool-gray: $tag-hover-cool-gray, @@ -801,6 +1031,41 @@ $tag-color-gray: component-tokens.get-var($tag-color-gray, 'tag-color-gray'); $tag-hover-gray: component-tokens.get-var($tag-hover-gray, 'tag-hover-gray'); +$tag-border-red: component-tokens.get-var($tag-border-red, 'tag-border-red'); + +$tag-border-blue: component-tokens.get-var($tag-border-blue, 'tag-border-blue'); + +$tag-border-cyan: component-tokens.get-var($tag-border-cyan, 'tag-border-cyan'); + +$tag-border-teal: component-tokens.get-var($tag-border-teal, 'tag-border-teal'); + +$tag-border-green: component-tokens.get-var( + $tag-border-green, + 'tag-border-green' +); + +$tag-border-magenta: component-tokens.get-var( + $tag-border-magenta, + 'tag-border-magenta' +); + +$tag-border-purple: component-tokens.get-var( + $tag-border-purple, + 'tag-border-purple' +); + +$tag-border-gray: component-tokens.get-var($tag-border-gray, 'tag-border-gray'); + +$tag-border-cool-gray: component-tokens.get-var( + $tag-border-cool-gray, + 'tag-border-cool-gray' +); + +$tag-border-warm-gray: component-tokens.get-var( + $tag-border-warm-gray, + 'tag-border-warm-gray' +); + $tag-background-cool-gray: component-tokens.get-var( $tag-background-cool-gray, 'tag-background-cool-gray' diff --git a/packages/themes/src/component-tokens/tag/tokens.js b/packages/themes/src/component-tokens/tag/tokens.js index da62a0758f04..c20bfdc86a7c 100644 --- a/packages/themes/src/component-tokens/tag/tokens.js +++ b/packages/themes/src/component-tokens/tag/tokens.js @@ -1,52 +1,75 @@ import { red20, + red40, + red50, red70, red80, red20Hover, red70Hover, magenta20, + magenta40, + magenta50, magenta70, magenta80, magenta20Hover, magenta70Hover, purple20, + purple40, + purple50, purple70, purple80, purple20Hover, purple70Hover, blue20, + blue40, + blue50, blue70, blue80, blue20Hover, blue70Hover, cyan20, + cyan40, + cyan50, cyan70, cyan80, cyan20Hover, cyan70Hover, teal20, + teal40, + teal50, teal70, teal80, teal20Hover, teal70Hover, green20, + green40, + green50, green70, green80, green20Hover, green70Hover, + gray10, gray20, + gray40, + gray50, gray70, - gray80, + gray100, gray20Hover, gray70Hover, + warmGray10, warmGray20, + warmGray40, + warmGray50, warmGray70, - warmGray80, + warmGray100, warmGray20Hover, warmGray70Hover, + coolGray10, coolGray20, + coolGray40, + coolGray50, coolGray70, - coolGray80, + coolGray100, coolGray20Hover, coolGray70Hover, } from '@carbon/colors'; @@ -204,10 +227,10 @@ export const tagBackgroundGray = { }; export const tagColorGray = { - whiteTheme: gray80, - g10: gray80, - g90: gray20, - g100: gray20, + whiteTheme: gray100, + g10: gray100, + g90: gray10, + g100: gray10, }; export const tagHoverGray = { @@ -225,10 +248,10 @@ export const tagBackgroundCoolGray = { }; export const tagColorCoolGray = { - whiteTheme: coolGray80, - g10: coolGray80, - g90: coolGray20, - g100: coolGray20, + whiteTheme: coolGray100, + g10: coolGray100, + g90: coolGray10, + g100: coolGray10, }; export const tagHoverCoolGray = { @@ -246,10 +269,10 @@ export const tagBackgroundWarmGray = { }; export const tagColorWarmGray = { - whiteTheme: warmGray80, - g10: warmGray80, - g90: warmGray20, - g100: warmGray20, + whiteTheme: warmGray100, + g10: warmGray100, + g90: warmGray10, + g100: warmGray10, }; export const tagHoverWarmGray = { @@ -258,3 +281,73 @@ export const tagHoverWarmGray = { g90: warmGray70Hover, g100: warmGray70Hover, }; + +export const tagBorderRed = { + whiteTheme: red40, + g10: red40, + g90: red50, + g100: red50, +}; + +export const tagBorderBlue = { + whiteTheme: blue40, + g10: blue40, + g90: blue50, + g100: blue50, +}; + +export const tagBorderCyan = { + whiteTheme: cyan40, + g10: cyan40, + g90: cyan50, + g100: cyan50, +}; + +export const tagBorderTeal = { + whiteTheme: teal40, + g10: teal40, + g90: teal50, + g100: teal50, +}; + +export const tagBorderGreen = { + whiteTheme: green40, + g10: green40, + g90: green50, + g100: green50, +}; + +export const tagBorderMagenta = { + whiteTheme: magenta40, + g10: magenta40, + g90: magenta50, + g100: magenta50, +}; + +export const tagBorderPurple = { + whiteTheme: purple40, + g10: purple40, + g90: purple50, + g100: purple50, +}; + +export const tagBorderGray = { + whiteTheme: gray40, + g10: gray40, + g90: gray50, + g100: gray50, +}; + +export const tagBorderCoolGray = { + whiteTheme: coolGray40, + g10: coolGray40, + g90: coolGray50, + g100: coolGray50, +}; + +export const tagBorderWarmGray = { + whiteTheme: warmGray40, + g10: warmGray40, + g90: warmGray50, + g100: warmGray50, +}; diff --git a/packages/themes/src/tokens/__tests__/metadata-test.js b/packages/themes/src/tokens/__tests__/metadata-test.js index 4cd4e8a70077..c0ab8c7f9b5a 100644 --- a/packages/themes/src/tokens/__tests__/metadata-test.js +++ b/packages/themes/src/tokens/__tests__/metadata-test.js @@ -1357,6 +1357,46 @@ test('metadata', () => { "name": "tag-hover-gray", "type": "color", }, + Object { + "name": "tag-border-red", + "type": "color", + }, + Object { + "name": "tag-border-blue", + "type": "color", + }, + Object { + "name": "tag-border-cyan", + "type": "color", + }, + Object { + "name": "tag-border-teal", + "type": "color", + }, + Object { + "name": "tag-border-green", + "type": "color", + }, + Object { + "name": "tag-border-magenta", + "type": "color", + }, + Object { + "name": "tag-border-purple", + "type": "color", + }, + Object { + "name": "tag-border-gray", + "type": "color", + }, + Object { + "name": "tag-border-cool-gray", + "type": "color", + }, + Object { + "name": "tag-border-warm-gray", + "type": "color", + }, Object { "name": "tag-background-cool-gray", "type": "color", diff --git a/packages/themes/src/tokens/components.js b/packages/themes/src/tokens/components.js index 96af31769bf0..8c98807421fd 100644 --- a/packages/themes/src/tokens/components.js +++ b/packages/themes/src/tokens/components.js @@ -74,6 +74,16 @@ export const tag = TokenGroup.create({ 'tag-background-gray', 'tag-color-gray', 'tag-hover-gray', + 'tag-border-red', + 'tag-border-blue', + 'tag-border-cyan', + 'tag-border-teal', + 'tag-border-green', + 'tag-border-magenta', + 'tag-border-purple', + 'tag-border-gray', + 'tag-border-cool-gray', + 'tag-border-warm-gray', 'tag-background-cool-gray', 'tag-color-cool-gray', 'tag-hover-cool-gray',