From 445d0622f813b52e078606c75a9b6f2022e09136 Mon Sep 17 00:00:00 2001 From: berber1016 Date: Sun, 31 Oct 2021 18:38:10 +0800 Subject: [PATCH] feat(list): add list --- package.json | 1 + src/list/Drag.tsx | 44 ++++++++++ src/list/DragItem.tsx | 60 +++++++++++++ src/list/Group.tsx | 22 +++++ src/list/Item.tsx | 53 +++++++++++ src/list/List.tsx | 60 +++++++++++++ src/list/constants.ts | 7 ++ src/list/demos/List.stories.tsx | 150 ++++++++++++++++++++++++++++++++ src/list/demos/ListPage.tsx | 21 +++++ src/list/demos/style.less | 7 ++ src/list/index.tsx | 0 src/list/inner/baseItem.tsx | 33 +++++++ src/list/interfance.ts | 54 ++++++++++++ src/list/style/index.less | 80 +++++++++++++++++ src/list/style/index.ts | 1 + yarn.lock | 5 ++ 16 files changed, 598 insertions(+) create mode 100644 src/list/Drag.tsx create mode 100644 src/list/DragItem.tsx create mode 100644 src/list/Group.tsx create mode 100644 src/list/Item.tsx create mode 100644 src/list/List.tsx create mode 100644 src/list/constants.ts create mode 100644 src/list/demos/List.stories.tsx create mode 100644 src/list/demos/ListPage.tsx create mode 100644 src/list/demos/style.less create mode 100644 src/list/index.tsx create mode 100644 src/list/inner/baseItem.tsx create mode 100644 src/list/interfance.ts create mode 100644 src/list/style/index.less create mode 100644 src/list/style/index.ts diff --git a/package.json b/package.json index df27a95193..b5567348d6 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "@types/react-resizable": "^1.7.3", "classnames": "^2.2.6", "date-fns": "^2.21.3", + "immutability-helper": "^3.1.1", "lodash": "^4.17.14", "moment": "^2.26.0", "raf": "^3.4.1", diff --git a/src/list/Drag.tsx b/src/list/Drag.tsx new file mode 100644 index 0000000000..335b684a3a --- /dev/null +++ b/src/list/Drag.tsx @@ -0,0 +1,44 @@ +import classNames from 'classnames'; +import React, { useEffect, useState } from 'react'; +import { DndProvider } from 'react-dnd'; +import { HTML5Backend } from 'react-dnd-html5-backend'; +import update from 'immutability-helper'; +import usePrefixCls from '../utils/hooks/use-prefix-cls'; +import { PREFIX } from './constants'; +import { DragListProps, OptionProps } from './interfance'; +import DragItem from './DragItem'; + + + +const Drag:React.FC = (props) => { + const { onChange,className,style,options:propsOPtions,disabled,...rest } = props; + + const [options, setOptions] = useState(propsOPtions); + useEffect(() => { + setOptions(propsOPtions); + }, [propsOPtions]); + const prefixCls = `${usePrefixCls(PREFIX)}`; + + const onMoved = (dragIndex?: number, hoverIndex?: number) => { + const dragCard = options[(dragIndex as number)]; + const updateOptions = update(options, { + $splice: [ + [dragIndex, 1], + [hoverIndex, 0, dragCard], + ], + }); + setOptions(updateOptions); + onChange?.(updateOptions) + }; + + return ( + +
+ {options?.map((option:OptionProps,index:number) => )} +
+
+ ) +} + +export default Drag \ No newline at end of file diff --git a/src/list/DragItem.tsx b/src/list/DragItem.tsx new file mode 100644 index 0000000000..179f7448fd --- /dev/null +++ b/src/list/DragItem.tsx @@ -0,0 +1,60 @@ +import React, { useRef } from 'react'; +import { DragSourceMonitor, DropTargetMonitor, useDrag, useDrop, XYCoord } from 'react-dnd'; +import { DragItemProps } from './interfance'; +import Item from './Item'; + + + +const DragItem:React.FC = (props) => { + const { label, value, onMoved,index,...rest} = props; + const ref = useRef(null) + const [{ handlerId }, drop] = useDrop({ + accept: 'drag-item', + collect(monitor) { + return { + handlerId: monitor.getHandlerId(), + } + }, + hover(item: {index:number,type:string,id:string}, monitor: DropTargetMonitor) { + if (!ref.current) { + return + } + const dragIndex = item.index + const hoverIndex = index + + if (dragIndex === hoverIndex) { + return + } + const hoverBoundingRect = ref.current?.getBoundingClientRect() + const hoverMiddleY = + (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2 + const clientOffset = monitor.getClientOffset() + const hoverClientY = (clientOffset as XYCoord).y - hoverBoundingRect.top + if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) { + return + } + + // Dragging upwards + if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) { + return + } + onMoved((dragIndex as number), hoverIndex) + // eslint-disable-next-line no-param-reassign + item.index = hoverIndex + }, + }) + const [, drag] = useDrag({ + item: { type:'drag-item',id:value,index }, + collect: (monitor: DragSourceMonitor) => ({ + isDragging: monitor.isDragging(), + }), + }) + + drag(drop(ref)); + return
+ +
+} + + +export default DragItem \ No newline at end of file diff --git a/src/list/Group.tsx b/src/list/Group.tsx new file mode 100644 index 0000000000..fceb31d550 --- /dev/null +++ b/src/list/Group.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import usePrefixCls from '../utils/hooks/use-prefix-cls'; +import { PREFIX } from './constants'; + +interface GroupPorps { + title?:string +} + +const Group:React.FC =(props) =>{ + const { title,children } = props; + const prefixCls = `${usePrefixCls(PREFIX)}--group`; + return ( +
+
+ {title} +
+ {children} +
+ ) +} + +export default Group; \ No newline at end of file diff --git a/src/list/Item.tsx b/src/list/Item.tsx new file mode 100644 index 0000000000..c019d9867f --- /dev/null +++ b/src/list/Item.tsx @@ -0,0 +1,53 @@ +import classNames from 'classnames'; +import React from 'react'; +import { isArray } from 'lodash'; +import { MoveOutlined } from '@gio-design/icons'; +import usePrefixCls from '../utils/hooks/use-prefix-cls'; +import { PREFIX } from './constants'; +import { ItemProps } from './interfance'; +import Checkbox from '../checkbox/Checkbox'; +import BaseItem from './inner/baseItem'; + +const Item:React.FC = (props)=>{ + const { label,className,style,prefix,suffix,children,disabled,selectedValue,disabledTooltip,isMultiple,isDrag,value,onMouseEnter,onMouseLeave,onClick} = props; + + const prefixCls = `${usePrefixCls(PREFIX)}__item`; + + const handleClick = () => { + if (!disabled && onClick) { + onClick({label:children ?? label,value}); + } + } + // multiple check + const actived = isMultiple && isArray(selectedValue) + ? (selectedValue as (string | number)[])?.indexOf(value) !== -1 + : selectedValue === value; + + return ( + + ) +} + +export default Item; \ No newline at end of file diff --git a/src/list/List.tsx b/src/list/List.tsx new file mode 100644 index 0000000000..87d2ee2f12 --- /dev/null +++ b/src/list/List.tsx @@ -0,0 +1,60 @@ +import classNames from 'classnames'; +import React from 'react'; +import { difference, isArray, isNil } from 'lodash'; +import { OptionProps, ItemProps, BaseListProps } from './interfance'; +import usePrefixCls from '../utils/hooks/use-prefix-cls'; +import { PREFIX } from './constants'; +import Item from './Item'; + +const List: React.FC = (props)=> { + const { className, style, options, children,disabled = false,value:controlledValue,isMultiple,prefix,suffix,onChange } = props; + const prefixCls = `${usePrefixCls(PREFIX)}`; + const handleClick = (option:OptionProps) => { + if(isMultiple){ + let multipleValues = []; + if((controlledValue as (string | number)[])?.indexOf(option.value) !== -1 && !isNil(controlledValue)){ + multipleValues = difference((controlledValue as (string | number)[]),[option.value]); + } else { + multipleValues = isArray(controlledValue) ? [...controlledValue,option.value] : [option.value]; + } + onChange?.(multipleValues) + } else if(controlledValue !== option.value){ + onChange?.(option.value) + } + + } + const renderChildrens = ()=> { + if(isArray(options)){ + return options?.map((option:OptionProps) => ) + } + return React.Children.map(children, (node:React.ReactElement,index) => { + const { props:{className:itemClassName,disabled:itemDisabled,prefix:itemPrefix,suffix:itemSuffix,onClick,...rest} } = node; + return React.cloneElement(node,{ + className:classNames(itemClassName, { + [`${prefixCls}__item--interval`]: index !== 0 + }), + disabled:itemDisabled ?? disabled, + prefix:itemPrefix ?? prefix, + suffix:itemSuffix ?? suffix, + isMultiple, + selectedValue:controlledValue, + onClick:(option:OptionProps)=>{ + handleClick(option); + onClick?.(option); + }, + ...rest + }) + }) + } + + return ( +
+ {renderChildrens()} +
+ ) +} + +export default List \ No newline at end of file diff --git a/src/list/constants.ts b/src/list/constants.ts new file mode 100644 index 0000000000..4ee0b59d5a --- /dev/null +++ b/src/list/constants.ts @@ -0,0 +1,7 @@ +export const DEFAULT_SHOW_ITEMS_COUNT = 10; +export const PREFIX = 'list-new'; + +export default { + DEFAULT_SHOW_ITEMS_COUNT, + PREFIX, +}; diff --git a/src/list/demos/List.stories.tsx b/src/list/demos/List.stories.tsx new file mode 100644 index 0000000000..77aa405714 --- /dev/null +++ b/src/list/demos/List.stories.tsx @@ -0,0 +1,150 @@ +import React, { useState } from 'react'; +import { Story, Meta } from '@storybook/react/types-6-0'; +import { PlusOutlined, FilterOutlined } from '@gio-design/icons'; +import List from '../List'; +import { ItemProps,OptionProps } from '../interfance' +import '../style'; +import Item from '../Item'; +import './style.less' +// import { DragList } from '../../components/list-pro'; +import DragList from '../Drag'; +import GroupList from '../Group'; + +export default { + title:'Upgraded/List', + component:List +} as Meta + +const Template: Story = (props) => { + // const { label,className,style,prefix,suffix,children,disabled, ellipsis,onClick} = props; + const [value, setValue] = useState(undefined); + // const [multipleValue,setMultipleValue] = useState(undefined); + return ( + <> +
+ + } suffix={}> + 第一条咸鱼 + + + 第二条咸鱼 + + + 第三条超级超级超级超级超级超级超级超级超级超级长的咸鱼 + + + 第四条disabled的咸鱼 + + + 第4-1条disabled的咸鱼 + + + 第4-1条disabled的咸鱼第4-1条disabled的咸鱼第4-1条disabled的咸鱼 + + + 第五条是带disabledTooltip的咸鱼 + + + 第六条是超级超级超级超级超级超级超级超级超级超级长带disabledTooltip的咸鱼 + + + 第七条是超级超级超级超级超级超级超级超级超级超级长不带disabledTooltip的咸鱼 + + +
+
+ + setValue(v)}> + + 第一条咸鱼 + + + 第二条咸鱼 + + + 第三条超级超级超级超级超级超级超级超级超级超级长的咸鱼 + + +
+ + ) +}; +const ItemTemplate: Story = () => ( +
+ + + 第一条咸鱼 + + }> + 第二条咸鱼 + + } suffix={}> + 第二-一条咸鱼 + + }> + 第三条咸鱼 + + + 第四条咸鱼 + + + 第五条咸鱼 + + + {'第六六六六六六条咸鱼'.repeat(5)} + + +
+ ) + +const dragTemplate: Story = () => { + const options = [ + { + label:'1', + value:'1' + }, + { + label:'2', + value:'2' + }, + { + label:'3', + value:'3' + } + ] + return
+ console.log('value',value)} /> +
+} +const groupTemplate: Story = () => { + const options = [ + { + label:'1', + value:'1' + }, + { + label:'2', + value:'2' + }, + { + label:'3', + value:'3' + } + ] + return
+ + console.log('value',value)} /> + + + console.log('value',value)} /> + +
+} + +export const Default = Template.bind({}); +export const Items = ItemTemplate.bind({}); +export const Drag = dragTemplate.bind({}); +export const Group = groupTemplate.bind({}); +Default.args = { + onChange:(value:OptionProps)=>{console.log('value',value)} +}; \ No newline at end of file diff --git a/src/list/demos/ListPage.tsx b/src/list/demos/ListPage.tsx new file mode 100644 index 0000000000..2b77b13249 --- /dev/null +++ b/src/list/demos/ListPage.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { Canvas, Title, Heading, Story, Subheading, ArgsTable } from '@storybook/addon-docs'; +import { useIntl } from 'react-intl'; +import Item from '../Item'; + +export default function ListPage() { + const { formatMessage } = useIntl(); + + return ( + <> + {formatMessage({ defaultMessage: 'List' })} + {formatMessage({ defaultMessage: '代码演示' })} + {formatMessage({ defaultMessage: '基本样式' })} + + + + + + + ); +} \ No newline at end of file diff --git a/src/list/demos/style.less b/src/list/demos/style.less new file mode 100644 index 0000000000..376251e1c7 --- /dev/null +++ b/src/list/demos/style.less @@ -0,0 +1,7 @@ +.demo-box { + width: 240px; + margin: 20px; + padding: 8px; + border: 0.5px dashed #dcdfed; + border-radius: 4px; +} diff --git a/src/list/index.tsx b/src/list/index.tsx new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/list/inner/baseItem.tsx b/src/list/inner/baseItem.tsx new file mode 100644 index 0000000000..23b7739677 --- /dev/null +++ b/src/list/inner/baseItem.tsx @@ -0,0 +1,33 @@ +import { isEmpty } from 'lodash'; +import React from 'react'; +import Tooltip from '../../tooltip'; +import Typogray from '../../typograhy' +import usePrefixCls from '../../utils/hooks/use-prefix-cls'; +import { PREFIX } from '../constants'; +import { BaseItemProps } from '../interfance'; + +const BaseItem: React.FC = (props) => { + const { label, children, disabled, disabledTooltip,prefix,suffix } = props; + const prefixCls = `${usePrefixCls(PREFIX)}__item`; + const context = children ?? label; + const prefixIcon = prefix ? {prefix} : undefined; + const suffixIcon = suffix ? {suffix} : undefined; + if (!label && !children) { + return null; + } + + if (typeof context === 'string') { + return ( + <> + {prefixIcon} + + {context} + + {suffixIcon} + + ) + } + return <>{context}; +} + +export default BaseItem; \ No newline at end of file diff --git a/src/list/interfance.ts b/src/list/interfance.ts new file mode 100644 index 0000000000..194f947e44 --- /dev/null +++ b/src/list/interfance.ts @@ -0,0 +1,54 @@ +import React from "react"; + +export interface ListProps { + className?:string; + style?:React.CSSProperties; + options?:OptionProps[]; + children?:React.ReactNode | React.ReactNode[] + value?:string | number | (string|number)[] + disabled?:boolean; + prefix?:string | React.ReactNode + suffix?:string | React.ReactNode + onChange?:(value:string | number | (string|number)[]) => void +} + +export interface BaseListProps extends ListProps { + isMultiple?:boolean +} + +export interface DragListProps extends Omit { + options:OptionProps[] + onChange?:(value: OptionProps[]) => void +} + + + +export interface OptionProps { + label: string | React.ReactNode + value: string | number + disabled?:boolean + disabledTooltip?:string +} + +export interface ItemProps extends Omit { + className?:string + style?:React.CSSProperties + selectedValue?:string | number | (string|number)[] + isMultiple?:boolean; + isDrag?:boolean; + onMouseEnter?: React.MouseEventHandler; + onMouseLeave?: React.MouseEventHandler; + onClick?:(option:OptionProps) => void; +} +export interface DragItemProps extends ItemProps { + onMoved?:(dragIndex?:number,hoverIndex?:number) => void; + index:number +} +export interface BaseItemProps extends Omit { + label?:string | React.ReactNode + ellipsis?:boolean + children?:React.ReactNode + actived?:boolean + prefix?:string | React.ReactNode + suffix?:string | React.ReactNode +} \ No newline at end of file diff --git a/src/list/style/index.less b/src/list/style/index.less new file mode 100644 index 0000000000..23c0ee9e85 --- /dev/null +++ b/src/list/style/index.less @@ -0,0 +1,80 @@ +@import '../../stylesheet/variables/index.less'; + +@list-prefix-cls: ~'@{component-prefix}-list-new'; + +.@{list-prefix-cls} { + margin: 0; + padding: 0; + list-style-type: none; + &--group { + margin-top: 8px; + &--title { + margin-bottom: 4px; + color: @gray-4; + font-weight: 500; + font-size: 12px; + font-family: @font-family-primary; + font-style: normal; + line-height: 20px; + } + // margin-bottom: 4px; + border-bottom: 1px solid #dfe4ee; + } + &__item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 7px 12px; + color: @gray-5; + font-weight: 'normal'; + font-size: 14px; + font-family: @font-family-primary; + font-style: normal; + line-height: 22px; + background-color: @palette-white; + vertical-align: middle; + border-radius: 4px; + cursor: pointer; + &:hover { + background-color: @gray-1; + } + + &:not(&--disabled):active { + color: @blue-3; + } + &--actived { + color: @blue-3; + background-color: @gray-1; + } + &--ellipsis { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + + &--disabled { + color: @gray-3; + cursor: not-allowed; + } + &--interval { + margin-top: 4px; + } + &--drag { + margin-right: 8px; + } + &--text { + flex-grow: 1; + } + > span > .gio-text--black { + color: inherit; + } + &-prefix-icon { + display: inherit; + margin-right: 8px; + } + &-suffix-icon { + display: inherit; + margin-left: 8px; + } + } +} diff --git a/src/list/style/index.ts b/src/list/style/index.ts new file mode 100644 index 0000000000..d74e52ee9f --- /dev/null +++ b/src/list/style/index.ts @@ -0,0 +1 @@ +import './index.less'; diff --git a/yarn.lock b/yarn.lock index 67576002b5..7954114354 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9713,6 +9713,11 @@ immer@8.0.1: resolved "https://registry.yarnpkg.com/immer/-/immer-8.0.1.tgz#9c73db683e2b3975c424fb0572af5889877ae656" integrity sha512-aqXhGP7//Gui2+UrEtvxZxSquQVXTpZ7KDxfCcKAF3Vysvw0CViVaW9RZ1j1xlIYqaaaipBoqdqeibkc18PNvA== +immutability-helper@^3.1.1: + version "3.1.1" + resolved "https://registry.npmjs.org/immutability-helper/-/immutability-helper-3.1.1.tgz#2b86b2286ed3b1241c9e23b7b21e0444f52f77b7" + integrity sha512-Q0QaXjPjwIju/28TsugCHNEASwoCcJSyJV3uO1sOIQGI0jKgm9f41Lvz0DZj3n46cNCyAZTsEYoY4C2bVRUzyQ== + import-fresh@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-2.0.0.tgz"