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

Issue:
list onReachBottom无法触发
- [ ] 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.32.2
- **Browser:** chrome101.0.4951.54

## What is expected?
可以正常触发onReachBottom

## Steps to reproduce
由于scrollTop保留小数点后位数，而scrollHeight以及clientHeight只保留整数，所以scrollHeight - (scrollTop   clientHeight)可能为大于0小于1的小数，导致onReachBottom无法触发，参考[MDN Element.scrollHeight
](https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight)


<!-- generated by arco-issue. DO NOT REMOVE -->

Pull request description:
fix: List's onReachBottom not triggered sometimes due to element.scro…
…llTop is a non-rounded number

<!--
  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 -->
<!-- Only support choose one type, if there are multiple types, you can add the `Type` column in the Changelog. -->

- [ ] New feature
- [x] Bug fix
- [ ] Enhancement
- [ ] Documentation change
- [ ] Coding style change
- [ ] Component style change
- [ ] Refactoring
- [ ] Test cases
- [ ] Continuous integration
- [ ] Typescript definition change
- [ ] Breaking change
- [ ] Others 

## Background and context

View https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight#problems_and_solutions

## 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 |
| --------- | ------------- | ------------- | -------------- |
|  List    |   修复 `List` 组件 `onReachBottom` 偶现无法触发的 bug。     |   Fixed the bug that `List` component `onReachBottom` could not be triggered occasionally.  |      Close #850        |

<!-- If there are multiple types, you can add the `Type` column in the Changelog, the value of the column is the same as `Types of changes` -->
## 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)


Patch:
diff --git a/components/Anchor/anchor.tsx b/components/Anchor/anchor.tsx
index 03464eb5..7ad31c7b 100644
--- a/components/Anchor/anchor.tsx
+++ b/components/Anchor/anchor.tsx
@@ -181,6 +181,9 @@ function Anchor(baseProps: AnchorPropsWithChildren, ref) {
       const promises = actions.map(({ el, top }) => {
         return new Promise((resolve) => {
           if (!stopScroll) {
+            if (el === scrollContainer.current) {
+              stopScroll = true;
+            }
             const targetTop = top - offset;
             if (!animation) {
               // Manually trigger scrolling as browser's default action is prevented when `props.hash` is false
@@ -189,12 +192,8 @@ function Anchor(baseProps: AnchorPropsWithChildren, ref) {
               }
               return resolve(null);
             }
-            slide(el as HTMLElement, targetTop, resolve);
-          }
-          if (!stopScroll && el === scrollContainer.current) {
-            stopScroll = true;
+            return slide(el as HTMLElement, targetTop, resolve);
           }
-
           resolve(null);
         });
       });
diff --git a/components/ConfigProvider/interface.ts b/components/ConfigProvider/interface.ts
index 6faafde9..086d2fc8 100644
--- a/components/ConfigProvider/interface.ts
+++ b/components/ConfigProvider/interface.ts
@@ -55,7 +55,7 @@ import { SwitchProps } from '../Switch/interface';
 import { TableProps } from '../Table/interface';
 import { TabsProps } from '../Tabs/interface';
 import { TagProps } from '../Tag/interface';
-import { TimelineProps } from '../Timeline/interface';
+import { TimelineProps, TimelineItemProps } from '../Timeline/interface';
 import { PickerProps as TimePickerCommonProps } from '../TimePicker/interface';
 import { TooltipProps } from '../Tooltip/interface';
 import { TransferProps } from '../Transfer/interface';
@@ -140,6 +140,7 @@ export type ComponentConfig = {
   TriggerProps?: TriggerProps;
   Tag?: TagProps;
   Timeline?: TimelineProps;
+  'Timeline.Item'?: TimelineItemProps;
   TimePicker?: TimePickerCommonProps;
   Tooltip?: TooltipProps;
   Transfer?: TransferProps;
diff --git a/components/Dropdown/index.tsx b/components/Dropdown/index.tsx
index 1f4d6251..6ca24011 100644
--- a/components/Dropdown/index.tsx
+++ b/components/Dropdown/index.tsx
@@ -45,8 +45,8 @@ function Dropdown(baseProps: DropdownProps, _) {
 
   const changePopupVisible = (visible: boolean) => {
     setPopupVisible(visible);
-    onVisibleChange && onVisibleChange(visible);
-    triggerProps && triggerProps.onVisibleChange && triggerProps.onVisibleChange(visible);
+    onVisibleChange?.(visible);
+    triggerProps?.onVisibleChange?.(visible);
   };
 
   const handleVisibleChange = (visible: boolean) => {
@@ -71,15 +71,17 @@ function Dropdown(baseProps: DropdownProps, _) {
               returnValueOfOnClickMenuItem = content.props.onClickMenuItem(key, event);
             }
 
-            // Set focus to avoid onblur
-            const child = triggerRef.current && triggerRef.current.getRootElement();
-            child && child.focus && child.focus();
+            const triggerRootElement = triggerRef.current && triggerRef.current.getRootElement();
 
             // Trigger onVisibleChange. Outer component can determine whether to change the state based on the current visibility value.
             if (returnValueOfOnClickMenuItem instanceof Promise) {
-              returnValueOfOnClickMenuItem.finally(() => changePopupVisible(false));
+              returnValueOfOnClickMenuItem.finally(() => {
+                changePopupVisible(false);
+                triggerRootElement?.focus();
+              });
             } else if (returnValueOfOnClickMenuItem !== false) {
               changePopupVisible(false);
+              triggerRootElement?.focus();
             }
           },
         })
diff --git a/components/Form/__demo__/form-context.md b/components/Form/__demo__/form-context.md
new file mode 100644
index 00000000..f2f01ea0
--- /dev/null
+++ b/components/Form/__demo__/form-context.md
@@ -0,0 +1,93 @@
+---
+order: 22
+title:
+  zh-CN: 获取表单上下文
+  en-US: Get Form Context
+---
+
+## zh-CN
+
+在函数式组件里可以使用 `Form.useFormContext` 获取 Form 组件上下文，便于表单控件的封装。(version `2.33.0`)
+
+## en-US
+
+In functional components, you can use `Form.useFormContext` to get a form context. Facilitate the encapsulation of form controls. (version `2.33.0`)
+
+```js
+import { Form, Input, Button, Switch, InputNumber } from '@arco-design/web-react';
+
+const FormItem = Form.Item;
+
+function DemoButton () {
+  const { form, disabled } = Form.useFormContext();
+
+  return (
+    <Button
+      type="text"
+      disabled={disabled}
+      onClick={() => {
+        form.setFieldsValue({ name: 'admin', age: 11 });
+      }}
+    >
+      Fill Form
+    </Button>
+  )
+}
+
+function Demo() {
+  const [form] = Form.useForm();
+  const [disabled, setDisabled] = React.useState(false);
+
+  return (
+    <Form
+      form={form}
+      style={{ width: 600 }}
+      initialValues={{ name: 'admin' }}
+      disabled={disabled}
+      onValuesChange={(v, vs) => {
+        console.log(v, vs);
+      }}
+      onSubmit={(v) => {
+        console.log(v);
+      }}
+    >
+      <FormItem label="Disabled" disabled={false}>
+        <Switch onChange={setDisabled} ></Switch>
+      </FormItem>
+      <FormItem label="Username" field="name" rules={[{ required: true }]}>
+        <Input placeholder='please enter your username' />
+      </FormItem>
+      <FormItem
+        label='Age'
+        field="age"
+        rules={[{ required: true, type: 'number', min: 0, max: 99 }]}
+      >
+        <InputNumber placeholder='please enter your age' />
+      </FormItem>
+      <FormItem
+        wrapperCol={{
+          offset: 5,
+        }}
+      >
+        <Button type="primary" htmlType="submit" style={{ marginRight: 24 }}>
+          Submit
+        </Button>
+        <Button
+          style={{ marginRight: 24 }}
+          onClick={() => {
+            form.resetFields();
+          }}
+        >
+          Reset
+        </Button>
+        <DemoButton />
+      </FormItem>
+    </Form>
+  );
+}
+
+ReactDOM.render(
+  <Demo/>,
+  CONTAINER
+);
+```
diff --git a/components/Form/__demo__/useWatch.md b/components/Form/__demo__/useWatch.md
new file mode 100644
index 00000000..80cd1fc3
--- /dev/null
+++ b/components/Form/__demo__/useWatch.md
@@ -0,0 +1,51 @@
+---
+order: 21
+title:
+  zh-CN: useWatch
+  en-US: useWatch
+---
+
+## zh-CN
+
+可以通过 `Form.useWatch` 监听表单内部字段值的变动。（`2.33.0` 支持）
+
+
+## en-US
+
+You can use `Form.useWatch` to monitor the changes of field values inside the form. (`2.33.0` support)
+
+
+```js
+import { Form, Typography, Input, InputNumber } from '@arco-design/web-react';
+
+function Demo() {
+
+  const [form] = Form.useForm();
+  const name = Form.useWatch('name', form);
+  const age = Form.useWatch('age', form);
+
+  return (
+    <div>
+      <Form form={form} >
+        <Form.Item label="Name" field="name">
+          <Input  placeholder="enter name"/>
+        </Form.Item>
+
+        <Form.Item label="Age" field="age">
+          <InputNumber  placeholder="enter age"/>
+        </Form.Item>
+        <Form.Item label=" ">
+          <Typography.Text code>
+            Name: {name}; Age: {age }
+          </Typography.Text>
+        </Form.Item>
+      </Form>
+    </div>
+  );
+}
+
+ReactDOM.render(
+  <Demo />,
+  CONTAINER
+);
+```
diff --git a/components/Form/hooks/useContext.ts b/components/Form/hooks/useContext.ts
new file mode 100644
index 00000000..40d70007
--- /dev/null
+++ b/components/Form/hooks/useContext.ts
@@ -0,0 +1,14 @@
+import { useContext } from 'react';
+import { FormInstance } from '../interface';
+import { FormContext } from '../context';
+
+const useFormContext = (): { form: FormInstance; disabled: boolean } => {
+  const formCtx = useContext(FormContext);
+
+  return {
+    form: formCtx.store,
+    disabled: formCtx.disabled,
+  };
+};
+
+export default useFormContext;
diff --git a/components/Form/hooks/useWatch.ts b/components/Form/hooks/useWatch.ts
new file mode 100644
index 00000000..d1246728
--- /dev/null
+++ b/components/Form/hooks/useWatch.ts
@@ -0,0 +1,43 @@
+import { useState, useContext, useEffect } from 'react';
+import get from 'lodash/get';
+import { FormInstance } from '../interface';
+import { FormContext } from '../context';
+import warn from '../../_util/warning';
+
+const useWatch = (field: string, form?: FormInstance) => {
+  const formCtx = useContext(FormContext);
+
+  const formInstance = form || formCtx.store;
+
+  const [value, setValue] = useState(formInstance?.getFieldValue(field));
+
+  useEffect(() => {
+    if (!formInstance) {
+      warn(true, 'formInstance is not available');
+      return;
+    }
+    const { registerWatcher } = formInstance?.getInnerMethods(true);
+
+    const updateValue = () => {
+      setValue((value) => {
+        const newValue = get(formInstance.getFieldsValue(), field);
+        if (JSON.stringify(value) !== JSON.stringify(newValue)) {
+          return newValue;
+        }
+        return value;
+      });
+    };
+
+    updateValue();
+
+    const cancelWatch = registerWatcher && registerWatcher(updateValue);
+
+    return () => {
+      cancelWatch && cancelWatch();
+    };
+  }, []);
+
+  return value;
+};
+
+export default useWatch;
diff --git a/components/Form/index.tsx b/components/Form/index.tsx
index aab4950c..519727dd 100644
--- a/components/Form/index.tsx
+++ b/components/Form/index.tsx
@@ -6,6 +6,8 @@ import FormControl from './control';
 import FormList from './form-list';
 import FormProvider from './form-provider';
 import useForm from './useForm';
+import useWatch from './hooks/useWatch';
+import useFormContext from './hooks/useContext';
 
 export { FormInstance, FormProps, FormItemProps, FormProviderProps };
 
@@ -17,6 +19,8 @@ export interface FormComponent extends RefForm {
   Control: typeof FormControl;
   Provider: typeof FormProvider;
   useForm: typeof useForm;
+  useFormContext: typeof useFormContext;
+  useWatch: typeof useWatch;
 }
 
 const FormComp: FormComponent = Form as FormComponent;
@@ -30,4 +34,8 @@ FormComp.Control = FormControl;
 
 FormComp.useForm = useForm;
 
+FormComp.useFormContext = useFormContext;
+
+FormComp.useWatch = useWatch;
+
 export default FormComp;
diff --git a/components/Form/interface.tsx b/components/Form/interface.tsx
index 98bdcc7e..9400d152 100644
--- a/components/Form/interface.tsx
+++ b/components/Form/interface.tsx
@@ -500,6 +500,7 @@ export type InnerMethodsReturnType<
 > = Pick<
   Store<FormData, FieldValue, FieldKey>,
   | 'registerField'
+  | 'registerWatcher'
   | 'innerSetInitialValues'
   | 'innerSetInitialValue'
   | 'innerSetCallbacks'
diff --git a/components/Form/store.tsx b/components/Form/store.tsx
index c3d43735..3e37947b 100644
--- a/components/Form/store.tsx
+++ b/components/Form/store.tsx
@@ -45,6 +45,8 @@ class Store<
 > {
   private registerFields: Control<FormData, FieldValue, FieldKey>[] = [];
 
+  private registerWatchers: (() => void)[] = [];
+
   // 和formControl 的 touched属性不一样。 只要被改过的字段，这里就会存储。并且不会跟随formControl被卸载而清除。
   // reset 的时候清除
   private touchedFields: { [key: string]: unknown } = {};
@@ -62,6 +64,10 @@ class Store<
       const { onValuesChange } = this.callbacks;
       onValuesChange && onValuesChange(value, this.getFields());
     }
+
+    this.registerWatchers.forEach((item) => {
+      item();
+    });
   }
 
   private triggerTouchChange(value: Partial<FormData>) {
@@ -79,6 +85,14 @@ class Store<
     this.callbacks = values;
   };
 
+  public registerWatcher = (item) => {
+    this.registerWatchers.push(item);
+
+    return () => {
+      this.registerWatchers = this.registerWatchers.filter((x) => x !== item);
+    };
+  };
+
   // 收集所有control字段，并在组件卸载时移除
   public registerField = (item: Control<FormData, FieldValue, FieldKey>) => {
     this.registerFields.push(item);
diff --git a/components/Form/useForm.ts b/components/Form/useForm.ts
index cfafc99b..8d745fc8 100644
--- a/components/Form/useForm.ts
+++ b/components/Form/useForm.ts
@@ -8,6 +8,7 @@ export function getFormInstance<
   FieldKey extends KeyType = keyof FormData
 >(): FormInstance<FormData, FieldValue, FieldKey> {
   const store = new Store<FormData, FieldValue, FieldKey>();
+
   return {
     getFieldsValue: store.getFieldsValue,
     getFieldValue: store.getFieldValue,
@@ -28,6 +29,7 @@ export function getFormInstance<
       if (inner) {
         [
           'registerField',
+          'registerWatcher',
           'innerSetInitialValues',
           'innerSetInitialValue',
           'innerSetCallbacks',
diff --git a/components/List/index.tsx b/components/List/index.tsx
index 477baece..3b297d78 100644
--- a/components/List/index.tsx
+++ b/components/List/index.tsx
@@ -147,7 +147,9 @@ function List<T extends unknown = any>(baseProps: ListProps<T>, ref) {
       const { scrollTop, scrollHeight, clientHeight } = refScrollElement.current;
       const scrollBottom = scrollHeight - (scrollTop + clientHeight);
 
-      if (scrollBottom <= offsetBottom) {
+      // https://github.com/arco-design/arco-design/issues/850
+      // offsetBottom + 1: scrollTop is a non-rounded number, while scrollHeight and clientHeight are both rounded
+      if (Math.abs(scrollBottom) < offsetBottom + 1) {
         if (refCanTriggerReachBottom.current) {
           setPageCurrentForScroll(pageCurrentForScroll + 1);
           onReachBottom && onReachBottom(pageCurrentForScroll + 1);
diff --git a/components/Statistic/index.tsx b/components/Statistic/index.tsx
index e3f1b1db..ec8c89bc 100644
--- a/components/Statistic/index.tsx
+++ b/components/Statistic/index.tsx
@@ -126,13 +126,14 @@ function Statistic(baseProps: StatisticProps, ref) {
               value
             ) : (
               <span className={`${prefixCls}-value-int`}>
-                <span className={`${prefixCls}-value-prefix`}>{prefix}</span> {int}
+                <span className={`${prefixCls}-value-prefix`}>{prefix}</span>
+                {int}
               </span>
             )}
 
             {decimal !== undefined || suffix ? (
               <span className={`${prefixCls}-value-decimal`}>
-                {isNumber(Number(value)) && decimal !== undefined && `.${decimal}`}{' '}
+                {isNumber(Number(value)) && decimal !== undefined && `.${decimal}`}
                 {suffix && <span className={`${prefixCls}-value-suffix`}>{suffix}</span>}
               </span>
             ) : null}
diff --git a/components/Statistic/style/index.less b/components/Statistic/style/index.less
index 734f84d2..8adf225f 100644
--- a/components/Statistic/style/index.less
+++ b/components/Statistic/style/index.less
@@ -31,10 +31,12 @@
 
       &-suffix {
         font-size: @statistic-font-suffix-size;
+        margin-left: @statistic-margin-suffix-left;
       }
 
       &-prefix {
         font-size: @statistic-size-value-icon;
+        margin-right: @statistic-margin-prefix-right;
       }
     }
   }
diff --git a/components/Statistic/style/token.less b/components/Statistic/style/token.less
index d546e9f7..851e57a3 100644
--- a/components/Statistic/style/token.less
+++ b/components/Statistic/style/token.less
@@ -12,3 +12,5 @@
 @statistic-color-extra-text: var(~'@{arco-cssvars-prefix}-color-text-2');
 @statistic-size-value-icon: 14px;
 @statistic-font-suffix-size: 14px;
+@statistic-margin-prefix-right: @spacing-2;
+@statistic-margin-suffix-left: @spacing-2;
diff --git a/components/Table/table.tsx b/components/Table/table.tsx
index f5b843c1..c1119c26 100644
--- a/components/Table/table.tsx
+++ b/components/Table/table.tsx
@@ -670,7 +670,9 @@ function Table<T extends unknown>(baseProps: TableProps<T>, ref: React.Ref<Table
       const scrollBarWidth = getScrollBarWidth(scrollWrapper);
       if (scrollBarWidth) {
         scrollbarChanged.current = true;
-        wrapper.style.overflowY = 'scroll';
+        if (wrapper) {
+          wrapper.style.overflowY = 'scroll';
+        }
 
         if (refTableFoot.current) {
           refTableFoot.current.style.overflowY = 'scroll';
diff --git a/components/Timeline/item.tsx b/components/Timeline/item.tsx
index 314f0b9f..f9c8fb19 100644
--- a/components/Timeline/item.tsx
+++ b/components/Timeline/item.tsx
@@ -2,8 +2,21 @@ import React, { useEffect, useRef, useContext, RefObject } from 'react';
 import cs from '../_util/classNames';
 import { ConfigContext, ConfigProviderProps } from '../ConfigProvider';
 import { TimelineItemProps } from './interface';
+import useMergeProps from '../_util/hooks/useMergeProps';
+
+const defaultProps: TimelineItemProps = {
+  dotType: 'solid',
+  lineType: 'solid',
+};
+
+function Item(baseProps: TimelineItemProps, ref: RefObject<HTMLDivElement>) {
+  const { getPrefixCls, componentConfig } = useContext<ConfigProviderProps>(ConfigContext);
+  const props = useMergeProps<TimelineItemProps>(
+    baseProps,
+    defaultProps,
+    componentConfig?.['Timeline.Item']
+  );
 
-function Item(props: TimelineItemProps, ref: RefObject<HTMLDivElement>) {
   const {
     children,
     className,
@@ -12,8 +25,8 @@ function Item(props: TimelineItemProps, ref: RefObject<HTMLDivElement>) {
     position,
     dot,
     dotColor,
-    dotType = 'solid',
-    lineType = 'solid',
+    dotType,
+    lineType,
     lineColor,
     direction,
     labelPosition,
@@ -22,7 +35,6 @@ function Item(props: TimelineItemProps, ref: RefObject<HTMLDivElement>) {
   } = props;
 
   const dotRef = useRef<HTMLDivElement>();
-  const { getPrefixCls } = useContext<ConfigProviderProps>(ConfigContext);
 
   const prefixCls = getPrefixCls('timeline');
 
diff --git a/components/Upload/request.ts b/components/Upload/request.ts
index 18330e5e..1d5b13ab 100644
--- a/components/Upload/request.ts
+++ b/components/Upload/request.ts
@@ -55,10 +55,11 @@ const uploadRequest: UploadRequest = function (options: RequestOptions) {
     onSuccess(getBody(xhr));
   };
   const formData = new FormData();
-  formData.append(name || 'file', file);
   if (data) {
     Object.keys(data).map((key) => formData.append(key, data[key]));
   }
+  formData.append(name || 'file', file);
+
   xhr.open('post', action, true);
   if (withCredentials && 'withCredentials' in xhr) {
     xhr.withCredentials = true;


Test patch:
diff --git a/components/Form/__test__/__snapshots__/demo.test.ts.snap b/components/Form/__test__/__snapshots__/demo.test.ts.snap
index b68cb0dc..67a9a864 100644
--- a/components/Form/__test__/__snapshots__/demo.test.ts.snap
+++ b/components/Form/__test__/__snapshots__/demo.test.ts.snap
@@ -3528,6 +3528,254 @@ exports[`renders Form/demo/disabled.md correctly 1`] = `
 </div>
 `;
 
+exports[`renders Form/demo/form-context.md correctly 1`] = `
+<form
+  class="arco-form arco-form-horizontal arco-form-size-default"
+  style="width:600px"
+>
+  <div
+    class="arco-row arco-row-align-start arco-row-justify-start arco-form-item arco-form-layout-horizontal"
+  >
+    <div
+      class="arco-col arco-col-5 arco-form-label-item"
+    >
+      <label>
+         Disabled
+      </label>
+    </div>
+    <div
+      class="arco-col arco-col-19 arco-form-item-wrapper"
+    >
+      <div
+        class="arco-form-item-control-wrapper"
+      >
+        <div
+          class="arco-form-item-control"
+        >
+          <div
+            class="arco-form-item-control-children"
+          >
+            <button
+              aria-checked="false"
+              class="arco-switch arco-switch-type-circle"
+              role="switch"
+              type="button"
+            >
+              <div
+                class="arco-switch-dot"
+              />
+            </button>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+  <div
+    class="arco-row arco-row-align-start arco-row-justify-start arco-form-item arco-form-layout-horizontal"
+  >
+    <div
+      class="arco-col arco-col-5 arco-form-label-item"
+    >
+      <label
+        for="name_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>
+         Username
+      </label>
+    </div>
+    <div
+      class="arco-col arco-col-19 arco-form-item-wrapper"
+    >
+      <div
+        class="arco-form-item-control-wrapper"
+      >
+        <div
+          class="arco-form-item-control"
+          id="name"
+        >
+          <div
+            class="arco-form-item-control-children"
+          >
+            <input
+              class="arco-input arco-input-size-default"
+              id="name_input"
+              placeholder="please enter your username"
+              value="admin"
+            />
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+  <div
+    class="arco-row arco-row-align-start arco-row-justify-start arco-form-item arco-form-layout-horizontal"
+  >
+    <div
+      class="arco-col arco-col-5 arco-form-label-item"
+    >
+      <label
+        for="age_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>
+         Age
+      </label>
+    </div>
+    <div
+      class="arco-col arco-col-19 arco-form-item-wrapper"
+    >
+      <div
+        class="arco-form-item-control-wrapper"
+      >
+        <div
+          class="arco-form-item-control"
+          id="age"
+        >
+          <div
+            class="arco-form-item-control-children"
+          >
+            <div
+              class="arco-input-group-wrapper arco-input-group-wrapper-default arco-input-has-suffix arco-input-number arco-input-number-mode-embed arco-input-number-size-default"
+            >
+              <span
+                class="arco-input-group"
+              >
+                <span
+                  class="arco-input-inner-wrapper arco-input-inner-wrapper-default"
+                >
+                  <input
+                    aria-valuemax="Infinity"
+                    aria-valuemin="-Infinity"
+                    class="arco-input arco-input-size-default"
+                    id="age_input"
+                    placeholder="please enter your age"
+                    role="spinbutton"
+                    value=""
+                  />
+                  <span
+                    class="arco-input-group-suffix"
+                  >
+                    <div
+                      class="arco-input-number-step-layer"
+                    >
+                      <div
+                        class="arco-input-number-step-button"
+                      >
+                        <svg
+                          aria-hidden="true"
+                          class="arco-icon arco-icon-up"
+                          fill="none"
+                          focusable="false"
+                          stroke="currentColor"
+                          stroke-width="4"
+                          viewBox="0 0 48 48"
+                        >
+                          <path
+                            d="M39.6 30.557 24.043 15 8.487 30.557"
+                          />
+                        </svg>
+                      </div>
+                      <div
+                        class="arco-input-number-step-button"
+                      >
+                        <svg
+                          aria-hidden="true"
+                          class="arco-icon arco-icon-down"
+                          fill="none"
+                          focusable="false"
+                          stroke="currentColor"
+                          stroke-width="4"
+                          viewBox="0 0 48 48"
+                        >
+                          <path
+                            d="M39.6 17.443 24.043 33 8.487 17.443"
+                          />
+                        </svg>
+                      </div>
+                    </div>
+                  </span>
+                </span>
+              </span>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+  <div
+    class="arco-row arco-row-align-start arco-row-justify-start arco-form-item arco-form-layout-horizontal"
+  >
+    <div
+      class="arco-col arco-col-24 arco-col-offset-5 arco-form-item-wrapper"
+    >
+      <div
+        class="arco-form-item-control-wrapper"
+      >
+        <div
+          class="arco-form-item-control"
+        >
+          <div
+            class="arco-form-item-control-children"
+          >
+            <button
+              class="arco-btn arco-btn-primary arco-btn-size-default arco-btn-shape-square"
+              style="margin-right:24px"
+              type="submit"
+            >
+              <span>
+                Submit
+              </span>
+            </button>
+            <button
+              class="arco-btn arco-btn-secondary arco-btn-size-default arco-btn-shape-square"
+              style="margin-right:24px"
+              type="button"
+            >
+              <span>
+                Reset
+              </span>
+            </button>
+            <button
+              class="arco-btn arco-btn-text arco-btn-size-default arco-btn-shape-square"
+              type="button"
+            >
+              <span>
+                Fill Form
+              </span>
+            </button>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</form>
+`;
+
 exports[`renders Form/demo/form-provider.md correctly 1`] = `
 <div>
   <form
@@ -7451,6 +7699,178 @@ exports[`renders Form/demo/update.md correctly 1`] = `
 </div>
 `;
 
+exports[`renders Form/demo/useWatch.md correctly 1`] = `
+<div>
+  <form
+    class="arco-form arco-form-horizontal arco-form-size-default"
+  >
+    <div
+      class="arco-row arco-row-align-start arco-row-justify-start arco-form-item arco-form-layout-horizontal"
+    >
+      <div
+        class="arco-col arco-col-5 arco-form-label-item"
+      >
+        <label
+          for="name_input"
+        >
+           Name
+        </label>
+      </div>
+      <div
+        class="arco-col arco-col-19 arco-form-item-wrapper"
+      >
+        <div
+          class="arco-form-item-control-wrapper"
+        >
+          <div
+            class="arco-form-item-control"
+            id="name"
+          >
+            <div
+              class="arco-form-item-control-children"
+            >
+              <input
+                class="arco-input arco-input-size-default"
+                id="name_input"
+                placeholder="enter name"
+                value=""
+              />
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+    <div
+      class="arco-row arco-row-align-start arco-row-justify-start arco-form-item arco-form-layout-horizontal"
+    >
+      <div
+        class="arco-col arco-col-5 arco-form-label-item"
+      >
+        <label
+          for="age_input"
+        >
+           Age
+        </label>
+      </div>
+      <div
+        class="arco-col arco-col-19 arco-form-item-wrapper"
+      >
+        <div
+          class="arco-form-item-control-wrapper"
+        >
+          <div
+            class="arco-form-item-control"
+            id="age"
+          >
+            <div
+              class="arco-form-item-control-children"
+            >
+              <div
+                class="arco-input-group-wrapper arco-input-group-wrapper-default arco-input-has-suffix arco-input-number arco-input-number-mode-embed arco-input-number-size-default"
+              >
+                <span
+                  class="arco-input-group"
+                >
+                  <span
+                    class="arco-input-inner-wrapper arco-input-inner-wrapper-default"
+                  >
+                    <input
+                      aria-valuemax="Infinity"
+                      aria-valuemin="-Infinity"
+                      class="arco-input arco-input-size-default"
+                      id="age_input"
+                      placeholder="enter age"
+                      role="spinbutton"
+                      value=""
+                    />
+                    <span
+                      class="arco-input-group-suffix"
+                    >
+                      <div
+                        class="arco-input-number-step-layer"
+                      >
+                        <div
+                          class="arco-input-number-step-button"
+                        >
+                          <svg
+                            aria-hidden="true"
+                            class="arco-icon arco-icon-up"
+                            fill="none"
+                            focusable="false"
+                            stroke="currentColor"
+                            stroke-width="4"
+                            viewBox="0 0 48 48"
+                          >
+                            <path
+                              d="M39.6 30.557 24.043 15 8.487 30.557"
+                            />
+                          </svg>
+                        </div>
+                        <div
+                          class="arco-input-number-step-button"
+                        >
+                          <svg
+                            aria-hidden="true"
+                            class="arco-icon arco-icon-down"
+                            fill="none"
+                            focusable="false"
+                            stroke="currentColor"
+                            stroke-width="4"
+                            viewBox="0 0 48 48"
+                          >
+                            <path
+                              d="M39.6 17.443 24.043 33 8.487 17.443"
+                            />
+                          </svg>
+                        </div>
+                      </div>
+                    </span>
+                  </span>
+                </span>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+    <div
+      class="arco-row arco-row-align-start arco-row-justify-start arco-form-item arco-form-layout-horizontal"
+    >
+      <div
+        class="arco-col arco-col-5 arco-form-label-item"
+      >
+        <label>
+            
+        </label>
+      </div>
+      <div
+        class="arco-col arco-col-19 arco-form-item-wrapper"
+      >
+        <div
+          class="arco-form-item-control-wrapper"
+        >
+          <div
+            class="arco-form-item-control"
+          >
+            <div
+              class="arco-form-item-control-children"
+            >
+              <span
+                class="arco-typography"
+              >
+                <code>
+                  Name: ; Age: 
+                </code>
+              </span>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </form>
+</div>
+`;
+
 exports[`renders Form/demo/validate.md correctly 1`] = `
 <form
   class="arco-form arco-form-horizontal arco-form-size-default"
diff --git a/components/Form/__test__/use.test.tsx b/components/Form/__test__/use.test.tsx
new file mode 100644
index 00000000..4dc64811
--- /dev/null
+++ b/components/Form/__test__/use.test.tsx
@@ -0,0 +1,137 @@
+import React from 'react';
+import { mount } from 'enzyme';
+import { act } from 'react-test-renderer';
+import { Form, Input } from '../..';
+import { sleep } from '../../../tests/util';
+
+function Demo1() {
+  const [form] = Form.useForm();
+  const name = Form.useWatch('name', form);
+  const age = Form.useWatch('age', form);
+
+  return (
+    <div>
+      <Form form={form}>
+        <Form.Item label="Name" field="name">
+          <Input placeholder="enter name" />
+        </Form.Item>
+        <Form.Item label="age" field="age">
+          <Input placeholder="enter name" />
+        </Form.Item>
+        <span id="name">{name}</span>
+        <span id="age">{age}</span>
+      </Form>
+    </div>
+  );
+}
+
+function DemoText() {
+  const name = Form.useWatch('name');
+  const age = Form.useWatch('age');
+
+  return (
+    <span>
+      <span id="name">{name}</span>
+      <span id="age">{age}</span>
+    </span>
+  );
+}
+
+function Demo2() {
+  const [form] = Form.useForm();
+
+  return (
+    <div>
+      <Form form={form}>
+        <Form.Item label="Name" field="name">
+          <Input placeholder="enter name" />
+        </Form.Item>
+        <Form.Item label="age" field="age">
+          <Input placeholder="enter name" />
+        </Form.Item>
+        <DemoText />
+      </Form>
+    </div>
+  );
+}
+
+describe('Form.useWatch', () => {
+  it('form.useWatch ', async () => {
+    const wrapper = mount(<Demo1 />);
+
+    act(() => {
+      wrapper
+        .find('input')
+        .at(0)
+        .simulate('change', {
+          target: { value: 'aaa' },
+        });
+    });
+
+    await sleep(10);
+
+    wrapper.update();
+
+    expect(wrapper.find('#name').last().text()).toBe('aaa');
+
+    act(() => {
+      wrapper
+        .find('input')
+        .at(1)
+        .simulate('change', {
+          target: { value: 'bbb' },
+        });
+    });
+    await sleep(10);
+
+    expect(wrapper.find('#age').last().text()).toBe('bbb');
+  });
+
+  it('form.useWatch child ', async () => {
+    const wrapper = mount(<Demo2 />);
+
+    act(() => {
+      wrapper
+        .find('input')
+        .at(0)
+        .simulate('change', {
+          target: { value: 'aaa' },
+        });
+    });
+
+    await sleep(10);
+
+    expect(wrapper.find('#name').last().text()).toBe('aaa');
+
+    act(() => {
+      wrapper
+        .find('input')
+        .at(1)
+        .simulate('change', {
+          target: { value: 'bbb' },
+        });
+    });
+    await sleep(10);
+
+    expect(wrapper.find('#age').last().text()).toBe('bbb');
+  });
+});
+
+describe('Form.useFormContext', () => {
+  it('form useFormContext', async () => {
+    let form = null;
+    let formInstance;
+    function Child() {
+      const { form } = Form.useFormContext();
+      formInstance = form;
+      return <span />;
+    }
+    mount(
+      <Form ref={(node) => (form = node)}>
+        <Child />
+      </Form>
+    );
+
+    expect(formInstance).toBe(form);
+  });
+});
diff --git a/components/Statistic/__test__/__snapshots__/demo.test.ts.snap b/components/Statistic/__test__/__snapshots__/demo.test.ts.snap
index 9e647c80..11b502c7 100644
--- a/components/Statistic/__test__/__snapshots__/demo.test.ts.snap
+++ b/components/Statistic/__test__/__snapshots__/demo.test.ts.snap
@@ -23,7 +23,7 @@ exports[`renders Statistic/demo/basic.md correctly 1`] = `
           <span
             class="arco-statistic-value-prefix"
           />
-           125,670
+          125,670
         </span>
       </div>
     </div>
@@ -43,12 +43,12 @@ exports[`renders Statistic/demo/basic.md correctly 1`] = `
           <span
             class="arco-statistic-value-prefix"
           />
-           40,509
+          40,509
         </span>
         <span
           class="arco-statistic-value-decimal"
         >
-          .00 
+          .00
         </span>
       </div>
       <div
@@ -118,12 +118,12 @@ exports[`renders Statistic/demo/countUp.md correctly 1`] = `
                 />
               </svg>
             </span>
-             50
+            50
           </span>
           <span
             class="arco-statistic-value-decimal"
           >
-            .32 
+            .32
             <span
               class="arco-statistic-value-suffix"
             >
@@ -194,12 +194,12 @@ exports[`renders Statistic/demo/countUp.md correctly 1`] = `
                 />
               </svg>
             </span>
-             2
+            2
           </span>
           <span
             class="arco-statistic-value-decimal"
           >
-            .59 
+            .59
             <span
               class="arco-statistic-value-suffix"
             >
@@ -394,12 +394,11 @@ exports[`renders Statistic/demo/prefix_suffix.md correctly 1`] = `
           <span
             class="arco-statistic-value-prefix"
           />
-           192393
+          192393
         </span>
         <span
           class="arco-statistic-value-decimal"
         >
-           
           <span
             class="arco-statistic-value-suffix"
           >
@@ -458,12 +457,11 @@ exports[`renders Statistic/demo/prefix_suffix.md correctly 1`] = `
           <span
             class="arco-statistic-value-prefix"
           />
-           934230
+          934230
         </span>
         <span
           class="arco-statistic-value-decimal"
         >
-           
           <span
             class="arco-statistic-value-suffix"
           >
@@ -551,12 +549,12 @@ exports[`renders Statistic/demo/prefix_suffix.md correctly 1`] = `
               />
             </svg>
           </span>
-           50
+          50
         </span>
         <span
           class="arco-statistic-value-decimal"
         >
-          .32 
+          .32
           <span
             class="arco-statistic-value-suffix"
           >
@@ -590,7 +588,7 @@ exports[`renders Statistic/demo/time.md correctly 1`] = `
         <span
           class="arco-statistic-value-prefix"
         />
-         2019/04/10 12:16:53
+        2019/04/10 12:16:53
       </span>
     </div>
   </div>
diff --git a/components/Statistic/__test__/index.test.tsx b/components/Statistic/__test__/index.test.tsx
index 0b24bc78..250570c3 100644
--- a/components/Statistic/__test__/index.test.tsx
+++ b/components/Statistic/__test__/index.test.tsx
@@ -14,7 +14,7 @@ describe('Statistic', () => {
     const component = mount(<Statistic groupSeparator value={100000} />);
 
     expect(component.find('.arco-statistic-value > .arco-statistic-value-int').text()).toBe(
-      ' 100,000'
+      '100,000'
     );
 
     act(() => {
@@ -22,7 +22,7 @@ describe('Statistic', () => {
     });
 
     expect(component.find('.arco-statistic-value > .arco-statistic-value-int').text()).toBe(
-      ' 200,000'
+      '200,000'
     );
   });
 
@@ -30,10 +30,10 @@ describe('Statistic', () => {
     const component = mount(<Statistic title="Title" value={50} prefix="Prefix" suffix="%" />);
 
     expect(component.find('.arco-statistic-value > .arco-statistic-value-int').text()).toBe(
-      'Prefix 50'
+      'Prefix50'
     );
     expect(component.find('.arco-statistic-value > .arco-statistic-value-decimal').text()).toBe(
-      ' %'
+      '%'
     );
   });
 
@@ -43,10 +43,10 @@ describe('Statistic', () => {
     );
 
     expect(component.find('.arco-statistic-value > .arco-statistic-value-int').text()).toBe(
-      ' 1,000,000'
+      '1,000,000'
     );
     expect(component.find('.arco-statistic-value > .arco-statistic-value-decimal').text()).toBe(
-      '.00 $'
+      '.00$'
     );
   });
 
@@ -54,7 +54,7 @@ describe('Statistic', () => {
     const component = mount(<Statistic value={1554869813383} format="YYYY/MM/DD HH:mm:ss" />);
 
     expect(component.find('.arco-statistic-value > .arco-statistic-value-int').text()).toBe(
-      ' 2019/04/10 12:16:53'
+      '2019/04/10 12:16:53'
     );
   });
 
diff --git a/components/Timeline/__test__/__snapshots__/index.test.tsx.snap b/components/Timeline/__test__/__snapshots__/index.test.tsx.snap
index ff00768d..71670251 100644
--- a/components/Timeline/__test__/__snapshots__/index.test.tsx.snap
+++ b/components/Timeline/__test__/__snapshots__/index.test.tsx.snap
@@ -20,3 +20,81 @@ exports[`ConfigProvider componentConfig.Timeline set className globally and comp
   role="list"
 />
 `;
+
+exports[`ConfigProvider componentConfig.Timeline.Item default 1`] = `
+<div
+  class="arco-timeline-item"
+  role="listitem"
+>
+  <div
+    class="arco-timeline-item-dot-wrapper"
+  >
+    <div
+      class="arco-timeline-item-dot-line arco-timeline-item-dot-line-is-undefined"
+      style="border-left-style:solid"
+    />
+    <div
+      class="arco-timeline-item-dot-content"
+    >
+      <div
+        class="arco-timeline-item-dot arco-timeline-item-dot-solid"
+      />
+    </div>
+  </div>
+  <div
+    class="arco-timeline-item-content-wrapper"
+  />
+</div>
+`;
+
+exports[`ConfigProvider componentConfig.Timeline.Item set className globally 1`] = `
+<div
+  class="arco-timeline-item global-default-class"
+  role="listitem"
+>
+  <div
+    class="arco-timeline-item-dot-wrapper"
+  >
+    <div
+      class="arco-timeline-item-dot-line arco-timeline-item-dot-line-is-undefined"
+      style="border-left-style:solid"
+    />
+    <div
+      class="arco-timeline-item-dot-content"
+    >
+      <div
+        class="arco-timeline-item-dot arco-timeline-item-dot-solid"
+      />
+    </div>
+  </div>
+  <div
+    class="arco-timeline-item-content-wrapper"
+  />
+</div>
+`;
+
+exports[`ConfigProvider componentConfig.Timeline.Item set className globally and component set className itself. 1`] = `
+<div
+  class="arco-timeline-item component-class"
+  role="listitem"
+>
+  <div
+    class="arco-timeline-item-dot-wrapper"
+  >
+    <div
+      class="arco-timeline-item-dot-line arco-timeline-item-dot-line-is-undefined"
+      style="border-left-style:solid"
+    />
+    <div
+      class="arco-timeline-item-dot-content"
+    >
+      <div
+        class="arco-timeline-item-dot arco-timeline-item-dot-solid"
+      />
+    </div>
+  </div>
+  <div
+    class="arco-timeline-item-content-wrapper"
+  />
+</div>
+`;
diff --git a/components/Timeline/__test__/index.test.tsx b/components/Timeline/__test__/index.test.tsx
index 08bf8e19..ef6ea69e 100644
--- a/components/Timeline/__test__/index.test.tsx
+++ b/components/Timeline/__test__/index.test.tsx
@@ -8,6 +8,7 @@ const TimelineItem = Timeline.Item;
 
 mountTest(Timeline);
 componentConfigTest(Timeline, 'Timeline');
+componentConfigTest(Timeline.Item, 'Timeline.Item');
 
 let wrapper = null;