Przeglądaj źródła

fix: Select renderOptionItem can't autoScroll to active item when use keyboard arrowDown/arrowUp, close #2263 (#2498)

* fix: Select renderOptionItem can't autoScroll to active item
* test: add e2e test
---------
Co-authored-by: pointhalo <[email protected]>
pointhalo 1 rok temu
rodzic
commit
8a0c32124f

+ 2 - 1
content/input/select/index-en-US.md

@@ -1244,7 +1244,7 @@ import { IconAppCenter, IconChevronDown } from '@douyinfe/semi-icons';
     Notice:
     Notice:
     1. The style passed in by props needs to be consumed on wrapper dom, otherwise it will not be able to be used normally in virtualization scenarios
     1. The style passed in by props needs to be consumed on wrapper dom, otherwise it will not be able to be used normally in virtualization scenarios
     2. The styles of selected, focused, disabled, etc. state need to be added by yourself, and you can get the relative boolean value from props
     2. The styles of selected, focused, disabled, etc. state need to be added by yourself, and you can get the relative boolean value from props
-    3. onMouseEnter needs to be bound on the wrapper dom, otherwise the display will be problematic when the upper and lower keyboards are operated
+    3. `onMouseEnter`、`className` needs to be bound on the wrapper dom, otherwise the display will be problematic when the upper and lower keyboards are operated
     4. If your custom item is Select.Option, you need to pass renderProps.onClick transparently to the onSelect prop of Option
     4. If your custom item is Select.Option, you need to pass renderProps.onClick transparently to the onSelect prop of Option
 
 
 ```jsx live=true
 ```jsx live=true
@@ -1272,6 +1272,7 @@ import { Select, Checkbox } from '@douyinfe/semi-ui';
             ['custom-option-render-focused']: focused,
             ['custom-option-render-focused']: focused,
             ['custom-option-render-disabled']: disabled,
             ['custom-option-render-disabled']: disabled,
             ['custom-option-render-selected']: selected,
             ['custom-option-render-selected']: selected,
+            className,
         });
         });
         // Notice:
         // Notice:
         // 1. The style passed in by props needs to be consumed on wrapper dom, otherwise it will not be able to be used normally in virtualization scenarios
         // 1. The style passed in by props needs to be consumed on wrapper dom, otherwise it will not be able to be used normally in virtualization scenarios

+ 4 - 3
content/input/select/index.md

@@ -1305,8 +1305,8 @@ import { IconAppCenter, IconChevronDown } from '@douyinfe/semi-icons';
 -   完全自定义:通过传入`renderOptionItem`,你可以完全接管列表中候选项的渲染,并且从回调入参中,获取到相关的状态值。实现更高自由度的结构渲染  
 -   完全自定义:通过传入`renderOptionItem`,你可以完全接管列表中候选项的渲染,并且从回调入参中,获取到相关的状态值。实现更高自由度的结构渲染  
     注意事项:
     注意事项:
     1. props 传入的 style 需在 wrapper dom 上进行消费,否则在虚拟化场景下会无法正常使用
     1. props 传入的 style 需在 wrapper dom 上进行消费,否则在虚拟化场景下会无法正常使用
-    2. 选中(selected)、聚焦(focused)、禁用(disabled)等状态的样式需自行加上,你可以从 props 中获取到相对的 boolean 值
-    3. onMouseEnter 需在 wrapper dom 上绑定,否则上下键盘操作时显示会有问题
+    2. props 传入的 className、onMouseEnter 需在 wrapper dom 上进行消费,否则上下键盘操作时显示会有问题
+    3. 选中(selected)、聚焦(focused)、禁用(disabled)等状态的样式需自行加上,你可以从 props 中获取到相对的 boolean 值
     4. 如果你的自定义 item 为 Select.Option,需要将 renderProps.onClick 透传给 Option 的 onSelect prop
     4. 如果你的自定义 item 为 Select.Option,需要将 renderProps.onClick 透传给 Option 的 onSelect prop
 
 
 ```jsx live=true
 ```jsx live=true
@@ -1335,13 +1335,14 @@ import { Select, Checkbox, Highlight } from '@douyinfe/semi-ui';
             ['custom-option-render-focused']: focused,
             ['custom-option-render-focused']: focused,
             ['custom-option-render-disabled']: disabled,
             ['custom-option-render-disabled']: disabled,
             ['custom-option-render-selected']: selected,
             ['custom-option-render-selected']: selected,
+            className
         });
         });
         const searchWords = [inputValue];
         const searchWords = [inputValue];
 
 
         // Notice:
         // Notice:
         // 1.props传入的style需在wrapper dom上进行消费,否则在虚拟化场景下会无法正常使用
         // 1.props传入的style需在wrapper dom上进行消费,否则在虚拟化场景下会无法正常使用
         // 2.选中(selected)、聚焦(focused)、禁用(disabled)等状态的样式需自行加上,你可以从props中获取到相对的boolean值
         // 2.选中(selected)、聚焦(focused)、禁用(disabled)等状态的样式需自行加上,你可以从props中获取到相对的boolean值
-        // 3.onMouseEnter需在wrapper dom上绑定,否则上下键盘操作时显示会有问题
+        // 3.onMouseEnter、className需在wrapper dom上绑定,否则上下键盘操作时显示会有问题
         
         
         return (
         return (
             <div style={style} className={optionCls} onClick={() => onClick()} onMouseEnter={e => onMouseEnter()}>
             <div style={style} className={optionCls} onClick={() => onClick()} onMouseEnter={e => onMouseEnter()}>

+ 25 - 0
cypress/e2e/select.spec.js

@@ -192,6 +192,31 @@ describe('Select', () => {
         cy.wait(300);
         cy.wait(300);
         cy.get('[data-cy=a-2]').should('have.class', 'semi-select-option-selected');
         cy.get('[data-cy=a-2]').should('have.class', 'semi-select-option-selected');
     });
     });
+
+    it('renderOptionItem, keyboard Up & Down', () => {
+        cy.visit('http://127.0.0.1:6006/iframe.html?path=/story/select--render-option-item');
+        cy.get('[data-cy=multiple]').click();
+        cy.wait(300);
+        cy.get('input').eq(0).type('{downArrow}');
+        cy.get('[data-cy=option-2]').should('have.class', 'custom-option-render-focused');
+        cy.get('input').eq(0).type('{downArrow}');
+        cy.get('[data-cy=option-3]').should('have.class', 'custom-option-render-focused');
+        cy.get('input').eq(0).type('{downArrow}');
+        cy.get('[data-cy=option-4]').should('have.class', 'custom-option-render-focused');
+        cy.get('input').eq(0).type('{downArrow}');
+        cy.get('[data-cy=option-5]').should('have.class', 'custom-option-render-focused');
+        cy.get('input').eq(0).type('{downArrow}');
+        cy.get('[data-cy=option-6]').should('have.class', 'custom-option-render-focused');
+        cy.get('input').eq(0).type('{downArrow}');
+        cy.get('[data-cy=option-7]').should('have.class', 'custom-option-render-focused');
+        cy.get('input').eq(0).type('{downArrow}');
+        cy.get('[data-cy=option-8]').should('have.class', 'custom-option-render-focused');
+        cy.get('input').eq(0).type('{downArrow}');
+        cy.get('[data-cy=option-1]').should('have.class', 'custom-option-render-focused');
+        cy.get('input').eq(0).type('{upArrow}');
+        cy.get('[data-cy=option-8]').should('have.class', 'custom-option-render-focused');
+    });
+
     // it('ellipsisTrigger', () => {
     // it('ellipsisTrigger', () => {
     //     cy.visit('http://127.0.0.1:6006/iframe.html?path=/story/select--fix-1560');
     //     cy.visit('http://127.0.0.1:6006/iframe.html?path=/story/select--fix-1560');
 
 

+ 20 - 1
packages/semi-ui/select/_story/select.stories.jsx

@@ -2695,6 +2695,18 @@ SelectPosition.story = {
 }
 }
 
 
 const RenderOptionDemo = () => {
 const RenderOptionDemo = () => {
+
+  const optionList = [
+      { value: 'db-4', label: 'Doubao-Pro-4k', otherKey: 0, 'data-cy': 'option-1' },
+      { value: 'db-32', label: 'Doubao-Pro-32K', otherKey: 1, 'data-cy': 'option-2' },
+      { value: 'db-128', label: 'Doubao-Pro-128K', otherKey: 2, 'data-cy': 'option-3' },
+      { value: 'db-lite-2', label: 'Doubao-Lite-4K', otherKey: 4, 'data-cy': 'option-4' },
+      { value: 'db-lite-32', label: 'Doubao-Lite-32K', otherKey: 5, 'data-cy': 'option-5' },
+      { value: 'db-lite-128', label: 'Doubao-Lite-128K', otherKey: 6, 'data-cy': 'option-6' },
+      { value: 'gpt-4', label: 'GPT-4', otherKey: 6, 'data-cy': 'option-7' },
+      { value: 'gpt-4-32', label: 'GPT-4-32K', otherKey: 7, 'data-cy': 'option-8' },
+  ];
+
   const renderOptionItem = renderProps => {
   const renderOptionItem = renderProps => {
     const {
     const {
       disabled,
       disabled,
@@ -2715,10 +2727,12 @@ const RenderOptionDemo = () => {
       ['custom-option-render-focused']: focused,
       ['custom-option-render-focused']: focused,
       ['custom-option-render-disabled']: disabled,
       ['custom-option-render-disabled']: disabled,
       ['custom-option-render-selected']: selected,
       ['custom-option-render-selected']: selected,
-    }); // Notice:
+    }, className); // Notice:
+
     // 1.props传入的style需在wrapper dom上进行消费,否则在虚拟化场景下会无法正常使用
     // 1.props传入的style需在wrapper dom上进行消费,否则在虚拟化场景下会无法正常使用
     // 2.选中(selected)、聚焦(focused)、禁用(disabled)等状态的样式需自行加上,你可以从props中获取到相对的boolean值
     // 2.选中(selected)、聚焦(focused)、禁用(disabled)等状态的样式需自行加上,你可以从props中获取到相对的boolean值
     // 3.onMouseEnter需在wrapper dom上绑定,否则上下键盘操作时显示会有问题
     // 3.onMouseEnter需在wrapper dom上绑定,否则上下键盘操作时显示会有问题
+    // 4.props传入的className需在wrapper dom上绑定,否则上下键盘操作时可能存在无法自动滚动展示的问题
 
 
     return (
     return (
       <div
       <div
@@ -2726,6 +2740,7 @@ const RenderOptionDemo = () => {
         className={optionCls}
         className={optionCls}
         onClick={() => onClick()}
         onClick={() => onClick()}
         onMouseEnter={e => onMouseEnter()}
         onMouseEnter={e => onMouseEnter()}
+        {...rest}
       >
       >
         <Checkbox checked={selected} />
         <Checkbox checked={selected} />
         <div className="option-right">{label}</div>
         <div className="option-right">{label}</div>
@@ -2742,6 +2757,8 @@ const RenderOptionDemo = () => {
         style={{
         style={{
           width: 300,
           width: 300,
         }}
         }}
+        data-cy="single"
+        maxHeight={180}
         renderOptionItem={renderOptionItem}
         renderOptionItem={renderOptionItem}
       />
       />
       <br />
       <br />
@@ -2751,6 +2768,8 @@ const RenderOptionDemo = () => {
         multiple
         multiple
         dropdownClassName="components-select-demo-renderOptionItem"
         dropdownClassName="components-select-demo-renderOptionItem"
         optionList={optionList}
         optionList={optionList}
+        maxHeight={180}
+        data-cy="multiple"
         style={{
         style={{
           width: 450,
           width: 450,
         }}
         }}

+ 12 - 3
packages/semi-ui/select/index.tsx

@@ -616,10 +616,19 @@ class Select extends BaseComponent<SelectProps, SelectState> {
                 return this.state.isFocusInContainer;
                 return this.state.isFocusInContainer;
             },
             },
             updateScrollTop: (index?: number) => {
             updateScrollTop: (index?: number) => {
-                let optionClassName = `.${prefixcls}-option-selected`;
-                if (index !== undefined) {
-                    optionClassName = `.${prefixcls}-option:nth-child(${index})`;
+                let optionClassName;
+                if ('renderOptionItem' in this.props) {
+                    optionClassName = `.${prefixcls}-option-custom-selected`;
+                    if (index !== undefined) {
+                        optionClassName = `.${prefixcls}-option-custom:nth-child(${index + 1})`;
+                    }
+                } else {
+                    optionClassName = `.${prefixcls}-option-selected`;
+                    if (index !== undefined) {
+                        optionClassName = `.${prefixcls}-option:nth-child(${index + 1})`;
+                    }
                 }
                 }
+
                 let destNode = document.querySelector(`#${prefixcls}-${this.selectOptionListID} ${optionClassName}`) as HTMLDivElement;
                 let destNode = document.querySelector(`#${prefixcls}-${this.selectOptionListID} ${optionClassName}`) as HTMLDivElement;
                 if (Array.isArray(destNode)) {
                 if (Array.isArray(destNode)) {
                     destNode = destNode[0];
                     destNode = destNode[0];

+ 7 - 1
packages/semi-ui/select/option.tsx

@@ -117,6 +117,12 @@ class Option extends PureComponent<OptionProps> {
 
 
         // Since there are empty, locale and other logic, the custom renderOptionItem is directly converged to the internal option instead of being placed in Select/index
         // Since there are empty, locale and other logic, the custom renderOptionItem is directly converged to the internal option instead of being placed in Select/index
         if (typeof renderOptionItem === 'function') {
         if (typeof renderOptionItem === 'function') {
+            const customRenderClassName = classNames(className,
+                {
+                    [`${prefixCls}-custom`]: true,
+                    [`${prefixCls}-custom-selected`]: selected
+                }
+            );
             return renderOptionItem({
             return renderOptionItem({
                 disabled,
                 disabled,
                 focused,
                 focused,
@@ -127,7 +133,7 @@ class Option extends PureComponent<OptionProps> {
                 inputValue,
                 inputValue,
                 onMouseEnter: (e: React.MouseEvent) => onMouseEnter(e),
                 onMouseEnter: (e: React.MouseEvent) => onMouseEnter(e),
                 onClick: (e: React.MouseEvent) => this.onClick({ value, label, children, ...rest }, e),
                 onClick: (e: React.MouseEvent) => this.onClick({ value, label, children, ...rest }, e),
-                className,
+                className: customRenderClassName,
                 ...rest
                 ...rest
             });
             });
         }
         }