Skip to content

Commit

Permalink
feat(tree): add tree component (#51)
Browse files Browse the repository at this point in the history
affects: @gio-design/components, @gio-design/icons, @gio-design/tokens, website
  • Loading branch information
huskylengcb authored Aug 4, 2020
1 parent 0ec777f commit edb6cda
Show file tree
Hide file tree
Showing 21 changed files with 657 additions and 41 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as React from 'react';
export interface ConfigConsumerProps {
getPrefixCls: (suffixCls: string, customizePrefixCls?: string) => string;
autoInsertSpaceInButton?: boolean;
virtual?: boolean;
}

export const ConfigContext = React.createContext<ConfigConsumerProps>({
Expand Down
127 changes: 127 additions & 0 deletions packages/components/src/components/tree/Tree.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import * as React from 'react';
import RcTree, { TreeNode, TreeProps as RcTreeProps } from 'rc-tree';
import classNames from 'classnames';
import { DataNode, Key } from 'rc-tree/lib/interface';
import { ConfigContext } from '../config-provider';
import renderSwitcherIcon from './iconUtil';

export interface GioTreeNodeAttribute {
eventKey: string;
prefixCls: string;
className: string;
expanded: boolean;
selected: boolean;
children: React.ReactNode;
title: React.ReactNode;
pos: string;
isLeaf: boolean;
selectable: boolean;
disabled: boolean;
}

export interface GioTreeNodeProps {
className?: string;
disabled?: boolean;
title?: string | React.ReactNode;
key?: Key;
eventKey?: string;
isLeaf?: boolean;
checked?: boolean;
expanded?: boolean;
selected?: boolean;
selectable?: boolean;
icon?: ((treeNode: GioTreeNodeAttribute) => React.ReactNode) | React.ReactNode;
children?: React.ReactNode;
[customProp: string]: any;
}

export type GioTreeNode = React.Component<GioTreeNodeProps, {}>;

export interface GioTreeNodeBaseEvent {
node: GioTreeNode;
nativeEvent: MouseEvent;
}

export interface GioTreeNodeSelectedEvent extends GioTreeNodeBaseEvent {
event: 'select';
selected?: boolean;
selectedNodes?: DataNode[];
}

export interface GioTreeNodeExpandedEvent extends GioTreeNodeBaseEvent {
expanded?: boolean;
}

export interface GioTreeNodeMouseEvent {
node: GioTreeNode;
event: React.DragEvent<HTMLElement>;
}

export interface GioTreeNodeDragEnterEvent extends GioTreeNodeMouseEvent {
expandedKeys: Key[];
}

export type TreeNodeNormal = DataNode;

export interface TreeProps extends Omit<RcTreeProps, 'prefixCls'> {
className?: string;
/** 是否自动展开父节点 */
autoExpandParent?: boolean;
/** 是否禁用树 */
disabled?: boolean;
/** 默认展开所有树节点 */
defaultExpandAll?: boolean;
/** 默认展开对应树节点 */
defaultExpandParent?: boolean;
/** 默认展开指定的树节点 */
defaultExpandedKeys?: Key[];
/** (受控)展开指定的树节点 */
expandedKeys?: Key[];
/** (受控)设置选中的树节点 */
selectedKeys?: Key[];
/** 默认选中的树节点 */
defaultSelectedKeys?: Key[];
selectable?: boolean;
loadedKeys?: Key[];
style?: React.CSSProperties;
showIcon?: boolean;
icon?: ((nodeProps: GioTreeNodeAttribute) => React.ReactNode) | React.ReactNode;
switcherIcon?: React.ReactElement<any>;
prefixCls?: string;
children?: React.ReactNode;
}

interface CompoundedComponent extends React.ForwardRefExoticComponent<TreeProps & React.RefAttributes<RcTree>> {
TreeNode: typeof TreeNode;
}

const Tree = React.forwardRef<RcTree, TreeProps>((props, ref) => {
const { getPrefixCls, virtual } = React.useContext(ConfigContext);
const { prefixCls: customizePrefixCls, className, showIcon, children, switcherIcon } = props;

const newProps = { ...props };
const prefixCls = getPrefixCls('tree', customizePrefixCls);

return (
<RcTree
ref={ref}
virtual={virtual}
{...newProps}
prefixCls={prefixCls}
className={classNames(className, {
[`${prefixCls}-icon-hide`]: !showIcon,
})}
switcherIcon={(nodeProps: GioTreeNodeProps) => renderSwitcherIcon(prefixCls, switcherIcon, nodeProps)}
>
{children}
</RcTree>
);
}) as CompoundedComponent;

Tree.TreeNode = TreeNode;

Tree.defaultProps = {
showIcon: false,
};

export default Tree;
26 changes: 26 additions & 0 deletions packages/components/src/components/tree/iconUtil.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import * as React from 'react';
import classNames from 'classnames';
import { CaretDownOutlined } from '@gio-design/icons';
import { GioTreeNodeProps } from './Tree';

export default function renderSwitcherIcon(
prefixCls: string,
switcherIcon: React.ReactNode | null | undefined,
{ isLeaf }: GioTreeNodeProps
) {
if (isLeaf) {
return null;
}
const switcherCls = `${prefixCls}-switcher-icon`;
if (React.isValidElement(switcherIcon)) {
return React.cloneElement(switcherIcon, {
className: classNames(switcherIcon?.props.className || '', switcherCls),
});
}

if (switcherIcon) {
return switcherIcon;
}

return <CaretDownOutlined className={switcherCls} />;
}
15 changes: 15 additions & 0 deletions packages/components/src/components/tree/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import Tree from './Tree';

export { EventDataNode } from 'rc-tree/lib/interface';

export {
TreeProps,
TreeNodeNormal,
GioTreeNode,
GioTreeNodeMouseEvent,
GioTreeNodeExpandedEvent,
GioTreeNodeSelectedEvent,
GioTreeNodeProps,
} from './Tree';

export default Tree;
140 changes: 140 additions & 0 deletions packages/components/src/components/tree/style/index.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
@import '../../../stylesheet/theme.less';
@import '../../../stylesheet/mixin/index.less';
@import '~@gio-design/tokens/dist/variables.less';

@tree-prefix-cls: ~'@{component-prefix}-tree';
@tree-node-prefix-cls: ~'@{tree-prefix-cls}-treenode';

.antTreeFn(@tree-prefix-cls);

.antTreeFn(@custom-tree-prefix-cls) {
@custom-tree-node-prefix-cls: ~'@{custom-tree-prefix-cls}-treenode';
.@{custom-tree-prefix-cls} {
.reset-component;
background: @color-background-tree-normal;
border-radius: @radius-border-tree;
transition: background-color 0.3s;

// =================== Virtual List ===================
&-list-holder-inner {
align-items: flex-start;
}

// ===================== TreeNode =====================
.@{custom-tree-node-prefix-cls} {
display: flex;
align-items: flex-start;
padding: 0;
outline: none;
width: 100%;
height: 40px;
line-height: 40px;
border-radius: @radius-border-tree;
margin: 2px 0;
font-size: 14px;
color: @color-text-tree-normal;
font-weight: 400;
&:hover {
background-color: @color-background-tree-hover;
}
// Disabled
&-disabled {
.@{custom-tree-prefix-cls}-node-content-wrapper {
color: @color-text-tree-disable;
cursor: not-allowed;
.@{custom-tree-prefix-cls}-iconEle {
svg {
color: @color-text-tree-disable;
path {
fill: @color-text-tree-disable;
}
}
}
}
}
// Selected
&-selected {
background-color: @color-background-tree-active;
.@{custom-tree-prefix-cls}-node-content-wrapper {
color: @color-text-tree-selected;
.@{custom-tree-prefix-cls}-iconEle {
svg {
color: @color-text-tree-selected;
path {
fill: @color-text-tree-selected;
}
}
}
}
}
}

// >>> Indent
&-indent {
align-self: stretch;
white-space: nowrap;
user-select: none;

&-unit {
display: inline-block;
width: 24px;
}
}

// >>> Switcher
& &-switcher {
flex: none;
width: 14px;
height: 14px;
margin: 0;
line-height: 40px;
text-align: center;
margin: 2px 8px 0;
color: @color-text-tree-switcher;
cursor: pointer;

&-noop {
cursor: default;
}

svg.@{custom-tree-prefix-cls}-switcher-icon {
color: @color-text-tree-switcher;
path {
fill: @color-text-tree-switcher;
}
}

&_close {
svg.@{custom-tree-prefix-cls}-switcher-icon {
transform: rotate(-90deg);
}
}
}

// >>> Title
& &-node-content-wrapper {
width: 100%;
cursor: pointer;

.@{custom-tree-prefix-cls}-title {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

// Icon
.@{custom-tree-prefix-cls}-iconEle {
display: inline-block;
width: 16px;
height: 16px;
line-height: 16px;
margin: 2px 8px 0 0;
text-align: center;
vertical-align: middle;
&:empty {
display: none;
}
}
}
}
}
1 change: 1 addition & 0 deletions packages/components/src/components/tree/style/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import './index.less';
24 changes: 24 additions & 0 deletions packages/icons/src/CaretDownOutlined.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import * as React from 'react';

function SvgCaretDownOutlined(props: React.SVGProps<SVGSVGElement>) {
return (
<svg viewBox='0 0 14 14' width='1em' height='1em' {...props}>
<defs>
<style />
</defs>
<g id='caret-down-outlined_svg__\u56FE\u5C42_2' data-name='\u56FE\u5C42 2'>
<g id='caret-down-outlined_svg__\u56FE\u5C42_1-2' data-name='\u56FE\u5C42 1'>
<path
d='M7.39 9.51a.5.5 0 01-.78 0l-3-3.7a.51.51 0 01-.06-.53A.5.5 0 014 5h6a.5.5 0 01.45.28.51.51 0 01-.06.53z'
fill='#323333'
fillRule='evenodd'
id='caret-down-outlined_svg__down-2'
/>
</g>
</g>
</svg>
);
}

const MemoSvgCaretDownOutlined = React.memo(SvgCaretDownOutlined);
export default MemoSvgCaretDownOutlined;
18 changes: 8 additions & 10 deletions packages/icons/src/LoadingOutlinedBlack.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
import React from 'react';
import * as React from 'react';

function LoadingOutlinedBlack(props: React.SVGProps<SVGSVGElement>) {
function SvgLoadingOutlinedBlack(props: React.SVGProps<SVGSVGElement>) {
return (
<svg width='16px' height='16px' viewBox='0 0 16 16' {...props}>
<g stroke='none' strokeWidth='1' fill='none' fillRule='evenodd'>
<g transform='translate(0.500000, 0.500000)' stroke='#313E75'>
<circle opacity='0.4' cx='7.5' cy='7.5' r='7.5' />
<path d='M15,7.5 C15,3.35786438 11.6421356,0 7.5,0' strokeLinecap='round' />
</g>
<svg width='1em' height='1em' viewBox='0 0 16 16' {...props}>
<g transform='translate(.5 .5)' stroke='#313E75' fill='none' fillRule='evenodd'>
<circle opacity={0.4} cx={7.5} cy={7.5} r={7.5} />
<path d='M15 7.5A7.5 7.5 0 007.5 0' strokeLinecap='round' />
</g>
</svg>
);
}

const MemoLoadingOutlinedBlack = React.memo(LoadingOutlinedBlack);
export default MemoLoadingOutlinedBlack;
const MemoSvgLoadingOutlinedBlack = React.memo(SvgLoadingOutlinedBlack);
export default MemoSvgLoadingOutlinedBlack;
18 changes: 8 additions & 10 deletions packages/icons/src/LoadingOutlinedWhite.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
import React from 'react';
import * as React from 'react';

function LoadingOutlinedWhite(props: React.SVGProps<SVGSVGElement>) {
function SvgLoadingOutlinedWhite(props: React.SVGProps<SVGSVGElement>) {
return (
<svg width='16px' height='16px' viewBox='0 0 16 16' {...props}>
<g stroke='none' strokeWidth='1' fill='none' fillRule='evenodd'>
<g transform='translate(0.500000, 0.500000)' stroke='#FFFFFF'>
<circle opacity='0.4' cx='7.5' cy='7.5' r='7.5' />
<path d='M15,7.5 C15,3.35786438 11.6421356,0 7.5,0' strokeLinecap='round' />
</g>
<svg width='1em' height='1em' viewBox='0 0 16 16' {...props}>
<g transform='translate(.5 .5)' stroke='#FFF' fill='none' fillRule='evenodd'>
<circle opacity={0.4} cx={7.5} cy={7.5} r={7.5} />
<path d='M15 7.5A7.5 7.5 0 007.5 0' strokeLinecap='round' />
</g>
</svg>
);
}

const MemoLoadingOutlinedWhite = React.memo(LoadingOutlinedWhite);
export default MemoLoadingOutlinedWhite;
const MemoSvgLoadingOutlinedWhite = React.memo(SvgLoadingOutlinedWhite);
export default MemoSvgLoadingOutlinedWhite;
Loading

0 comments on commit edb6cda

Please sign in to comment.