Shared:Components:Tag: rewrite to typescript

This commit is contained in:
Timofey Boyko 2023-12-22 12:02:34 +03:00
parent c688317de6
commit 7da76a565a
11 changed files with 334 additions and 338 deletions

View File

@ -1,210 +0,0 @@
import React from "react";
import PropTypes from "prop-types";
import { ReactSVG } from "react-svg";
// @ts-expect-error TS(2307): Cannot find module 'PUBLIC_DIR/images/cross.react.... Remove this comment to see the full error message
import CrossIconReactSvgUrl from "PUBLIC_DIR/images/cross.react.svg?url";
// @ts-expect-error TS(2307): Cannot find module 'PUBLIC_DIR/images/tag.react.sv... Remove this comment to see the full error message
import TagIconReactSvgUrl from "PUBLIC_DIR/images/tag.react.svg?url";
import DropDown from "../drop-down";
import DropDownItem from "../drop-down-item";
import IconButton from "../icon-button";
import Text from "../text";
import {
StyledTag,
StyledDropdownIcon,
StyledDropdownText,
} from "./styled-tag";
const Tag = ({
tag,
label,
isNewTag,
isDisabled,
isDefault,
isLast,
onDelete,
onClick,
advancedOptions,
tagMaxWidth,
id,
className,
style,
icon
}: any) => {
const [openDropdown, setOpenDropdown] = React.useState(false);
const tagRef = React.useRef(null);
const isMountedRef = React.useRef(true);
const onClickOutside = React.useCallback((e: any) => {
if (e?.target?.className?.includes("advanced-tag") || !isMountedRef.current)
return;
setOpenDropdown(false);
}, []);
React.useEffect(() => {
if (openDropdown) {
return document.addEventListener("click", onClickOutside);
}
document.removeEventListener("click", onClickOutside);
return () => {
document.removeEventListener("click", onClickOutside);
};
}, [openDropdown, onClickOutside]);
React.useEffect(() => {
return () => {
isMountedRef.current = false;
};
}, []);
const openDropdownAction = (e: any) => {
if (e?.target?.className?.includes("backdrop-active")) return;
setOpenDropdown(true);
};
const onClickAction = React.useCallback(
(e: any) => {
if (onClick && !isDisabled) {
onClick(e.target.dataset.tag);
}
},
[onClick, isDisabled]
);
const onDeleteAction = React.useCallback(
(e: any) => {
if (e.target != tagRef.current && onDelete) {
onDelete && onDelete(tag);
}
},
[onDelete, tag, tagRef]
);
return <>
{!!advancedOptions ? (
<>
<StyledTag
id={id}
className={`tag advanced-tag ${className ? ` ${className}` : ""}`}
style={style}
ref={tagRef}
onClick={openDropdownAction}
// @ts-expect-error TS(2769): No overload matches this call.
isDisabled={isDisabled}
isDefault={isDefault}
isLast={isLast}
tagMaxWidth={tagMaxWidth}
isClickable={!!onClick}
>
// @ts-expect-error TS(2322): Type '{ children: string; className: string; "font... Remove this comment to see the full error message
<Text className={"tag-text"} font-size={"13px"} noSelect>
...
</Text>
</StyledTag>
// @ts-expect-error TS(2769): No overload matches this call.
<DropDown
open={openDropdown}
forwardedRef={tagRef}
clickOutsideAction={onClickOutside}
// directionX={"right"}
manualY={"4px"}
>
{advancedOptions.map((tag: any, index: any) => (
<DropDownItem
className="tag__dropdown-item tag"
key={`${tag}_${index}`}
onClick={onClickAction}
data-tag={tag}
>
<StyledDropdownIcon
className="tag__dropdown-item-icon"
src={TagIconReactSvgUrl}
/>
<StyledDropdownText
className="tag__dropdown-item-text"
fontWeight={600}
fontSize={"12px"}
truncate
>
{tag}
</StyledDropdownText>
</DropDownItem>
))}
</DropDown>
</>
) : (
<StyledTag
title={label}
onClick={onClickAction}
// @ts-expect-error TS(2769): No overload matches this call.
isNewTag={isNewTag}
isDisabled={isDisabled}
isDefault={isDefault}
tagMaxWidth={tagMaxWidth}
data-tag={label}
id={id}
className={`tag${className ? ` ${className}` : ""}`}
style={style}
isLast={isLast}
isClickable={!!onClick}
>
{icon ? (
<ReactSVG className="third-party-tag" src={icon} />
) : (
<>
// @ts-expect-error TS(2322): Type '{ children: any; className: string; title: a... Remove this comment to see the full error message
<Text
className={"tag-text"}
title={label}
font-size={"13px"}
noSelect
truncate
>
{label}
</Text>
{isNewTag && (
<IconButton
// @ts-expect-error TS(2322): Type '{ className: string; iconName: any; size: st... Remove this comment to see the full error message
className={"tag-icon"}
iconName={CrossIconReactSvgUrl}
size={"10px"}
onClick={onDeleteAction}
/>
)}
</>
)}
</StyledTag>
)}
</>;
};
Tag.propTypes = {
/** Accepts the tag id */
tag: PropTypes.string,
/** Accepts the tag label */
label: PropTypes.string,
/** Accepts class */
className: PropTypes.string,
/** Accepts id */
id: PropTypes.string,
/** Accepts css style */
style: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
/** Accepts the tag styles as new and adds the delete button */
isNewTag: PropTypes.bool,
/** Accepts the tag styles as disabled and disables clicking */
isDisabled: PropTypes.bool,
/** Accepts the function that is called when the tag is clicked */
onClick: PropTypes.func,
/** Accepts the function that ist called when the tag delete button is clicked */
onDelete: PropTypes.func,
/** Accepts the max width of the tag */
tagMaxWidth: PropTypes.string,
/** Accepts the dropdown options */
advancedOptions: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
};
export default React.memo(Tag);

View File

@ -1,72 +0,0 @@
import React from "react";
import Tag from ".";
export default {
title: "Components/Tag",
component: Tag,
parameters: {
design: {
type: "figma",
url: "https://www.figma.com/file/ZiW5KSwb4t7Tj6Nz5TducC/UI-Kit-DocSpace-1.0.0?type=design&node-id=62-2597&mode=design&t=TBNCKMQKQMxr44IZ-0",
},
},
};
const Template = (args: any) => <Tag {...args} />;
export const Default = Template.bind({});
// @ts-expect-error TS(2339): Property 'args' does not exist on type '(args: any... Remove this comment to see the full error message
Default.args = {
tag: "script",
label: "Script",
isNewTag: false,
isDisabled: false,
onDelete: (tag: any) => console.log(tag),
onClick: (tag: any) => console.log(tag),
advancedOptions: null,
tagMaxWidth: "160px",
id: "",
className: "",
style: { color: "red" },
};
export const WithDropDown = Template.bind({});
// @ts-expect-error TS(2339): Property 'args' does not exist on type '(args: any... Remove this comment to see the full error message
WithDropDown.args = {
tag: "script",
label: "Script",
isNewTag: false,
isDisabled: false,
onDelete: (tag: any) => console.log(tag),
onClick: (tag: any) => console.log(tag),
advancedOptions: ["Option 1", "Option 2"],
};
export const NewTag = Template.bind({});
// @ts-expect-error TS(2339): Property 'args' does not exist on type '(args: any... Remove this comment to see the full error message
NewTag.args = {
tag: "script",
label: "Script",
isNewTag: true,
isDisabled: false,
onDelete: (tag: any) => console.log(tag),
onClick: (tag: any) => console.log(tag),
advancedOptions: null,
};
export const DisabledTag = Template.bind({});
// @ts-expect-error TS(2339): Property 'args' does not exist on type '(args: any... Remove this comment to see the full error message
DisabledTag.args = {
tag: "script",
label: "No tag",
isNewTag: false,
isDisabled: true,
onDelete: (tag: any) => console.log(tag),
onClick: (tag: any) => console.log(tag),
advancedOptions: null,
};

View File

@ -1,42 +0,0 @@
import React from "react";
// @ts-expect-error TS(7016): Could not find a declaration file for module 'enzy... Remove this comment to see the full error message
import { mount } from "enzyme";
import Tag from ".";
const baseProps = {
tag: "script",
label: "Script",
isNewTag: false,
isDisabled: false,
onDelete: (tag: any) => console.log(tag),
onClick: (tag: any) => console.log(tag),
advancedOptions: null,
tagMaxWidth: "160px",
};
// @ts-expect-error TS(2582): Cannot find name 'describe'. Do you need to instal... Remove this comment to see the full error message
describe("<Tag />", () => {
// @ts-expect-error TS(2582): Cannot find name 'it'. Do you need to install type... Remove this comment to see the full error message
it("renders without error", () => {
const wrapper = mount(<Tag {...baseProps} />);
// @ts-expect-error TS(2304): Cannot find name 'expect'.
expect(wrapper).toExist();
});
// @ts-expect-error TS(2582): Cannot find name 'it'. Do you need to install type... Remove this comment to see the full error message
it("accepts id", () => {
const wrapper = mount(<Tag {...baseProps} id="testId" />);
// @ts-expect-error TS(2304): Cannot find name 'expect'.
expect(wrapper.prop("id")).toEqual("testId");
});
// @ts-expect-error TS(2582): Cannot find name 'it'. Do you need to install type... Remove this comment to see the full error message
it("accepts className", () => {
const wrapper = mount(<Tag {...baseProps} className="test" />);
// @ts-expect-error TS(2304): Cannot find name 'expect'.
expect(wrapper.prop("className")).toEqual("test");
});
});

View File

@ -83,6 +83,7 @@ import { SaveCancelButtons } from "./save-cancel-buttons";
import { TimePicker } from "./time-picker";
import { ArticleItem } from "./article-item";
import { ToggleContent } from "./toggle-content";
import { Tag } from "./tag";
export type {
TFallbackAxisSideDirection,
@ -95,6 +96,7 @@ export type {
};
export {
Tag,
ToggleContent,
ArticleItem,
TimePicker,

View File

@ -1,13 +1,20 @@
import styled, { css } from "styled-components";
import { ReactSVG } from "react-svg";
import Text from "../text";
import Base from "../themes/base";
import { Text } from "../text";
const StyledTag = styled.div`
import { Base } from "../../themes";
const StyledTag = styled.div<{
tagMaxWidth?: string;
isLast?: boolean;
isDisabled?: boolean;
isNewTag?: boolean;
isDefault?: boolean;
isClickable?: boolean;
}>`
width: fit-content;
// @ts-expect-error TS(2339): Property 'tagMaxWidth' does not exist on type 'The... Remove this comment to see the full error message
max-width: ${(props) => (props.tagMaxWidth ? props.tagMaxWidth : "100%")};
display: flex;
@ -20,28 +27,23 @@ const StyledTag = styled.div`
${(props) =>
props.theme.interfaceDirection === "rtl"
? css`
// @ts-expect-error TS(2339): Property 'isLast' does not exist on type 'ThemedSt... Remove this comment to see the full error message
margin-left: ${props.isLast ? "0" : "4px"};
`
: css`
// @ts-expect-error TS(2339): Property 'isLast' does not exist on type 'ThemedSt... Remove this comment to see the full error message
margin-right: ${props.isLast ? "0" : "4px"};
`}
background: ${(props) =>
// @ts-expect-error TS(2339): Property 'isDisabled' does not exist on type 'Them... Remove this comment to see the full error message
props.isDisabled
? props.theme.tag.disabledBackground
// @ts-expect-error TS(2339): Property 'isNewTag' does not exist on type 'Themed... Remove this comment to see the full error message
: props.isNewTag
? props.theme.tag.newTagBackground
: props.theme.tag.background};
? props.theme.tag.newTagBackground
: props.theme.tag.background};
border-radius: 6px;
.tag-text {
color: ${(props) =>
// @ts-expect-error TS(2339): Property 'isDefault' does not exist on type 'Theme... Remove this comment to see the full error message
props.isDefault
? props.theme.tag.defaultTagColor
: props.theme.tag.color};
@ -68,14 +70,12 @@ const StyledTag = styled.div`
}
${(props) =>
// @ts-expect-error TS(2339): Property 'isClickable' does not exist on type 'The... Remove this comment to see the full error message
props.isClickable &&
// @ts-expect-error TS(2339): Property 'isDisabled' does not exist on type 'Them... Remove this comment to see the full error message
!props.isDisabled &&
css`
cursor: pointer;
&:hover {
background: ${(props) => props.theme.tag.hoverBackground};
background: ${props.theme.tag.hoverBackground};
}
`}
`;

View File

@ -0,0 +1,185 @@
import React from "react";
import { ReactSVG } from "react-svg";
import CrossIconReactSvgUrl from "PUBLIC_DIR/images/cross.react.svg?url";
import TagIconReactSvgUrl from "PUBLIC_DIR/images/tag.react.svg?url";
import { DropDown } from "../drop-down";
import { DropDownItem } from "../drop-down-item";
import { IconButton } from "../icon-button";
import { Text } from "../text";
import {
StyledTag,
StyledDropdownIcon,
StyledDropdownText,
} from "./Tag.styled";
import { TagProps } from "./Tag.types";
export const TagPure = ({
tag,
label,
isNewTag,
isDisabled,
isDefault,
isLast,
onDelete,
onClick,
advancedOptions,
tagMaxWidth,
id,
className,
style,
icon,
}: TagProps) => {
const [openDropdown, setOpenDropdown] = React.useState(false);
const tagRef = React.useRef<HTMLDivElement | null>(null);
const isMountedRef = React.useRef(true);
const onClickOutside = React.useCallback((e: Event) => {
const target = e.target as HTMLElement;
if (target.className?.includes("advanced-tag") || !isMountedRef.current)
return;
setOpenDropdown(false);
}, []);
React.useEffect(() => {
if (openDropdown) {
return document.addEventListener("click", onClickOutside);
}
document.removeEventListener("click", onClickOutside);
return () => {
document.removeEventListener("click", onClickOutside);
};
}, [openDropdown, onClickOutside]);
React.useEffect(() => {
return () => {
isMountedRef.current = false;
};
}, []);
const openDropdownAction = (e: React.MouseEvent<HTMLDivElement>) => {
const target = e.target as HTMLDivElement;
if (target?.className?.includes("backdrop-active")) return;
setOpenDropdown(true);
};
const onClickAction = React.useCallback(
(e: React.MouseEvent | React.ChangeEvent) => {
if (onClick && !isDisabled) {
const target = e.target as HTMLDivElement;
onClick(target.dataset.tag);
}
},
[onClick, isDisabled],
);
const onDeleteAction = React.useCallback(
(e: React.MouseEvent) => {
if (e.target !== tagRef.current) {
onDelete?.(tag);
}
},
[onDelete, tag, tagRef],
);
return advancedOptions ? (
<>
<StyledTag
id={id}
className={`tag advanced-tag ${className ? ` ${className}` : ""}`}
style={style}
ref={tagRef}
onClick={openDropdownAction}
isDisabled={isDisabled}
isDefault={isDefault}
isLast={isLast}
tagMaxWidth={tagMaxWidth}
isClickable={!!onClick}
data-testid="tag"
>
<Text className="tag-text" font-size="13px" noSelect>
...
</Text>
</StyledTag>
<DropDown
open={openDropdown}
forwardedRef={tagRef}
clickOutsideAction={onClickOutside}
// directionX={"right"}
manualY="4px"
>
{advancedOptions.map((t, index) => (
<DropDownItem
className="tag__dropdown-item tag"
key={`${t}_${index * 50}`}
onClick={onClickAction}
data-tag={tag}
>
<StyledDropdownIcon
className="tag__dropdown-item-icon"
src={TagIconReactSvgUrl}
/>
<StyledDropdownText
className="tag__dropdown-item-text"
fontWeight={600}
fontSize="12px"
truncate
>
{tag}
</StyledDropdownText>
</DropDownItem>
))}
</DropDown>
</>
) : (
<StyledTag
title={label}
onClick={onClickAction}
isNewTag={isNewTag}
isDisabled={isDisabled}
isDefault={isDefault}
tagMaxWidth={tagMaxWidth}
data-tag={label}
id={id}
className={`tag${className ? ` ${className}` : ""}`}
style={style}
isLast={isLast}
isClickable={!!onClick}
data-testid="tag"
>
{icon ? (
<ReactSVG className="third-party-tag" src={icon} />
) : (
<>
<Text
className="tag-text"
title={label}
font-size="13px"
noSelect
truncate
>
{label}
</Text>
{isNewTag && (
<IconButton
className="tag-icon"
iconName={CrossIconReactSvgUrl}
size={10}
onClick={onDeleteAction}
/>
)}
</>
)}
</StyledTag>
);
};
const Tag = React.memo(TagPure);
export { Tag };

View File

@ -0,0 +1,27 @@
export interface TagProps {
/** Accepts the tag id */
tag: string;
/** Accepts the tag label */
label?: string;
/** Accepts class */
className?: string;
/** Accepts id */
id?: string;
/** Accepts css style */
style?: React.CSSProperties;
/** Accepts the tag styles as new and adds the delete button */
isNewTag?: boolean;
/** Accepts the tag styles as disabled and disables clicking */
isDisabled?: boolean;
/** Accepts the function that is called when the tag is clicked */
onClick: (tag?: string) => void;
/** Accepts the function that ist called when the tag delete button is clicked */
onDelete?: (tag?: string) => void;
/** Accepts the max width of the tag */
tagMaxWidth?: string;
/** Accepts the dropdown options */
advancedOptions?: React.ReactNode[];
icon?: string;
isDefault?: boolean;
isLast?: boolean;
}

View File

@ -0,0 +1 @@
export { Tag } from "./Tag";

View File

@ -0,0 +1,67 @@
import { Meta, StoryObj } from "@storybook/react";
import { TagPure } from "./Tag";
const meta = {
title: "Components/Tag",
component: TagPure,
parameters: {
design: {
type: "figma",
url: "https://www.figma.com/file/ZiW5KSwb4t7Tj6Nz5TducC/UI-Kit-DocSpace-1.0.0?type=design&node-id=62-2597&mode=design&t=TBNCKMQKQMxr44IZ-0",
},
},
} satisfies Meta<typeof TagPure>;
type Story = StoryObj<typeof TagPure>;
export default meta;
export const Default: Story = {
args: {
tag: "script",
label: "Script",
isNewTag: false,
isDisabled: false,
onDelete: () => {},
onClick: () => {},
tagMaxWidth: "160px",
id: "",
className: "",
style: { color: "red" },
},
};
export const WithDropDown: Story = {
args: {
tag: "script",
label: "Script",
isNewTag: false,
isDisabled: false,
onDelete: () => {},
onClick: () => {},
advancedOptions: ["Option 1", "Option 2"],
},
};
export const NewTag: Story = {
args: {
tag: "script",
label: "Script",
isNewTag: true,
isDisabled: false,
onDelete: () => {},
onClick: () => {},
},
};
export const DisabledTag: Story = {
args: {
tag: "script",
label: "No tag",
isNewTag: false,
isDisabled: true,
onDelete: () => {},
onClick: () => {},
},
};

View File

@ -0,0 +1,38 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import "@testing-library/jest-dom";
import { Tag } from "./Tag";
const baseProps = {
tag: "script",
label: "Script",
isNewTag: false,
isDisabled: false,
onDelete: () => {},
onClick: () => {},
tagMaxWidth: "160px",
};
describe("<Tag />", () => {
it("renders without error", () => {
render(<Tag {...baseProps} />);
expect(screen.getByTestId("tag")).toBeInTheDocument();
});
// it("accepts id", () => {
// const wrapper = mount(<Tag {...baseProps} id="testId" />);
// // @ts-expect-error TS(2304): Cannot find name 'expect'.
// expect(wrapper.prop("id")).toEqual("testId");
// });
// it("accepts className", () => {
// const wrapper = mount(<Tag {...baseProps} className="test" />);
// // @ts-expect-error TS(2304): Cannot find name 'expect'.
// expect(wrapper.prop("className")).toEqual("test");
// });
});