Browse Source

fix: [select] fix scrollIntoView crash for virtualization Select #308 (#310)

* fix: [select] fix scrollIntoView crash for virtualization Select #308

* chore: [select] remove as any for optionNotExist

Co-authored-by: chenyuling <[email protected]>
boomboomchen 3 years ago
parent
commit
944cf44923

+ 6 - 5
packages/semi-foundation/select/foundation.ts

@@ -2,7 +2,7 @@
 /* eslint-disable max-len */
 import BaseFoundation, { DefaultAdapter } from '../base/foundation';
 import KeyCode from '../utils/keyCode';
-import { isNumber, isString, isEqual } from 'lodash-es';
+import { isNumber, isString, isEqual, omit } from 'lodash-es';
 import warning from '../utils/warning';
 import isNullOrUndefined from '../utils/isNullOrUndefined';
 import { BasicOptionProps } from './optionFoundation';
@@ -230,9 +230,9 @@ export default class SelectFoundation extends BaseFoundation<SelectAdapter> {
             selections.set(optionExist.label, optionExist);
         } else if (noMatchOptionInList) {
             // If the current value does not have a corresponding item in the optionList, construct an option and update it to the selection. However, it does not need to be inserted into the list
-            let optionNotExist = { value: propValue, label: propValue, _notExist: true };
+            let optionNotExist = { value: propValue, label: propValue, _notExist: true, _scrollIndex: -1 } as BasicOptionProps;
             if (onChangeWithObject) {
-                optionNotExist = { ...propValue as BasicOptionProps, _notExist: true } as any;
+                optionNotExist = { ...propValue as BasicOptionProps, _notExist: true, _scrollIndex: -1 };
             }
             selections.set(optionNotExist.label, optionNotExist);
         }
@@ -277,7 +277,7 @@ export default class SelectFoundation extends BaseFoundation<SelectAdapter> {
                         // The current value does not exist in the current optionList or the list before the change. Construct an option and update it to the selection
                         let optionNotExist = { value: selectedValue, label: selectedValue, _notExist: true };
                         onChangeWithObject ? (optionNotExist = { ...propValue[i] as any, _notExist: true }) : null;
-                        selections.set(optionNotExist.label, optionNotExist);
+                        selections.set(optionNotExist.label, { ...optionNotExist, _scrollIndex: -1 });
                     }
                 }
             });
@@ -409,7 +409,7 @@ export default class SelectFoundation extends BaseFoundation<SelectAdapter> {
             this._notifyDeselect(value, { value, label, ...rest });
             selections.delete(label);
         } else if (maxLimit && selections.size === maxLimit) {
-            this._adapter.notifyMaxLimit({ value, label, ...rest });
+            this._adapter.notifyMaxLimit({ value, label, ...omit(rest, '_scrollIndex') });
             return;
         } else {
             this._notifySelect(value, { value, label, ...rest });
@@ -787,6 +787,7 @@ export default class SelectFoundation extends BaseFoundation<SelectAdapter> {
         delete option._parentGroup;
         delete option._show;
         delete option._selected;
+        delete option._scrollIndex;
         if ('_keyInOptionList' in option) {
             option.key = option._keyInOptionList;
             delete option._keyInOptionList;

+ 25 - 31
packages/semi-ui/select/_story/select.stories.js

@@ -2439,7 +2439,7 @@ class VirtualizeClassDemo extends React.Component {
   constructor(props) {
     super(props);
     // this.handleSearch = this.handleSearch.bind(this);
-    let newOptions = Array.from({ length: 1000 }, (v, i) => ({ label: `option-${i}`, value: i }));
+    let newOptions = Array.from({ length: 1000 }, (v, i) => ({ label: `o-${i}`, value: `v-${v}-${i}` }));
     this.state = {
       optionList: newOptions,
     };
@@ -2807,50 +2807,44 @@ _Highlight.story = {
 };
 
 export const ScrollIntoView = () => (
-  <>
-    <div>
+  <div>
       <p>single selection</p>
-      <Select defaultValue="option-11" defaultOpen style={{ marginBottom: 300, width: 120 }}>
-        {new Array(50).fill(null).map((item, idx) => (
-          <Option value={`${idx}`} key={idx}>{`option-${idx}`}</Option>
-        ))}
+      <Select defaultValue='v-11' defaultOpen style={{ width: 120, marginBottom: 300 }}>
+          {new Array(50).fill(null).map((item, idx) => (
+              <Option value={`v-${idx}`} key={idx}>{`option-${idx}`}</Option>
+          ))}
       </Select>
       <p>single selection with no selected item</p>
       <Select style={{ marginBottom: 300, width: 120 }}>
-        {new Array(50).fill(null).map((item, idx) => (
-          <Option value={`${idx}`} key={idx}>{`option-${idx}`}</Option>
-        ))}
+          {new Array(50).fill(null).map((item, idx) => (
+              <Option value={`v-${idx}`} key={idx}>{`option-${idx}`}</Option>
+          ))}
       </Select>
       <p>The selected node is the last</p>
-      <Select defaultValue="option-49" defaultOpen style={{ marginBottom: 300, width: 120 }}>
-        {new Array(50).fill(null).map((item, idx) => (
-          <Option value={`${idx}`} key={idx}>{`option-${idx}`}</Option>
-        ))}
+      <Select defaultValue='v-49' defaultOpen style={{ marginBottom: 300, width: 120 }}>
+          {new Array(50).fill(null).map((item, idx) => (
+              <Option value={`v-${idx}`} key={idx}>{`option-${idx}`}</Option>
+          ))}
       </Select>
       <p>The selected node is the first</p>
-      <Select defaultValue="option-0" style={{ marginBottom: 300, width: 120 }}>
-        {new Array(50).fill(null).map((item, idx) => (
-          <Option value={`${idx}`} key={idx}>{`option-${idx}`}</Option>
-        ))}
+      <Select defaultValue='v-0' style={{ marginBottom: 300, width: 120 }}>
+          {new Array(50).fill(null).map((item, idx) => (
+              <Option value={`v-${idx}`} key={idx}>{`option-${idx}`}</Option>
+          ))}
       </Select>
       <p>multiple selection</p>
-      <Select
-        defaultValue={['option-25', 'option-9']}
-        multiple
-        style={{ marginBottom: 300, width: 220 }}
-      >
-        {new Array(30).fill(null).map((item, idx) => (
-          <Option value={`${idx}`} key={idx}>{`option-${idx}`}</Option>
-        ))}
+      <Select defaultValue={['v-25', 'v-9']} multiple style={{ marginBottom: 300, width: 220 }}>
+          {new Array(30).fill(null).map((item, idx) => (
+              <Option value={`v-${idx}`} key={idx}>{`option-${idx}`}</Option>
+          ))}
       </Select>
       <p>multiple selection with no selected item</p>
       <Select multiple style={{ marginBottom: 300, width: 220 }}>
-        {new Array(30).fill(null).map((item, idx) => (
-          <Option value={`${idx}`} key={idx}>{`option-${idx}`}</Option>
-        ))}
+          {new Array(30).fill(null).map((item, idx) => (
+              <Option value={`v-${idx}`} key={idx}>{`option-${idx}`}</Option>
+          ))}
       </Select>
-    </div>
-  </>
+  </div>
 );
 
 ScrollIntoView.story = {

+ 20 - 8
packages/semi-ui/select/index.tsx

@@ -8,7 +8,7 @@ import ConfigContext from '../configProvider/context';
 import SelectFoundation, { SelectAdapter } from '@douyinfe/semi-foundation/select/foundation';
 import { cssClasses, strings, numbers } from '@douyinfe/semi-foundation/select/constants';
 import BaseComponent, { ValidateStatus } from '../_base/baseComponent';
-import { isEqual, isString, noop } from 'lodash-es';
+import { isEqual, isString, noop, get, isNumber } from 'lodash-es';
 import Tag from '../tag/index';
 import TagGroup from '../tag/group';
 import LocaleCosumer from '../locale/localeConsumer';
@@ -418,7 +418,12 @@ class Select extends BaseComponent<SelectProps, SelectState> {
                 let options = [];
                 const { optionList } = this.props;
                 if (optionList && optionList.length) {
-                    options = optionList.map(itemOpt => ({ _show: true, _selected: false, ...itemOpt }));
+                    options = optionList.map((itemOpt, index) => ({ 
+                        _show: true, 
+                        _selected: false, 
+                        _scrollIndex: index,
+                        ...itemOpt 
+                    }));
                     optionGroups[0] = { children: options, label: '' };
                 } else {
                     const result = getOptionsFromGroup(children);
@@ -926,13 +931,20 @@ class Select extends BaseComponent<SelectProps, SelectState> {
             return;
         }
         if (virtualize) {
-            let minKey;
-            selections.forEach((v, k) => {
-                const tempKey = Number(String(k).match(/option-(.*)/)[1]);
-                minKey = (typeof minKey === 'number' && minKey < tempKey) ? minKey : tempKey;
+            let minItemIndex = -1;
+            selections.forEach(item => {
+                const itemIndex = get(item, '_scrollIndex');
+                /* When the itemIndex is legal */
+                if (isNumber(itemIndex) && itemIndex >= 0) {
+                    minItemIndex = minItemIndex !== -1 && minItemIndex < itemIndex
+                        ? minItemIndex
+                        : itemIndex;
+                }
             });
-            if (minKey) {
-                this.virtualizeListRef.current.scrollToItem(minKey, 'center');
+            if (minItemIndex !== -1) {
+                try {
+                    this.virtualizeListRef.current.scrollToItem(minItemIndex, 'center');
+                } catch (error) { }
             }
         } else {
             this.foundation.updateScrollTop();

+ 9 - 3
packages/semi-ui/select/utils.tsx

@@ -3,7 +3,7 @@ import warning from '@douyinfe/semi-foundation/utils/warning';
 import { OptionProps } from './option';
 import { OptionGroupProps } from './optionGroup';
 
-const generateOption = (child: React.ReactElement, parent?: any): OptionProps => {
+const generateOption = (child: React.ReactElement, parent: any, index: number): OptionProps => {
     const childProps = child.props;
     if (!child || !childProps) {
         return null;
@@ -14,6 +14,7 @@ const generateOption = (child: React.ReactElement, parent?: any): OptionProps =>
         label: childProps.label || childProps.children || childProps.value,
         _show: true,
         _selected: false,
+        _scrollIndex: index,
         ...childProps,
         _parentGroup: parent,
     };
@@ -34,11 +35,13 @@ const getOptionsFromGroup = (selectChildren: React.ReactNode) => {
     // eslint-disable-next-line max-len
     const childNodes = React.Children.toArray(selectChildren).filter((childNode: React.ReactElement) => childNode && childNode.props);
     let type = '';
+    let optionIndex = -1;
 
     childNodes.forEach((child: React.ReactElement<any, any>) => {
         if (child.type.isSelectOption) {
             type = 'option';
-            const option = generateOption(child);
+            optionIndex++;
+            const option = generateOption(child, undefined, optionIndex);
             emptyGroup.children.push(option);
             options.push(option);
         } else if (child.type.isSelectOptionGroup) {
@@ -47,7 +50,10 @@ const getOptionsFromGroup = (selectChildren: React.ReactNode) => {
             // eslint-disable-next-line prefer-const
             let { children, ...restGroupProps } = child.props;
             children = React.Children.toArray(children);
-            const childrenOption = children.map((option: React.ReactElement) => generateOption(option, restGroupProps));
+            const childrenOption = children.map((option: React.ReactElement) => {
+                optionIndex++;
+                return generateOption(option, restGroupProps, optionIndex);
+            });
             const group = {
                 ...child.props,
                 children: childrenOption,