Repository: arco-design/arco-design (ts)

Issue:
Color transition of Icon in vertical Menu is not in sync with text
- [x] I'm sure this does not appear in [the issue list of the repository](https://github.com/arco-design/arco-design/issues)

## Basic Info 
- **Package Name And Version:** @arco-design/web-react: 2.22.0
- **System:** Mac OS
- **Browser:** Chrome 96
- **Reproduction Link:** https://arco.design/react/components/menu#%E7%BC%A9%E8%B5%B7%E5%86%85%E5%B5%8C%E8%8F%9C%E5%8D%95

## What is expected?
Color transition is in sync

## Steps to reproduce
1. Open "Navigation 2"
2. Click on "Navigation 2" > "Menu 1"
3. See the color transition of "Navigator 2". The icon becomes blue first. Then the text becomes blue.


<!-- generated by arco-issue. DO NOT REMOVE -->

Pull request description:
style: Menu icon color transition of selected items
<!--
  Thanks so much for your PR and contribution.

  Before submitting, please make sure to follow the Pull Request Guidelines: https://github.com/arco-design/arco-design/blob/main/CONTRIBUTING.md
-->

<!-- Put an `x` in "[ ]" to check a box) -->

## Types of changes

<!-- What types of changes does this PR introduce -->
- [ ] New feature
- [ ] Bug fix
- [ ] Documentation change
- [ ] Coding style change
- [x] Component style change
- [ ] Refactoring
- [ ] Performance improvement
- [ ] Test cases
- [ ] Continuous integration
- [ ] Typescript definition change
- [ ] Breaking change
- [ ] Others 

## Background and context

<!-- Explain what problem does the PR solve -->
<!-- Link to related open issues if applicable -->

## Solution

<!-- Describe how the problem is fixed in detail -->

## How is the change tested?

<!-- Unit tests should be added/updated for bug fixes and new features, if applicable -->
<!-- Please describe how you tested the change. E.g. Creating/updating unit tests or attaching a screenshot of how it works with your change -->

## Changelog

| Component | Changelog(CN) | Changelog(EN) | Related issues |
| --------- | ------------- | ------------- | -------------- |
| Menu | 修复 `Menu` 组件已选中菜单项中的 Icon 颜色变化没有过渡效果的问题。 |  Fix the problem that the color change of Icon in the selected menu item of the `Menu` component has no transition effect. |  Close #238    |

## Checklist:

- [ ] Test suite passes (`npm run test`)
- [x] Provide changelog for relevant changes (e.g. bug fixes and new features) if applicable.
- [x] Changes are submitted to the appropriate branch (e.g. features should be submitted to `feature` branch and others should be submitted to `main` branch)

## Other information

<!-- Please describe what other information that should be taken care of. E.g. describe the impact if introduce a breaking change -->


Patch:
diff --git a/components/Form/__demo__/validateLevel.md b/components/Form/__demo__/validateLevel.md
index eb34bf8a..5924f93d 100644
--- a/components/Form/__demo__/validateLevel.md
+++ b/components/Form/__demo__/validateLevel.md
@@ -28,28 +28,22 @@ function Demo() {
       form={form}
       style={{ width: 600 }}
     >
-
       <Form.Item
-        label="URL"
+        field="email"
+        label="Email"
+        rules={[
+          {
+            type: 'email',
+            validateLevel: 'warning',
+          },
+          {
+            required: true,
+            type: 'string',
+            minLength: 6,
+          },
+        ]}
       >
-        <Form.Item
-          field="url"
-          help="sd"
-          noStyle={{showErrorTip: true}}
-          rules={[
-            {
-              type: 'url',
-              validateLevel: 'warning',
-            },
-            {
-              required: true,
-              type: 'string',
-              minLength: 6,
-            },
-          ]}
-        >
         <Input placeholder="input placeholder" />
-        </Form.Item>
       </Form.Item>
       <FormItem
         label='Age'
@@ -75,13 +69,13 @@ function Demo() {
         </Button>
         <Button type="primary" onClick={async () => {
           try {
-            await form.validate(['name']);
-            Message.success('Username 校验通过')
+            await form.validate(['email']);
+            Message.success('Email 校验通过')
           } catch (e) {
-            Message.error('Username 校验失败')
+            Message.error('Email 校验失败')
           }
         }} style={{ marginRight: 24 }}>
-          Validate Username
+          Validate Email
         </Button>
         <Button
           style={{ marginRight: 24 }}
diff --git a/components/Form/control.tsx b/components/Form/control.tsx
index a377a5c3..c1dfd684 100644
--- a/components/Form/control.tsx
+++ b/components/Form/control.tsx
@@ -41,7 +41,7 @@ export default class Control<
 
   private errors: FieldError<FieldValue> = null;
 
-  private warnings: React.ReactNode[] = [];
+  private warnings: React.ReactNode = null;
 
   private isDestroyed = false;
 
@@ -75,7 +75,7 @@ export default class Control<
 
     // destroy errors
     const { updateFormItem } = this.context;
-    updateFormItem && updateFormItem(this.props.field as string, { errors: null, warnings: [] });
+    updateFormItem && updateFormItem(this.props.field as string, { errors: null, warnings: null });
     this.isDestroyed = true;
   }
 
@@ -149,7 +149,7 @@ export default class Control<
             this.touched = info.data.touched;
           }
           if (info.data && 'warnings' in info.data) {
-            this.warnings = [].concat(info.data.warnings);
+            this.warnings = info.data.warnings;
           }
           if (info.data && 'errors' in info.data) {
             this.errors = info.data.errors;
@@ -236,14 +236,14 @@ export default class Control<
     if (_rules && _rules.length && field) {
       return schemaValidate(field, value, _rules).then(({ error, warning }) => {
         this.errors = error ? error[field] : null;
-        this.warnings = warning || [];
+        this.warnings = warning || null;
         this.updateFormItem();
         return Promise.resolve({ error, value, field });
       });
     }
     if (this.errors) {
       this.errors = null;
-      this.warnings = [];
+      this.warnings = null;
       this.updateFormItem();
     }
     return Promise.resolve({ error: null, value, field });
diff --git a/components/Form/form-item.tsx b/components/Form/form-item.tsx
index 0903f5a5..0719008b 100644
--- a/components/Form/form-item.tsx
+++ b/components/Form/form-item.tsx
@@ -30,7 +30,7 @@ const Col = Grid.Col;
 
 interface FormItemTipProps extends Pick<FormItemProps, 'prefixCls' | 'help'> {
   errors: FieldError[];
-  warnings: ReactNode[][];
+  warnings: ReactNode[];
 }
 
 // 错误提示文字
@@ -46,15 +46,13 @@ const FormItemTip: React.FC<FormItemTipProps> = ({
     }
   });
   const warningTip = [];
-  warnings.map((items, i) => {
-    if (items && items.length) {
-      items.forEach((item) => {
-        warningTip.push(
-          <div key={i} className={`${prefixCls}-message-help_warning`}>
-            {item}
-          </div>
-        );
-      });
+  warnings.map((item, i) => {
+    if (item) {
+      warningTip.push(
+        <div key={i} className={`${prefixCls}-message-help_warning`}>
+          {item}
+        </div>
+      );
     }
   });
   const isHelpTip = !isUndefined(help) || !!warningTip.length;
@@ -96,7 +94,7 @@ const Item = <
     [key: string]: FieldError<FieldValue>;
   }>(null);
   const [warnings, setWarnings] = useState<{
-    [key: string]: ReactNode[];
+    [key: string]: ReactNode;
   }>(null);
   const formContext = useContext(FormContext);
   const prefixCls = formContext.prefixCls || getPrefixCls('form');
@@ -109,7 +107,7 @@ const Item = <
     field: string,
     params: {
       errors?: FieldError<FieldValue>;
-      warnings?: ReactNode[];
+      warnings?: ReactNode;
     } = {}
   ) => {
     if (isDestroyed.current) {
@@ -128,7 +126,7 @@ const Item = <
     });
     setWarnings((current) => {
       const newVal = { ...(current || {}) };
-      if (warnings && warnings.length) {
+      if (warnings) {
         newVal[field] = warnings;
       } else {
         delete newVal[field];
@@ -241,7 +239,7 @@ const Item = <
     return children;
   };
 
-  const FormItemContext = RawFormItemContext as unknown as RawFormItemContextType<
+  const FormItemContext = (RawFormItemContext as unknown) as RawFormItemContextType<
     FormData,
     FieldValue,
     FieldKey
diff --git a/components/Form/interface.tsx b/components/Form/interface.tsx
index ed47ab33..33fe6788 100644
--- a/components/Form/interface.tsx
+++ b/components/Form/interface.tsx
@@ -430,7 +430,7 @@ export type FormItemContextProps<
 > = FormContextProps<FormData, FieldValue, FieldKey> & {
   updateFormItem?: (
     field: string,
-    params: { errors?: FieldError<FieldValue>; warnings?: ReactNode[] }
+    params: { errors?: FieldError<FieldValue>; warnings?: ReactNode }
   ) => void;
 };
 
diff --git a/components/Form/store.tsx b/components/Form/store.tsx
index 5d446a5a..ca833395 100644
--- a/components/Form/store.tsx
+++ b/components/Form/store.tsx
@@ -45,7 +45,7 @@ export type StoreChangeInfo<T> = {
   };
   data?: {
     errors?: FieldError;
-    warnings?: string;
+    warnings?: React.ReactNode;
     touched?: boolean;
   };
 };
@@ -204,8 +204,8 @@ class Store<
     if (!field) return;
     const prev = cloneDeep(this.store);
     set(this.store, field, value);
-    this.triggerValuesChange({ [field]: value } as unknown as Partial<FormData>);
-    this.triggerTouchChange({ [field]: value } as unknown as Partial<FormData>);
+    this.triggerValuesChange(({ [field]: value } as unknown) as Partial<FormData>);
+    this.triggerTouchChange(({ [field]: value } as unknown) as Partial<FormData>);
 
     this.notify('innerSetValue', { prev, field, ...options, changeValues: { [field]: value } });
   };
@@ -252,14 +252,16 @@ class Store<
   };
 
   // 外部调用，设置多个表单控件的值，以及 error，touch 信息。
-  public setFields = (obj: {
-    [field in FieldKey]?: {
-      value?: FieldValue;
-      error?: FieldError<FieldValue>;
-      touched?: boolean;
-      warning?: string; // TODO
-    };
-  }) => {
+  public setFields = (
+    obj: {
+      [field in FieldKey]?: {
+        value?: FieldValue;
+        error?: FieldError<FieldValue>;
+        touched?: boolean;
+        warning?: React.ReactNode;
+      };
+    }
+  ) => {
     const fields = Object.keys(obj) as FieldKey[];
     const changeValues = {} as any;
     fields.forEach((field) => {
@@ -350,7 +352,7 @@ class Store<
     if (fields && isArray(fields)) {
       const changeValues = {} as any;
       fields.forEach((field) => {
-        set(this.store, field, this.initialValues[field as unknown as keyof FormData]);
+        set(this.store, field, this.initialValues[(field as unknown) as keyof FormData]);
         changeValues[field] = get(this.store, field);
       });
 
diff --git a/components/List/index.tsx b/components/List/index.tsx
index 3bbdd96c..530057e7 100644
--- a/components/List/index.tsx
+++ b/components/List/index.tsx
@@ -68,7 +68,7 @@ function List<T extends unknown = any>(baseProps: ListProps<T>, ref) {
 
   const refDom = useRef(null);
   const refVirtualList = useRef<VirtualListHandle>(null);
-  const refScrollList = useRef<HTMLDivElement>(null);
+  const refScrollElement = useRef<HTMLDivElement>(null);
   const refItemListWrapper = useRef<HTMLDivElement>(null);
   const refCanTriggerReachBottom = useRef(true);
 
@@ -95,7 +95,7 @@ function List<T extends unknown = any>(baseProps: ListProps<T>, ref) {
           const node = refItemListWrapper.current.children[index];
           node &&
             scrollIntoView(node as HTMLElement, {
-              boundary: refScrollList.current,
+              boundary: refScrollElement.current,
             });
         }
       },
@@ -139,11 +139,11 @@ function List<T extends unknown = any>(baseProps: ListProps<T>, ref) {
   const throttledScrollHandler = useCallback(
     throttle(() => {
       if (onListScroll) {
-        onListScroll(refScrollList.current);
+        onListScroll(refScrollElement.current);
         return;
       }
 
-      const { scrollTop, scrollHeight, clientHeight } = refScrollList.current;
+      const { scrollTop, scrollHeight, clientHeight } = refScrollElement.current;
       const scrollBottom = scrollHeight - (scrollTop + clientHeight);
 
       if (scrollBottom <= offsetBottom) {
@@ -242,6 +242,7 @@ function List<T extends unknown = any>(baseProps: ListProps<T>, ref) {
 
   const renderList = () => {
     const listItems = renderListItems();
+    const isVirtual = virtualListProps && Array.isArray(listItems);
     const paginationElement = pagination ? (
       <Pagination
         {...paginationProps}
@@ -261,7 +262,6 @@ function List<T extends unknown = any>(baseProps: ListProps<T>, ref) {
         className={cs(`${prefixCls}-wrapper`, wrapperClassName)}
       >
         <div
-          ref={refScrollList}
           style={style}
           className={cs(
             prefixCls,
@@ -273,16 +273,27 @@ function List<T extends unknown = any>(baseProps: ListProps<T>, ref) {
             },
             className
           )}
-          onScroll={needHandleScroll ? throttledScrollHandler : undefined}
+          ref={(ref) => {
+            if (!isVirtual) {
+              refScrollElement.current = ref;
+            }
+          }}
+          onScroll={!isVirtual && needHandleScroll ? throttledScrollHandler : undefined}
         >
           {header ? <div className={`${prefixCls}-header`}>{header}</div> : null}
 
-          {virtualListProps && Array.isArray(listItems) ? (
+          {isVirtual ? (
             <VirtualList
-              ref={refVirtualList}
+              ref={(ref) => {
+                if (ref) {
+                  refVirtualList.current = ref;
+                  refScrollElement.current = ref.dom as HTMLDivElement;
+                }
+              }}
               className={`${prefixCls}-content ${prefixCls}-virtual`}
               data={listItems}
               isStaticItemHeight={false}
+              onScroll={needHandleScroll ? throttledScrollHandler : undefined}
               {...virtualListProps}
             >
               {(child) => child}
diff --git a/components/Menu/style/index.less b/components/Menu/style/index.less
index d393f9d7..4f6535ad 100644
--- a/components/Menu/style/index.less
+++ b/components/Menu/style/index.less
@@ -12,6 +12,16 @@
   }
 }
 
+@keyframes ~'@{prefix}-menu-selected-item-label-enter' {
+  from {
+    opacity: 0;
+  }
+
+  to {
+    opacity: 1;
+  }
+}
+
 .@{menu-prefix-cls} {
   position: relative;
   box-sizing: border-box;
@@ -30,6 +40,10 @@
     width: @menu-item-indent-spacing;
   }
 
+  .text-color-transition() {
+    transition: color @transition-duration-2 @transition-timing-function-linear;
+  }
+
   // menu-item 一些与 theme 和 mode 无关的样式
   .item-base() {
     @style: {
@@ -43,8 +57,12 @@
       }
 
       &.@{menu-prefix-cls}-selected {
-        transition: color @transition-duration-2 @transition-timing-function-linear;
         font-weight: @menu-font-weight-item-selected;
+        .text-color-transition();
+
+        svg {
+          .text-color-transition();
+        }
       }
 
       .@{prefix}-icon {
@@ -164,7 +182,7 @@
         @item-selected-style: {
           &.@{menu-prefix-cls}-selected {
             background: none;
-            transition: color @transition-duration-2 @transition-timing-function-linear;
+            .text-color-transition();
 
             &:hover {
               background-color: ~'@{menu-@{theme}-color-bg-item_hover}';
@@ -297,6 +315,7 @@
     bottom: -@menu-horizontal-padding-vertical;
     height: @menu-height-label-item-selected;
     background-color: @menu-color-label-item-selected;
+    animation: ~'@{prefix}-menu-selected-item-label-enter' @transition-duration-2 @transition-timing-function-linear;
   }
 
   &-pop-button {
diff --git a/components/Select/README.en-US.md b/components/Select/README.en-US.md
index ab561fe8..42ed2aff 100644
--- a/components/Select/README.en-US.md
+++ b/components/Select/README.en-US.md
@@ -14,14 +14,14 @@ When users need to select one or more from a group of similar data, they can use
 
 |Property|Description|Type|DefaultValue|Version|
 |---|---|---|---|---|
-|defaultValue|To set default value|`string \| string[] \| number \| number[]`|`-`|-|
-|value|To set value|`string \| string[] \| number \| number[]`|`-`|-|
+|defaultValue|To set default value|`string \| string[] \| number \| number[] \| LabeledValue \| LabeledValue[]`|`-`|-|
+|value|To set value|`string \| string[] \| number \| number[] \| LabeledValue \| LabeledValue[]`|`-`|-|
 |inputValue|To set input value|`string`|`-`|-|
 |mode|Set mode of Select(**`tags` recommends using `mode: multiple; allowCreate: true` instead, this mode will be removed in the next major version**)|`'multiple' \| 'tags'`|`-`|-|
 |options|Select options|`(\| string\| number\| { label: ReactNode \| string; value: string \| number; disabled?: boolean; extra?: any })[]`|`-`|`extra` in 2.2.0|
 |labelInValue|Whether to embed label in value, turn the format of value from string to `{ value: string, label: ReactNode }`|`boolean`|`-`|-|
 |filterOption|If it's true, filter options by input value. If it's a function, filter options base on the function.|`boolean \| ((inputValue: string, option: ReactElement) => boolean)`|`true`|-|
-|renderFormat|Customize the content that will be displayed in the Select.If the `Option` corresponding to `value` does not exist, the first parameter will be `null`|`(option: OptionInfo \| null, value: string \| number) => ReactNode`|`-`|-|
+|renderFormat|Customize the content that will be displayed in the Select.If the `Option` corresponding to `value` does not exist, the first parameter will be `null`|`(option: OptionInfo \| null, value: string \| number \| LabeledValue) => ReactNode`|`-`|-|
 |defaultActiveFirstOption|Whether to highlight the first option by default|`boolean`|`true`|-|
 |unmountOnExit|Whether to destroy the DOM when hiding|`boolean`|`true`|-|
 |defaultPopupVisible|Whether to show dropdown by default.|`boolean`|`-`|2.14.0|
@@ -37,7 +37,7 @@ When users need to select one or more from a group of similar data, they can use
 |dropdownMenuClassName|The className of dropdown menu.|`string \| string[]`|`-`|-|
 |virtualListProps|Pass properties used by VirtualList.|`AvailableVirtualListProps`|`-`|2.1.0|
 |onChange|Callback when select an option or input value change.|`(value, option: OptionInfo \| OptionInfo[]) => void`|`-`|-|
-|onDeselect|Called when a option is deselected.Only called for `multiple` mode.|`(value: OptionProps['value'], option: OptionInfo) => void`|`-`|-|
+|onDeselect|Called when a option is deselected.Only called for `multiple` mode.|`(value: string \| number \| LabeledValue, option: OptionInfo) => void`|`-`|-|
 |onClear|Called when clear|`(visible: boolean) => void`|`-`|-|
 |onSearch|Callback when input changed|`(value: string, reason: InputValueChangeReason) => void`|`-`|-|
 |onFocus|Callback when get focus|`(e) => void`|`-`|-|
diff --git a/components/Select/README.zh-CN.md b/components/Select/README.zh-CN.md
index 62df683e..82bc6d9f 100644
--- a/components/Select/README.zh-CN.md
+++ b/components/Select/README.zh-CN.md
@@ -14,14 +14,14 @@
 
 |参数名|描述|类型|默认值|版本|
 |---|---|---|---|---|
-|defaultValue|选择框的默认值|`string \| string[] \| number \| number[]`|`-`|-|
-|value|选择器的值（受控模式）|`string \| string[] \| number \| number[]`|`-`|-|
+|defaultValue|选择框的默认值|`string \| string[] \| number \| number[] \| LabeledValue \| LabeledValue[]`|`-`|-|
+|value|选择器的值（受控模式）|`string \| string[] \| number \| number[] \| LabeledValue \| LabeledValue[]`|`-`|-|
 |inputValue|输入框的值（受控模式）|`string`|`-`|-|
 |mode|是否开启多选模式或标签模式 (**`tags` 推荐使用 `mode: multiple; allowCreate: true` 替代，下一大版本将移除此模式**)|`'multiple' \| 'tags'`|`-`|-|
 |options|指定可选项|`(\| string\| number\| { label: ReactNode \| string; value: string \| number; disabled?: boolean; extra?: any })[]`|`-`|`extra` in 2.2.0|
 |labelInValue|设置 `onChange` 回调中 `value` 的格式。默认是string，设置为true时候，value格式为： { label: string, value: string }|`boolean`|`-`|-|
 |filterOption|是否根据输入的值筛选数据。如果传入函数的话，接收 `inputValue` 和 `option` 两个参数，当option符合筛选条件时，返回 `true`，反之返回 `false`。|`boolean \| ((inputValue: string, option: ReactElement) => boolean)`|`true`|-|
-|renderFormat|定制回显内容。返回值将会显示在下拉框内。若 `value` 对应的 `Option` 不存在，则第一个参数是 null|`(option: OptionInfo \| null, value: string \| number) => ReactNode`|`-`|-|
+|renderFormat|定制回显内容。返回值将会显示在下拉框内。若 `value` 对应的 `Option` 不存在，则第一个参数是 null|`(option: OptionInfo \| null, value: string \| number \| LabeledValue) => ReactNode`|`-`|-|
 |defaultActiveFirstOption|是否默认高亮第一个选项|`boolean`|`true`|-|
 |unmountOnExit|是否在隐藏的时候销毁 DOM 结构|`boolean`|`true`|-|
 |defaultPopupVisible|下拉框是否默认打开。|`boolean`|`-`|2.14.0|
@@ -37,7 +37,7 @@
 |dropdownMenuClassName|下拉列表的类。|`string \| string[]`|`-`|-|
 |virtualListProps|传递虚拟滚动属性。|`AvailableVirtualListProps`|`-`|2.1.0|
 |onChange|点击选择框的回调|`(value, option: OptionInfo \| OptionInfo[]) => void`|`-`|-|
-|onDeselect|取消选中的时候触发的回调，(只在 `multiple` 模式下触发)。|`(value: OptionProps['value'], option: OptionInfo) => void`|`-`|-|
+|onDeselect|取消选中的时候触发的回调，(只在 `multiple` 模式下触发)。|`(value: string \| number \| LabeledValue, option: OptionInfo) => void`|`-`|-|
 |onClear|点击清除时触发，参数是当前下拉框的展开状态。|`(visible: boolean) => void`|`-`|-|
 |onSearch|搜索时的回调|`(value: string, reason: InputValueChangeReason) => void`|`-`|-|
 |onFocus|获得焦点时的回调|`(e) => void`|`-`|-|
diff --git a/components/Select/interface.tsx b/components/Select/interface.tsx
index f58dec5f..6be06d7e 100644
--- a/components/Select/interface.tsx
+++ b/components/Select/interface.tsx
@@ -19,6 +19,13 @@ export interface OptionInfo extends PropsWithChildren<OptionProps> {
   _origin: 'children' | 'options' | 'userCreatedOptions' | 'userCreatingOption';
 }
 
+export type LabeledValue = {
+  value: string | number;
+  label: ReactNode;
+};
+
+export type SelectInnerStateValue = string | number | string[] | number[];
+
 /**
  * @title Select
  */
@@ -27,12 +34,12 @@ export interface SelectProps extends SelectViewCommonProps {
    * @zh 选择框的默认值
    * @en To set default value
    */
-  defaultValue?: string | string[] | number | number[];
+  defaultValue?: string | string[] | number | number[] | LabeledValue | LabeledValue[];
   /**
    * @zh 选择器的值（受控模式）
    * @en To set value
    */
-  value?: string | string[] | number | number[];
+  value?: string | string[] | number | number[] | LabeledValue | LabeledValue[];
   /**
    * @zh 输入框的值（受控模式）
    * @en To set input value
@@ -71,7 +78,7 @@ export interface SelectProps extends SelectViewCommonProps {
    * Customize the content that will be displayed in the Select.
    * If the `Option` corresponding to `value` does not exist, the first parameter will be `null`
    */
-  renderFormat?: (option: OptionInfo | null, value: string | number) => ReactNode;
+  renderFormat?: (option: OptionInfo | null, value: string | number | LabeledValue) => ReactNode;
   /**
    * @zh 是否默认高亮第一个选项
    * @en Whether to highlight the first option by default
@@ -157,7 +164,7 @@ export interface SelectProps extends SelectViewCommonProps {
    * @zh 取消选中的时候触发的回调，(只在 `multiple` 模式下触发)。
    * @en Called when a option is deselected.Only called for `multiple` mode.
    */
-  onDeselect?: (value: OptionProps['value'], option: OptionInfo) => void;
+  onDeselect?: (value: string | number | LabeledValue, option: OptionInfo) => void;
   /**
    * @zh 点击清除时触发，参数是当前下拉框的展开状态。
    * @en Called when clear
diff --git a/components/Select/select.tsx b/components/Select/select.tsx
index 91947678..82e9c7f3 100644
--- a/components/Select/select.tsx
+++ b/components/Select/select.tsx
@@ -1,4 +1,5 @@
 import React, {
+  ReactText,
   useContext,
   useEffect,
   useImperativeHandle,
@@ -21,6 +22,8 @@ import {
   OptionInfo,
   InputValueChangeReason,
   SelectHandle,
+  SelectInnerStateValue,
+  LabeledValue,
 } from './interface';
 import SelectView, { SelectViewHandle } from '../_class/select-view';
 import VirtualList from '../_class/VirtualList';
@@ -101,7 +104,6 @@ function Select(baseProps: SelectProps, ref) {
   const prefixCls = getPrefixCls('select');
   const isMultipleMode = mode === 'multiple';
 
-  // state
   // TODO: 统一 useMergeValue 函数的表现
   const [stateValue, setValue] = useState(
     getValidValue(props.defaultValue, isMultipleMode, labelInValue)
@@ -267,29 +269,6 @@ function Select(baseProps: SelectProps, ref) {
     }
   }, [inputValue]);
 
-  const triggerOnChangeCallback = (
-    value: SelectProps['value'],
-    option: OptionInfo | Array<OptionInfo>
-  ) => {
-    if (onChange) {
-      if (labelInValue && !isEmptyValue(value, isMultipleMode)) {
-        if (Array.isArray(value)) {
-          onChange(
-            (value as any).map((v, index) => ({
-              value: v,
-              label: (option as Array<OptionInfo>)[index]?.children,
-            })),
-            option
-          );
-        } else {
-          onChange({ value, label: (option as OptionInfo)?.children }, option);
-        }
-      } else {
-        onChange(value, option);
-      }
-    }
-  };
-
   const getOptionInfoByValue = (value: OptionProps['value']): OptionInfo => {
     const option = optionInfoMap.get(value);
     if (option) {
@@ -328,16 +307,43 @@ function Select(baseProps: SelectProps, ref) {
     ];
   };
 
+  // Object should be returned when labelInValue is true
+  const getValueForCallbackParameter = (
+    value: SelectInnerStateValue,
+    option: OptionInfo | Array<OptionInfo>,
+    isEmpty = isEmptyValue(value, isMultipleMode)
+  ): SelectProps['value'] => {
+    if (labelInValue && !isEmpty) {
+      if (Array.isArray(value)) {
+        return value.map((optionValue, index) => ({
+          value: optionValue,
+          label: (option as OptionInfo[])[index]?.children,
+        }));
+      }
+      return { value, label: (option as OptionInfo)?.children };
+    }
+    return value;
+  };
+
+  const tryUpdateSelectValue = (value: SelectInnerStateValue) => {
+    setValue(value);
+    if (onChange) {
+      const option =
+        value === undefined
+          ? undefined
+          : Array.isArray(value)
+          ? value.map(getOptionInfoByValue)
+          : getOptionInfoByValue(value);
+      onChange(getValueForCallbackParameter(value, option), option);
+    }
+  };
+
   // 多选时，选择一个选项
   const checkOption = (valueToAdd) => {
     const option = optionInfoMap.get(valueToAdd);
     if (option) {
       const newValue = (value as string[]).concat(valueToAdd);
-      setValue(newValue);
-      triggerOnChangeCallback(
-        newValue,
-        newValue.map((item) => getOptionInfoByValue(item))
-      );
+      tryUpdateSelectValue(newValue);
     }
   };
 
@@ -346,12 +352,14 @@ function Select(baseProps: SelectProps, ref) {
     // 取消选中时不需要检查option是否存在，因为可能已被外部剔除了此选项
     const option = getOptionInfoByValue(valueToRemove);
     const newValue = (value as string[]).filter((v) => v !== valueToRemove);
-    setValue(newValue);
-    triggerOnChangeCallback(
-      newValue,
-      newValue.map((item) => getOptionInfoByValue(item))
-    );
-    onDeselect && onDeselect(valueToRemove, option);
+    tryUpdateSelectValue(newValue);
+
+    if (onDeselect) {
+      onDeselect(
+        getValueForCallbackParameter(valueToRemove, option, false) as ReactText | LabeledValue,
+        option
+      );
+    }
   };
 
   const handleOptionClick = (optionValue: OptionProps['value'], disabled: boolean) => {
@@ -370,8 +378,7 @@ function Select(baseProps: SelectProps, ref) {
       }
     } else {
       if (optionValue !== value) {
-        setValue(optionValue);
-        triggerOnChangeCallback(optionValue, getOptionInfoByValue(optionValue));
+        tryUpdateSelectValue(optionValue);
       }
       setTimeout(() => {
         tryUpdatePopupVisible(false);
@@ -527,11 +534,7 @@ function Select(baseProps: SelectProps, ref) {
         });
 
         if (needUpdate) {
-          setValue(newValue);
-          triggerOnChangeCallback(
-            newValue,
-            newValue.map((item) => getOptionInfoByValue(item))
-          );
+          tryUpdateSelectValue(newValue);
         }
 
         hasSeparator = true;
@@ -601,14 +604,9 @@ function Select(baseProps: SelectProps, ref) {
           const item = optionInfoMap.get(v);
           return item && item.disabled;
         });
-        setValue(newValue);
-        triggerOnChangeCallback(
-          newValue,
-          newValue.map((v) => getOptionInfoByValue(v))
-        );
+        tryUpdateSelectValue(newValue);
       } else {
-        setValue(undefined);
-        triggerOnChangeCallback(undefined, undefined);
+        tryUpdateSelectValue(undefined);
       }
       tryUpdateInputValue('', 'manual');
       onClear && onClear(popupVisible);
@@ -666,7 +664,10 @@ function Select(baseProps: SelectProps, ref) {
               const option = getOptionInfoByValue(value);
               let text = value;
               if (isFunction(renderFormat)) {
-                text = (renderFormat as Function)(option || null, value);
+                text = renderFormat(
+                  option || null,
+                  getValueForCallbackParameter(value, option) as ReactText | LabeledValue
+                );
               } else if (option) {
                 text = option.children;
               } else if (labelInValue && typeof props.value === 'object') {
diff --git a/components/Select/utils.tsx b/components/Select/utils.tsx
index 5413c296..8376b545 100644
--- a/components/Select/utils.tsx
+++ b/components/Select/utils.tsx
@@ -1,7 +1,13 @@
 import React, { ReactElement } from 'react';
 import get from 'lodash/get';
 import Option from './option';
-import { OptionInfo, OptionProps, OptionsType, SelectProps } from './interface';
+import {
+  SelectInnerStateValue,
+  OptionInfo,
+  OptionProps,
+  OptionsType,
+  SelectProps,
+} from './interface';
 import { isArray, isString, isNumber, isObject } from '../_util/is';
 import getHighlightText from '../_util/getHighlightText';
 
@@ -16,7 +22,7 @@ function isEmptyValue(value, isMultiple: boolean) {
   return isMultiple ? !isArray(value) || !value.length : value === undefined;
 }
 
-function getValidValue(value, isMultiple: boolean, labelInValue: boolean): SelectProps['value'] {
+function getValidValue(value, isMultiple: boolean, labelInValue: boolean): SelectInnerStateValue {
   // Compatible when labelInValue is set, value is passed in the object
   if (labelInValue) {
     if (isMultiple) {
diff --git a/components/_class/VirtualList/index.tsx b/components/_class/VirtualList/index.tsx
index 18eb89ab..0ad2316c 100644
--- a/components/_class/VirtualList/index.tsx
+++ b/components/_class/VirtualList/index.tsx
@@ -72,6 +72,7 @@ interface VirtualListState {
 }
 
 export type VirtualListHandle = {
+  dom: HTMLElement;
   scrollTo: (
     arg:
       | number
@@ -459,6 +460,7 @@ const VirtualList: React.ForwardRefExoticComponent<
   useImperativeHandle<any, VirtualListHandle>(
     ref,
     () => ({
+      dom: refList.current,
       // Scroll to a certain height or an element
       scrollTo: (arg) => {
         refRafId.current && caf(refRafId.current);
diff --git a/stories/components/List.jsx b/stories/components/List.jsx
index 6bcb03b0..4e630211 100644
--- a/stories/components/List.jsx
+++ b/stories/components/List.jsx
@@ -113,6 +113,7 @@ function Demo() {
             </List.Item>
           );
         }}
+        onReachBottom={() => console.log('list reach bottom')}
       />
     </div>
   );


Test patch:
diff --git a/components/Form/__test__/__snapshots__/demo.test.ts.snap b/components/Form/__test__/__snapshots__/demo.test.ts.snap
index b1d38eca..0aad05b1 100644
--- a/components/Form/__test__/__snapshots__/demo.test.ts.snap
+++ b/components/Form/__test__/__snapshots__/demo.test.ts.snap
@@ -7487,8 +7487,24 @@ exports[`renders Form/demo/validateLevel.md correctly 1`] = `
     <div
       class="arco-col arco-col-5 arco-form-label-item"
     >
-      <label>
-         URL
+      <label
+        for="email_input"
+      >
+        <strong
+          class="arco-form-item-symbol"
+        >
+          <svg
+            fill="currentColor"
+            height="1em"
+            viewBox="0 0 1024 1024"
+            width="1em"
+          >
+            <path
+              d="M583.338667 17.066667c18.773333 0 34.133333 15.36 34.133333 34.133333v349.013333l313.344-101.888a34.133333 34.133333 0 0 1 43.008 22.016l42.154667 129.706667a34.133333 34.133333 0 0 1-21.845334 43.178667l-315.733333 102.4 208.896 287.744a34.133333 34.133333 0 0 1-7.509333 47.786666l-110.421334 80.213334a34.133333 34.133333 0 0 1-47.786666-7.509334L505.685333 706.218667 288.426667 1005.226667a34.133333 34.133333 0 0 1-47.786667 7.509333l-110.421333-80.213333a34.133333 34.133333 0 0 1-7.509334-47.786667l214.186667-295.253333L29.013333 489.813333a34.133333 34.133333 0 0 1-22.016-43.008l42.154667-129.877333a34.133333 34.133333 0 0 1 43.008-22.016l320.512 104.106667L412.672 51.2c0-18.773333 15.36-34.133333 34.133333-34.133333h136.533334z"
+            />
+          </svg>
+        </strong>
+         Email
       </label>
     </div>
     <div
@@ -7499,13 +7515,14 @@ exports[`renders Form/demo/validateLevel.md correctly 1`] = `
       >
         <div
           class="arco-form-item-control"
+          id="email"
         >
           <div
             class="arco-form-item-control-children"
           >
             <input
               class="arco-input arco-input-size-default"
-              id="url_input"
+              id="email_input"
               placeholder="input placeholder"
               value=""
             />
@@ -7647,7 +7664,7 @@ exports[`renders Form/demo/validateLevel.md correctly 1`] = `
               type="button"
             >
               <span>
-                Validate Username
+                Validate Email
               </span>
             </button>
             <button