diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f47a111..07222dcd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p - new icons: - `state-confirmed-all` - `state-declined-all` +- `` + - added `outlined` property ### Fixed diff --git a/src/components/Tag/Tag.tsx b/src/components/Tag/Tag.tsx index b0923489..f8b8d661 100644 --- a/src/components/Tag/Tag.tsx +++ b/src/components/Tag/Tag.tsx @@ -10,6 +10,9 @@ import { TestIconProps } from "../Icon/TestIcon"; import decideContrastColorValue from "./../../common/utils/colorDecideContrastvalue"; +const WHITE = '#FFFFFF'; +const BLACK = '#000000'; + export interface TagProps extends Omit< BlueprintTagProps, @@ -22,11 +25,16 @@ export interface TagProps * Sets the background color of a tag, depends on the `Color` object provided by the * [npm color module](https://www.npmjs.com/export package/color) v3. You can use it with * all allowed [CSS color values](https://developer.mozilla.org/de/docs/Web/CSS/color_value). - * - * The front color is set automatically, so the tag label is always readable. + * When `outlined` is true, it becomes the border. When `outlined` is false, it behaves like expected (fills the background). */ backgroundColor?: Color | string; + /** + * Display tag with outlined style — transparent background with only a colored border. + * Works with `backgroundColor`, `intent`, or default colors for the border styling. + */ + outlined?: boolean; + /** * visual appearance and "thickness" of the tag */ @@ -59,25 +67,51 @@ function Tag({ small = false, large = false, backgroundColor, + color, + outlined = false, ...otherProps }: TagProps) { - otherProps["interactive"] = otherProps.interactive ?? !!otherProps.onClick ? true : false; - if (backgroundColor) { - const additionalStyles = otherProps.style ?? {}; - let color = Color("#ffffff"); + otherProps["interactive"] = otherProps.interactive ?? !!otherProps.onClick; + + const additionalStyles = otherProps.style ?? {}; + + if (outlined) { + let colorObj = Color(BLACK); try { - color = Color(backgroundColor); - } catch (ex) { + colorObj = Color(backgroundColor); + } catch (ex: unknown) { // eslint-disable-next-line no-console console.warn("Received invalid background color for tag: " + backgroundColor); } + otherProps["style"] = { + ...additionalStyles, + borderColor: colorObj.rgb().toString(), + color: colorObj.rgb().toString() + }; + } else if (!outlined && backgroundColor) { + let backgroundObj = Color(WHITE); + + try { + backgroundObj = Color(backgroundColor); + } catch { + // eslint-disable-next-line no-console + console.warn("Received invalid background color for tag: " + backgroundColor); + } + + let colorObj = Color(decideContrastColorValue({ testColor: backgroundObj })); + if (color) { + try { + colorObj = Color(color); + } catch { + // eslint-disable-next-line no-console + console.warn("Received invalid color for tag: " + color); + } + } otherProps["style"] = { ...additionalStyles, - ...{ - backgroundColor: color.rgb().toString(), - color: decideContrastColorValue({ testColor: color }), - }, + backgroundColor: backgroundObj.rgb().toString(), + color: colorObj.rgb().toString(), }; } const leftIcon = !!icon && typeof icon === "string" ? : icon; @@ -89,6 +123,7 @@ function Tag({ (intent ? ` ${intentClassName(intent)}` : "") + (small ? ` ${eccgui}-tag--small` : "") + (large ? ` ${eccgui}-tag--large` : "") + + (outlined ? ` ${eccgui}-tag--outlined` : "") + (className ? " " + className : "") } minimal={minimal} diff --git a/src/components/Tag/tag.scss b/src/components/Tag/tag.scss index 8abfb2e1..7f56d0d1 100644 --- a/src/components/Tag/tag.scss +++ b/src/components/Tag/tag.scss @@ -272,6 +272,12 @@ $tag-round-adjustment: 0 !default; } } +.#{$ns}-tag.#{$eccgui}-tag--outlined, +.#{$ns}-tag.#{$ns}-minimal.#{$eccgui}-tag--outlined { + background-color: transparent; + border-color: currentcolor; +} + @media print { .#{$eccgui}-tag__item { print-color-adjust: exact; diff --git a/src/components/Tag/tests/Tag.test.tsx b/src/components/Tag/tests/Tag.test.tsx new file mode 100644 index 00000000..87b0e7e0 --- /dev/null +++ b/src/components/Tag/tests/Tag.test.tsx @@ -0,0 +1,107 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; + +import "@testing-library/jest-dom"; + +import { Default } from "../stories/Tag.stories"; +import Tag from "../Tag"; + +const eccgui = "eccgui"; + +describe("Tag", () => { + it("renders tag content", () => { + render(Tag label); + expect(screen.getByText("Tag label")).toBeInTheDocument(); + }); + + it("always has base class and default emphasis class", () => { + const { container } = render(label); + const tag = container.firstChild as HTMLElement; + expect(tag).toHaveClass(`${eccgui}-tag__item`); + expect(tag).toHaveClass(`${eccgui}-tag--normalemphasis`); + }); + + it("applies small class when small prop is set", () => { + const { container } = render(label); + expect(container.firstChild).toHaveClass(`${eccgui}-tag--small`); + }); + + it("applies large class when large prop is set", () => { + const { container } = render(label); + expect(container.firstChild).toHaveClass(`${eccgui}-tag--large`); + }); + + it("applies correct emphasis class", () => { + const { container } = render(label); + expect(container.firstChild).toHaveClass(`${eccgui}-tag--strongeremphasis`); + }); + + it("applies intent class when intent prop is set", () => { + const { container } = render(label); + expect(container.firstChild).toHaveClass(`${eccgui}-intent--primary`); + }); + + it("applies outlined class when outlined prop is set", () => { + const { container } = render(label); + expect(container.firstChild).toHaveClass(`${eccgui}-tag--outlined`); + }); + + it("does not apply outlined class by default", () => { + const { container } = render(label); + expect(container.firstChild).not.toHaveClass(`${eccgui}-tag--outlined`); + }); + + describe("backgroundColor", () => { + it("sets background-color and text color inline styles", () => { + const { container } = render(label); + const tag = container.firstChild as HTMLElement; + expect(tag.style.backgroundColor).toBeTruthy(); + expect(tag.style.color).toBeTruthy(); + }); + + it("does not set inline background-color when outlined", () => { + const { container } = render(label); + const tag = container.firstChild as HTMLElement; + expect(tag.style.backgroundColor).toBeFalsy(); + }); + + it("sets border-color from backgroundColor when outlined", () => { + const { container } = render(label); + const tag = container.firstChild as HTMLElement; + expect(tag.style.borderColor).toBeTruthy(); + }); + + it("sets black border-color by default when outlined without backgroundColor", () => { + const { container } = render(label); + const tag = container.firstChild as HTMLElement; + expect(tag.style.borderColor).toBe("rgb(0, 0, 0)"); + }); + + it("does not set any inline color style when no backgroundColor is provided", () => { + const { container } = render(label); + const tag = container.firstChild as HTMLElement; + expect(tag.style.backgroundColor).toBeFalsy(); + expect(tag.style.color).toBeFalsy(); + expect(tag.style.borderColor).toBeFalsy(); + }); + }); + + describe("icon", () => { + it("renders icon when icon prop is a string", () => { + const { container } = render(label); + expect(container.getElementsByClassName(`${eccgui}-icon`).length).toBeGreaterThan(0); + }); + }); + + describe("interactive", () => { + it("is not interactive by default", () => { + const { container } = render(label); + expect(container.firstChild).not.toHaveClass("bp5-interactive"); + }); + + it("is interactive when onClick is provided", () => { + const { container } = render(label); + expect(container.firstChild).toHaveClass("bp5-interactive"); + }); + }); +});