Skip to content

Commit

Permalink
feat(Tag): add actions
Browse files Browse the repository at this point in the history
  • Loading branch information
plagoa committed Dec 22, 2023
1 parent 124dcce commit 2f96540
Show file tree
Hide file tree
Showing 7 changed files with 312 additions and 109 deletions.
40 changes: 27 additions & 13 deletions packages/core/src/Tag/Tag.styles.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,31 @@
import { CSSProperties } from "react";

import { theme } from "@hitachivantara/uikit-styles";

import { chipClasses } from "@mui/material/Chip";

import { outlineStyles } from "../utils/focusUtils";

import { createClasses } from "../utils/classes";

export const { staticClasses, useClasses } = createClasses("HvTag", {
root: {},
root: {
color: theme.colors.base_dark,

[`& .${chipClasses.avatar}`]: {
width: 12,
height: 12,
marginLeft: 2,
marginRight: 0,
},
},

chipRoot: {
"&.MuiChip-root": {
[`&.${chipClasses.root}`]: {
height: 16,
borderRadius: 0,
maxWidth: 180,
fontFamily: theme.fontFamily.body,

"&:focus-visible": {
backgroundColor: theme.alpha("base_light", 0.3),
},
Expand All @@ -34,23 +46,25 @@ export const { staticClasses, useClasses } = createClasses("HvTag", {
},
},

"& .MuiChip-label": {
paddingLeft: theme.space.xs,
paddingRight: theme.space.xs,
...(theme.typography.caption1 as CSSProperties),
color: theme.colors.base_dark,
"& p": {
color: theme.colors.base_dark,
},
[`& .${chipClasses.label}`]: {
paddingLeft: 4,
paddingRight: 4,
...(theme.typography.caption2 as CSSProperties),
color: "currentcolor",
},

"& .MuiChip-deleteIcon": {
marginRight: 0,
[`& .${chipClasses.deleteIcon}`]: {
margin: 0,
width: 16,
height: 16,
padding: 0,
color: "currentColor",
"& svg .color0": {
fill: "currentcolor",
},
"&:hover": {
backgroundColor: theme.colors.containerBackgroundHover,
color: "unset",
},
"&:focus": {
...outlineStyles,
Expand Down
10 changes: 10 additions & 0 deletions packages/core/src/Tag/Tag.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@ describe("Tag", () => {
expect(clickMock).toHaveBeenCalledOnce();
});

it("triggers onClick when pressing enter or space key", async () => {
const clickMock = vi.fn();
render(<HvTag label="TAG" onClick={clickMock} selectable />);

const tag = screen.getByRole("button", { name: "TAG" });
await userEvent.type(tag, "{space}");
await userEvent.type(tag, "{enter}");
expect(clickMock).toHaveBeenCalledTimes(3);
});

it("triggers onDelete when pressing delete key", async () => {
const clickMock = vi.fn();
const deleteMock = vi.fn();
Expand Down
51 changes: 45 additions & 6 deletions packages/core/src/Tag/Tag.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
import { HTMLAttributes } from "react";
import React, { HTMLAttributes } from "react";
import { HvColorAny, getColor } from "@hitachivantara/uikit-styles";
import Chip, { ChipProps as MuiChipProps } from "@mui/material/Chip";

import { CloseXS } from "@hitachivantara/uikit-react-icons";
import {
Checkbox,
CheckboxCheck,
CloseXS,
} from "@hitachivantara/uikit-react-icons";

import { useTheme } from "../hooks/useTheme";
import { useDefaultProps } from "../hooks/useDefaultProps";
import { ExtractNames } from "../utils/classes";

import { useControlled } from "../hooks/useControlled";

import { staticClasses, useClasses } from "./Tag.styles";

export { staticClasses as tagClasses };

export type HvTagClasses = ExtractNames<typeof useClasses>;

export interface HvTagProps extends Omit<MuiChipProps, "color" | "classes"> {
export interface HvTagProps
extends Omit<MuiChipProps, "color" | "classes" | "onSelect"> {
/** The label of the tag element. */
label?: React.ReactNode;
/** Indicates that the form element is disabled. */
Expand All @@ -31,7 +38,7 @@ export interface HvTagProps extends Omit<MuiChipProps, "color" | "classes"> {
* */
onDelete?: (event: React.MouseEvent<HTMLElement>) => void;
/** Callback triggered when any item is clicked. */
onClick?: (event: React.MouseEvent<HTMLElement>) => void;
onClick?: (event: React.MouseEvent<HTMLElement>, selected?: boolean) => void;
/** Aria properties to apply to delete button in tag
* @deprecated no longer used
*/
Expand All @@ -44,6 +51,12 @@ export interface HvTagProps extends Omit<MuiChipProps, "color" | "classes"> {
ref?: MuiChipProps["ref"];
/** @ignore */
component?: MuiChipProps["component"];
/** Determines whether or not the tag is selectable. */
selectable?: boolean;
/** Defines if the tag is selected. When defined the tag state becomes controlled. */
selected?: boolean;
/** When uncontrolled, defines the initial selected state. */
defaultSelected?: boolean;
}

const getCategoricalColor = (customColor, colors) => {
Expand All @@ -66,6 +79,9 @@ export const HvTag = (props: HvTagProps) => {
label,
disabled,
type = "semantic",
selectable,
selected,
defaultSelected = false,
color,
deleteIcon,
onDelete,
Expand All @@ -79,12 +95,16 @@ export const HvTag = (props: HvTagProps) => {
const { colors } = useTheme();
const { classes, cx, css } = useClasses(classesProp);

const [isSelected, setIsSelected] = useControlled(
selected,
Boolean(defaultSelected)
);

const defaultDeleteIcon = (
<CloseXS
role="none"
className={cx(classes.button, classes.tagButton)}
iconSize="XS"
color="base_dark"
{...deleteButtonProps}
/>
);
Expand All @@ -105,6 +125,20 @@ export const HvTag = (props: HvTagProps) => {
},
});

const onClickHandler = (event) => {
if (disabled) return;
if (selectable) setIsSelected(!isSelected);
onClick?.(event, !isSelected);
};

const colorOverride = (disabled && ["atmo3", "secondary_60"]) || undefined;

const avatarIcon = isSelected ? (
<CheckboxCheck color={colorOverride} iconSize="XS" />
) : (
<Checkbox color={colorOverride} iconSize="XS" />
);

return (
<Chip
label={label}
Expand All @@ -129,7 +163,12 @@ export const HvTag = (props: HvTagProps) => {
}}
deleteIcon={deleteIcon || defaultDeleteIcon}
onDelete={disabled ? undefined : onDelete}
onClick={disabled ? undefined : onClick}
onClick={onClickHandler}
aria-pressed={isSelected}
{...(selectable &&
type === "semantic" && {
avatar: avatarIcon,
})}
{...others}
/>
);
Expand Down
75 changes: 75 additions & 0 deletions packages/core/src/Tag/stories/Selectable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { useState } from "react";

import { HvTag, HvTypography, theme } from "@hitachivantara/uikit-react-core";

import { css } from "@emotion/css";

const tags = [
{
label: "Asset 1",
color: undefined,
bgColor: "cat1",
},
{
label: "Asset 2",
color: theme.colors.negative_20,
bgColor: "negative",
},
{
label: "Asset 3",
color: undefined,
bgColor: "warning",
},
{
label: "Asset 4",
color: theme.colors.atmo1,
bgColor: "positive",
},
];

export const Selectable = () => {
const [selectedTags, setSelectedTags] = useState<any>([]);

const handleSelect = (
event: React.MouseEvent<HTMLElement>,
selected?: boolean,
label?: string
) => {
if (selected) {
setSelectedTags((prev) => [...prev, label]);
} else {
setSelectedTags((prev) => prev.filter((item: any) => item !== label));
}
};

return (
<div
className={css({
display: "flex",
flexDirection: "column",
gap: theme.space.sm,
})}
>
<div className={css({ display: "flex", gap: theme.space.sm })}>
{tags.map((tag) => (
<HvTag
label={tag.label}
color={tag.bgColor}
selectable
onDelete={() => {
alert("On Delete Action");
}}
onClick={(event, selected) =>
handleSelect(event, selected, tag.label)
}
classes={{ root: css({ color: tag.color || "unset" }) }}
/>
))}
</div>
<div className={css({ display: "flex" })}>
<HvTypography variant="label">Selected tags:</HvTypography>&nbsp;
<HvTypography>{selectedTags.join(", ")}</HvTypography>
</div>
</div>
);
};
76 changes: 76 additions & 0 deletions packages/core/src/Tag/stories/SelectableControlled.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { useState } from "react";

import { HvTag, HvTypography, theme } from "@hitachivantara/uikit-react-core";

import { css } from "@emotion/css";

const tags = [
{
label: "Asset 1",
color: undefined,
bgColor: "cat1",
},
{
label: "Asset 2",
color: theme.colors.negative_20,
bgColor: "negative",
},
{
label: "Asset 3",
color: undefined,
bgColor: "warning",
},
{
label: "Asset 4",
color: theme.colors.atmo1,
bgColor: "positive",
},
];

export const SelectableControlled = () => {
const [selectedTags, setSelectedTags] = useState<any>(["Asset 1", "Asset 3"]);

const handleSelect = (
event: React.MouseEvent<HTMLElement>,
selected?: boolean,
tag?: React.ReactNode
) => {
if (selected) {
setSelectedTags((prev) => [...prev, tag]);
} else {
setSelectedTags((prev) => prev.filter((item: any) => item !== tag));
}
};

return (
<div
className={css({
display: "flex",
flexDirection: "column",
gap: theme.space.sm,
})}
>
<div className={css({ display: "flex", gap: theme.space.sm })}>
{tags.map((tag) => (
<HvTag
label={tag.label}
color={tag.bgColor}
selectable
selected={selectedTags.includes(tag.label)}
onDelete={() => {
alert("On Delete Action");
}}
onClick={(event, selected) =>
handleSelect(event, selected, tag.label)
}
classes={{ root: css({ color: tag.color || "unset" }) }}
/>
))}
</div>
<div className={css({ display: "flex" })}>
<HvTypography variant="label">Selected tags:</HvTypography>&nbsp;
<HvTypography>{selectedTags.join(", ")}</HvTypography>
</div>
</div>
);
};

0 comments on commit 2f96540

Please sign in to comment.