Skip to content

Commit

Permalink
feat(core): Tree revisited!
Browse files Browse the repository at this point in the history
  • Loading branch information
ggdaltoso committed Feb 18, 2024
1 parent 71277c4 commit 53e26c6
Show file tree
Hide file tree
Showing 13 changed files with 216 additions and 217 deletions.
1 change: 1 addition & 0 deletions packages/core/.storybook/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { ImageLoader } from 'esbuild-vanilla-image-loader';
export default {
// stories: [, '../stories/(?!all)*.stories.tsx'],
stories: [
'../stories/tree.stories.tsx',
'../stories/tabs.stories.tsx',
'../stories/list.stories.tsx',
'../stories/taskbar.stories.tsx',
Expand Down
150 changes: 68 additions & 82 deletions packages/core/components/Tree/Node.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
import * as React from 'react';
import styled from '@xstyled/styled-components';

import treeMidLines from './imgs/tree-mid.png';
import treeLastLines from './imgs/tree-last.png';
import treeNodeChildrenLine from './imgs/tree-node-children.png';
import {
Bat,
BatExec,
Expand All @@ -16,6 +12,9 @@ import {
FolderOpen,
MediaCd,
} from '@react95/icons';
import * as styles from './Tree.css';
import { Frame, FrameProps } from '../Frame/Frame';
import cn from 'classnames';

export const icons = {
FILE_MEDIA: MediaCd,
Expand All @@ -28,71 +27,6 @@ export const icons = {
FILE_EXECUTABLE: BatExec,
} as const;

const NodeItem = styled.div<{ isOpen: boolean }>`
list-style-type: none;
background-repeat: no-repeat;
background-image: url(${treeMidLines});
&:last-child {
background-image: url(${({ isOpen }) =>
isOpen ? treeMidLines : treeLastLines});
}
`;

const NodeInfo = styled.div`
display: flex;
align-items: center;
user-select: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
`;

const FolderStatus = styled.div`
display: flex;
align-items: center;
justify-content: center;
width: 10px;
height: 10px;
border: 1;
border-color: borderDarkest;
background-color: inputBackground;
font-size: 11px;
`;

const IconContainer = styled.div<{ hasChildren: boolean }>`
display: flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
margin-right: 6;
margin-left: ${({ hasChildren }) => (hasChildren ? 8 : 18)}px;
> svg {
width: 14px;
height: 14px;
}
`;

const NodeChildren = styled.ul`
padding: 0 0 0 22;
background-image: url(${treeNodeChildrenLine});
background-repeat: repeat-y;
`;

const Label = styled.span`
outline: none;
padding: 1;
:focus {
border-width: 1;
border-style: dotted;
padding: 0;
}
`;

const NodeIcon: React.FC<{ hasChildren: boolean; isOpen: boolean }> = ({
hasChildren,
isOpen,
Expand Down Expand Up @@ -122,7 +56,9 @@ export type NodeProps = {
event: React.MouseEvent | React.KeyboardEvent,
props: NodeProps,
): void;
};
} & Omit<FrameProps, 'id' | 'children'>;

export type NodeRootProps = Omit<NodeProps, 'children'>;

const Node: React.FC<NodeProps> = ({
children = [],
Expand Down Expand Up @@ -152,33 +88,83 @@ const Node: React.FC<NodeProps> = ({
};

return (
<NodeItem isOpen={isOpen} {...rest}>
<NodeInfo>
<Frame as="li" {...rest} className={styles.node}>
<div className={styles.nodeContent}>
{hasChildren && (
<FolderStatus onClick={() => setIsOpen(!isOpen)}>
<div
className={styles.folderStatus}
onClick={() => setIsOpen(!isOpen)}
>
{isOpen ? '-' : '+'}
</FolderStatus>
</div>
)}
<IconContainer hasChildren={hasChildren}>

<div className={styles.iconContainer({ hasChildren })}>
{icon || <NodeIcon hasChildren={hasChildren} isOpen={isOpen} />}
</IconContainer>
<Label
</div>
<label
className={styles.label}
tabIndex={0}
onDoubleClick={() => setIsOpen(!isOpen)}
onClick={onClickHandler}
onKeyDown={onKeyDownHandler}
>
{label}
</Label>
</NodeInfo>
</label>
</div>
{hasChildren && isOpen && (
<NodeChildren>
<menu className={styles.tree}>
{children?.map(dataNode => (
<Node key={dataNode.id} {...dataNode} />
))}
</NodeChildren>
</menu>
)}
</NodeItem>
</Frame>
);
};

export const NodeRoot: React.FC<NodeRootProps> = ({
id,
icon,
label,
onClick = () => {},
...rest
}) => {
const onClickHandler = (event: React.MouseEvent | React.KeyboardEvent) => {
onClick(event, {
id,
icon,
label,
});
};

const onKeyDownHandler = (event: React.KeyboardEvent) => {
if (event.key === ' ') {
onClickHandler(event);
}
};

return (
<Frame as="p" {...rest} className={cn(styles.node, styles.nodeRoot)}>
<div className={styles.nodeContent}>
<div
className={cn(
styles.iconContainer.classNames.base,
styles.iconContainer.classNames.variants.hasChildren.true,
)}
>
{icon || <NodeIcon hasChildren={false} isOpen={true} />}
</div>
<label
className={styles.label}
tabIndex={0}
onClick={onClickHandler}
onKeyDown={onKeyDownHandler}
>
{label}
</label>
</div>
</Frame>
);
};

Expand Down
107 changes: 107 additions & 0 deletions packages/core/components/Tree/Tree.css.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { globalStyle, style } from '@vanilla-extract/css';
import { contract } from '../ThemeProvider/themes/contract.css';
import { calc } from '@vanilla-extract/css-utils';
import { recipe } from '@vanilla-extract/recipes';

const base = { listStyle: 'none', margin: '0', padding: '0' };

export const tree = style(base);

globalStyle(`${tree} menu`, { ...base, marginLeft: contract.space[6] });

export const node = style({
marginLeft: contract.space[12],
borderLeftWidth: 'thin',
borderLeftStyle: 'dotted',
borderLeftColor: contract.colors.borderDarkest,
position: 'relative',
selectors: {
'&:last-child': {
borderLeftColor: 'transparent',
},
'&:before': {
position: 'absolute',
left: 0,
width: contract.space[16],
height: contract.space[10],
verticalAlign: 'top',
borderBottomWidth: 'thin',
borderBottomStyle: 'dotted',
borderBottomColor: contract.colors.borderDarkest,
content: '""',
display: 'inline-block',
},
'&:last-child:before': {
borderLeftWidth: 'thin',
borderLeftStyle: 'dotted',
borderLeftColor: contract.colors.borderDarkest,
left: calc.negate(contract.space[1]),
width: contract.space[15],
},
},
});

export const nodeRoot = style({
padding: 0,
margin: 0,
borderLeft: 'unset',
selectors: {
'&:before': {
content: 'unset',
},
},
});

export const nodeContent = style({
display: 'flex',
alignItems: 'center',
position: 'relative',
});

export const label = style({
outline: 'none',
padding: contract.space[1],
selectors: {
'&:focus': {
borderWidth: contract.space[1],
borderStyle: 'dotted',
padding: contract.space[0],
},
},
});

export const folderStatus = style({
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
minWidth: contract.space[10],
height: contract.space[10],
borderWidth: contract.space[1],
borderStyle: 'solid',
borderColor: contract.colors.borderDarkest,
backgroundColor: contract.colors.inputBackground,
fontSize: '11px',
marginLeft: calc.negate(contract.space[5]),
userSelect: 'none',
});

export const iconContainer = recipe({
base: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
minWidth: contract.space[20],
minHeight: contract.space[20],
marginRight: contract.space[2],
},
variants: {
hasChildren: {
true: {
marginLeft: contract.space[2],
},
false: {
marginLeft: contract.space[6],
},
},
},
});
2 changes: 1 addition & 1 deletion packages/core/components/Tree/Tree.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ describe('<Tree />', () => {
{
id: 1,
label: 'baz',
iconName: icons.FILE_MEDIA,
icon: <icons.FILE_MEDIA variant="16x16_4" />,
},
],
},
Expand Down
31 changes: 17 additions & 14 deletions packages/core/components/Tree/Tree.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,32 @@
import React, { forwardRef } from 'react';
import styled from 'styled-components';
import Node, { icons, NodeProps } from './Node';

const TreeParent = styled.ul`
padding: 0;
`;
import Node, { icons, NodeProps, NodeRoot } from './Node';
import { tree } from './Tree.css';
import { Frame, FrameProps } from '../Frame/Frame';

export type TreeProps = {
data: Array<NodeProps>;
root?: Omit<NodeProps, 'children'>;
};

type TreeComposition = React.ForwardRefExoticComponent<
TreeProps & React.RefAttributes<HTMLUListElement>
> & {
icons: typeof icons;
};
} & Omit<FrameProps<'menu'>, 'as'>;

const Tree = forwardRef<HTMLUListElement, TreeProps>(
({ data, ...rest }, ref) => (
<TreeParent {...rest} ref={ref}>
{data.map(dataNode => (
<Node key={dataNode.id} {...dataNode} />
))}
</TreeParent>
),
({ data, root, ...rest }, ref) => {
return (
<>
{root && <NodeRoot {...root} />}
<Frame {...rest} className={tree} as="menu" ref={ref}>
{data.map(dataNode => (
<Node key={dataNode.id} {...dataNode} />
))}
</Frame>
</>
);
},
) as TreeComposition;

Tree.defaultProps = {
Expand Down

0 comments on commit 53e26c6

Please sign in to comment.