From ed8b8f73e4278cceae40a5949a6bbccabc34f910 Mon Sep 17 00:00:00 2001 From: Bruno Henriques Date: Wed, 27 Nov 2024 18:36:28 +0000 Subject: [PATCH] refactor(Tag): remove MUI Chip dependency --- package-lock.json | 54 ++++---- .../core/src/ButtonBase/ButtonBase.styles.ts | 9 +- packages/core/src/Tag/Tag.styles.tsx | 114 ++++++++--------- packages/core/src/Tag/Tag.tsx | 117 ++++++++++-------- .../core/src/TagsInput/TagsInput.test.tsx | 5 +- packages/core/src/utils/keyboardUtils.ts | 4 + packages/styles/package.json | 1 + packages/styles/src/themes/pentahoPlus.ts | 36 +++--- 8 files changed, 181 insertions(+), 159 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4a7c6d6021..f545592a18 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1897,15 +1897,15 @@ } }, "node_modules/@emotion/serialize": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.2.tgz", - "integrity": "sha512-grVnMvVPK9yUVE6rkKfAJlYZgo0cu3l9iMC77V7DW6E1DUIrU68pSEXRmFZFOFB1QFo57TncmOcvcbMDWsL4yA==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", + "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", "license": "MIT", "dependencies": { "@emotion/hash": "^0.9.2", "@emotion/memoize": "^0.9.0", "@emotion/unitless": "^0.10.0", - "@emotion/utils": "^1.4.1", + "@emotion/utils": "^1.4.2", "csstype": "^3.0.2" } }, @@ -1954,9 +1954,9 @@ } }, "node_modules/@emotion/utils": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.1.tgz", - "integrity": "sha512-BymCXzCG3r72VKJxaYVwOXATqXIZ85cuvg0YOUDxMGNrKc1DJRZk8MgV5wyXRyEayIMd4FuXJIUgTBXvDNW5cA==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", + "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==", "license": "MIT" }, "node_modules/@emotion/weak-memoize": { @@ -4636,9 +4636,9 @@ } }, "node_modules/@mui/base/node_modules/@mui/utils": { - "version": "6.1.7", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.1.7.tgz", - "integrity": "sha512-Gr7cRZxBoZ0BIa3Xqf/2YaUrBLyNPJvXPQH3OsD9WMZukI/TutibbQBVqLYpgqJn8pKSjbD50Yq2auG0wI1xOw==", + "version": "6.1.9", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.1.9.tgz", + "integrity": "sha512-N7uzBp7p2or+xanXn3aH2OTINC6F/Ru/U8h6amhRZEev8bJhKN86rIDIoxZZ902tj+09LXtH83iLxFMjMHyqNA==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0", @@ -8305,15 +8305,20 @@ "license": "Apache-2.0" }, "node_modules/@swc/helpers": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz", - "integrity": "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==", + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", "license": "Apache-2.0", "dependencies": { - "@swc/counter": "^0.1.3", - "tslib": "^2.4.0" + "tslib": "^2.8.0" } }, + "node_modules/@swc/helpers/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/@swc/jest": { "version": "0.2.37", "resolved": "https://registry.npmjs.org/@swc/jest/-/jest-0.2.37.tgz", @@ -9166,9 +9171,9 @@ } }, "node_modules/@types/node": { - "version": "20.17.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.6.tgz", - "integrity": "sha512-VEI7OdvK2wP7XHnsuXbAJnEpEkF6NjSN45QJlL4VGqZSXsnicpesdTWsg9RISeSdYd3yeRj/y3k5KGjUXYnFwQ==", + "version": "20.17.8", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.8.tgz", + "integrity": "sha512-ahz2g6/oqbKalW9sPv6L2iRbhLnojxjYWspAqhjvqSWBgGebEJT5GvRmk0QXPj3sbC6rU0GTQjPLQkmR8CObvA==", "license": "MIT", "dependencies": { "undici-types": "~6.19.2" @@ -20442,9 +20447,9 @@ } }, "node_modules/jest-watch-typeahead/node_modules/char-regex": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-2.0.1.tgz", - "integrity": "sha512-oSvEeo6ZUD7NepqAat3RqoucZ5SeqLJgOvVIwkafu6IP3V0pO38s/ypdVUmDDK6qIIHNlYHJAKX9E7R7HoKElw==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-2.0.2.tgz", + "integrity": "sha512-cbGOjAptfM2LVmWhwRFHEKTPkLwNddVmuqYZQt895yXwAsWsXObCG+YN4DGQ/JBtT4GP1a1lPPdio2z413LmTg==", "dev": true, "license": "MIT", "engines": { @@ -22359,9 +22364,9 @@ } }, "node_modules/magic-string": { - "version": "0.30.12", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.12.tgz", - "integrity": "sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==", + "version": "0.30.14", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.14.tgz", + "integrity": "sha512-5c99P1WKTed11ZC0HMJOj6CDIue6F8ySu+bJL+85q1zBEIY8IklrJ1eiKC2NDRh3Ct3FcvmJPyQHb9erXMTJNw==", "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" @@ -33502,6 +33507,7 @@ "csstype": "^3.1.3" }, "devDependencies": { + "@emotion/serialize": "^1.3.1", "vite": "^5.1.0" } }, diff --git a/packages/core/src/ButtonBase/ButtonBase.styles.ts b/packages/core/src/ButtonBase/ButtonBase.styles.ts index a1b8b13b21..bdc576a7e7 100644 --- a/packages/core/src/ButtonBase/ButtonBase.styles.ts +++ b/packages/core/src/ButtonBase/ButtonBase.styles.ts @@ -11,12 +11,13 @@ export const { staticClasses, useClasses } = createClasses("HvButtonBase", { padding: 0, // Background color common for almost all variants - "&:hover": { - backgroundColor: theme.colors.containerBackgroundHover, + ":where(:not($disabled))": { + ":hover, :focus-visible": { + backgroundColor: theme.colors.containerBackgroundHover, + }, }, - "&:focus-visible": { + ":focus-visible": { ...outlineStyles, - backgroundColor: theme.colors.containerBackgroundHover, }, // Default button - no size specified diff --git a/packages/core/src/Tag/Tag.styles.tsx b/packages/core/src/Tag/Tag.styles.tsx index 5fc1d16a66..46554979ef 100644 --- a/packages/core/src/Tag/Tag.styles.tsx +++ b/packages/core/src/Tag/Tag.styles.tsx @@ -1,4 +1,3 @@ -import { chipClasses } from "@mui/material/Chip"; import { createClasses } from "@hitachivantara/uikit-react-utils"; import { theme } from "@hitachivantara/uikit-styles"; @@ -6,73 +5,64 @@ import { outlineStyles } from "../utils/focusUtils"; export const { staticClasses, useClasses } = createClasses("HvTag", { root: { + display: "inline-flex", + alignItems: "center", + justifyContent: "center", + cursor: "default", color: theme.colors.base_dark, backgroundColor: "var(--bgColor)", + height: 16, + borderRadius: 0, // theme.radii.round, + maxWidth: 180, + whiteSpace: "nowrap", - [`& .${chipClasses.avatar}`]: { - width: 12, - height: 12, - marginLeft: 2, - marginRight: 0, + ":hover, :focus": { + backgroundColor: "var(--bgColor)", }, - - [`&.${chipClasses.root}`]: { - height: 16, - borderRadius: 0, - maxWidth: 180, - fontFamily: theme.fontFamily.body, - ":hover, :focus": { - backgroundColor: "var(--bgColor)", - }, - ":focus-visible": { - ...outlineStyles, - }, - - "&$categorical": { - borderRadius: 8, - "& $label": { - color: theme.colors.secondary, - }, - }, + }, + categorical: { + borderRadius: 8, + "& $label": { + color: theme.colors.secondary, }, + }, - "&$disabled": { - opacity: 1, + disabled: { + backgroundColor: theme.colors.atmo3, + ":hover, :focus": { backgroundColor: theme.colors.atmo3, - "& $label": { - color: theme.colors.secondary_60, - }, }, - - [`& .${chipClasses.label}`]: { - paddingLeft: 4, - paddingRight: 4, - ...theme.typography.caption2, - color: "currentcolor", + "& $label": { + color: theme.colors.secondary_60, }, - - [`& .${chipClasses.deleteIcon}`]: { - margin: 0, - width: 16, - height: 16, - padding: 0, - color: "currentColor", - "&:hover": { - backgroundColor: theme.colors.containerBackgroundHover, - color: "unset", - }, - "&:focus": { - ...outlineStyles, - borderRadius: 0, - }, - "&:focus:not(:focus-visible)": { - outline: "0 !important", - boxShadow: "none !important", - }, + }, + label: { + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap", + paddingLeft: 4, + paddingRight: 4, + ...theme.typography.caption2, + color: "currentcolor", + }, + deleteIcon: { + margin: 0, + width: 16, + height: 16, + padding: 0, + color: "currentColor", + "&:hover": { + backgroundColor: theme.colors.containerBackgroundHover, + }, + "&:focus": { + ...outlineStyles, + borderRadius: 0, + }, + "&:focus:not(:focus-visible)": { + outline: "0 !important", + boxShadow: "none !important", }, }, - - disabled: {}, selected: {}, clickable: { cursor: "pointer", @@ -80,10 +70,12 @@ export const { staticClasses, useClasses } = createClasses("HvTag", { ...outlineStyles, }, }, - - categorical: {}, - label: {}, - deleteIcon: {}, + icon: { + width: 12, + height: 12, + marginLeft: 2, + marginRight: 0, + }, /** @deprecated use `root` instead */ chipRoot: {}, diff --git a/packages/core/src/Tag/Tag.tsx b/packages/core/src/Tag/Tag.tsx index ea6c1a479f..abfae34015 100644 --- a/packages/core/src/Tag/Tag.tsx +++ b/packages/core/src/Tag/Tag.tsx @@ -1,5 +1,4 @@ -import { forwardRef } from "react"; -import Chip, { ChipProps as MuiChipProps } from "@mui/material/Chip"; +import { cloneElement, forwardRef, isValidElement } from "react"; import { Checkbox, CheckboxCheck, @@ -12,7 +11,9 @@ import { } from "@hitachivantara/uikit-react-utils"; import { getColor, HvColorAny, theme } from "@hitachivantara/uikit-styles"; +import { HvButtonBase, HvButtonBaseProps } from "../ButtonBase"; import { useControlled } from "../hooks/useControlled"; +import { isDeleteKey } from "../utils/keyboardUtils"; import { staticClasses, useClasses } from "./Tag.styles"; export { staticClasses as tagClasses }; @@ -20,7 +21,10 @@ export { staticClasses as tagClasses }; export type HvTagClasses = ExtractNames; export interface HvTagProps - extends Omit { + extends Omit< + HvButtonBaseProps, + "type" | "color" | "classes" | "onClick" | "onToggle" + > { /** The label of the tag element. */ label?: React.ReactNode; /** Indicates that the form element is disabled. */ @@ -35,7 +39,7 @@ export interface HvTagProps * The callback fired when the delete icon is pressed. * This function has to be provided to the component, in order to render the delete icon * */ - onDelete?: (event: React.MouseEvent) => void; + onDelete?: React.EventHandler; /** Callback triggered when any item is clicked. */ onClick?: (event: React.MouseEvent, selected?: boolean) => void; /** Aria properties to apply to delete button in tag @@ -46,10 +50,6 @@ export interface HvTagProps deleteButtonProps?: React.HTMLAttributes; /** A Jss Object used to override or extend the styles applied to the component. */ classes?: HvTagClasses; - /** @ignore */ - 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. */ @@ -68,12 +68,13 @@ export interface HvTagProps */ export const HvTag = forwardRef< // no-indent - HTMLDivElement, + HTMLElement, HvTagProps >(function HvTag(props, ref) { const { classes: classesProp, className, + component, style, label, disabled, @@ -82,9 +83,11 @@ export const HvTag = forwardRef< selected, defaultSelected = false, color, - deleteIcon, + deleteIcon: deleteIconProp, onDelete, onClick, + onKeyDown, + onKeyUp, // TODO: remove from API // eslint-disable-next-line @typescript-eslint/no-unused-vars deleteButtonArialLabel = "Delete tag", @@ -98,67 +101,81 @@ export const HvTag = forwardRef< Boolean(defaultSelected), ); - const defaultDeleteIcon = ( - - ); + const handleDeleteClick = (event: React.MouseEvent) => { + // Stop the event from bubbling up to the `Chip` + event.stopPropagation(); + onDelete?.(event); + }; const backgroundColor = (type === "semantic" && getColor(color, "neutral_20")) || (type === "categorical" && theme.alpha(getColor(color, "cat1")!, 0.2)) || undefined; - const isClickable = !!(onClick || onDelete) && !disabled; + const isClickable = !!(onClick || onDelete || selectable); const CheckboxIcon = isSelected ? CheckboxCheck : Checkbox; - const avatarIcon = ( - - ); + + const deleteIcon = + deleteIconProp && isValidElement(deleteIconProp) ? ( + cloneElement(deleteIconProp, { + className: cx(classes.deleteIcon, { + [classes.disabledDeleteIcon]: disabled, + }), + onClick: handleDeleteClick, + }) + ) : ( + + ); return ( - ) => { + // Ignore events from children. + if (event.currentTarget === event.target && isDeleteKey(event)) { + onDelete?.(event); + } + + onKeyUp?.(event); }} - deleteIcon={deleteIcon || defaultDeleteIcon} - onDelete={disabled ? undefined : onDelete} - onClick={(event) => { + onClick={(event: React.MouseEvent) => { if (disabled) return; if (selectable) setIsSelected(!isSelected); onClick?.(event, !isSelected); }} aria-pressed={isSelected} - {...(selectable && - type === "semantic" && { - avatar: avatarIcon, - })} {...others} - /> + > + {selectable && type === "semantic" && ( + + )} + {label} + {onDelete && deleteIcon} + ); }); diff --git a/packages/core/src/TagsInput/TagsInput.test.tsx b/packages/core/src/TagsInput/TagsInput.test.tsx index d87dab366e..7952a2816d 100644 --- a/packages/core/src/TagsInput/TagsInput.test.tsx +++ b/packages/core/src/TagsInput/TagsInput.test.tsx @@ -238,11 +238,10 @@ describe("TagsInput Component", () => { expect(input).toBeNull(); const clickableButtons = screen.queryAllByRole("button"); - expect(clickableButtons.length).toBe(1); + expect(clickableButtons.length).toBe(0); // in readonly mode the button shouldn't have the close icon - const button = clickableButtons[0]; - expect(button.querySelector("[data-name=CloseXS]")).toBeNull(); + expect(document.querySelector("[data-name=CloseXS]")).toBeNull(); }); it("should call the suggestions callback when the input is changed", () => { diff --git a/packages/core/src/utils/keyboardUtils.ts b/packages/core/src/utils/keyboardUtils.ts index 7dd9fce172..89cae00c9f 100644 --- a/packages/core/src/utils/keyboardUtils.ts +++ b/packages/core/src/utils/keyboardUtils.ts @@ -29,3 +29,7 @@ export const isKey = (event: any, keyCode: Key) => { export const isOneOfKeys = (event: any, keys: Key[]) => { return keys.some((key) => isKey(event, key)); }; + +export function isDeleteKey(event: React.KeyboardEvent) { + return isOneOfKeys(event, ["Backspace", "Delete"]); +} diff --git a/packages/styles/package.json b/packages/styles/package.json index 513fbf3574..4b95e26ac7 100644 --- a/packages/styles/package.json +++ b/packages/styles/package.json @@ -32,6 +32,7 @@ "csstype": "^3.1.3" }, "devDependencies": { + "@emotion/serialize": "^1.3.1", "vite": "^5.1.0" }, "files": [ diff --git a/packages/styles/src/themes/pentahoPlus.ts b/packages/styles/src/themes/pentahoPlus.ts index c3a3b94453..fbb0c0d396 100644 --- a/packages/styles/src/themes/pentahoPlus.ts +++ b/packages/styles/src/themes/pentahoPlus.ts @@ -1,4 +1,4 @@ -import type { CSSProperties } from "react"; +import type { CSSObject } from "@emotion/serialize"; import { makeTheme } from "../makeTheme"; import { radii } from "../tokens"; @@ -578,22 +578,21 @@ const pentahoPlus = makeTheme((theme) => ({ HvTag: { classes: { root: { - "&&.MuiChip-root": { - borderRadius: theme.radii.full, - padding: theme.spacing("2px", 0), - "& .MuiChip-label": { - paddingLeft: 8, - paddingRight: 8, - }, - "& .MuiChip-avatar": { - marginLeft: theme.space.xs, - }, - "& .MuiChip-deleteIcon": { - borderRadius: "inherit", - paddingRight: 4, - }, - }, + borderRadius: theme.radii.full, + padding: theme.spacing("2px", 0), + }, + label: { + paddingLeft: 8, + paddingRight: 8, + }, + icon: { + marginLeft: theme.space.xs, + }, + deleteIcon: { + borderRadius: "inherit", + paddingRight: 4, }, + selected: {}, }, }, HvInlineEditor: { @@ -1054,7 +1053,10 @@ const pentahoPlus = makeTheme((theme) => ({ }, }, }, - } satisfies Record | { classes?: CSSProperties }>, + } satisfies Record< + string, + Record & { classes?: Record } + >, header: { height: "64px", secondLevelHeight: "56px",