From 38aa5e661524800fca7636cf5aef2eae1d3f160b Mon Sep 17 00:00:00 2001 From: DenisVorop Date: Thu, 17 Aug 2023 12:14:30 +0300 Subject: [PATCH 1/3] refactor(CollapsableItem): rewrite complexity styles to pseudo elements --- src/components/CollapsableItem.tsx | 159 ++++++++++++++++++++++++ src/components/index.tsx | 1 + src/stories/CollapsableItem.stories.tsx | 59 +++++++++ 3 files changed, 219 insertions(+) create mode 100644 src/components/CollapsableItem.tsx create mode 100644 src/stories/CollapsableItem.stories.tsx diff --git a/src/components/CollapsableItem.tsx b/src/components/CollapsableItem.tsx new file mode 100644 index 00000000..763e3529 --- /dev/null +++ b/src/components/CollapsableItem.tsx @@ -0,0 +1,159 @@ +import React, { FC, ReactNode } from 'react'; +import styled, { css } from 'styled-components'; +import { backgroundColor, gray4, gray7, radiusM } from '@taskany/colors'; + +export const collapseOffset = 20; + +const dotSize = 8; +const halfDotSize = dotSize / 2; +const doubleDotSize = dotSize * 2; + +const dot = css` + width: ${dotSize}px; + height: ${dotSize}px; + border-radius: ${radiusM}; + background: ${gray7}; +`; + +const StyledCollapsableHeader = styled.div<{ + isRoot?: boolean; + hasChild?: boolean; + isOpen: boolean; + hasContent: boolean; +}>` + ${({ isOpen, hasChild, isRoot, hasContent }) => + !isRoot && + hasContent && + css` + &::before { + content: ''; + ${dot}; + + position: absolute; + top: 16px; + left: ${hasChild && isOpen + ? `calc(-${doubleDotSize}px - ${collapseOffset}px)` + : `calc(-${doubleDotSize}px)`}; + z-index: 1; + } + `}; +`; + +const StyledHeaderContent = styled.div<{ highlighted: boolean; hasChild?: boolean; isOpen: boolean }>` + position: relative; + + border-radius: ${radiusM}; + ${({ highlighted }) => + highlighted && + ` + background: ${gray4}; + `} + + ${({ hasChild, isOpen }) => + hasChild && + isOpen && + css` + &::after { + content: ''; + ${dot}; + + position: absolute; + top: 50%; + transform: translateY(-50%); + left: calc(-2 * ${dotSize}px); + } + `}; +`; + +const StyledCollapsableContainer = styled.div<{ + hasChild?: boolean; + isOpen: boolean; + hasContent: boolean; + isRoot?: boolean; +}>` + position: relative; + + ${({ isRoot, isOpen, hasChild }) => + !isRoot && + isOpen && + hasChild && { + marginLeft: `${collapseOffset}px`, + }}; + + ${({ hasChild }) => + hasChild && + css` + &::before { + content: ''; + position: absolute; + width: 1px; + height: calc(100% - ${doubleDotSize}px - ${doubleDotSize}px - ${dotSize}px - 2px); + background-color: ${gray7}; + left: calc(-${doubleDotSize}px + ${halfDotSize}px); + top: calc(${doubleDotSize}px + ${halfDotSize}px); + } + `}; + + ${({ hasChild, isOpen, isRoot, hasContent }) => + !isRoot && hasContent + ? css` + &:last-child::after { + content: ''; + position: absolute; + + width: 1px; + height: 100%; + background: ${backgroundColor}; + + top: ${doubleDotSize}px; + left: ${`calc(-${halfDotSize}px - ${dotSize}px - ${ + (isOpen && hasChild ? 1 : 0) * collapseOffset + }px)`}; + } + ` + : !isRoot && + css` + &:last-child::after { + content: ''; + position: absolute; + ${dot}; + + top: ${doubleDotSize}px; + left: calc(-2 * ${dotSize}px); + } + `}; +`; + +export const CollapsableContentItem: FC<{ + children?: ReactNode; + className?: string; +}> = ({ children, className }) =>
{children}
; + +export const CollapsableItem: FC<{ + children?: ReactNode; + header: ReactNode; + isRoot?: boolean; + isOpen: boolean; + hasChild?: boolean; + onClick?: () => void; +}> = ({ children, header, isOpen, isRoot, hasChild, onClick }) => { + const hasContent = Boolean(children); + + return ( + + + + {header} + + + + {isOpen && children} + + ); +}; diff --git a/src/components/index.tsx b/src/components/index.tsx index 2250130b..e36d72d1 100644 --- a/src/components/index.tsx +++ b/src/components/index.tsx @@ -51,3 +51,4 @@ export * from './UserMenuItem'; export * from './Table'; export * from './UserGroup'; export * from './Popup'; +export * from './CollapsableItem'; diff --git a/src/stories/CollapsableItem.stories.tsx b/src/stories/CollapsableItem.stories.tsx new file mode 100644 index 00000000..b31a4339 --- /dev/null +++ b/src/stories/CollapsableItem.stories.tsx @@ -0,0 +1,59 @@ +import React, { ComponentPropsWithoutRef, useState } from 'react'; +import { Meta, StoryFn } from '@storybook/react'; + +import { CollapsableItem, CollapsableContentItem, Text } from '../components'; + +const meta: Meta = { + title: 'CollapsableItem', + component: CollapsableItem, +}; + +export default meta; +type Story = StoryFn; + +type ItemProps = Omit, 'onClick' | 'isOpen'>; + +const Item = ({ header, ...restProps }: ItemProps) => { + const [isOpen, setIsOpen] = useState(false); + return ( + {header}} + onClick={restProps.children ? () => setIsOpen((prev) => !prev) : undefined} + {...restProps} + /> + ); +}; + +export const Default: Story = () => { + return ( + + + + + + + Hi + Every + One + + + + + + + + + + + + + + + + + + + + ); +}; From f35fa077040608302f2766aa97148f7250188386 Mon Sep 17 00:00:00 2001 From: DenisVorop Date: Fri, 18 Aug 2023 13:05:10 +0300 Subject: [PATCH 2/3] chore: add more examples for CollapsableItem story --- src/stories/CollapsableItem.stories.tsx | 80 ++++++++++++++++++++++++- 1 file changed, 77 insertions(+), 3 deletions(-) diff --git a/src/stories/CollapsableItem.stories.tsx b/src/stories/CollapsableItem.stories.tsx index b31a4339..6ec23448 100644 --- a/src/stories/CollapsableItem.stories.tsx +++ b/src/stories/CollapsableItem.stories.tsx @@ -1,7 +1,19 @@ import React, { ComponentPropsWithoutRef, useState } from 'react'; import { Meta, StoryFn } from '@storybook/react'; -import { CollapsableItem, CollapsableContentItem, Text } from '../components'; +import { + CollapsableItem, + CollapsableContentItem, + Text, + Table, + TableRow, + TableCell, + GoalIcon, + UserPic, + Dot, + Tag, + CircleProgressBar, +} from '../components'; const meta: Meta = { title: 'CollapsableItem', @@ -15,16 +27,46 @@ type ItemProps = Omit, 'onClick const Item = ({ header, ...restProps }: ItemProps) => { const [isOpen, setIsOpen] = useState(false); + + const handleOpen = () => { + setTimeout( + () => { + setIsOpen((prev) => !prev); + }, + Math.random() > 0.5 ? 300 : 0, + ); + }; + return ( {header}} - onClick={restProps.children ? () => setIsOpen((prev) => !prev) : undefined} + onClick={restProps.children ? () => handleOpen() : undefined} {...restProps} /> ); }; +const data = Array.from({ length: 4 }, (_, i) => ({ + title: `Title ${i + 1}`, + projectId: (Math.random() * 10).toString(16).slice(2, 8).padEnd(6, 'AB').toUpperCase(), + tags: Array.from({ length: Math.ceil(Math.random() * 5) }, (_, i) => `Tag ${i + 1}`), + progress: Math.round(Math.random() * 100), + children: Array.from({ length: 3 }, (_, i) => ({ + title: `Title ${i + 1}`, + projectId: (Math.random() * 10).toString(16).slice(2, 8).padEnd(6, 'AB').toUpperCase(), + tags: Array.from({ length: Math.ceil(Math.random() * 5) }, (_, i) => `Tag ${i + 1}`), + progress: Math.round(Math.random() * 100), + children: Array.from({ length: 3 }, (_, i) => ({ + title: `Title ${i + 1}`, + projectId: (Math.random() * 10).toString(16).slice(2, 8).padEnd(6, 'AB').toUpperCase(), + tags: Array.from({ length: Math.ceil(Math.random() * 5) }, (_, i) => `Tag ${i + 1}`), + progress: Math.round(Math.random() * 100), + children: undefined, + })), + })), +})); + export const Default: Story = () => { return ( @@ -44,7 +86,39 @@ export const Default: Story = () => { - + + + {data.map(({ title, projectId, tags, progress }) => ( + + + + + + + + {title} + + + + + + + + {projectId} + + + + {tags.map((t) => ( + + ))} + + + + + + ))} +
+
From 575b1de32b48f4f94a1502c8abb7b474fccdfd64 Mon Sep 17 00:00:00 2001 From: DenisVorop Date: Mon, 21 Aug 2023 12:27:17 +0300 Subject: [PATCH 3/3] refactor(CollapsableItem): renamed prop hasChild to hasNestedCollapsableItems --- src/components/CollapsableItem.tsx | 45 +++++++++++++++---------- src/stories/CollapsableItem.stories.tsx | 16 ++++----- 2 files changed, 35 insertions(+), 26 deletions(-) diff --git a/src/components/CollapsableItem.tsx b/src/components/CollapsableItem.tsx index 763e3529..b92db7b1 100644 --- a/src/components/CollapsableItem.tsx +++ b/src/components/CollapsableItem.tsx @@ -17,11 +17,11 @@ const dot = css` const StyledCollapsableHeader = styled.div<{ isRoot?: boolean; - hasChild?: boolean; + hasNestedCollapsableItems?: boolean; isOpen: boolean; hasContent: boolean; }>` - ${({ isOpen, hasChild, isRoot, hasContent }) => + ${({ isOpen, hasNestedCollapsableItems, isRoot, hasContent }) => !isRoot && hasContent && css` @@ -31,7 +31,7 @@ const StyledCollapsableHeader = styled.div<{ position: absolute; top: 16px; - left: ${hasChild && isOpen + left: ${hasNestedCollapsableItems && isOpen ? `calc(-${doubleDotSize}px - ${collapseOffset}px)` : `calc(-${doubleDotSize}px)`}; z-index: 1; @@ -39,7 +39,7 @@ const StyledCollapsableHeader = styled.div<{ `}; `; -const StyledHeaderContent = styled.div<{ highlighted: boolean; hasChild?: boolean; isOpen: boolean }>` +const StyledHeaderContent = styled.div<{ highlighted: boolean; hasNestedCollapsableItems?: boolean; isOpen: boolean }>` position: relative; border-radius: ${radiusM}; @@ -49,8 +49,8 @@ const StyledHeaderContent = styled.div<{ highlighted: boolean; hasChild?: boolea background: ${gray4}; `} - ${({ hasChild, isOpen }) => - hasChild && + ${({ hasNestedCollapsableItems, isOpen }) => + hasNestedCollapsableItems && isOpen && css` &::after { @@ -66,22 +66,22 @@ const StyledHeaderContent = styled.div<{ highlighted: boolean; hasChild?: boolea `; const StyledCollapsableContainer = styled.div<{ - hasChild?: boolean; + hasNestedCollapsableItems?: boolean; isOpen: boolean; hasContent: boolean; isRoot?: boolean; }>` position: relative; - ${({ isRoot, isOpen, hasChild }) => + ${({ isRoot, isOpen, hasNestedCollapsableItems }) => !isRoot && isOpen && - hasChild && { + hasNestedCollapsableItems && { marginLeft: `${collapseOffset}px`, }}; - ${({ hasChild }) => - hasChild && + ${({ hasNestedCollapsableItems }) => + hasNestedCollapsableItems && css` &::before { content: ''; @@ -94,7 +94,7 @@ const StyledCollapsableContainer = styled.div<{ } `}; - ${({ hasChild, isOpen, isRoot, hasContent }) => + ${({ hasNestedCollapsableItems, isOpen, isRoot, hasContent }) => !isRoot && hasContent ? css` &:last-child::after { @@ -107,7 +107,7 @@ const StyledCollapsableContainer = styled.div<{ top: ${doubleDotSize}px; left: ${`calc(-${halfDotSize}px - ${dotSize}px - ${ - (isOpen && hasChild ? 1 : 0) * collapseOffset + (isOpen && hasNestedCollapsableItems ? 1 : 0) * collapseOffset }px)`}; } ` @@ -134,21 +134,30 @@ export const CollapsableItem: FC<{ header: ReactNode; isRoot?: boolean; isOpen: boolean; - hasChild?: boolean; + hasNestedCollapsableItems?: boolean; onClick?: () => void; -}> = ({ children, header, isOpen, isRoot, hasChild, onClick }) => { +}> = ({ children, header, isOpen, isRoot, hasNestedCollapsableItems, onClick }) => { const hasContent = Boolean(children); return ( - + - + {header} diff --git a/src/stories/CollapsableItem.stories.tsx b/src/stories/CollapsableItem.stories.tsx index 6ec23448..1295117b 100644 --- a/src/stories/CollapsableItem.stories.tsx +++ b/src/stories/CollapsableItem.stories.tsx @@ -69,11 +69,11 @@ const data = Array.from({ length: 4 }, (_, i) => ({ export const Default: Story = () => { return ( - - - + + + - + Hi Every @@ -84,8 +84,8 @@ export const Default: Story = () => { - - + + {data.map(({ title, projectId, tags, progress }) => ( @@ -122,9 +122,9 @@ export const Default: Story = () => { - + - +