diff --git a/components/cascader/menu.tsx b/components/cascader/menu.tsx index dc15f39627..74ffafdf08 100644 --- a/components/cascader/menu.tsx +++ b/components/cascader/menu.tsx @@ -20,7 +20,7 @@ export default class CascaderMenu extends Component { useVirtual: PropTypes.bool, children: PropTypes.node, }; - virtualEl: VirtualList | null; + virtualEl: InstanceType | null; menuEl: HTMLDivElement; componentDidMount() { @@ -42,7 +42,6 @@ export default class CascaderMenu extends Component { } if (useVirtual) { - // @ts-expect-error VirtualList 尚未做优化,因此无法取得 getInstance const instance = this.virtualEl!.getInstance(); setTimeout(() => instance.scrollTo(selectedIndex), 0); } else { @@ -89,7 +88,7 @@ export default class CascaderMenu extends Component { this.menuEl = ref; }; - saveVirtualRef = (ref: VirtualList) => { + saveVirtualRef = (ref: InstanceType) => { this.virtualEl = ref; }; diff --git a/components/tree/view/tree.tsx b/components/tree/view/tree.tsx index 7ec6b58ef4..3505101cb1 100644 --- a/components/tree/view/tree.tsx +++ b/components/tree/view/tree.tsx @@ -417,7 +417,7 @@ export class Tree extends Component { dragNodesKeys: Key[]; normalListRef: React.MutableRefObject; - virtualListRef: React.RefObject; + virtualListRef: React.RefObject>; constructor(props: TreeProps) { super(props); diff --git a/components/virtual-list/__docs__/demo/basic/index.tsx b/components/virtual-list/__docs__/demo/basic/index.tsx index 4bf62a6797..7cb9bd82ac 100644 --- a/components/virtual-list/__docs__/demo/basic/index.tsx +++ b/components/virtual-list/__docs__/demo/basic/index.tsx @@ -4,8 +4,7 @@ import { VirtualList } from '@alifd/next'; const dataSource = []; -const generateLi = (index = 'index') => { - const data = []; +const generateLi = (index: number) => { if (index % 3 === 0) { return (
  • = []; -function generateLi(index) { +function generateLi(index: number) { return (
  • key-{index}
  • ); } -function generateData(len) { +function generateData(len: number) { for (let i = 0; i < len; i++) { dataSource.push(generateLi(i)); } diff --git a/components/virtual-list/__docs__/demo/item-size-getter/index.tsx b/components/virtual-list/__docs__/demo/item-size-getter/index.tsx index e3f6336d04..59b792e003 100644 --- a/components/virtual-list/__docs__/demo/item-size-getter/index.tsx +++ b/components/virtual-list/__docs__/demo/item-size-getter/index.tsx @@ -1,10 +1,10 @@ -import React from 'react'; +import React, { type ReactElement, createRef } from 'react'; import ReactDOM from 'react-dom'; import { VirtualList } from '@alifd/next'; -const dataSource = []; +const dataSource: Array = []; -function generateLi(index) { +function generateLi(index: number) { if (index % 3 === 0) { return (
  • > = createRef(); + state = { initial: 20, dataSource: generateData(1000), @@ -36,12 +38,13 @@ class App extends React.Component { componentDidMount() { setTimeout(() => { - const instance = this.refs.virtual.getInstance(); - instance.scrollTo(50); + const instance = + this.virtualListRef.current && this.virtualListRef.current.getInstance(); + instance && instance.scrollTo(50); }, 200); } - getHeight(index) { + getHeight(index: number) { return index % 3 === 0 ? 30 : 20; } @@ -59,7 +62,7 @@ class App extends React.Component {
    diff --git a/components/virtual-list/__docs__/index.en-us.md b/components/virtual-list/__docs__/index.en-us.md index 3b7aae0730..7d5ba6cab0 100644 --- a/components/virtual-list/__docs__/index.en-us.md +++ b/components/virtual-list/__docs__/index.en-us.md @@ -10,12 +10,12 @@ ### VirtualList -| 参数 | 说明 | 类型 | 默认值 | -| ------------- | ----------------------------------------------------------------------------------------- | -------- | ------------------------------------------------ | -| children | child node to be rendered | any | - | -| minSize | min count of items to be loaded | Number | 1 | -| pageSize | the number of items in one page | Number | 10 | -| itemsRenderer | items parent dom,by default (items, ref) =>
      {items}


    **signature**:
    Function() => void | Function | (items, ref) => <ul ref={ref}>{items}</ul> | -| threshold | height of threshold | Number | 100 | -| itemSizeGetter | get item's height

    **signature**:
    Function() => void | Function | - | -| jumpIndex | the index you want to jump to, set itemSizeGetter if the height of items vary | Number | 0 | +| Param | Description | Type | Default Value | Required | +| -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- | -------- | +| children | Children of the virtual list component | React.ReactElement \| Array\ | - | | +| minSize | The minimum number of items to be loaded | number | 1 | | +| pageSize | The number of items to be rendered in one screen | number | 10 | | +| itemsRenderer | The parent render function | (
    items: React.ReactNodeArray,
    ref: (instance: React.ReactInstance \| null) => React.ReactInstance \| null
    ) => React.ReactNode | `(items, ref) =>
      {items}
    ` | | +| threshold | The height of the buffer | number | 100 | | +| itemSizeGetter | The function to get the height of the item | (index?: number) => void | - | | +| jumpIndex | Set the jump position, need to set itemSizeGetter to take effect, if not set, the element is assumed to be of equal height and the height of the first element is taken as the default height | number | 0 | | diff --git a/components/virtual-list/__docs__/index.md b/components/virtual-list/__docs__/index.md index 29b50a7305..06c5174e19 100644 --- a/components/virtual-list/__docs__/index.md +++ b/components/virtual-list/__docs__/index.md @@ -16,12 +16,12 @@ ### VirtualList -| 参数 | 说明 | 类型 | 默认值 | -| -------------- | -------------------------------------------------------------------------------------------- | -------- | ------------------------------------------------ | -| children | 渲染的子节点 | any | - | -| minSize | 最小加载数量 | Number | 1 | -| pageSize | 一屏数量 | Number | 10 | -| itemsRenderer | 父渲染函数,默认为 (items, ref) =>
      {items}


    **签名**:
    Function() => void | Function | (items, ref) => <ul ref={ref}>{items}</ul> | -| threshold | 缓冲区高度 | Number | 100 | -| itemSizeGetter | 获取item高度的函数

    **签名**:
    Function() => void | Function | - | -| jumpIndex | 设置跳转位置,需要设置 itemSizeGetter 才能生效, 不设置认为元素等高并取第一个元素高度作为默认高 | Number | 0 | +| 参数 | 说明 | 类型 | 默认值 | 是否必填 | +| -------------- | ---------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- | -------- | +| children | 渲染的子节点 | React.ReactElement \| Array\ | - | | +| minSize | 最小加载数量 | number | 1 | | +| pageSize | 一屏数量 | number | 10 | | +| itemsRenderer | 父渲染函数 | (
    items: React.ReactNodeArray,
    ref: (instance: React.ReactInstance \| null) => React.ReactInstance \| null
    ) => React.ReactNode | `(items, ref) =>
      {items}
    ` | | +| threshold | 缓冲区高度 | number | 100 | | +| itemSizeGetter | 获取item高度的函数 | (index?: number) => void | - | | +| jumpIndex | 设置跳转位置,需要设置 itemSizeGetter 才能生效, 不设置认为元素等高并取第一个元素高度作为默认高 | number | 0 | | diff --git a/components/virtual-list/__tests__/a11y-spec.tsx b/components/virtual-list/__tests__/a11y-spec.tsx index 2ec305363c..4f43f3ff9d 100644 --- a/components/virtual-list/__tests__/a11y-spec.tsx +++ b/components/virtual-list/__tests__/a11y-spec.tsx @@ -1,13 +1,9 @@ import React from 'react'; -import Enzyme from 'enzyme'; -import Adapter from 'enzyme-adapter-react-16'; import VirtualList from '../index'; import '../style'; -import { unmount, testReact } from '../../util/__tests__/legacy/a11y/validate'; +import { testReact } from '../../util/__tests__/a11y/validate'; -Enzyme.configure({ adapter: new Adapter() }); - -const generateData = len => { +const generateData = (len: number) => { const dataSource = []; for (let i = 0; i < len; i++) { @@ -17,25 +13,12 @@ const generateData = len => { return dataSource; }; -/* eslint-disable no-undef, react/jsx-filename-extension */ describe('VirtualList A11y', () => { - let wrapper; - - afterEach(() => { - if (wrapper) { - wrapper.unmount(); - wrapper = null; - } - unmount(); - }); - it('should not have any violations', async () => { - wrapper = await testReact({generateData(10)}); - return wrapper; + await testReact({generateData(10)}); }); it('should not have any violations for jump index', async () => { - wrapper = await testReact({generateData(10)}); - return wrapper; + await testReact({generateData(10)}); }); }); diff --git a/components/virtual-list/__tests__/index-spec.tsx b/components/virtual-list/__tests__/index-spec.tsx index cd0bc25daa..15f055770b 100644 --- a/components/virtual-list/__tests__/index-spec.tsx +++ b/components/virtual-list/__tests__/index-spec.tsx @@ -1,39 +1,8 @@ import React from 'react'; -import ReactDOM from 'react-dom'; -import Enzyme from 'enzyme'; -import Adapter from 'enzyme-adapter-react-16'; -import assert from 'power-assert'; import VirtualList from '../index'; import '../style'; -Enzyme.configure({ adapter: new Adapter() }); - -const render = element => { - let inc; - const container = document.createElement('div'); - document.body.appendChild(container); - ReactDOM.render(element, container, function () { - inc = this; - }); - return { - setProps: props => { - const clonedElement = React.cloneElement(element, props); - ReactDOM.render(clonedElement, container); - }, - unmount: () => { - ReactDOM.unmountComponentAtNode(container); - document.body.removeChild(container); - }, - instance: () => { - return inc; - }, - find: selector => { - return container.querySelectorAll(selector); - }, - }; -}; - -const generateData = len => { +const generateData = (len: number) => { const dataSource = []; for (let i = 0; i < len; i++) { @@ -48,15 +17,6 @@ const generateData = len => { }; describe('VirtualList', () => { - let wrapper; - - afterEach(() => { - if (wrapper) { - wrapper.unmount(); - wrapper = null; - } - }); - it('should render', () => { function App() { return ( @@ -72,8 +32,8 @@ describe('VirtualList', () => { ); } - wrapper = render(); - assert(wrapper.find('li').length === 10); + cy.mount(); + cy.get('li').should('have.length', 10); }); it('should render much more', () => { @@ -91,11 +51,11 @@ describe('VirtualList', () => { ); } - wrapper = render(); - assert(wrapper.find('li').length < 20); + cy.mount(); + cy.get('li').should('have.length.at.most', 20); }); - it('should support jumpIndex', done => { + it('should support jumpIndex', () => { function App() { return (
    { ); } - wrapper = render(); - setTimeout(() => { - assert(wrapper.find('li')[0].innerText > 40); - done(); - }, 100); + cy.mount(); + + cy.get('li') + .should('be.visible') + .first() + .invoke('text') + .then(text => { + expect(parseInt(text, 10)).to.be.above(40); + }); + }); + + it('should render single item', () => { + const singleItem = ( +
  • + {0} +
  • + ); + function App() { + return ( +
    + { + return 20; + }} + > + {singleItem} + +
    + ); + } + + cy.mount(); }); }); diff --git a/components/virtual-list/index.tsx b/components/virtual-list/index.tsx index 39de6706ac..f53504f996 100644 --- a/components/virtual-list/index.tsx +++ b/components/virtual-list/index.tsx @@ -1,4 +1,7 @@ import ConfigProvider from '../config-provider'; +import { type VirtualListProps } from './types'; import VirtualList from './virtual-list'; +export type { VirtualListProps }; + export default ConfigProvider.config(VirtualList); diff --git a/components/virtual-list/mobile/index.tsx b/components/virtual-list/mobile/index.tsx index 9fcfaf9487..c284d38c19 100644 --- a/components/virtual-list/mobile/index.tsx +++ b/components/virtual-list/mobile/index.tsx @@ -1,3 +1,4 @@ +// @ts-expect-error meet-react does not export VirtualList import { VirtualList as MeetVirtualList } from '@alifd/meet-react'; import NextVirtualList from '../index'; diff --git a/components/virtual-list/types.ts b/components/virtual-list/types.ts index 17c70a9a19..ecfc135a2d 100644 --- a/components/virtual-list/types.ts +++ b/components/virtual-list/types.ts @@ -1,43 +1,62 @@ -/// +import type React from 'react'; +import { type CommonProps } from '../util'; -import React from 'react'; -import { CommonProps } from '../util'; +export interface VirtualListState { + size: number; + from: number; +} +/** + * @api VirtualList + */ export interface VirtualListProps extends React.HTMLAttributes, CommonProps { /** * 渲染的子节点 + * @en children of the virtual list component */ - children?: any; + children?: React.ReactElement | Array; /** * 最小加载数量 + * @en the minimum number of items to be loaded + * @defaultValue 1 */ minSize?: number; /** * 一屏数量 + * @en the number of items to be rendered in one screen + * @defaultValue 10 */ pageSize?: number; /** - * 父渲染函数,默认为 (items, ref) =>
      {items}
    + * 父渲染函数 + * @en the parent render function + * @defaultValue `(items, ref) =>
      {items}
    ` */ - itemsRenderer?: (items: any, ref: any) => React.ReactNode; + itemsRenderer?: ( + items: React.ReactNodeArray, + ref: (instance: React.ReactInstance | null) => React.ReactInstance | null + ) => React.ReactNode; /** * 缓冲区高度 + * @en the height of the buffer + * @defaultValue 100 */ threshold?: number; /** * 获取item高度的函数 + * @en the function to get the height of the item */ - itemSizeGetter?: () => void; + itemSizeGetter?: (index?: number) => void; /** * 设置跳转位置,需要设置 itemSizeGetter 才能生效, 不设置认为元素等高并取第一个元素高度作为默认高 + * @en set the jump position, need to set itemSizeGetter to take effect, if not set, the element is assumed to be of equal height and the height of the first element is taken as the default height + * @defaultValue 0 */ jumpIndex?: number; } - -export default class VirtualList extends React.Component {} diff --git a/components/virtual-list/virtual-list.tsx b/components/virtual-list/virtual-list.tsx index 511a8a500c..6030490d4b 100644 --- a/components/virtual-list/virtual-list.tsx +++ b/components/virtual-list/virtual-list.tsx @@ -1,14 +1,15 @@ import PropTypes from 'prop-types'; -import React, { Component } from 'react'; +import React, { Component, type LegacyRef, type CSSProperties, type ReactInstance } from 'react'; import cx from 'classnames'; import { polyfill } from 'react-lifecycles-compat'; import { findDOMNode } from 'react-dom'; import { events } from '../util'; +import { type VirtualListProps, type VirtualListState } from './types'; const NOOP = () => {}; const MAX_SYNC_UPDATES = 40; -const isEqualSubset = (a, b) => { +const isEqualSubset = (a: T, b: Partial): boolean => { for (const key in b) { if (a[key] !== b[key]) { return false; @@ -18,75 +19,67 @@ const isEqualSubset = (a, b) => { return true; }; -const getOffset = el => { +const getOffset = (el: HTMLElement) => { let offset = el.clientLeft || 0; do { offset += el.offsetTop || 0; - el = el.offsetParent; + el = el.offsetParent as HTMLElement; } while (el); return offset; }; -const constrain = (from, size, { children, minSize }) => { +const constrain = (from: number, size: number, { children, minSize }: VirtualListProps) => { + // @ts-expect-error children 未考虑非数组是单个 ReactElement 的情况 const length = children && children.length; - size = Math.max(size, minSize); - if (size > length) { - size = length; + size = Math.max(size, minSize!); + if (size > length!) { + size = length!; } - from = from ? Math.max(Math.min(from, length - size), 0) : 0; + from = from ? Math.max(Math.min(from, length! - size!), 0) : 0; return { from, size }; }; /** VirtualList */ -class VirtualList extends Component { +class VirtualList extends Component { static displayName = 'VirtualList'; static propTypes = { prefix: PropTypes.string, - /** - * 渲染的子节点 - */ children: PropTypes.any, - /** - * 最小加载数量 - */ minSize: PropTypes.number, - /** - * 一屏数量 - */ pageSize: PropTypes.number, - /** - * 父渲染函数,默认为 (items, ref) =>
      {items}
    - */ itemsRenderer: PropTypes.func, - /** - * 缓冲区高度 - */ threshold: PropTypes.number, - /** - * 获取item高度的函数 - */ itemSizeGetter: PropTypes.func, - /** - * 设置跳转位置,需要设置 itemSizeGetter 才能生效, 不设置认为元素等高并取第一个元素高度作为默认高 - */ jumpIndex: PropTypes.number, className: PropTypes.string, }; static defaultProps = { prefix: 'next-', - itemsRenderer: (items, ref) =>
      {items}
    , + itemsRenderer: (items: ReactInstance, ref: LegacyRef) => ( +
      {items}
    + ), minSize: 1, pageSize: 10, jumpIndex: 0, threshold: 100, }; - - constructor(props) { + cache: { [key: number]: number }; + cacheAdd: { [key: number]: number }; + cachedScroll: null | number; + unstable: boolean; + updateCounter: number; + updateCounterTimeoutId?: number; + el: HTMLElement | null; + items: ReactInstance | null; + defaultItemHeight: number; + scrollParent: HTMLElement | Window; + + constructor(props: VirtualListProps) { super(props); const { jumpIndex } = props; - const { from, size } = constrain(jumpIndex, 0, props); + const { from, size } = constrain(jumpIndex!, 0, props); this.state = { from, size }; this.cache = {}; this.cacheAdd = {}; @@ -96,7 +89,7 @@ class VirtualList extends Component { this.updateCounter = 0; } - static getDerivedStateFromProps(nextProps, prevState) { + static getDerivedStateFromProps(nextProps: VirtualListProps, prevState: VirtualListState) { const { from, size } = prevState; return constrain(from, size, nextProps); @@ -112,7 +105,7 @@ class VirtualList extends Component { this.updateFrame(this.scrollTo.bind(this, jumpIndex)); } - componentDidUpdate(prevProps) { + componentDidUpdate(prevProps: VirtualListProps) { const oldIndex = prevProps.jumpIndex; const newIndex = this.props.jumpIndex; @@ -130,7 +123,7 @@ class VirtualList extends Component { } if (!this.updateCounterTimeoutId) { - this.updateCounterTimeoutId = setTimeout(() => { + this.updateCounterTimeoutId = window.setTimeout(() => { this.updateCounter = 0; delete this.updateCounterTimeoutId; }, 0); @@ -146,7 +139,7 @@ class VirtualList extends Component { events.off(this.scrollParent, 'mousewheel', NOOP); } - maybeSetState(b, cb) { + maybeSetState(b: VirtualListState, cb: () => void) { if (isEqualSubset(this.state, b)) { return cb(); } @@ -159,8 +152,8 @@ class VirtualList extends Component { } getScrollParent() { - let el = this.getEl(); - el = el.parentElement; + let el = this.getEl() as HTMLElement; + el = el.parentElement!; switch (window.getComputedStyle(el).overflowY) { case 'auto': @@ -186,30 +179,32 @@ class VirtualList extends Component { // always return document.documentElement[scrollKey] as 0, so take // whichever has a value. document.body[scrollKey] || document.documentElement[scrollKey] - : scrollParent[scrollKey]; + : (scrollParent as HTMLElement)[scrollKey]; const max = this.getScrollSize() - this.getViewportSize(); const scroll = Math.max(0, Math.min(actual, max)); - const el = this.getEl(); - this.cachedScroll = getOffset(scrollParent) + scroll - getOffset(el); + const el = this.getEl() as HTMLElement; + this.cachedScroll = getOffset(scrollParent as HTMLElement) + scroll - getOffset(el); return this.cachedScroll; } - setScroll(offset) { + setScroll(offset: number) { const { scrollParent } = this; - offset += getOffset(this.getEl()); + offset += getOffset(this.getEl() as HTMLElement); if (scrollParent === window) { return window.scrollTo(0, offset); } - offset -= getOffset(this.scrollParent); - scrollParent.scrollTop = offset; + offset -= getOffset(this.scrollParent as HTMLElement); + (scrollParent as HTMLElement).scrollTop = offset; } getViewportSize() { const { scrollParent } = this; - return scrollParent === window ? window.innerHeight : scrollParent.clientHeight; + return scrollParent === window + ? window.innerHeight + : (scrollParent as HTMLElement).clientHeight; } getScrollSize() { @@ -218,26 +213,26 @@ class VirtualList extends Component { const key = 'scrollHeight'; return scrollParent === window ? Math.max(body[key], documentElement[key]) - : scrollParent[key]; + : (scrollParent as HTMLElement)[key]; } getStartAndEnd(threshold = this.props.threshold) { const scroll = this.getScroll(); const trueScroll = scroll; - const start = Math.max(0, trueScroll - threshold); - const end = trueScroll + this.getViewportSize() + threshold; + const start = Math.max(0, trueScroll! - threshold!); + const end = trueScroll! + this.getViewportSize() + threshold!; return { start, end }; } // Called by 'scroll' and 'resize' events, clears scroll position cache. - updateFrameAndClearCache(cb) { + updateFrameAndClearCache(cb: () => void) { this.cachedScroll = null; return this.updateFrame(cb); } - updateFrame(cb) { + updateFrame(cb?: () => void) { this.updateScrollParent(); if (typeof cb !== 'function') { @@ -266,13 +261,14 @@ class VirtualList extends Component { // Just an empty listener. After that onscroll events will be fired synchronously. } - updateVariableFrame(cb) { + updateVariableFrame(cb: () => void) { if (!this.props.itemSizeGetter) { this.cacheSizes(); } const { start, end } = this.getStartAndEnd(); const { pageSize, children } = this.props; + // @ts-expect-error children 未考虑非数组是单个 ReactElement 的情况 const length = children.length; let space = 0; let from = 0; @@ -293,7 +289,7 @@ class VirtualList extends Component { while (size < maxSize && space < end) { const itemSize = this.getSizeOf(from + size); if (itemSize === null || itemSize === undefined) { - size = Math.min(size + pageSize, maxSize); + size = Math.min(size + pageSize!, maxSize); break; } space += itemSize; @@ -303,7 +299,7 @@ class VirtualList extends Component { this.maybeSetState({ from, size }, cb); } - getSpaceBefore(index, cache = {}) { + getSpaceBefore(index: number, cache = {} as { [key: number]: number }) { if (!index) { return 0; } @@ -336,14 +332,15 @@ class VirtualList extends Component { cacheSizes() { const { cache } = this; const { from } = this.state; - const { children, props = {} } = this.items; + // @ts-expect-error items是ReactInstance,但是通过解构同时获取children和props会报错,后续用 in 做类型判断 + const { children, props = {} } = this.items!; const itemEls = children || props.children || []; try { //