diff --git a/packages/shared/components/index.ts b/packages/shared/components/index.ts index 6bb83e2fb7..b752071f09 100644 --- a/packages/shared/components/index.ts +++ b/packages/shared/components/index.ts @@ -85,6 +85,7 @@ import { TimePicker } from "./time-picker"; import { ArticleItem } from "./article-item"; import { ToggleContent } from "./toggle-content"; import { Tag } from "./tag"; +import { Tags } from "./tags"; export type { TFallbackAxisSideDirection, @@ -98,6 +99,7 @@ export type { export { Tag, + Tags, ToggleContent, ArticleItem, TimePicker, diff --git a/packages/shared/components/tags/Tags.stories.tsx b/packages/shared/components/tags/Tags.stories.tsx new file mode 100644 index 0000000000..b581257c55 --- /dev/null +++ b/packages/shared/components/tags/Tags.stories.tsx @@ -0,0 +1,27 @@ +import { Meta, StoryObj } from "@storybook/react"; + +import { Tags } from "./Tags"; + +type TagsType = typeof Tags; +type Story = StoryObj; + +const meta: Meta = { + title: "Components/Tags", + component: Tags, + 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", + }, + }, +}; + +export default meta; + +export const Default: Story = { + args: { + tags: ["tag1", "tag2"], + columnCount: 2, + onSelectTag: () => {}, + }, +}; diff --git a/packages/shared/components/tags/StyledTags.ts b/packages/shared/components/tags/Tags.styled.ts similarity index 74% rename from packages/shared/components/tags/StyledTags.ts rename to packages/shared/components/tags/Tags.styled.ts index 0939585267..382a53bb72 100644 --- a/packages/shared/components/tags/StyledTags.ts +++ b/packages/shared/components/tags/Tags.styled.ts @@ -1,4 +1,4 @@ -import styled, { css } from "styled-components"; +import styled from "styled-components"; const StyledTags = styled.div` width: 100%; diff --git a/packages/shared/components/tags/Tags.tsx b/packages/shared/components/tags/Tags.tsx new file mode 100644 index 0000000000..6ee6ce6cc9 --- /dev/null +++ b/packages/shared/components/tags/Tags.tsx @@ -0,0 +1,163 @@ +import React, { FC } from "react"; + +import { Tag } from "../tag"; + +import StyledTags from "./Tags.styled"; +import { isTagType } from "./Tags.utils"; +import type { TagType, TagsProps } from "./Tags.types"; + +export const Tags: FC = ({ + id, + tags, + style, + className, + columnCount, + onSelectTag, +}) => { + const [renderedTags, setRenderedTags] = React.useState([]); + + const tagsRef = React.useRef(null); + + const updateRenderedTags = React.useCallback(() => { + if (tags && tagsRef) { + if (!columnCount) return; + + const newTags: TagType[] = []; + const containerWidth = tagsRef.current?.offsetWidth ?? 0; + + if (tags.length === 1) { + const firstTag = tags[0]; + + if (isTagType(firstTag) && firstTag?.isDefault) { + const tag = { ...firstTag, maxWidth: `100%` }; + newTags.push(tag); + } else if (isTagType(firstTag) && firstTag?.isThirdParty) { + const tag = { ...firstTag, maxWidth: `36px` }; + newTags.push(tag); + } else { + const label = isTagType(firstTag) ? firstTag.label : firstTag; + + const tag = { label, maxWidth: `100%` }; + newTags.push(tag); + } + + return setRenderedTags(newTags); + } + + if ( + columnCount >= tags.length || + (tags.length === 2 && + isTagType(tags[0]) && + tags[0]?.isThirdParty && + isTagType(tags[1]) && + tags[1]?.isDefault) + ) { + const thirdPartyTagCount = + isTagType(tags[0]) && tags[0]?.isThirdParty ? 1 : 0; + + const currentTagMaxWidth = + (containerWidth - + thirdPartyTagCount * 40 - + (tags.length - thirdPartyTagCount) * 4) / + (tags.length - thirdPartyTagCount); + + const maxWidthPercent = Math.floor( + (currentTagMaxWidth / containerWidth) * 100, + ); + + for (let i = 0; i < tags.length; i += 1) { + const tag = tags[i]; + const isTag = isTagType(tag); + + if (isTag && tag?.isThirdParty) { + const tagNew = { ...tag, maxWidth: `36px` }; + newTags.push(tagNew); + } else if (isTag && tag?.isDefault) { + const tagNew = { ...tag, maxWidth: `${maxWidthPercent}%` }; + newTags.push(tagNew); + } else { + const tagNew = { + label: isTag ? tag.label : tag, + maxWidth: `${maxWidthPercent}%`, + }; + newTags.push(tagNew); + } + } + } else { + const tagWithDropdown = { + label: "", + key: "selector", + advancedOptions: tags.slice( + columnCount, + tags.length, + ) as React.ReactNode[], + }; + + const currentTagMaxWidth = + (containerWidth - columnCount * 4 - 35) / columnCount; + + const maxWidthPercent = Math.floor( + (currentTagMaxWidth / containerWidth) * 100, + ); + + if (columnCount !== 0) { + for (let i = 0; i < columnCount; i += 1) { + const tag = tags[i]; + const isTag = isTagType(tag); + + if (isTag && tag?.isThirdParty) { + const tagNew = { ...tag, maxWidth: `36px` }; + newTags.push(tagNew); + } else if (isTag && tag?.isDefault) { + const tagNew = { ...tag, maxWidth: `${maxWidthPercent}%` }; + newTags.push(tagNew); + } else { + const tagNew = { + label: isTag ? tag.label : tag, + maxWidth: `${maxWidthPercent}%`, + }; + newTags.push(tagNew); + } + } + } + + newTags.push(tagWithDropdown); + + newTags[newTags.length - 1].maxWidth = `35px`; + } + + setRenderedTags(newTags); + } + }, [tags, tagsRef, columnCount]); + + React.useEffect(() => { + updateRenderedTags(); + }, [updateRenderedTags]); + + return ( + + {renderedTags?.length > 0 && + renderedTags.map((tag, idx) => { + return ( + + ); + })} + + ); +}; diff --git a/packages/shared/components/tags/Tags.types.ts b/packages/shared/components/tags/Tags.types.ts new file mode 100644 index 0000000000..d396979733 --- /dev/null +++ b/packages/shared/components/tags/Tags.types.ts @@ -0,0 +1,28 @@ +export type TagType = { + key?: string; + isDefault?: boolean; + isThirdParty?: boolean; + /** Accepts the tag label */ + label: string; + /** Accepts the max width of the tag */ + maxWidth?: string; + /** Accepts the dropdown options */ + advancedOptions?: React.ReactNode[]; + /** Accepts the tag styles as disabled and disables clicking */ + isDisabled?: boolean; +}; + +export interface TagsProps { + /** Accepts id */ + id?: string; + /** Accepts the tags */ + tags: Array; + /** Accepts class */ + className?: string; + /** Accepts the tag column count */ + columnCount: number; + /** Accepts css style */ + style?: React.CSSProperties; + /** Accepts the function that is called when the tag is selected */ + onSelectTag: (tag?: string) => void; +} diff --git a/packages/shared/components/tags/Tags.utils.ts b/packages/shared/components/tags/Tags.utils.ts new file mode 100644 index 0000000000..2eb099b733 --- /dev/null +++ b/packages/shared/components/tags/Tags.utils.ts @@ -0,0 +1,5 @@ +import type { TagType } from "./Tags.types"; + +export const isTagType = (tag: TagType | string): tag is TagType => { + return typeof tag === "object"; +}; diff --git a/packages/shared/components/tags/index.ts b/packages/shared/components/tags/index.ts new file mode 100644 index 0000000000..62fc1866d7 --- /dev/null +++ b/packages/shared/components/tags/index.ts @@ -0,0 +1 @@ +export { Tags } from "./Tags"; diff --git a/packages/shared/components/tags/index.tsx b/packages/shared/components/tags/index.tsx deleted file mode 100644 index d6b527bcca..0000000000 --- a/packages/shared/components/tags/index.tsx +++ /dev/null @@ -1,151 +0,0 @@ -import React from "react"; -import PropTypes from "prop-types"; -import Tag from "../tag"; - -import StyledTags from "./StyledTags"; - -const Tags = ({ - id, - className, - style, - tags, - columnCount, - onSelectTag -}: any) => { - const [renderedTags, setRenderedTags] = React.useState(null); - - const tagsRef = React.useRef(null); - - const updateRenderedTags = React.useCallback(() => { - if (tags && tagsRef) { - if (!columnCount) return; - - const newTags = []; - // @ts-expect-error TS(2531): Object is possibly 'null'. - const containerWidth = tagsRef.current.offsetWidth; - - if (tags.length === 1) { - if (tags[0]?.isDefault) { - const tag = { ...tags[0], maxWidth: `100%` }; - newTags.push(tag); - } else if (tags[0]?.isThirdParty) { - const tag = { ...tags[0], maxWidth: `36px` }; - newTags.push(tag); - } else { - const tag = { label: tags[0].label || tags[0], maxWidth: `100%` }; - newTags.push(tag); - } - - // @ts-expect-error TS(2345): Argument of type 'any[]' is not assignable to para... Remove this comment to see the full error message - return setRenderedTags(newTags); - } - - if ( - columnCount >= tags.length || - (tags.length === 2 && tags[0]?.isThirdParty && tags[1]?.isDefault) - ) { - const thirdPartyTagCount = tags[0]?.isThirdParty ? 1 : 0; - - const currentTagMaxWidth = - (containerWidth - - thirdPartyTagCount * 40 - - (tags.length - thirdPartyTagCount) * 4) / - (tags.length - thirdPartyTagCount); - - const maxWidthPercent = Math.floor( - (currentTagMaxWidth / containerWidth) * 100 - ); - - for (let i = 0; i < tags.length; i++) { - if (tags[i]?.isThirdParty) { - const tag = { ...tags[i], maxWidth: `36px` }; - newTags.push(tag); - } else if (tags[i]?.isDefault) { - const tag = { ...tags[i], maxWidth: `${maxWidthPercent}%` }; - newTags.push(tag); - } else { - const tag = { label: tags[i], maxWidth: `${maxWidthPercent}%` }; - newTags.push(tag); - } - } - } else { - const tagWithDropdown = { - key: "selector", - advancedOptions: tags.slice(columnCount, tags.length), - }; - - const currentTagMaxWidth = - (containerWidth - columnCount * 4 - 35) / columnCount; - - const maxWidthPercent = Math.floor( - (currentTagMaxWidth / containerWidth) * 100 - ); - - if (columnCount !== 0) { - for (let i = 0; i < columnCount; i++) { - if (tags[i]?.isThirdParty) { - const tag = { ...tags[i], maxWidth: `36px` }; - newTags.push(tag); - } else if (tags[i]?.isDefault) { - const tag = { ...tags[i], maxWidth: `${maxWidthPercent}%` }; - newTags.push(tag); - } else { - const tag = { label: tags[i], maxWidth: `${maxWidthPercent}%` }; - newTags.push(tag); - } - } - } - - newTags.push(tagWithDropdown); - - newTags[newTags.length - 1].maxWidth = `35px`; - } - - // @ts-expect-error TS(2345): Argument of type 'any[]' is not assignable to para... Remove this comment to see the full error message - setRenderedTags(newTags); - } - }, [tags, tagsRef, columnCount]); - - React.useEffect(() => { - updateRenderedTags(); - }, [tags, tagsRef, columnCount]); - - return ( - - // @ts-expect-error TS(2339): Property 'length' does not exist on type 'never'. - {renderedTags?.length > 0 && - // @ts-expect-error TS(2531): Object is possibly 'null'. - renderedTags.map((tag: any, index: any) => ( - - ))} - - ); -}; - -Tags.propTypes = { - /** Accepts the tags */ - tags: PropTypes.array, - /** Accepts the tag column count */ - columnCount: PropTypes.number, - /** Accepts class */ - className: PropTypes.string, - /** Accepts id */ - id: PropTypes.string, - /** Accepts css style */ - style: PropTypes.oneOfType([PropTypes.object, PropTypes.array]), - /** Accepts the function that is called when the tag is selected */ - onSelectTag: PropTypes.func, -}; - -export default Tags; diff --git a/packages/shared/components/tags/tags.stories.tsx b/packages/shared/components/tags/tags.stories.tsx deleted file mode 100644 index 20ac1f4153..0000000000 --- a/packages/shared/components/tags/tags.stories.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import React from "react"; -import Tags from "."; - -export default { - title: "Components/Tags", - component: Tags, - 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) => ; - -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 = { - tags: ["tag1", "tag2"], - id: "", - className: "", - columnCount: 2, - style: {}, - onSelectTag: (tags: any) => {}, -};