DouyinFE / semi-design

🚀A modern, comprehensive, flexible design system and React UI library. 🎨 Provide more than 3000+ Design Tokens, easy to build your design system. Make Semi Design to Any Design. 🧑🏻‍💻 Design to Code in one click
https://semi.design
Other
8.39k stars 709 forks source link

[Select] renderOptionItem can't scrolll to active item when use keyboard arrow up and down #2263

Closed pointhalo closed 1 month ago

pointhalo commented 4 months ago

Is there an existing issue for this?

Which Component

Select

Semi Version

2.59.0

Current Behavior

20240529162450_rec_

Expected Behavior

No response

Steps To Reproduce

import React from 'react';
import { Select, Checkbox, Highlight } from '@douyinfe/semi-ui';

() => {
    const [inputValue, setInputValue] = useState('');
    const renderOptionItem = renderProps => {
        const {
            disabled,
            selected,
            label,
            value,
            focused,
            className,
            style,
            onMouseEnter,
            onClick,
            empty,
            emptyContent,
            ...rest
        } = renderProps;
        const optionCls = classNames({
            ['custom-option-render']: true,
            ['custom-option-render-focused']: focused,
            ['custom-option-render-disabled']: disabled,
            ['custom-option-render-selected']: selected,
        });
        const searchWords = [inputValue];

        // Notice:
        // 1.props传入的style需在wrapper dom上进行消费,否则在虚拟化场景下会无法正常使用
        // 2.选中(selected)、聚焦(focused)、禁用(disabled)等状态的样式需自行加上,你可以从props中获取到相对的boolean值
        // 3.onMouseEnter需在wrapper dom上绑定,否则上下键盘操作时显示会有问题

        return (
            <div style={style} className={optionCls} onClick={() => onClick()} onMouseEnter={e => onMouseEnter()}>
                <Checkbox checked={selected} />
                <div className="option-right">
                    <Highlight sourceString={label} searchWords={searchWords} />
                </div>
            </div>
        );
    };

    const optionList = [
        { value: 'abc', label: '抖音', otherKey: 0 },
        { value: 'ulikecam', label: '轻颜相机', disabled: true, otherKey: 1 },
        { value: 'jianying', label: '剪映', otherKey: 2 },
        { value: 'toutiao', label: '今日头条', otherKey: 3 },
        { value: 'toutiao4', label: '今日头条4', otherKey: 4 },
        { value: 'toutiao5', label: '今日头条5', otherKey: 5 },
        { value: 'toutiao6', label: '今日头条6', otherKey: 6 },
        { value: 'toutiao7', label: '今日头条7', otherKey: 7 },
        { value: 'toutiao8', label: '今日头条8', otherKey: 8 },
        { value: 'toutiao9', label: '今日头条9', otherKey: 9 },
    ];

    return (
        <>
            <Select
                filter
                placeholder="多选"
                multiple
                onSearch={(v) => setInputValue(v)}
                dropdownClassName="components-select-demo-renderOptionItem"
                optionList={optionList}
                style={{ width: 320 }}
                renderOptionItem={renderOptionItem}
            />
        </>
    );
};

ReproducibleCode

No response

Environment

- OS:
- browser:

Anything else?

问题原因,使用特定classname 来做滚动,renderOptionItem时未考虑这一点。 img_v3_02ba_29c46c6b-74b0-4809-b522-d6d9a13069ag

pointhalo commented 4 months ago

修改建议:

用户在使用 renderOptionItem 时,从props中获取 classname并将其绑定在自定义 dom的最外层上

icwoker commented 3 months ago

把option的render函数改成这样

if (typeof renderOptionItem === 'function') {
            return (
                <div className='semi-select-custom-option'>
                    {
                        renderOptionItem({
                            disabled,
                            focused,
                            selected,
                            style,
                            label,
                            value,
                            inputValue,
                            onMouseEnter: (e: React.MouseEvent) => onMouseEnter(e),
                            onClick: (e: React.MouseEvent) => this.onClick({ value, label, children, ...rest }, e),
                            className,
                            ...rest
                        })
                    }
                </div>
            );
        }

然后把index中的updateScrollTop函数改成这样

  updateScrollTop: (index?: number) => {
                let optionClassName = `.${prefixcls}-option-selected`;
                let renderLocateClassName = `.semi-select-custom-option`;
                if (index !== undefined) {
                    optionClassName = `.${prefixcls}-option:nth-child(${index})`;
                }
                let destNode = document.querySelector(`#${prefixcls}-${this.selectOptionListID} ${optionClassName}`) as HTMLDivElement;
                if (this.props.renderOptionItem) {
                    destNode = document.querySelectorAll(`${renderLocateClassName}`)[index ? index : 0] as HTMLDivElement;
                }
                if (Array.isArray(destNode)) {
                    destNode = destNode[0];
                }
                console.log(destNode);
                if (destNode) {
                    /**
                     * Scroll the first selected item into view.
                     * The reason why ScrollIntoView is not used here is that it may cause page to move.
                     */
                    const destParent = destNode.parentNode as HTMLDivElement;
                    destParent.scrollTop = destNode.offsetTop -
                        destParent.offsetTop -
                        (destParent.clientHeight / 2) +
                        (destNode.clientHeight / 2);
                }
            },

效果: QQ录屏20240618133438 00_00_00-00_00_30

这种改法可以吗?会不会影响之前的样式。

pointhalo commented 3 months ago

这种改法可以吗?会不会影响之前的样式。 @icwoker

你好,这个改法会对现有的用户造成影响(无论他是否需要这个滚动功能),在 renderOptionItem的情况下,它肯定是希望更彻底地掌控每个 Option的 Render结构的,否则它使用更简单的往 label传 reactNode也能做一些简单的定制渲染。所以额外多出一个 div wrapper不是很合适,可能会受到现在已经使用这个能力的用户的质疑。

这里我们倾向于将 classname在入参中传出去,并且强调,如果需要键盘滚动功能,那么它需要消费 renderOptionItem中传入的 classname,并且绑定到它的最外层容器上。

如果无需滚动功能的用户,它升级后无影响。 需要滚动功能的用户,毕竟它现在都是不work的,是一定需要改动的。那么升级后需要再改动一下它的用例代码即可,也是可以接受的。 这样可以把影响面降到最低。

+            const customRenderClassName = classNames(className,
+                {
+                    [`${prefixCls}-custom-option`]: true,
+                    [`${prefixCls}-custom-option-selected`]: selected
+                }
+            );

                   if (typeof renderOptionItem === 'function') {
                        return renderOptionItem({
                            disabled,
                            focused,
                            selected,
                            style,
                            label,
                            value,
                            inputValue,
                            onMouseEnter: (e: React.MouseEvent) => onMouseEnter(e),
                            onClick: (e: React.MouseEvent) => this.onClick({ value, label, children, ...rest }, e),
+                          className: customRenderClassName
                            ...rest
                        });
        }