From 38aa5e661524800fca7636cf5aef2eae1d3f160b Mon Sep 17 00:00:00 2001 From: DenisVorop Date: Thu, 17 Aug 2023 12:14:30 +0300 Subject: [PATCH] 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 + + + + + + + + + + + + + + + + + + + + ); +};