Skip to content

Commit

Permalink
feat(Tree): support scrollIntoView, close #2930
Browse files Browse the repository at this point in the history
  • Loading branch information
wangw11056 committed Jun 12, 2024
1 parent 740d523 commit d71eb25
Show file tree
Hide file tree
Showing 3 changed files with 185 additions and 1 deletion.
134 changes: 134 additions & 0 deletions components/tree/__tests__/index-spec.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { Component, createRef, useState } from 'react';
import propTypes from 'prop-types';
import type { MountReturn } from 'cypress/react';
import { KEYCODE } from '../../util';
import Tree from '../index';
import Button from '../../button/index';
Expand Down Expand Up @@ -278,6 +279,33 @@ function renderTreeNodeWithData(dataSource: DataNode[]) {
return drill(dataSource);
}

function isInViewport(targetSelector: string, targetIndex = 0) {
const getBoundingClientRect = () => {
return Cypress.$(targetSelector)[targetIndex].getBoundingClientRect();
};

return cy.window().then(win => {
const { innerWidth: width, innerHeight: height } = win;
return cy
.get(targetSelector)
.its('length')
.should('be.at.least', 1) // 确保至少有一个匹配的元素
.then(async () => {
const elementRect = getBoundingClientRect();
const isTopVisible = elementRect.top >= 0 && elementRect.top <= height;
const isBottomVisible = elementRect.bottom >= 0 && elementRect.bottom <= height;

if (isTopVisible || isBottomVisible) {
const isLeftVisible = elementRect.left >= 0 && elementRect.left <= width;
const isRightVisible = elementRect.right >= 0 && elementRect.right <= width;
return isTopVisible && isLeftVisible && isBottomVisible && isRightVisible;
}

return false;
});
});
}

class ExpandDemo extends Component {
state = {
expandedKeys: ['2'],
Expand Down Expand Up @@ -1431,3 +1459,109 @@ describe('Tree', () => {
});
});
});

function generateDataSource(count = 100) {
return new Array(count).fill(null).map((__, i) => {
return {
label: '服装',
key: `${i}`,
className: `k-${i}`,
children: [
{
label: '男装',
key: `${i}_${i}`,
className: `k-${i}-${i}`,
children: [
{
label: '外套',
key: `${i}_${i}_${i}`,
className: `k-${i}-${i}-${i}`,
},
{
label: '夹克',
key: `${i}_${i}_${i}_${i}`,
className: `k-${i}-${i}-${i}-${i}`,
},
],
},
],
};
});
}

describe.only('should support scrollIntoView when use filterTreeNode property', () => {
const dataSource = generateDataSource();
dataSource.push({
label: '服装',
key: '100',
className: 'k-100',
children: [
{
label: '女装',
key: '100_100',
className: 'k-100-100',
children: [
{
label: '裙子',
key: '100_100_100',
className: 'k-100-100-100',
},
{
label: '毛衣',
key: '100_100_100_100',
className: 'k-100-100-100-100',
},
],
},
],
});
let expandedKeys = ['100_100_100_100'];

function checkNodeIsInViewByKey(key: string) {
cy.mount(
<Tree
expandedKeys={expandedKeys}
autoExpandParent
dataSource={dataSource}
scrollIntoView
filterTreeNode={node => expandedKeys.indexOf(node.props.eventKey!) > -1}
/>
).as('wrapper');
findTreeNodeByKey(key).first().should('has.class', 'next-filtered');
isInViewport('.next-filtered').should('be.true');
}

it('scrollIntoView property should default to false', () => {
cy.mount(
<Tree
expandedKeys={expandedKeys}
autoExpandParent
dataSource={dataSource}
filterTreeNode={node => expandedKeys.indexOf(node.props.eventKey!) > -1}
/>
);
findTreeNodeByKey('100-100-100-100').first().should('has.class', 'next-filtered');
isInViewport('.next-filtered').should('be.false');
});

it('valid of scrollIntoView', () => {
checkNodeIsInViewByKey('100-100-100-100');
});

it('should support scroll update when expandedKeys change', () => {
checkNodeIsInViewByKey('100-100-100-100');

cy.then(() => {
expandedKeys = new Array(100).fill(null).map((_, i) => `${i}_${i}`);
});
cy.get<MountReturn>('@wrapper').then(({ component, rerender }) => {
return rerender(
React.cloneElement(component as React.ReactElement, {
expandedKeys,
})
);
});
findTreeNodeByKey('0-0').first().should('has.class', 'next-filtered');
isInViewport('.next-filtered').should('be.true');
});
});
8 changes: 8 additions & 0 deletions components/tree/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -731,4 +731,12 @@ export interface TreeProps<
* @skip
*/
dir?: string;

/**
* 将元素滚动到视图中,仅在 filterTreeNode 开启时生效
* @en Scroll the element to the view, only effective when filterTreeNode is enabled
* @defaultValue false
* @skip
*/
scrollIntoView?: boolean;
}
44 changes: 43 additions & 1 deletion components/tree/view/tree.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { Component, Children, cloneElement, createRef } from 'react';
import { findDOMNode } from 'react-dom';
import PropTypes from 'prop-types';
import { cloneDeep } from 'lodash';
import { polyfill } from 'react-lifecycles-compat';
Expand Down Expand Up @@ -366,6 +367,7 @@ export class Tree extends Component<TreeProps, TreeState> {
immutable: PropTypes.bool,
virtualListProps: PropTypes.object,
clickToCheck: PropTypes.bool,
scrollIntoView: PropTypes.bool,
};

static defaultProps = {
Expand Down Expand Up @@ -408,15 +410,19 @@ export class Tree extends Component<TreeProps, TreeState> {
immutable: false,
virtualListProps: {},
clickToCheck: false,
scrollIntoView: false,
};

tabbableKey: string | null;
dropPosition: number;
dragNode: NodeInstance | null;
dragNodesKeys: Key[];

normalListRef: React.RefObject<HTMLUListElement> | null;
virtualListRef: React.RefObject<VirtualList>;

highlightTimer: number | undefined;

constructor(props: TreeProps) {
super(props);

Expand Down Expand Up @@ -468,7 +474,10 @@ export class Tree extends Component<TreeProps, TreeState> {

bindCtx(this, ['handleExpand', 'handleSelect', 'handleCheck', 'handleBlur']);

this.normalListRef = createRef();
this.virtualListRef = createRef();

this.handleTreeNodeScrollIntoView();
}

static getDerivedStateFromProps(props: TreeProps, state: TreeState) {
Expand Down Expand Up @@ -531,6 +540,39 @@ export class Tree extends Component<TreeProps, TreeState> {
};
}

componentDidUpdate() {
this.handleTreeNodeScrollIntoView();
}

handleTreeNodeScrollIntoView() {
const { dataSource, scrollIntoView } = this.props;
if (!Array.isArray(dataSource) || !dataSource.length) {
return;
}

if ('filterTreeNode' in this.props && !!scrollIntoView) {
this.scrollTreeNodeIntoView();
}
}

// scroll into focus item
scrollTreeNodeIntoView() {
const { prefix } = this.props;

clearTimeout(this.highlightTimer);
this.highlightTimer = window.setTimeout(() => {
try {
const treeNode = findDOMNode(this.normalListRef?.current) as HTMLElement;
const itemNode = treeNode.querySelector<
HTMLElement & { scrollIntoViewIfNeeded?: () => void }
>(`.${prefix}tree-node.${prefix}filtered`);
itemNode && itemNode.scrollIntoViewIfNeeded && itemNode.scrollIntoViewIfNeeded();
} catch (ex) {
// I don't care...
}
});
}

setFocusKey() {
const { selectedKeys = [] } = this.state;
this.setState({
Expand Down Expand Up @@ -1239,7 +1281,7 @@ export class Tree extends Component<TreeProps, TreeState> {
return (
<ul
role="tree"
ref={ref}
ref={ref || this.normalListRef}
aria-multiselectable={multiple}
onBlur={this.handleBlur}
className={newClassName}
Expand Down

0 comments on commit d71eb25

Please sign in to comment.