Skip to content

Commit

Permalink
refactor(CollapsableItem): rewrite complexity styles to pseudo elements
Browse files Browse the repository at this point in the history
  • Loading branch information
DenisVorop committed Aug 17, 2023
1 parent 7b29e8c commit 38aa5e6
Show file tree
Hide file tree
Showing 3 changed files with 219 additions and 0 deletions.
159 changes: 159 additions & 0 deletions src/components/CollapsableItem.tsx
Original file line number Diff line number Diff line change
@@ -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 }) => <div className={className}>{children}</div>;

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 (
<StyledCollapsableContainer isOpen={isOpen} hasChild={hasChild} isRoot={isRoot} hasContent={hasContent}>
<StyledCollapsableHeader
isOpen={isOpen}
hasChild={hasChild}
isRoot={isRoot}
hasContent={hasContent}
onClick={onClick}
>
<StyledHeaderContent isOpen={isOpen} hasChild={hasChild} highlighted={!!onClick && !isOpen}>
{header}
</StyledHeaderContent>
</StyledCollapsableHeader>

{isOpen && children}
</StyledCollapsableContainer>
);
};
1 change: 1 addition & 0 deletions src/components/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,4 @@ export * from './UserMenuItem';
export * from './Table';
export * from './UserGroup';
export * from './Popup';
export * from './CollapsableItem';
59 changes: 59 additions & 0 deletions src/stories/CollapsableItem.stories.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof CollapsableItem> = {
title: 'CollapsableItem',
component: CollapsableItem,
};

export default meta;
type Story = StoryFn<typeof meta>;

type ItemProps = Omit<ComponentPropsWithoutRef<typeof CollapsableItem>, 'onClick' | 'isOpen'>;

const Item = ({ header, ...restProps }: ItemProps) => {
const [isOpen, setIsOpen] = useState(false);
return (
<CollapsableItem
isOpen={isOpen}
header={<Text style={{ padding: 10 }}>{header}</Text>}
onClick={restProps.children ? () => setIsOpen((prev) => !prev) : undefined}
{...restProps}
/>
);
};

export const Default: Story = () => {
return (
<Item header="Level A" hasChild isRoot>
<Item header="Level A-A" hasChild>
<Item header="Level A-A-A" hasChild>
<Item header="Level A-A-A-A" />
<Item header="Level A-A-A-B" hasChild>
<Item header="Level A-A-A-B-A">
<CollapsableContentItem>Hi</CollapsableContentItem>
<CollapsableContentItem>Every</CollapsableContentItem>
<CollapsableContentItem>One</CollapsableContentItem>
</Item>
</Item>
<Item header="Level A-A-A-C" />
</Item>
<Item header="Level A-A-B" />
<Item header="Level A-A-C" />
<Item header="Level A-A-D" hasChild>
<Item header="Level A-A-D-A" hasChild>
<Item header="Level A-A-D-A-A" />
</Item>
</Item>
</Item>
<Item header="Level A-B" hasChild>
<Item header="Level A-B-A" />
<Item header="Level A-B-B" hasChild>
<Item header="Level A-B-B-A" />
</Item>
</Item>
</Item>
);
};

0 comments on commit 38aa5e6

Please sign in to comment.