Skip to content
This repository has been archived by the owner on Nov 14, 2023. It is now read-only.

Commit

Permalink
feat: VirtualSelectBox (#65)
Browse files Browse the repository at this point in the history
* feat: VirtualSelectBox

* add test

* format code

* add test

* fix

* virtual use pan type
  • Loading branch information
wangkailang authored Sep 2, 2019
1 parent ef73e0a commit c1b9940
Show file tree
Hide file tree
Showing 20 changed files with 1,021 additions and 65 deletions.
2 changes: 1 addition & 1 deletion .storybook/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import './style.scss';

addDecorator (
withOptions ({
name: 'wizard ui',
name: 'WIZARD UI',
url: 'https://github.com/xsky-fe/wizard-ui',
sidebarAnimations: true,
// stories 根据字母,数组小到大排序,根据执行顺序排序
Expand Down
163 changes: 163 additions & 0 deletions docs/content/components/virtual-select-box.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
---
title: VirtualSelectBox 滚动下拉
date: 2019-08-28
author: wangkailang
---
在大量异步数据中选择需要的数据,支持滚动加载和搜索。

## 限制条件

异步 `API` 支持获取部分数据(分页),query 的格式如下:
```js isShow
{
// 取 10 条数据
limit: 10,
// 从第 20 条数据开始取
offset: 20
}
```
表示从第 20 条数据开始取10 条数据。

## 基本用法
- `fetchData` 异步数据 `API`, `Promise` 返回数据结构没有严格要求,通用模拟结构如下:
```js isShow
const fetchData = () => new Promise((resolve, reject) => {
setTimeout(() => {
resolve({
response: {
resNames: [xxx],
paging: {
totalCount: xxx
}
}
});
reject({
error: xxx,
});
}, time);
});
```
- `item` 选中项,允许为空对象 `{}` 或者 `""`

具体使用:
```js isShow
<VirtualSelectBox
item={item}
fetchData={getDatas}
/>
```

## 代码演示

### 空数组
```jsx
() => {
const limit = 30;
const getEmptyDatas = () => new Promise(resolve => {
setTimeout(() => {
resolve({
response: {
resNames: [],
paging: {
totalCount: 0
}
}
});
}, 500);
});
async function fetchEmptyDatas(isReloading, dQuery = {}) {
const actionResult = await getEmptyDatas(dQuery);
const items = actionResult.response.resNames;
const totalCount = actionResult.response.paging.totalCount;
const query = {
...dQuery,
limit,
offset: 0,
};
return {
query,
items,
totalCount,
}
}

return (
<VirtualSelectBox
item={{}}
fetchData={fetchEmptyDatas}
/>
);
}
```
### 有数据
```jsx
() => {
const limit = 30;
const TOTAL = 180;
const getDatas = query => new Promise(resolve => {
setTimeout(() => {
const { limit = 0, offset = 0 } = query;
let rlt = [];
if (offset <= TOTAL) {
const len = Math.min(limit, TOTAL - offset);
for (let i = 0; len - i > 0; i++) {
rlt.push({ id: offset + i, name: `list-${offset + i}` });
}
}
resolve({
response: {
resNames: rlt,
paging: {
totalCount: TOTAL
}
}
});
}, 500);
});
async function fetchDatas(isReloading, dQuery = {}) {
const actionResult = await getDatas(dQuery);
const items = actionResult.response.resNames;
const totalCount = actionResult.response.paging.totalCount;
const query = {
...dQuery,
limit,
offset: 0,
};
return {
query,
items,
totalCount,
}
}
const [item, setItem] = React.useState({ id: 1, name: 'list-1' });
const onSelect = React.useCallback(async (item) => {
setItem(item);
}, [item, setItem]);

const [clear, setClear] = React.useState(true);

return (
<div>
<div>
<Checkbox checked={clear} onChange={() => setClear(!clear)}>
允许清除
</Checkbox>
</div>
<VirtualSelectBox
onSelect={onSelect}
item={item}
fetchData={fetchDatas}
clear={clear}
/>
</div>
)
}
```

## API
```jsx previewOnly
<PropTable of="virtualSelectBox" />
```



4 changes: 2 additions & 2 deletions src/components/DropdownButton/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ describe('Dropdown', () => {
const classNames = [
'.dropdown.btn-group.btn-group-lg',
'.dropdown.btn-group.btn-group-sm',
'.dropdown.btn-group.btn-group-xs'
'.dropdown.btn-group.btn-group-xs',
];
sizes.forEach((size, index) => {
const dropdown = mount(<DropdownButton id="a1" bsSize={size} title="size" />);
Expand Down Expand Up @@ -48,4 +48,4 @@ describe('Dropdown', () => {
const dropdown = mount(<DropdownButton id="a1" pullRight title="pullRight" />);
expect(dropdown.find('.dropdown-menu.dropdown-menu-right').length).toBe(1);
});
});
});
8 changes: 6 additions & 2 deletions src/components/DropdownButton/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import * as React from 'react';
import * as PropTypes from 'prop-types';
import { DropdownButton as BootstrapDropdownButton, MenuItem, ButtonGroup } from 'react-bootstrap';
import SubMenu from '../SubMenu';
import { DropdownButtonMenuItem, DropdownButtonProps, DefaultDropdownButtonProps } from '../../interface';
import {
DropdownButtonMenuItem,
DropdownButtonProps,
DefaultDropdownButtonProps,
} from '../../interface';
import { cloneDeep } from 'lodash';
import './style.scss';

Expand Down Expand Up @@ -139,7 +143,7 @@ DropdownButton.propTypes = {
title: PropTypes.oneOfType([PropTypes.element, PropTypes.string]),
};

const defaultProps: DefaultDropdownButtonProps = { componentClass: ButtonGroup, }
const defaultProps: DefaultDropdownButtonProps = { componentClass: ButtonGroup };

DropdownButton.defaultProps = defaultProps;

Expand Down
8 changes: 4 additions & 4 deletions src/components/Icon/style.scss
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
@import '../../style/variables.scss';
svg {
&.icon {
.icon {
svg {
display: inline-block;
stroke-width: 0;
stroke: currentColor;
fill: currentColor;
}
&.primary {
color: $purple-normal
color: $purple-normal;
}
}
}
2 changes: 1 addition & 1 deletion src/components/SubMenu/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ describe('SubMenu', () => {
const subMenu = mount(<SubMenu title="title" children="text" />);
expect(subMenu.find('ul.dropdown-menu').length).toBe(1);
});
});
});
22 changes: 6 additions & 16 deletions src/components/VirtualList/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react';
import { shallow } from 'enzyme';
import renderer from 'react-test-renderer';
import VirtualList from './index';
import { VirtualRowArgs } from '../../interface';
import { VirtualRowArgs, VirtualItem } from '../../interface';
import AsyncVirtualList from '../../../stories/demos/AsyncVirtualList';

function createNodeMock(element: React.ReactElement) {
Expand All @@ -16,7 +16,7 @@ function createNodeMock(element: React.ReactElement) {
}
const snapOptions = { createNodeMock };

const rowRenderer = (i: VirtualRowArgs) => <div key={i.index}>item</div>;
const rowRenderer = (i: VirtualRowArgs<VirtualItem>) => <div key={i.index}>item</div>;

describe('VirtualList', () => {
it('render without crush', () => {
Expand Down Expand Up @@ -92,21 +92,11 @@ describe('VirtualList', () => {
});

it('scrolling equal row height', () => {
const list = renderer
.create(
<AsyncVirtualList/>,
snapOptions,
)
.toJSON();
const list = renderer.create(<AsyncVirtualList />, snapOptions).toJSON();
expect(list).toMatchSnapshot();
})
});
it('scrolling dynamic row height', () => {
const list = renderer
.create(
<AsyncVirtualList random />,
snapOptions,
)
.toJSON();
const list = renderer.create(<AsyncVirtualList random />, snapOptions).toJSON();
expect(list).toMatchSnapshot();
})
});
});
24 changes: 15 additions & 9 deletions src/components/VirtualList/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@ import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { sum, isFunction } from 'lodash';
import { VirtualListState, VirtualAnchorItem, VirtualListDefaultProps, VirtualListProps } from '../../interface';
import {
VirtualListState,
VirtualAnchorItem,
VirtualListDefaultProps,
VirtualListProps,
VirtualItem,
} from '../../interface';
import CSS from 'csstype';
import './style.scss';

Expand All @@ -21,7 +27,7 @@ function getHeight(el: HTMLDivElement) {
return height + marginTop + marginBottom;
}

const defaultProps: VirtualListDefaultProps = {
const defaultProps: VirtualListDefaultProps<VirtualItem> = {
height: '100%',
data: [],
runwayItems: RUNWAY_ITEMS,
Expand All @@ -32,7 +38,7 @@ const defaultProps: VirtualListDefaultProps = {
debug: true,
};

export default class VirtualList extends React.Component<VirtualListProps, VirtualListState> {
export default class VirtualList<T extends VirtualItem> extends React.Component<VirtualListProps<T>, VirtualListState> {
static propTypes = {
/** 行高 */
rowHeight: PropTypes.oneOfType([PropTypes.func, PropTypes.number]).isRequired,
Expand Down Expand Up @@ -62,7 +68,7 @@ export default class VirtualList extends React.Component<VirtualListProps, Virtu
placeholder: PropTypes.oneOfType([PropTypes.func, PropTypes.node]),
/** 是否展示没有多余的数据 */
noMore: PropTypes.bool,
/** 没有需要加载的数据时展示 */
/** 没有需要加载的数据时展示 */
noMoreHint: PropTypes.oneOfType([PropTypes.func, PropTypes.node]),
/** Debug */
debug: PropTypes.bool,
Expand All @@ -74,7 +80,7 @@ export default class VirtualList extends React.Component<VirtualListProps, Virtu
totalHeight: number;
anchorItem: VirtualAnchorItem;
anchorScrollTop: number;
constructor(props: VirtualListProps) {
constructor(props: VirtualListProps<T>) {
super(props);
this.holder = React.createRef();
this.list = React.createRef();
Expand Down Expand Up @@ -117,7 +123,7 @@ export default class VirtualList extends React.Component<VirtualListProps, Virtu
this.handleResize(data);
window.addEventListener('resize', this.resizeHandler);
}
componentDidUpdate(prevProps: VirtualListProps) {
componentDidUpdate(prevProps: VirtualListProps<T>) {
const { isEstimate, debug, data, isReloading } = this.props;
const { startIndex } = this.state;

Expand Down Expand Up @@ -178,15 +184,15 @@ export default class VirtualList extends React.Component<VirtualListProps, Virtu
resizeHandler = () => {
this.handleResize(this.props.data);
};
handleResize = (data: object[], flushCache?: boolean) => {
handleResize = (data: T[], flushCache?: boolean) => {
this.recomputeRowHeight(data, flushCache);
this.handleScroll();
};
correctAnchor = () => {
const { index, offset } = this.anchorItem;
this.anchorScrollTop = sum(this.heightCache.slice(0, index)) + offset;
};
recomputeRowHeight = (nextData: object[], flushCache: boolean = true) => {
recomputeRowHeight = (nextData: T[], flushCache: boolean = true) => {
if (flushCache) {
this.heightCache = [];
}
Expand Down Expand Up @@ -315,7 +321,7 @@ export default class VirtualList extends React.Component<VirtualListProps, Virtu
if (endIndex < data.length) return;
if (this.noMore) return;
if (onQueryChange && query) {
const { offset, limit } = query;
const { offset = 0, limit = 0 } = query;
onQueryChange({
...query,
offset: offset + limit,
Expand Down
Loading

0 comments on commit c1b9940

Please sign in to comment.