Repository: ant-design/ant-design-mobile
Pull request #5834 (2022-11-25 07:20:54)

chore: master merge feature


Patch:
diff --git a/.github/workflows/pr-check-ci.yml b/.github/workflows/pr-check-ci.yml
new file mode 100644
index 000000000..2dc133c0d
--- /dev/null
+++ b/.github/workflows/pr-check-ci.yml
@@ -0,0 +1,31 @@
+# Used to merge master/feature branches with each other
+name: PR Check CI
+
+on:
+  schedule:
+    - cron: '*/5 * * * *'
+
+permissions:
+  contents: read
+
+jobs:
+  pr-check-ci:
+    permissions:
+      checks: read # for actions-cool/check-pr-ci to get check reference
+      contents: write # for actions-cool/check-pr-ci to merge PRs
+      issues: write # for actions-cool/check-pr-ci to update issues
+      pull-requests: write # for actions-cool/check-pr-ci to update PRs
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions-cool/check-pr-ci@v1
+        with:
+          filter-label: 'BranchAutoMerge'
+          filter-creator-authority: 'write'
+          filter-head-ref: 'master, feature, next, master-merge-feature, feature-merge-master, next-merge-master, next-merge-feature'
+          filter-support-fork: false
+          skip-run-names: 'deploy preview, pr-check-ci, build preview failed, suggest-related-links'
+          conflict-review-body: '😅 This branch has conflicts that must be resolved!'
+          success-review: true
+          success-merge: true
+          merge-method: 'merge'
+          merge-title: 'chore: auto merge branches (#${number})'
diff --git a/.prettierrc.js b/.prettierrc.js
index 7fbbb5b4d..47fdc02f3 100644
--- a/.prettierrc.js
+++ b/.prettierrc.js
@@ -7,7 +7,7 @@ module.exports = {
   jsxSingleQuote: true,
   quoteProps: 'preserve',
   arrowParens: 'avoid',
-  proseWrap: 'preserve',
+  proseWrap: 'never',
   overrides: [
     {
       'files': ['*.md'],
diff --git a/src/components/calendar/calendar.en.md b/src/components/calendar/calendar.en.md
index 09e353459..a4f153c24 100644
--- a/src/components/calendar/calendar.en.md
+++ b/src/components/calendar/calendar.en.md
@@ -14,29 +14,31 @@ When the user needs to enter a date, he can select it in the pop-up date panel.
 
 <code src="./demos/demo3.tsx"></code>
 
+<code src="./demos/demo4.tsx"></code>
+
 ## Calendar
 
 ### Props
 
-| Name              | Description                                                                     | Type                                                                                                                                 | Default    |
-| ----------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | ---------- | --- | --- |
-| allowClear        | Whether to allow clearing after another click.                                  | `boolean`                                                                                                                            | `true`     |
-| defaultValue      | The default selected date or date range.                                        | Same as `value` prop.                                                                                                                | -          |
-| max               | Maximum value of a selectable range.                                            | `Date`                                                                                                                               | -          |
-| min               | Minimum value of a selectable range.                                            | `Date`                                                                                                                               | -          |     | -   |
-| maxPage           | Maximum visible page of date.                                                   | `Page`                                                                                                                               |
-| minPage           | Minimum visible page of date.                                                   | `Page`                                                                                                                               | -          |
-| nextMonthButton   | Contents of the Next Month button on the navigation pane                        | `React.ReactNode`                                                                                                                    | `>`        |
-| nextYearButton    | Contents of the next Year button on the navigation pane                         | `React.ReactNode`                                                                                                                    | `>>`       |
-| onChange          | Trigger when selected date changes.                                             | `(val: Date \| null) => void` when selection mode is "single". `(val: [Date, Date] \| null) => void` when selection mode is "range". | -          |
-| onPageChange      | Trigger when changed year or month.                                             | `(year: number, month: number) => void`                                                                                              | -          |
-| prevMonthButton   | Contents of the Last Month button on the navigation pane                        | `React.ReactNode`                                                                                                                    | `<`        |
-| prevYearButton    | Contents of the Last year button on the navigation pane                         | `React.ReactNode`                                                                                                                    | `<<`       |
-| renderLabel       | The label render function.                                                      | `(date: Date) => ReactNode \| null \| undefined`                                                                                     | -          |
-| selectionMode     | The selection mode. Disable selection when this prop is not set.                | `'single' \| 'range'`                                                                                                                | -          |
-| shouldDisableDate | Set whether the date is disable selection. The min and max Settings are ignored | `(date: Date) => boolean`                                                                                                            | -          |
-| value             | The selected date or date range.                                                | `Date \| null` when selection mode is "single". `[Date, Date] \| null` when selection mode is "range"                                | -          |
-| weekStartsOn      | Week starts on which day.                                                       | `'Monday' \| 'Sunday'`                                                                                                               | `'Sunday'` |
+| Name | Description | Type | Default |
+| --- | --- | --- | --- | --- | --- |
+| allowClear | Whether to allow clearing after another click. | `boolean` | `true` |
+| defaultValue | The default selected date or date range. | Same as `value` prop. | - |
+| max | Maximum value of a selectable range. | `Date` | - |
+| min | Minimum value of a selectable range. | `Date` | - |  | - |
+| maxPage | Maximum visible page of date. | `Page` |
+| minPage | Minimum visible page of date. | `Page` | - |
+| nextMonthButton | Contents of the Next Month button on the navigation pane | `React.ReactNode` | `>` |
+| nextYearButton | Contents of the next Year button on the navigation pane | `React.ReactNode` | `>>` |
+| onChange | Trigger when selected date changes. | `(val: Date \| null) => void` when selection mode is "single". `(val: [Date, Date] \| null) => void` when selection mode is "range". | - |
+| onPageChange | Trigger when changed year or month. | `(year: number, month: number) => void` | - |
+| prevMonthButton | Contents of the Last Month button on the navigation pane | `React.ReactNode` | `<` |
+| prevYearButton | Contents of the Last year button on the navigation pane | `React.ReactNode` | `<<` |
+| renderLabel | The label render function. | `(date: Date) => ReactNode \| null \| undefined` | - |
+| selectionMode | The selection mode. Disable selection when this prop is not set. | `'single' \| 'range'` | - |
+| shouldDisableDate | Set whether the date is disable selection. The min and max Settings are ignored | `(date: Date) => boolean` | - |
+| value | The selected date or date range. | `Date \| null` when selection mode is "single". `[Date, Date] \| null` when selection mode is "range" | - |
+| weekStartsOn | Week starts on which day. | `'Monday' \| 'Sunday'` | `'Sunday'` |
 
 ### CSS Variables
 
@@ -44,10 +46,10 @@ Not supported yet.
 
 ### Ref
 
-| Name        | Description            | Type                                             |
-| ----------- | ---------------------- | ------------------------------------------------ |
-| jumpTo      | Jump to specified page | `(page: Page \| ((page: Page) => Page)) => void` |
-| jumpToToday | Jump to today's page   | `() => void`                                     |
+| Name | Description | Type |
+| --- | --- | --- |
+| jumpTo | Jump to specified page | `(page: Page \| ((page: Page) => Page)) => void` |
+| jumpToToday | Jump to today's page | `() => void` |
 
 ```ts
 type Page = { month: number; year: number }
diff --git a/src/components/calendar/calendar.tsx b/src/components/calendar/calendar.tsx
index 5963156e4..f9f361b11 100644
--- a/src/components/calendar/calendar.tsx
+++ b/src/components/calendar/calendar.tsx
@@ -216,6 +216,8 @@ export const Calendar = forwardRef<CalendarRef, CalendarProps>((p, ref) => {
       let isSelect = false
       let isBegin = false
       let isEnd = false
+      let isSelectRowBegin = false
+      let isSelectRowEnd = false
       if (dateRange) {
         const [begin, end] = dateRange
         isBegin = d.isSame(begin, 'day')
@@ -224,6 +226,14 @@ export const Calendar = forwardRef<CalendarRef, CalendarProps>((p, ref) => {
           isBegin ||
           isEnd ||
           (d.isAfter(begin, 'day') && d.isBefore(end, 'day'))
+        if (isSelect) {
+          isSelectRowBegin =
+            (cells.length % 7 === 0 || d.isSame(d.startOf('month'), 'day')) &&
+            !isBegin
+          isSelectRowEnd =
+            (cells.length % 7 === 6 || d.isSame(d.endOf('month'), 'day')) &&
+            !isEnd
+        }
       }
       const inThisMonth = d.month() === current.month()
       const disabled = props.shouldDisableDate
@@ -241,6 +251,8 @@ export const Calendar = forwardRef<CalendarRef, CalendarProps>((p, ref) => {
               [`${classPrefix}-cell-selected`]: isSelect,
               [`${classPrefix}-cell-selected-begin`]: isBegin,
               [`${classPrefix}-cell-selected-end`]: isEnd,
+              [`${classPrefix}-cell-selected-row-begin`]: isSelectRowBegin,
+              [`${classPrefix}-cell-selected-row-end`]: isSelectRowEnd,
             }
           )}
           onClick={() => {
diff --git a/src/components/calendar/calendar.zh.md b/src/components/calendar/calendar.zh.md
index abf74aaa9..774ac645d 100644
--- a/src/components/calendar/calendar.zh.md
+++ b/src/components/calendar/calendar.zh.md
@@ -8,33 +8,31 @@
 
 ## 示例
 
-<code src="./demos/demo1.tsx"></code>
-<code src="./demos/demo2.tsx"></code>
-<code src="./demos/demo3.tsx"></code>
+<code src="./demos/demo1.tsx"></code> <code src="./demos/demo2.tsx"></code> <code src="./demos/demo3.tsx"></code> <code src="./demos/demo4.tsx"></code>
 
 ## Calendar
 
 ### 属性
 
-| 属性              | 说明                                           | 类型                                                                                           | 默认值     |
-| ----------------- | ---------------------------------------------- | ---------------------------------------------------------------------------------------------- | ---------- |
-| allowClear        | 是否允许再次点击后清除                         | `boolean`                                                                                      | `true`     |
-| defaultValue      | 默认选择的日期                                 | 同 `value` 属性                                                                                | -          |
-| max               | 可选择范围的最大值                             | `Date`                                                                                         | -          |
-| min               | 可选择范围的最小值                             | `Date`                                                                                         | -          |
-| maxPage           | 可切换到的最晚日期                             | `Page`                                                                                         | -          |
-| minPage           | 可切换到的最早日期                             | `Page`                                                                                         | -          |
-| nextMonthButton   | 导航窗格上的“下一月”按钮的内容                 | `React.ReactNode`                                                                              | `>`        |
-| nextYearButton    | 导航窗格上的“下一年”按钮的内容                 | `React.ReactNode`                                                                              | `>>`       |
-| onChange          | 选择日期变化时触发                             | 单选模式下为 `(val: Date \| null) => void`，多选模式下为 `(val: [Date, Date] \| null) => void` | -          |
-| onPageChange      | 切换月或年时触发                               | `(year: number, month: number) => void`                                                        | -          |
-| prevMonthButton   | 导航窗格上的“上一月”按钮的内容                 | `React.ReactNode`                                                                              | `<`        |
-| prevYearButton    | 导航窗格上的“上一年”按钮的内容                 | `React.ReactNode`                                                                              | `<<`       |
-| renderLabel       | 标注信息的渲染函数                             | `(date: Date) => ReactNode \| null \| undefined`                                               | -          |
-| selectionMode     | 选择模式，不设置的话表示不支持选择             | `'single' \| 'range'`                                                                          | -          |
-| shouldDisableDate | 判断日期是否可选，使用后会忽略 min 和 max 设置 | `(date: Date) => boolean`                                                                      | -          |
-| value             | 选择的日期                                     | 单选模式下为 `Date \| null`，多选模式下为 `[Date, Date] \| null`                               | -          |
-| weekStartsOn      | 每周以周几作为第一天                           | `'Monday' \| 'Sunday'`                                                                         | `'Sunday'` |
+| 属性 | 说明 | 类型 | 默认值 |
+| --- | --- | --- | --- |
+| allowClear | 是否允许再次点击后清除 | `boolean` | `true` |
+| defaultValue | 默认选择的日期 | 同 `value` 属性 | - |
+| max | 可选择范围的最大值 | `Date` | - |
+| min | 可选择范围的最小值 | `Date` | - |
+| maxPage | 可切换到的最晚日期 | `Page` | - |
+| minPage | 可切换到的最早日期 | `Page` | - |
+| nextMonthButton | 导航窗格上的“下一月”按钮的内容 | `React.ReactNode` | `>` |
+| nextYearButton | 导航窗格上的“下一年”按钮的内容 | `React.ReactNode` | `>>` |
+| onChange | 选择日期变化时触发 | 单选模式下为 `(val: Date \| null) => void`，多选模式下为 `(val: [Date, Date] \| null) => void` | - |
+| onPageChange | 切换月或年时触发 | `(year: number, month: number) => void` | - |
+| prevMonthButton | 导航窗格上的“上一月”按钮的内容 | `React.ReactNode` | `<` |
+| prevYearButton | 导航窗格上的“上一年”按钮的内容 | `React.ReactNode` | `<<` |
+| renderLabel | 标注信息的渲染函数 | `(date: Date) => ReactNode \| null \| undefined` | - |
+| selectionMode | 选择模式，不设置的话表示不支持选择 | `'single' \| 'range'` | - |
+| shouldDisableDate | 判断日期是否可选，使用后会忽略 min 和 max 设置 | `(date: Date) => boolean` | - |
+| value | 选择的日期 | 单选模式下为 `Date \| null`，多选模式下为 `[Date, Date] \| null` | - |
+| weekStartsOn | 每周以周几作为第一天 | `'Monday' \| 'Sunday'` | `'Sunday'` |
 
 ### CSS 变量
 
@@ -42,10 +40,10 @@
 
 ### Ref
 
-| 属性        | 说明                 | 类型                                             |
-| ----------- | -------------------- | ------------------------------------------------ |
-| jumpTo      | 跳转至指定日期的页面 | `(page: Page \| ((page: Page) => Page)) => void` |
-| jumpToToday | 跳转至今日           | `() => void`                                     |
+| 属性 | 说明 | 类型 |
+| --- | --- | --- |
+| jumpTo | 跳转至指定日期的页面 | `(page: Page \| ((page: Page) => Page)) => void` |
+| jumpToToday | 跳转至今日 | `() => void` |
 
 ```ts
 type Page = { month: number; year: number }
diff --git a/src/components/calendar/demos/demo4.less b/src/components/calendar/demos/demo4.less
new file mode 100644
index 000000000..f0bc9bdd9
--- /dev/null
+++ b/src/components/calendar/demos/demo4.less
@@ -0,0 +1,86 @@
+@prefix: ~'adm-calendar';
+
+.calendar-custom {
+  .@{prefix}-cell {
+    justify-content: center;
+    height: 38px;
+
+    &-bottom {
+      display: none;
+    }
+
+    &.@{prefix}-cell-selected.@{prefix}-cell.@{prefix}-cell-selected {
+      background: rgba(229, 242, 250, 1);
+
+      .@{prefix}-cell-top {
+        height: 38px;
+        width: 38px;
+        border-radius: 99px;
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        background: rgba(229, 242, 250, 1);
+        color: var(--adm-color-primary);
+      }
+
+      &.@{prefix}-cell-selected-begin {
+        background: linear-gradient(
+          to right,
+          var(--adm-color-white) 50%,
+          #e5f2fa 51%,
+          #e5f2fa
+        );
+        .@{prefix}-cell-top {
+          height: 38px;
+          width: 38px;
+          border-radius: 99px;
+          display: flex;
+          justify-content: center;
+          align-items: center;
+          background: var(--adm-color-primary);
+          color: var(--adm-color-white);
+        }
+
+        &.@{prefix}-cell-selected-row-end {
+          background: transparent;
+        }
+      }
+
+      &.@{prefix}-cell-selected-begin.@{prefix}-cell-selected-end {
+        background: var(--adm-color-white);
+      }
+
+      &.@{prefix}-cell-selected-end {
+        background: linear-gradient(
+          to right,
+          #e5f2fa 50%,
+          var(--adm-color-white) 51%,
+          var(--adm-color-white)
+        );
+        .@{prefix}-cell-top {
+          height: 38px;
+          width: 38px;
+          border-radius: 99px;
+          display: flex;
+          justify-content: center;
+          align-items: center;
+          background: var(--adm-color-primary);
+          color: var(--adm-color-white);
+        }
+
+        &.@{prefix}-cell-selected-row-start {
+          background: transparent;
+        }
+      }
+
+      &.@{prefix}-cell-selected-row-begin {
+        border-top-left-radius: 99px;
+        border-bottom-left-radius: 99px;
+      }
+      &.@{prefix}-cell-selected-row-end {
+        border-top-right-radius: 99px;
+        border-bottom-right-radius: 99px;
+      }
+    }
+  }
+}
diff --git a/src/components/calendar/demos/demo4.tsx b/src/components/calendar/demos/demo4.tsx
new file mode 100644
index 000000000..93adfc363
--- /dev/null
+++ b/src/components/calendar/demos/demo4.tsx
@@ -0,0 +1,27 @@
+import dayjs from 'dayjs'
+import React, { useState } from 'react'
+import { Calendar } from 'antd-mobile'
+import { DemoBlock } from 'demos'
+import './demo4.less'
+
+export default () => {
+  const today = dayjs()
+  const [val, setVal] = useState<[Date, Date] | null>(() => [
+    today.subtract(2, 'day').toDate(),
+    today.add(2, 'day').toDate(),
+  ])
+  return (
+    <>
+      <DemoBlock title='自定义样式'>
+        <Calendar
+          className='calendar-custom'
+          selectionMode='range'
+          value={val}
+          onChange={val => {
+            setVal(val)
+          }}
+        />
+      </DemoBlock>
+    </>
+  )
+}
diff --git a/src/components/floating-bubble/demos/demo4.tsx b/src/components/floating-bubble/demos/demo4.tsx
new file mode 100644
index 000000000..1e084fc82
--- /dev/null
+++ b/src/components/floating-bubble/demos/demo4.tsx
@@ -0,0 +1,39 @@
+import React, { useState } from 'react'
+import { FloatingBubble, Divider } from 'antd-mobile'
+import { DemoDescription } from 'demos'
+import { MessageFill } from 'antd-mobile-icons'
+
+export default () => {
+  const [offset, setOffset] = useState({ x: -24, y: -24 })
+
+  return (
+    <div
+      style={{
+        textAlign: 'center',
+        padding: '50vh 32px 0',
+      }}
+    >
+      <DemoDescription>受控模式</DemoDescription>
+      <Divider />
+
+      <DemoDescription>
+        <div>x: {offset.x}</div>
+        <div>y: {offset.y}</div>
+      </DemoDescription>
+
+      <FloatingBubble
+        axis='xy'
+        style={{
+          '--initial-position-bottom': '0',
+          '--initial-position-right': '0',
+        }}
+        onOffsetChange={offset => {
+          setOffset(offset)
+        }}
+        offset={offset}
+      >
+        <MessageFill fontSize={32} />
+      </FloatingBubble>
+    </div>
+  )
+}
diff --git a/src/components/floating-bubble/floating-bubble.tsx b/src/components/floating-bubble/floating-bubble.tsx
index 046988858..e77e44b22 100644
--- a/src/components/floating-bubble/floating-bubble.tsx
+++ b/src/components/floating-bubble/floating-bubble.tsx
@@ -1,4 +1,4 @@
-import React, { FC, useRef } from 'react'
+import React, { FC, useRef, useEffect, useState } from 'react'
 import { useSpring, animated, to } from '@react-spring/web'
 import { useDrag } from '@use-gesture/react'
 import { mergeProps } from '../../utils/with-default-props'
@@ -6,11 +6,16 @@ import { NativeProps, withNativeProps } from '../../utils/native-props'
 
 const classPrefix = `adm-floating-bubble`
 
+type Offset = { x: number; y: number }
+
 export type FloatingBubbleProps = {
   onClick?: (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void
   axis?: 'x' | 'y' | 'xy' | 'lock'
   magnetic?: 'x' | 'y'
   children?: React.ReactNode
+  offset?: Offset
+  defaultOffset?: Offset
+  onOffsetChange?: (offset: Offset) => void
 } & NativeProps<
   | '--initial-position-left'
   | '--initial-position-right'
@@ -25,6 +30,7 @@ export type FloatingBubbleProps = {
 
 const defaultProps = {
   axis: 'y',
+  defaultOffset: { x: 0, y: 0 },
 }
 
 export const FloatingBubble: FC<FloatingBubbleProps> = p => {
@@ -33,20 +39,31 @@ export const FloatingBubble: FC<FloatingBubbleProps> = p => {
   const boundaryRef = useRef<HTMLDivElement>(null)
   const buttonRef = useRef<HTMLDivElement>(null)
 
+  const [innerValue, setInnerValue] = useState<Offset>(
+    props.offset === undefined ? props.defaultOffset : props.offset
+  )
+
+  useEffect(() => {
+    if (props.offset === undefined) return
+    api.start({ x: props.offset.x, y: props.offset.y })
+  }, [props.offset])
+
   /**
    * memoize the `to` function
    * inside a component that renders frequently
    * to prevent an unintended restart
    */
   const [{ x, y, opacity }, api] = useSpring(() => ({
-    x: 0,
-    y: 0,
+    x: innerValue.x,
+    y: innerValue.y,
     opacity: 1,
   }))
+
   const bind = useDrag(
     state => {
       let nextX = state.offset[0]
       let nextY = state.offset[1]
+
       if (state.last && props.magnetic) {
         const boundary = boundaryRef.current
         const button = buttonRef.current
@@ -76,10 +93,16 @@ export const FloatingBubble: FC<FloatingBubbleProps> = p => {
           }
         }
       }
-      api.start({
-        x: nextX,
-        y: nextY,
-      })
+
+      const nextOffest = { x: nextX, y: nextY }
+      if (props.offset === undefined) {
+        // Uncontrolled mode
+        api.start(nextOffest)
+      } else {
+        setInnerValue(nextOffest)
+      }
+      props.onOffsetChange?.(nextOffest)
+
       // active status
       api.start({
         opacity: state.active ? 0.8 : 1,
diff --git a/src/components/floating-bubble/index.en.md b/src/components/floating-bubble/index.en.md
index 71c627351..22501a24b 100644
--- a/src/components/floating-bubble/index.en.md
+++ b/src/components/floating-bubble/index.en.md
@@ -14,28 +14,33 @@ Suitable for providing extra functionality to the current page.
 
 <code src="./demos/demo3.tsx"></code>
 
+<code src="./demos/demo4.tsx"></code>
+
 ## FloatingBubble
 
 ### Props
 
-| Name     | Description                                                                                                                                                       | Type                           | Default |
-| -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------ | ------- |
-| axis     | The direction in which the drag can be performed, `'xy'` means free movement, `'lock'` means movement is only allowed in the direction in which the drag started. | `'x' \| 'y' \| 'xy' \| 'lock'` | `'y'`   |
-| children | Content of the bubble.                                                                                                                                            | ReactNode                      | -----   |
-| magnetic | Automatic magnetic attraction to the boundary.                                                                                                                    | `'x' \| 'y'`                   | -       |
+| Name | Description | Type | Default |
+| --- | --- | --- | --- |
+| axis | The direction in which the drag can be performed, `'xy'` means free movement, `'lock'` means movement is only allowed in the direction in which the drag started. | `'x' \| 'y' \| 'xy' \| 'lock'` | `'y'` |
+| children | Content of the bubble. | ReactNode | ----- |
+| magnetic | Automatic magnetic attraction to the boundary. | `'x' \| 'y'` | - |
+| defaultOffset | Default offset position | `{ x: number; y: number }` | `{ x: 0, y: 0 }` |
+| offset | Offset position | `{ x: number; y: number }` |  |
+| onOffsetChange | Callback function when changing | `(offset:{ x: number; y: number }) => void` | - |
 
 ### CSS Variables
 
-| Name                      | Description                                         | Default                    |
-| ------------------------- | --------------------------------------------------- | -------------------------- |
-| --background              | background of the bubble.                           | `var(--adm-color-primary)` |
-| --border-radius           | border-radius of the bubble.                        | `50%`                      |
-| --edge-distance           | Minimum distance to the edge of the screen          | `0`                        |
-| --initial-position-bottom | Initial distance from the bottom side of the screen | -                          |
-| --initial-position-left   | Initial distance from the left side of the screen   | -                          |
-| --initial-position-right  | Initial distance from the right side of the screen  | -                          |
-| --initial-position-top    | Initial distance from the top side of the screen    | -                          |
-| --size                    | Size of the bubble.                                 | `48px`                     |
-| --z-index                 | z-index of the bubble.                              | `1`                        |
+| Name | Description | Default |
+| --- | --- | --- |
+| --background | background of the bubble. | `var(--adm-color-primary)` |
+| --border-radius | border-radius of the bubble. | `50%` |
+| --edge-distance | Minimum distance to the edge of the screen | `0` |
+| --initial-position-bottom | Initial distance from the bottom side of the screen | - |
+| --initial-position-left | Initial distance from the left side of the screen | - |
+| --initial-position-right | Initial distance from the right side of the screen | - |
+| --initial-position-top | Initial distance from the top side of the screen | - |
+| --size | Size of the bubble. | `48px` |
+| --z-index | z-index of the bubble. | `1` |
 
 **You must either set `--initial-position-top` or `--initial-position-bottom`. Also, you must either set `--initial-position-left` or `--initial-position-right`.**
diff --git a/src/components/floating-bubble/index.zh.md b/src/components/floating-bubble/index.zh.md
index 4e1d4db1b..faa95b10f 100644
--- a/src/components/floating-bubble/index.zh.md
+++ b/src/components/floating-bubble/index.zh.md
@@ -14,28 +14,33 @@
 
 <code src="./demos/demo3.tsx"></code>
 
+<code src="./demos/demo4.tsx"></code>
+
 ## FloatingBubble
 
 ### 属性
 
-| 属性     | 描述                                                                                 | 类型                           | 默认值 |
-| -------- | ------------------------------------------------------------------------------------ | ------------------------------ | ------ |
-| axis     | 可以进行拖动的方向，`'xy'` 表示自由移动，`'lock'` 表示只允许在拖拽开始时的方向上移动 | `'x' \| 'y' \| 'xy' \| 'lock'` | `'y'`  |
-| children | 气泡的内容                                                                           | ReactNode                      | -      |
-| magnetic | 自动磁吸到边界                                                                       | `'x' \| 'y'`                   | -      |
+| 属性 | 描述 | 类型 | 默认值 |
+| --- | --- | --- | --- |
+| axis | 可以进行拖动的方向，`'xy'` 表示自由移动，`'lock'` 表示只允许在拖拽开始时的方向上移动 | `'x' \| 'y' \| 'xy' \| 'lock'` | `'y'` |
+| children | 气泡的内容 | ReactNode | - |
+| magnetic | 自动磁吸到边界 | `'x' \| 'y'` | - |
+| defaultOffset | 默认偏移位置 | `{ x: number; y: number }` | `{ x: 0, y: 0 }` |
+| offset | 偏移位置 | `{ x: number; y: number }` |  |
+| onOffsetChange | 偏移位置变化时的回调函数 | `(offset:{ x: number; y: number }) => void` | - |
 
 ### CSS 变量
 
-| 变量名                    | 描述                           | 默认值                     |
-| ------------------------- | ------------------------------ | -------------------------- |
-| --background              | 气泡的背景色                   | `var(--adm-color-primary)` |
-| --border-radius           | 气泡的 border-radius           | `50%`                      |
-| --edge-distance           | 和屏幕四周的最小间距           | `0`                        |
-| --initial-position-bottom | 初始状态组件距离屏幕下边的距离 | -                          |
-| --initial-position-left   | 初始状态组件距离屏幕左边的距离 | -                          |
-| --initial-position-right  | 初始状态组件距离屏幕右边的距离 | -                          |
-| --initial-position-top    | 初始状态组件距离屏幕上边的距离 | -                          |
-| --size                    | 气泡大小                       | `48px`                     |
-| --z-index                 | 气泡的 z-index                 | `1`                        |
+| 变量名 | 描述 | 默认值 |
+| --- | --- | --- |
+| --background | 气泡的背景色 | `var(--adm-color-primary)` |
+| --border-radius | 气泡的 border-radius | `50%` |
+| --edge-distance | 和屏幕四周的最小间距 | `0` |
+| --initial-position-bottom | 初始状态组件距离屏幕下边的距离 | - |
+| --initial-position-left | 初始状态组件距离屏幕左边的距离 | - |
+| --initial-position-right | 初始状态组件距离屏幕右边的距离 | - |
+| --initial-position-top | 初始状态组件距离屏幕上边的距离 | - |
+| --size | 气泡大小 | `48px` |
+| --z-index | 气泡的 z-index | `1` |
 
 **在使用时，你必须设置 `--initial-position-top` 和 `--initial-position-bottom` 其中的一项，同时，也必须设置 `--initial-position-left` 和 `--initial-position-right` 其中的一项。**
diff --git a/src/components/image-uploader/demos/demo2.tsx b/src/components/image-uploader/demos/demo2.tsx
index e48b77a37..a7f1606f0 100644
--- a/src/components/image-uploader/demos/demo2.tsx
+++ b/src/components/image-uploader/demos/demo2.tsx
@@ -24,6 +24,28 @@ const CustomeSize: FC = () => {
   )
 }
 
+// 自定义列数
+// columns 属性存在时，不支持自定义 --cell-size 属性，详见 FAQ。
+const CustomeColumns: FC = () => {
+  const [fileList, setFileList] = useState<ImageUploadItem[]>([
+    {
+      url: demoSrc,
+    },
+    {
+      url: demoSrc,
+    },
+  ])
+
+  return (
+    <ImageUploader
+      columns={5}
+      value={fileList}
+      onChange={setFileList}
+      upload={mockUpload}
+    />
+  )
+}
+
 // 自定义上传按钮
 const CustomUploadButton: FC = () => {
   const [fileList, setFileList] = useState<ImageUploadItem[]>([
@@ -59,6 +81,10 @@ export default () => {
         <CustomeSize />
       </DemoBlock>
 
+      <DemoBlock title='自定义列数'>
+        <CustomeColumns />
+      </DemoBlock>
+
       <DemoBlock title='自定义上传按钮'>
         <CustomUploadButton />
       </DemoBlock>
diff --git a/src/components/image-uploader/image-uploader.less b/src/components/image-uploader/image-uploader.less
index c6f69a576..50743d28d 100644
--- a/src/components/image-uploader/image-uploader.less
+++ b/src/components/image-uploader/image-uploader.less
@@ -2,9 +2,18 @@
 
 .@{class-prefix-image-uploader} {
   --cell-size: 80px;
+  --gap: 12px;
+  ---gap: var(--gap);
+  ---gap-horizontal: var(--gap-horizontal, var(--gap));
+  ---gap-vertical: var(--gap-vertical, var(--gap));
+
+  &-grid,
   &-space {
-    --gap: 12px;
+    --gap: var(---gap);
+    --gap-horizontal: var(---gap-horizontal);
+    --gap-vertical: var(---gap-vertical);
   }
+
   &-cell {
     position: relative;
     width: var(--cell-size);
@@ -92,4 +101,12 @@
       border-radius: 4px;
     }
   }
+
+  .@{class-prefix-image-uploader}-gap-measure {
+    position: absolute;
+    left: 0;
+    top: 0;
+    height: var(--gap-horizontal);
+    width: 0;
+  }
 }
diff --git a/src/components/image-uploader/image-uploader.tsx b/src/components/image-uploader/image-uploader.tsx
index fb801dabc..3612aca87 100644
--- a/src/components/image-uploader/image-uploader.tsx
+++ b/src/components/image-uploader/image-uploader.tsx
@@ -1,14 +1,22 @@
-import React, { FC, InputHTMLAttributes, useRef, useState } from 'react'
-import { AddOutline } from 'antd-mobile-icons'
+import React, {
+  FC,
+  InputHTMLAttributes,
+  useRef,
+  useState,
+  CSSProperties,
+} from 'react'
+import { AddOutline, CloseOutline } from 'antd-mobile-icons'
 import { mergeProps } from '../../utils/with-default-props'
 import ImageViewer, { ImageViewerShowHandler } from '../image-viewer'
 import PreviewItem from './preview-item'
 import { usePropsValue } from '../../utils/use-props-value'
-import { useIsomorphicLayoutEffect, useUnmount } from 'ahooks'
+import { useIsomorphicLayoutEffect, useUnmount, useSize } from 'ahooks'
 import Space from '../space'
 import { NativeProps, withNativeProps } from '../../utils/native-props'
+import { measureCSSLength } from '../../utils/measure-css-length'
 import { useConfig } from '../config-provider'
 import type { ImageProps } from '../image'
+import Grid, { GridProps } from '../grid'
 
 export type TaskStatus = 'pending' | 'fail' | 'success'
 
@@ -31,6 +39,7 @@ export type UploadTask = Pick<Task, 'id' | 'status'>
 export type ImageUploaderProps = {
   defaultValue?: ImageUploadItem[]
   value?: ImageUploadItem[]
+  columns?: GridProps['columns']
   onChange?: (items: ImageUploadItem[]) => void
   onUploadQueueChange?: (tasks: UploadTask[]) => void
   accept?: string
@@ -40,6 +49,7 @@ export type ImageUploaderProps = {
   disableUpload?: boolean
   showUpload?: boolean
   deletable?: boolean
+  deleteIcon?: React.ReactNode
   capture?: InputHTMLAttributes<unknown>['capture']
   onPreview?: (index: number, item: ImageUploadItem) => void
   beforeUpload?: (
@@ -57,13 +67,14 @@ export type ImageUploaderProps = {
     file: ImageUploadItem,
     fileList: ImageUploadItem[]
   ) => React.ReactNode
-} & NativeProps<'--cell-size'>
+} & NativeProps<'--cell-size' | '--gap' | '--gap-vertical' | '--gap-horizontal'>
 
 const classPrefix = `adm-image-uploader`
 
 const defaultProps = {
   disableUpload: false,
   deletable: true,
+  deleteIcon: <CloseOutline className={`${classPrefix}-cell-delete-icon`} />,
   showUpload: true,
   multiple: false,
   maxCount: 0,
@@ -77,10 +88,34 @@ const defaultProps = {
 export const ImageUploader: FC<ImageUploaderProps> = p => {
   const { locale } = useConfig()
   const props = mergeProps(defaultProps, p)
+  const { columns } = props
   const [value, setValue] = usePropsValue(props)
 
   const [tasks, setTasks] = useState<Task[]>([])
 
+  const containerRef = useRef<HTMLDivElement>(null)
+
+  const containerSize = useSize(containerRef)
+  const gapMeasureRef = useRef<HTMLDivElement>(null)
+  const [cellSize, setCellSize] = useState<number>(80)
+
+  useIsomorphicLayoutEffect(() => {
+    const gapMeasure = gapMeasureRef.current
+    if (columns && containerSize && gapMeasure) {
+      const width = containerSize.width
+      const gap = measureCSSLength(
+        window.getComputedStyle(gapMeasure).getPropertyValue('height')
+      )
+      setCellSize((width - gap * (columns - 1)) / columns)
+    }
+  }, [containerSize?.width])
+
+  const style: CSSProperties & {
+    '--cell-size': string
+  } = {
+    '--cell-size': cellSize + 'px',
+  }
+
   useIsomorphicLayoutEffect(() => {
     setTasks(prev =>
       prev.filter(task => {
@@ -223,6 +258,7 @@ export const ImageUploader: FC<ImageUploaderProps> = p => {
           key={fileItem.key ?? index}
           url={fileItem.thumbnailUrl ?? fileItem.url}
           deletable={props.deletable}
+          deleteIcon={props.deleteIcon}
           imageFit={props.imageFit}
           onClick={() => {
             if (props.preview) {
@@ -241,54 +277,71 @@ export const ImageUploader: FC<ImageUploaderProps> = p => {
     })
   }
 
+  const contentNode = (
+    <>
+      {renderImages()}
+      {tasks.map(task => {
+        if (!props.showFailed && task.status === 'fail') {
+          return null
+        }
+        return (
+          <PreviewItem
+            key={task.id}
+            file={task.file}
+            deletable={task.status !== 'pending'}
+            deleteIcon={props.deleteIcon}
+            status={task.status}
+            imageFit={props.imageFit}
+            onDelete={() => {
+              setTasks(tasks.filter(x => x.id !== task.id))
+            }}
+          />
+        )
+      })}
+      {showUpload && (
+        <div className={`${classPrefix}-upload-button-wrap`}>
+          {props.children ? (
+            props.children
+          ) : (
+            <span
+              className={`${classPrefix}-cell ${classPrefix}-upload-button`}
+              role='button'
+              aria-label={locale.ImageUploader.upload}
+            >
+              <span className={`${classPrefix}-upload-button-icon`}>
+                <AddOutline />
+              </span>
+            </span>
+          )}
+          {!props.disableUpload && (
+            <input
+              capture={props.capture}
+              accept={props.accept}
+              multiple={props.multiple}
+              type='file'
+              className={`${classPrefix}-input`}
+              onChange={onChange}
+              aria-hidden
+            />
+          )}
+        </div>
+      )}
+    </>
+  )
+
   return withNativeProps(
     props,
-    <div className={classPrefix}>
-      <Space className={`${classPrefix}-space`} wrap block>
-        {renderImages()}
-        {finalTasks.map(task => {
-          return (
-            <PreviewItem
-              key={task.id}
-              file={task.file}
-              deletable={task.status !== 'pending'}
-              status={task.status}
-              imageFit={props.imageFit}
-              onDelete={() => {
-                setTasks(prev => prev.filter(x => x.id !== task.id))
-              }}
-            />
-          )
-        })}
-        {showUpload && (
-          <div className={`${classPrefix}-upload-button-wrap`}>
-            {props.children ? (
-              props.children
-            ) : (
-              <span
-                className={`${classPrefix}-cell ${classPrefix}-upload-button`}
-                role='button'
-                aria-label={locale.ImageUploader.upload}
-              >
-                <span className={`${classPrefix}-upload-button-icon`}>
-                  <AddOutline />
-                </span>
-              </span>
-            )}
-            {!props.disableUpload && (
-              <input
-                capture={props.capture}
-                accept={props.accept}
-                multiple={props.multiple}
-                type='file'
-                className={`${classPrefix}-input`}
-                onChange={onChange}
-                aria-hidden
-              />
-            )}
-          </div>
-        )}
-      </Space>
+    <div className={classPrefix} ref={containerRef}>
+      {columns ? (
+        <Grid className={`${classPrefix}-grid`} columns={columns} style={style}>
+          <div className={`${classPrefix}-gap-measure`} ref={gapMeasureRef} />
+          {contentNode.props.children}
+        </Grid>
+      ) : (
+        <Space className={`${classPrefix}-space`} wrap block>
+          {contentNode.props.children}
+        </Space>
+      )}
     </div>
   )
 }
diff --git a/src/components/image-uploader/index.en.md b/src/components/image-uploader/index.en.md
index ade729693..b330c3423 100644
--- a/src/components/image-uploader/index.en.md
+++ b/src/components/image-uploader/index.en.md
@@ -10,38 +10,40 @@
 
 ### Props
 
-| Name                | Description                                                                                                                                                              | Type                                                                                                       | Default   |
-| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------- | --------- |
-| accept              | The file types, `image/gif` `image/jpeg` `image/jpg` `image/png`                                                                                                         | `string`                                                                                                   | `image/*` |
-| beforeUpload        | Callback function before file reading, return `null` to terminate file reading, support return `Promise`                                                                 | `(file: File, files: File[]) => Promise<File \| null> \| File \| null`                                     | -         |
-| capture             | Picture selection mode, the optional value is `camera` (directly adjust the camera)                                                                                      | `boolean \| string`                                                                                        | -         |
-| children            | Custom upload button                                                                                                                                                     | `ReactNode`                                                                                                | -         |
-| defaultValue        | Default list of uploaded files                                                                                                                                           | `ImageUploadItem[]`                                                                                        | -         |
-| deletable           | Whether to show the delete button                                                                                                                                        | `boolean`                                                                                                  | `true`    |
-| disableUpload       | Whether to disable the upload button                                                                                                                                     | `boolean`                                                                                                  | `false`   |
-| imageFit            | The fill mode of the image, same as [Image](/components/image#props)                                                                                                     | `'contain' \| 'cover' \| 'fill' \| 'none' \| 'scale-down'`                                                 | `cover`   |
-| maxCount            | To control how many pictures to be uploaded at most, the upload button would be automatically hidden if the number is exceeded, `0` means no limit                       | `number`                                                                                                   | `0`       |
-| multiple            | Whether to support selecting multiple pictures                                                                                                                           | `boolean`                                                                                                  | `false`   |
-| onChange            | Triggered when the list of uploaded files changing                                                                                                                       | `(items: ImageUploadItem[]) => void`                                                                       | -         |
-| onCountExceed       | Triggered when the number of pictures selected by the user exceeds the maximum limit                                                                                     | `(exceed: number) => void`                                                                                 | -         |
-| onDelete            | Triggered when the successfully uploaded image is deleted, if it returns false, it means that it is prevented from being deleted, and it supports the return of Promise. | `(item: ImageUploadItem) => boolean \| Promise<boolean> \| void`                                           | -         |
-| onPreview           | Callback when the preview picture is clicked                                                                                                                             | `(index: number, item: ImageUploadItem) => void`                                                           | -         |
-| preview             | Whether to support preview                                                                                                                                               | `boolean`                                                                                                  | `true`    |
-| showFailed          | Whether to display pictures in the failed uploading state                                                                                                                | `boolean`                                                                                                  | `true`    |
-| showUpload          | Whether to display the upload button                                                                                                                                     | `boolean`                                                                                                  | `true`    |
-| upload              | Upload method, the input parameter is the file object that needs to be uploaded, after asynchronous processing, the upload result is returned                            | `(file: File) => Promise<ImageUploadItem>`                                                                 | -         |
-| value               | List of uploaded files                                                                                                                                                   | `ImageUploadItem[]`                                                                                        | -         |
-| onUploadQueueChange | Triggered when the image upload queue changes                                                                                                                            | `(tasks: UploadTask[]) => void`                                                                            |
-| renderItem          | Custom item of uploadList                                                                                                                                                | `(originNode: React.ReactElement, file: ImageUploadItem, fileList: ImageUploadItem[] ) => React.ReactNode` | -         |
+| Name | Description | Type | Default |
+| --- | --- | --- | --- |
+| accept | The file types, `image/gif` `image/jpeg` `image/jpg` `image/png` | `string` | `image/*` |
+| beforeUpload | Callback function before file reading, return `null` to terminate file reading, support return `Promise` | `(file: File, files: File[]) => Promise<File \| null> \| File \| null` | - |
+| capture | Picture selection mode, the optional value is `camera` (directly adjust the camera) | `boolean \| string` | - |
+| children | Custom upload button | `ReactNode` | - |
+| columns | Number of the displayed columns | `number` | - |
+| defaultValue | Default list of uploaded files | `ImageUploadItem[]` | - |
+| deletable | Whether to show the delete button | `boolean` | `true` |
+| deleteIcon | The customized `icon` in delete button | `ReactNode` | `<CloseOutline />` |
+| disableUpload | Whether to disable the upload button | `boolean` | `false` |
+| imageFit | The fill mode of the image, same as [Image](/components/image#props) | `'contain' \| 'cover' \| 'fill' \| 'none' \| 'scale-down'` | `cover` |
+| maxCount | To control how many pictures to be uploaded at most, the upload button would be automatically hidden if the number is exceeded, `0` means no limit | `number` | `0` |
+| multiple | Whether to support selecting multiple pictures | `boolean` | `false` |
+| onChange | Triggered when the list of uploaded files changing | `(items: ImageUploadItem[]) => void` | - |
+| onCountExceed | Triggered when the number of pictures selected by the user exceeds the maximum limit | `(exceed: number) => void` | - |
+| onDelete | Triggered when the successfully uploaded image is deleted, if it returns false, it means that it is prevented from being deleted, and it supports the return of Promise. | `(item: ImageUploadItem) => boolean \| Promise<boolean> \| void` | - |
+| onPreview | Callback when the preview picture is clicked | `(index: number, item: ImageUploadItem) => void` | - |
+| preview | Whether to support preview | `boolean` | `true` |
+| showFailed | Whether to display pictures in the failed uploading state | `boolean` | `true` |
+| showUpload | Whether to display the upload button | `boolean` | `true` |
+| upload | Upload method, the input parameter is the file object that needs to be uploaded, after asynchronous processing, the upload result is returned | `(file: File) => Promise<ImageUploadItem>` | - |
+| value | List of uploaded files | `ImageUploadItem[]` | - |
+| onUploadQueueChange | Triggered when the image upload queue changes | `(tasks: UploadTask[]) => void` |
+| renderItem | Custom item of uploadList | `(originNode: React.ReactElement, file: ImageUploadItem, fileList: ImageUploadItem[] ) => React.ReactNode` | - |
 
 ### ImageUploadItem
 
-| Name         | Description                                   | Type               | Default         |
-| ------------ | --------------------------------------------- | ------------------ | --------------- |
-| extra        | Attach some extra data                        | `any`              | -               |
-| key          | Unique identifier                             | `string \| number` | The array index |
-| thumbnailUrl | The resource address of the thumbnail picture | `string`           | `url`           |
-| url          | The resource address of the picture           | `string`           | -               |
+| Name | Description | Type | Default |
+| --- | --- | --- | --- |
+| extra | Attach some extra data | `any` | - |
+| key | Unique identifier | `string \| number` | The array index |
+| thumbnailUrl | The resource address of the thumbnail picture | `string` | `url` |
+| url | The resource address of the picture | `string` | - |
 
 ### UploadTask
 
@@ -52,9 +54,12 @@
 
 ### CSS Variables
 
-| Name        | Description                         | Default |
-| ----------- | ----------------------------------- | ------- |
-| --cell-size | The size of image and upload button | `80px`  |
+| Name | Description | Default |
+| --- | --- | --- |
+| --cell-size | The size of image and upload button(Valid only in non-columns mode) | `80px` |
+| --gap | The gap between items. | `12px` |
+| --gap-horizontal | The horizontal gap between items. | `var(--gap)` |
+| --gap-vertical | The vertical gap between items. | `var(--gap)` |
 
 ## FAQ
 
@@ -63,3 +68,7 @@
 The `capture` provided by ImageUploader comes from the native HTML capability, and in some operating systems/browser environments, this attribute may not be supported, so this problem cannot be avoided.
 
 See the discussion in this [issue](https://github.com/ant-design/ant-design-mobile/issues/5254).
+
+### Description of the `columns` Prop
+
+The `columns` relies on [Grid](./grid) layout and when present, the custom `--cell-size` is not supported, as the size of image and upload button is calculated automatically.
diff --git a/src/components/image-uploader/index.zh.md b/src/components/image-uploader/index.zh.md
index a0eed56ec..9ef102f4c 100644
--- a/src/components/image-uploader/index.zh.md
+++ b/src/components/image-uploader/index.zh.md
@@ -10,38 +10,40 @@
 
 ### 属性
 
-| 属性                | 说明                                                                          | 类型                                                                                                       | 默认值    |
-| ------------------- | ----------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | --------- |
-| accept              | 文件类型，`image/gif` `image/jpeg` `image/jpg` `image/png`                    | `string`                                                                                                   | `image/*` |
-| beforeUpload        | 文件读取前的回调函数，返回 `null` 可终止文件读取，支持返回 `Promise`          | `(file: File, files: File[]) => Promise<File \| null> \| File \| null`                                     | -         |
-| capture             | 图片选取模式，可选值为 `camera`（直接调起摄像头）                             | `boolean \| string`                                                                                        | -         |
-| children            | 自定义上传按钮                                                                | `ReactNode`                                                                                                | -         |
-| defaultValue        | 默认已上传的文件列表                                                          | `ImageUploadItem[]`                                                                                        | -         |
-| deletable           | 是否展示删除按钮                                                              | `boolean`                                                                                                  | `true`    |
-| disableUpload       | 是否禁用上传按钮                                                              | `boolean`                                                                                                  | `false`   |
-| imageFit            | 图片填充模式，同 [Image](/zh/components/image#属性)                           | `'contain' \| 'cover' \| 'fill' \| 'none' \| 'scale-down'`                                                 | `cover`   |
-| maxCount            | 最多上传几张图片，超出数量会自动隐藏上传按钮，`0` 表示不做限制                | `number`                                                                                                   | `0`       |
-| multiple            | 是否支持选择多张图片                                                          | `boolean`                                                                                                  | `false`   |
-| onChange            | 已上传的文件列表变化时触发                                                    | `(items: ImageUploadItem[]) => void`                                                                       | -         |
-| onCountExceed       | 用户选择的图片数量超出最大限制时触发                                          | `(exceed: number) => void`                                                                                 | -         |
-| onDelete            | 删除已上传成功的图片时触发，如果返回 `false` 表示阻止删除，支持返回 `Promise` | `(item: ImageUploadItem) => boolean \| Promise<boolean> \| void`                                           | -         |
-| onPreview           | 点击预览图片                                                                  | `(index: number, item: ImageUploadItem) => void`                                                           | -         |
-| preview             | 是否支持预览                                                                  | `boolean`                                                                                                  | `true`    |
-| showFailed          | 是否展示上传失败状态下的图片                                                  | `boolean`                                                                                                  | `true`    |
-| showUpload          | 是否展示上传按钮                                                              | `boolean`                                                                                                  | `true`    |
-| upload              | 上传方法，入参是需要被上传的文件对象，经过异步处理之后，返回上传结果          | `(file: File) => Promise<ImageUploadItem>`                                                                 | -         |
-| value               | 已上传的文件列表                                                              | `ImageUploadItem[]`                                                                                        | -         |
-| onUploadQueueChange | 图片上传队列变化时触发                                                        | `(tasks: UploadTask[]) => void`                                                                            | -         |
-| renderItem          | 自定义上传列表项                                                              | `(originNode: React.ReactElement, file: ImageUploadItem, fileList: ImageUploadItem[] ) => React.ReactNode` | -         |
+| 属性 | 说明 | 类型 | 默认值 |
+| --- | --- | --- | --- |
+| accept | 文件类型，`image/gif` `image/jpeg` `image/jpg` `image/png` | `string` | `image/*` |
+| beforeUpload | 文件读取前的回调函数，返回 `null` 可终止文件读取，支持返回 `Promise` | `(file: File, files: File[]) => Promise<File \| null> \| File \| null` | - |
+| capture | 图片选取模式，可选值为 `camera`（直接调起摄像头） | `boolean \| string` | - |
+| children | 自定义上传按钮 | `ReactNode` | - |
+| columns | 列数 | `number` | - |
+| defaultValue | 默认已上传的文件列表 | `ImageUploadItem[]` | - |
+| deletable | 是否展示删除按钮 | `boolean` | `true` |
+| deleteIcon | 删除按钮的 `icon` 图标 | `ReactNode` | `<CloseOutline />` |
+| disableUpload | 是否禁用上传按钮 | `boolean` | `false` |
+| imageFit | 图片填充模式，同 [Image](/zh/components/image#属性) | `'contain' \| 'cover' \| 'fill' \| 'none' \| 'scale-down'` | `cover` |
+| maxCount | 最多上传几张图片，超出数量会自动隐藏上传按钮，`0` 表示不做限制 | `number` | `0` |
+| multiple | 是否支持选择多张图片 | `boolean` | `false` |
+| onChange | 已上传的文件列表变化时触发 | `(items: ImageUploadItem[]) => void` | - |
+| onCountExceed | 用户选择的图片数量超出最大限制时触发 | `(exceed: number) => void` | - |
+| onDelete | 删除已上传成功的图片时触发，如果返回 `false` 表示阻止删除，支持返回 `Promise` | `(item: ImageUploadItem) => boolean \| Promise<boolean> \| void` | - |
+| onPreview | 点击预览图片 | `(index: number, item: ImageUploadItem) => void` | - |
+| preview | 是否支持预览 | `boolean` | `true` |
+| showFailed | 是否展示上传失败状态下的图片 | `boolean` | `true` |
+| showUpload | 是否展示上传按钮 | `boolean` | `true` |
+| upload | 上传方法，入参是需要被上传的文件对象，经过异步处理之后，返回上传结果 | `(file: File) => Promise<ImageUploadItem>` | - |
+| value | 已上传的文件列表 | `ImageUploadItem[]` | - |
+| onUploadQueueChange | 图片上传队列变化时触发 | `(tasks: UploadTask[]) => void` | - |
+| renderItem | 自定义上传列表项 | `(originNode: React.ReactElement, file: ImageUploadItem, fileList: ImageUploadItem[] ) => React.ReactNode` | - |
 
 ### ImageUploadItem
 
-| 属性         | 说明                                     | 类型               | 默认值   |
-| ------------ | ---------------------------------------- | ------------------ | -------- |
-| extra        | 附带一些额外信息，可以业务中自己随意定义 | `any`              | -        |
-| key          | 唯一标识                                 | `string \| number` | 数组下标 |
-| thumbnailUrl | 缩略图的资源地址                         | `string`           | `url`    |
-| url          | 图片的资源地址                           | `string`           | -        |
+| 属性 | 说明 | 类型 | 默认值 |
+| --- | --- | --- | --- |
+| extra | 附带一些额外信息，可以业务中自己随意定义 | `any` | - |
+| key | 唯一标识 | `string \| number` | 数组下标 |
+| thumbnailUrl | 缩略图的资源地址 | `string` | `url` |
+| url | 图片的资源地址 | `string` | - |
 
 ### UploadTask
 
@@ -52,9 +54,12 @@
 
 ### CSS 变量
 
-| 属性        | 说明                 | 默认值 |
-| ----------- | -------------------- | ------ |
-| --cell-size | 图片和上传按钮的大小 | `80px` |
+| 属性 | 说明 | 默认值 |
+| --- | --- | --- |
+| --cell-size | 图片和上传按钮的大小（仅在非 columns 模式下有效） | `80px` |
+| --gap | 间距大小 | `12px` |
+| --gap-horizontal | 水平方向的间距大小 | `var(--gap)` |
+| --gap-vertical | 垂直方向的间距大小 | `var(--gap)` |
 
 ## FAQ
 
@@ -63,3 +68,7 @@
 ImageUploader 所提供的 `capture` 是来自 HTML 原生的能力，而在部分操作系统/浏览器环境下，可能并不支持该属性，因此这个问题无法避免。
 
 详见此 [issue](https://github.com/ant-design/ant-design-mobile/issues/5254) 中的讨论。
+
+### `columns` 属性说明
+
+`columns` 属性依赖 [Grid](./grid) 布局，该属性存在时，不支持自定义 `--cell-size` 属性，因为图片和上传按钮的大小是自动计算的。
diff --git a/src/components/image-uploader/preview-item.tsx b/src/components/image-uploader/preview-item.tsx
index ef8a30ee0..cfba2c10d 100644
--- a/src/components/image-uploader/preview-item.tsx
+++ b/src/components/image-uploader/preview-item.tsx
@@ -1,5 +1,4 @@
 import React, { FC, useEffect, useMemo } from 'react'
-import { CloseOutline } from 'antd-mobile-icons'
 import classNames from 'classnames'
 import { TaskStatus } from './image-uploader'
 import Image from '../image'
@@ -11,6 +10,7 @@ type Props = {
   onClick?: () => void
   onDelete?: () => void
   deletable: boolean
+  deleteIcon: React.ReactNode
   url?: string
   file?: File
   status?: TaskStatus
@@ -21,7 +21,7 @@ const classPrefix = `adm-image-uploader`
 
 const PreviewItem: FC<Props> = props => {
   const { locale } = useConfig()
-  const { url, file, deletable, onDelete, imageFit } = props
+  const { url, file, deletable, deleteIcon, onDelete, imageFit } = props
   const src = useMemo(() => {
     if (url) {
       return url
@@ -57,7 +57,7 @@ const PreviewItem: FC<Props> = props => {
     return (
       deletable && (
         <span className={`${classPrefix}-cell-delete`} onClick={onDelete}>
-          <CloseOutline className={`${classPrefix}-cell-delete-icon`} />
+          {deleteIcon}
         </span>
       )
     )
diff --git a/src/components/mask/demos/demo1.tsx b/src/components/mask/demos/demo1.tsx
index 300cf7b08..df65da9a8 100644
--- a/src/components/mask/demos/demo1.tsx
+++ b/src/components/mask/demos/demo1.tsx
@@ -75,6 +75,21 @@ const CustomOpacity: FC = () => {
   )
 }
 
+// 背景蒙层的颜色 - 自定义
+const CustomColor: FC = () => {
+  const [visible, setVisible] = useState(false)
+  return (
+    <>
+      <Mask
+        visible={visible}
+        onMaskClick={() => setVisible(false)}
+        color='rgba(219, 10, 10, 0.5)'
+      />
+      <Button onClick={() => setVisible(true)}>显示自定义颜色的背景蒙层</Button>
+    </>
+  )
+}
+
 // 白色的背景蒙层
 const White: FC = () => {
   const [visible, setVisible] = useState(false)
@@ -105,8 +120,9 @@ export default () => {
         </Space>
       </DemoBlock>
 
-      <DemoBlock title='白色的背景蒙层'>
+      <DemoBlock title='背景蒙层的颜色'>
         <White />
+        <CustomColor />
       </DemoBlock>
 
       <DemoBlock title='自定义内容'>
diff --git a/src/components/mask/index.en.md b/src/components/mask/index.en.md
index a3d6fc87d..bae4f6a1f 100644
--- a/src/components/mask/index.en.md
+++ b/src/components/mask/index.en.md
@@ -14,19 +14,19 @@ Often used in the background layer of a modal window to make the visual focus st
 
 ### Props
 
-| Name              | Description                                                                                            | Type                                                            | Default     |
-| ----------------- | ------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------- | ----------- |
-| afterClose        | Triggered when completely closed                                                                       | `() => void`                                                    | -           |
-| afterShow         | Triggered after fully displayed                                                                        | `() => void`                                                    | -           |
-| color             | Color of the mask                                                                                      | `'black' \| 'white'`                                            | `'black'`   |
-| destroyOnClose    | Uninstall content when invisible                                                                       | `boolean`                                                       | `false`     |
-| disableBodyScroll | Whether to disable `body` scrolling                                                                    | `boolean`                                                       | `true`      |
-| forceRender       | Render content forcely                                                                                 | `boolean`                                                       | `false`     |
-| getContainer      | To get the specified mounted `HTML` node, if `null` returned, it would be rendered to the current node | `HTMLElement \| () => HTMLElement \| null`                      | `null`      |
-| onMaskClick       | Triggered when the mask is clicked                                                                     | `(event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void` | -           |
-| opacity           | Opacity                                                                                                | `'default' \| 'thin' \| 'thick' \| number`                      | `'default'` |
-| stopPropagation   | Stop the propagation of some events.                                                                   | `PropagationEvent[]`                                            | `['click']` |
-| visible           | Whether to show or hide                                                                                | `boolean`                                                       | `true`      |
+| Name | Description | Type | Default |
+| --- | --- | --- | --- |
+| afterClose | Triggered when completely closed | `() => void` | - |
+| afterShow | Triggered after fully displayed | `() => void` | - |
+| color | Color of the mask | `'black' \| 'white' \| string` | `'black'` |
+| destroyOnClose | Uninstall content when invisible | `boolean` | `false` |
+| disableBodyScroll | Whether to disable `body` scrolling | `boolean` | `true` |
+| forceRender | Render content forcely | `boolean` | `false` |
+| getContainer | To get the specified mounted `HTML` node, if `null` returned, it would be rendered to the current node | `HTMLElement \| () => HTMLElement \| null` | `null` |
+| onMaskClick | Triggered when the mask is clicked | `(event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void` | - |
+| opacity | Opacity | `'default' \| 'thin' \| 'thick' \| number` | `'default'` |
+| stopPropagation | Stop the propagation of some events. | `PropagationEvent[]` | `['click']` |
+| visible | Whether to show or hide | `boolean` | `true` |
 
 ### CSS Variables
 
diff --git a/src/components/mask/index.zh.md b/src/components/mask/index.zh.md
index a177a66aa..63b922360 100644
--- a/src/components/mask/index.zh.md
+++ b/src/components/mask/index.zh.md
@@ -14,19 +14,19 @@
 
 ### 属性
 
-| 属性              | 说明                                                         | 类型                                                            | 默认值      |
-| ----------------- | ------------------------------------------------------------ | --------------------------------------------------------------- | ----------- |
-| afterClose        | 完全关闭后触发                                               | `() => void`                                                    | -           |
-| afterShow         | 完全展示后触发                                               | `() => void`                                                    | -           |
-| color             | 背景蒙层的颜色                                               | `'black' \| 'white'`                                            | `'black'`   |
-| destroyOnClose    | 不可见时卸载内容                                             | `boolean`                                                       | `false`     |
-| disableBodyScroll | 是否禁用 `body` 滚动                                         | `boolean`                                                       | `true`      |
-| forceRender       | 强制渲染内容                                                 | `boolean`                                                       | `false`     |
-| getContainer      | 指定挂载的 `HTML` 节点，如果为 `null` 的话，会渲染到当前节点 | `HTMLElement \| () => HTMLElement \| null`                      | `null`      |
-| onMaskClick       | 点击蒙层自身触发                                             | `(event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void` | -           |
-| opacity           | 透明度                                                       | `'default' \| 'thin' \| 'thick' \| number`                      | `'default'` |
-| stopPropagation   | 阻止某些事件的冒泡                                           | `PropagationEvent[]`                                            | `['click']` |
-| visible           | 是否可见                                                     | `boolean`                                                       | `true`      |
+| 属性 | 说明 | 类型 | 默认值 |
+| --- | --- | --- | --- |
+| afterClose | 完全关闭后触发 | `() => void` | - |
+| afterShow | 完全展示后触发 | `() => void` | - |
+| color | 背景蒙层的颜色 | `'black' \| 'white' \| string` | `'black'` |
+| destroyOnClose | 不可见时卸载内容 | `boolean` | `false` |
+| disableBodyScroll | 是否禁用 `body` 滚动 | `boolean` | `true` |
+| forceRender | 强制渲染内容 | `boolean` | `false` |
+| getContainer | 指定挂载的 `HTML` 节点，如果为 `null` 的话，会渲染到当前节点 | `HTMLElement \| () => HTMLElement \| null` | `null` |
+| onMaskClick | 点击蒙层自身触发 | `(event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void` | - |
+| opacity | 透明度 | `'default' \| 'thin' \| 'thick' \| number` | `'default'` |
+| stopPropagation | 阻止某些事件的冒泡 | `PropagationEvent[]` | `['click']` |
+| visible | 是否可见 | `boolean` | `true` |
 
 ### CSS 变量
 
diff --git a/src/components/mask/mask.tsx b/src/components/mask/mask.tsx
index b1bd26229..3f1471f68 100644
--- a/src/components/mask/mask.tsx
+++ b/src/components/mask/mask.tsx
@@ -22,6 +22,10 @@ const opacityRecord = {
   thin: 0.35,
   thick: 0.75,
 }
+const colorRecord: Record<string, string> = {
+  black: '0, 0, 0',
+  white: '255, 255, 255',
+}
 
 export type MaskProps = {
   visible?: boolean
@@ -29,7 +33,7 @@ export type MaskProps = {
   destroyOnClose?: boolean
   forceRender?: boolean
   disableBodyScroll?: boolean
-  color?: 'black' | 'white'
+  color?: 'white' | 'black' | (string & {})
   opacity?: 'default' | 'thin' | 'thick' | number
   getContainer?: GetContainer
   afterShow?: () => void
@@ -59,8 +63,8 @@ export const Mask: React.FC<MaskProps> = p => {
 
   const background = useMemo(() => {
     const opacity = opacityRecord[props.opacity] ?? props.opacity
-    const rgb = props.color === 'white' ? '255, 255, 255' : '0, 0, 0'
-    return `rgba(${rgb}, ${opacity})`
+    const rgb = colorRecord[props.color]
+    return rgb ? `rgba(${rgb}, ${opacity})` : props.color
   }, [props.color, props.opacity])
 
   const [active, setActive] = useState(props.visible)
diff --git a/src/components/notice-bar/demos/demo1.tsx b/src/components/notice-bar/demos/demo1.tsx
index b31a062cf..40601a539 100644
--- a/src/components/notice-bar/demos/demo1.tsx
+++ b/src/components/notice-bar/demos/demo1.tsx
@@ -25,6 +25,14 @@ export default () => {
         <NoticeBar content={demoLongText} color='alert' />
       </DemoBlock>
 
+      <DemoBlock title='多行展示' padding='0'>
+        <NoticeBar
+          content='适用于当前页面内信息的通知，是一种较醒目的页面内通知方式'
+          wrap
+          color='alert'
+        />
+      </DemoBlock>
+
       <DemoBlock title='自定义' padding='0' background='transparent'>
         <Space block direction='vertical'>
           <NoticeBar
diff --git a/src/components/notice-bar/index.en.md b/src/components/notice-bar/index.en.md
index 39e7de768..35cd60489 100644
--- a/src/components/notice-bar/index.en.md
+++ b/src/components/notice-bar/index.en.md
@@ -14,16 +14,17 @@ It is applicable to the notification of information in the current page, which i
 
 ### Props
 
-| Name      | Description                                                          | Type                                        | Default            |
-| --------- | -------------------------------------------------------------------- | ------------------------------------------- | ------------------ |
-| closeable | Whether it can be closed                                             | `boolean`                                   | `false`            |
-| color     | The type of the NoticeBar                                            | `'default' \| 'alert' \| 'error' \| 'info'` | `'default'`        |
-| content   | The content of the NoticeBar                                         | `React.ReactNode`                           | -                  |
-| delay     | Delay to start scrolling, unit `ms`                                  | `number`                                    | `2000`             |
-| extra     | Additional operating area, displayed to the left of the close button | `React.ReactNode`                           | -                  |
-| icon      | Radio icon on the left                                               | `React.ReactNode`                           | `<SoundOutline />` |
-| onClose   | Callback when closed                                                 | `() => void`                                | -                  |
-| speed     | Scroll speed, unit `px/s`                                            | `number`                                    | `50`               |
+| Name | Description | Type | Default |
+| --- | --- | --- | --- |
+| closeable | Whether it can be closed | `boolean` | `false` |
+| color | The type of the NoticeBar | `'default' \| 'alert' \| 'error' \| 'info'` | `'default'` |
+| content | The content of the NoticeBar | `React.ReactNode` | - |
+| delay | Delay to start scrolling, unit `ms` | `number` | `2000` |
+| extra | Additional operating area, displayed to the left of the close button | `React.ReactNode` | - |
+| icon | Radio icon on the left | `React.ReactNode` | `<SoundOutline />` |
+| onClose | Callback when closed | `() => void` | - |
+| speed | Scroll speed, unit `px/s` | `number` | `50` |
+| wrap | Whether to display multiple lines | `boolean` | `false` |
 
 ### CSS Variables
 
@@ -32,6 +33,6 @@ It is applicable to the notification of information in the current page, which i
 | --background-color | background color                  | `#b2b2b2` |
 | --border-color     | border color                      | `#a0a0a0` |
 | --font-size        | Font size of notice text content. | `15px`    |
-| --height           | Height of the whole element.      | `38px`    |
+| --height           | Height of the whole element.      | `40px`    |
 | --icon-font-size   | Font size of left icon.           | `18px`    |
 | --text-color       | text color                        | `#ffffff` |
diff --git a/src/components/notice-bar/index.zh.md b/src/components/notice-bar/index.zh.md
index 968491b2c..7c9896a4e 100644
--- a/src/components/notice-bar/index.zh.md
+++ b/src/components/notice-bar/index.zh.md
@@ -14,16 +14,17 @@
 
 ### 属性
 
-| 属性      | 说明                             | 类型                                        | 默认值             |
-| --------- | -------------------------------- | ------------------------------------------- | ------------------ |
-| closeable | 是否可关闭                       | `boolean`                                   | `false`            |
-| color     | 通告栏的类型                     | `'default' \| 'alert' \| 'error' \| 'info'` | `'default'`        |
-| content   | 公告内容                         | `React.ReactNode`                           | -                  |
-| delay     | 开始滚动的延迟，单位 `ms`        | `number`                                    | `2000`             |
-| extra     | 额外操作区域，显示在关闭按钮左侧 | `React.ReactNode`                           | -                  |
-| icon      | 左侧广播图标                     | `React.ReactNode`                           | `<SoundOutline />` |
-| onClose   | 关闭时的回调                     | `() => void`                                | -                  |
-| speed     | 滚动速度，单位 `px/s`            | `number`                                    | `50`               |
+| 属性 | 说明 | 类型 | 默认值 |
+| --- | --- | --- | --- |
+| closeable | 是否可关闭 | `boolean` | `false` |
+| color | 通告栏的类型 | `'default' \| 'alert' \| 'error' \| 'info'` | `'default'` |
+| content | 公告内容 | `React.ReactNode` | - |
+| delay | 开始滚动的延迟，单位 `ms` | `number` | `2000` |
+| extra | 额外操作区域，显示在关闭按钮左侧 | `React.ReactNode` | - |
+| icon | 左侧广播图标 | `React.ReactNode` | `<SoundOutline />` |
+| onClose | 关闭时的回调 | `() => void` | - |
+| speed | 滚动速度，单位 `px/s` | `number` | `50` |
+| wrap | 是否多行展示 | `boolean` | `false` |
 
 ### CSS 变量
 
@@ -32,6 +33,6 @@
 | --background-color | 背景色         | `#ababab` |
 | --border-color     | 边框颜色       | `#999999` |
 | --font-size        | 文字字号       | `15px`    |
-| --height           | 整体高度       | `38px`    |
+| --height           | 整体高度       | `40px`    |
 | --icon-font-size   | 左侧图标的字号 | `18px`    |
 | --text-color       | 文字颜色       | `#ffffff` |
diff --git a/src/components/notice-bar/notice-bar.less b/src/components/notice-bar/notice-bar.less
index f87938f11..c6211f0c5 100644
--- a/src/components/notice-bar/notice-bar.less
+++ b/src/components/notice-bar/notice-bar.less
@@ -6,12 +6,11 @@
   --text-color: var(--adm-color-white);
   --font-size: var(--adm-font-size-7);
   --icon-font-size: var(--adm-font-size-10);
-  --height: 38px;
+  --height: 40px;
 
   height: var(--height);
   box-sizing: border-box;
   font-size: var(--font-size);
-  line-height: var(--height);
   padding: 0 12px;
   display: flex;
   align-items: center;
@@ -46,7 +45,6 @@
     flex-shrink: 0;
     margin-right: 8px;
     font-size: var(--icon-font-size);
-    line-height: var(--height);
   }
   & .@{class-prefix-notice-bar}-content {
     flex: 1;
@@ -59,10 +57,14 @@
     & .@{class-prefix-notice-bar}-content-inner {
       width: auto;
       transition-timing-function: linear;
-      position: absolute;
       white-space: nowrap;
+
+      .@{class-prefix-notice-bar}-wrap& {
+        white-space: normal;
+      }
     }
   }
+
   & .@{class-prefix-notice-bar}-right {
     flex-shrink: 0;
     margin-left: 12px;
@@ -80,4 +82,12 @@
   &-close-icon {
     font-size: var(--adm-font-size-10);
   }
+
+  &-wrap {
+    height: auto;
+    align-items: flex-start;
+    padding-top: 8px;
+    padding-bottom: 8px;
+    line-height: 22px;
+  }
 }
diff --git a/src/components/notice-bar/notice-bar.tsx b/src/components/notice-bar/notice-bar.tsx
index cf7d16a63..12bcb9167 100644
--- a/src/components/notice-bar/notice-bar.tsx
+++ b/src/components/notice-bar/notice-bar.tsx
@@ -10,22 +10,24 @@ import { useMutationEffect } from '../../utils/use-mutation-effect'
 const classPrefix = `adm-notice-bar`
 
 export type NoticeBarProps = {
-  /** 通告栏的类型 */
+  /** The type of the NoticeBar */
   color?: 'default' | 'alert' | 'error' | 'info'
-  /** 开始滚动的延迟，单位 ms */
+  /** TDelay to start scrolling, unit ms */
   delay?: number
-  /** 滚动速度，单位 px/s */
+  /** Scroll speed, unit px/s */
   speed?: number
-  /** 公告内容 */
+  /** The content of the NoticeBar */
   content: React.ReactNode
-  /** 是否可关闭 */
+  /** Whether it can be closed */
   closeable?: boolean
-  /** 关闭时的回调 */
+  /** Callback when closed */
   onClose?: () => void
-  /** 额外操作区域，显示在关闭按钮左侧 */
+  /** Additional operating area, displayed to the left of the close button */
   extra?: React.ReactNode
-  /** 左侧广播图标 */
+  /** Radio icon on the left */
   icon?: React.ReactNode
+  /** Whether to display multiple lines */
+  wrap?: boolean
 } & NativeProps<
   | '--background-color'
   | '--border-color'
@@ -39,6 +41,7 @@ const defaultProps = {
   color: 'default',
   delay: 2000,
   speed: 50,
+  wrap: false,
   icon: <SoundOutline />,
 }
 
@@ -56,7 +59,7 @@ export const NoticeBar = memo<NoticeBarProps>(p => {
   const animatingRef = useRef(false)
 
   function start() {
-    if (delayLockRef.current) return
+    if (delayLockRef.current || props.wrap) return
 
     const container = containerRef.current
     const text = textRef.current
@@ -111,7 +114,11 @@ export const NoticeBar = memo<NoticeBarProps>(p => {
 
   return withNativeProps(
     props,
-    <div className={classNames(classPrefix, `${classPrefix}-${props.color}`)}>
+    <div
+      className={classNames(classPrefix, `${classPrefix}-${props.color}`, {
+        [`${classPrefix}-wrap`]: props.wrap,
+      })}
+    >
       {props.icon && (
         <span className={`${classPrefix}-left`}>{props.icon}</span>
       )}
diff --git a/src/components/stepper/demos/demo1.tsx b/src/components/stepper/demos/demo1.tsx
index 02fd3b6d3..c7aeaa77d 100644
--- a/src/components/stepper/demos/demo1.tsx
+++ b/src/components/stepper/demos/demo1.tsx
@@ -29,6 +29,17 @@ export default () => {
       <DemoBlock title='格式化到一位小数'>
         <Stepper digits={1} />
       </DemoBlock>
+
+      <DemoBlock title='自定义格式'>
+        <Stepper
+          defaultValue={93}
+          formatter={value => `$ ${value}`}
+          parser={text => parseFloat(text.replace('$', ''))}
+          onChange={value => {
+            console.log(value, typeof value)
+          }}
+        />
+      </DemoBlock>
     </>
   )
 }
diff --git a/src/components/stepper/index.en.md b/src/components/stepper/index.en.md
index 897b7ccb7..b92c3bdbe 100644
--- a/src/components/stepper/index.en.md
+++ b/src/components/stepper/index.en.md
@@ -16,37 +16,39 @@ It is suitable for inputting and adjusting the current value within a certain ra
 
 ### Props
 
-| Name          | Description                                                                                    | Type                                              | Default |
-| ------------- | ---------------------------------------------------------------------------------------------- | ------------------------------------------------- | ------- |
-| allowEmpty    | Whether to allow empty content.                                                                | `boolean`                                         | `false` |
-| defaultValue  | Default value                                                                                  | `number \| null`                                  | `0`     |
-| digits        | Format to a fixed number of digits after the decimal point, set to `0` means format to integer | `number`                                          | -       |
-| disabled      | Whether to disabled Stepper                                                                    | `boolean`                                         | `false` |
-| inputReadOnly | Whether input readonly or not                                                                  | `boolean`                                         | `false` |
-| max           | Max value                                                                                      | `number`                                          | -       |
-| min           | Min value                                                                                      | `number`                                          | -       |
-| onBlur        | Triggered when the input lose focus                                                            | `(e: React.FocusEvent<HTMLInputElement>) => void` | -       |
-| onChange      | Callback when value is changed                                                                 | `(value: number \| null) => void`                 | -       |
-| onFocus       | Triggered when the input get focus                                                             | `(e: React.FocusEvent<HTMLInputElement>) => void` | -       |
-| step          | Change the number of steps each time, it can be a decimal                                      | `number`                                          | `1`     |
-| value         | Current number, controlled value                                                               | `number \| null`                                  | -       |
+| Name | Description | Type | Default | Version |
+| --- | --- | --- | --- | --- |
+| allowEmpty | Whether to allow empty content. | `boolean` | `false` |
+| defaultValue | Default value | `number \| null` | `0` |
+| digits | Format to a fixed number of digits after the decimal point, set to `0` means format to integer. Will use `formatter` as display value when configured | `number` | - |
+| disabled | Whether to disabled Stepper | `boolean` | `false` |
+| formatter | Format value in input | `(value?: number) => string` | - | 5.26.0 |
+| inputReadOnly | Whether input readonly or not | `boolean` | `false` |
+| max | Max value | `number` | - |
+| min | Min value | `number` | - |
+| onBlur | Triggered when the input lose focus | `(e: React.FocusEvent<HTMLInputElement>) => void` | - |
+| onChange | Callback when value is changed | `(value: number \| null) => void` | - |
+| onFocus | Triggered when the input get focus | `(e: React.FocusEvent<HTMLInputElement>) => void` | - |
+| parser | Parse input text into number which should work with `formatter` | `(text: string) => number` | - | 5.26.0 |
+| step | Change the number of steps each time, it can be a decimal | `number` | `1` |
+| value | Current number, controlled value | `number \| null` | - |
 
 When `allowEmpty` is `true`, the `value` parameter of `onChange` may be `null`, please pay attention when using it.
 
 ### CSS Variables
 
-| Name                      | Description                           | Default                     |
-| ------------------------- | ------------------------------------- | --------------------------- |
-| --active-border           | In the focus state, the border style. | `var(--border)`             |
-| --border                  | Border style.                         | `none`                      |
-| --border-inner            | Inner border style.                   | `solid 2px transparent`     |
-| --border-radius           | Radius of the stepper.                | `2px`                       |
-| --button-background-color | Background color of the button.       | `#f5f5f5`                   |
-| --button-font-size        | Font size of the button.              | `15px`                      |
-| --button-text-color       | Font color of the button.             | `var(--adm-color-primary)`  |
-| --button-width            | Width of the button.                  | `var(--height)`             |
-| --height                  | Height of the stepper.                | `28px`                      |
-| --input-background-color  | Background color of input.            | `#f5f5f5`                   |
-| --input-font-color        | Font color of the input.              | `var(--adm-color-text)`     |
-| --input-font-size         | Font size of input.                   | `var(--adm-font-size-main)` |
-| --input-width             | Width of the input.                   | `44px`                      |
+| Name | Description | Default |
+| --- | --- | --- |
+| --active-border | In the focus state, the border style. | `var(--border)` |
+| --border | Border style. | `none` |
+| --border-inner | Inner border style. | `solid 2px transparent` |
+| --border-radius | Radius of the stepper. | `2px` |
+| --button-background-color | Background color of the button. | `#f5f5f5` |
+| --button-font-size | Font size of the button. | `15px` |
+| --button-text-color | Font color of the button. | `var(--adm-color-primary)` |
+| --button-width | Width of the button. | `var(--height)` |
+| --height | Height of the stepper. | `28px` |
+| --input-background-color | Background color of input. | `#f5f5f5` |
+| --input-font-color | Font color of the input. | `var(--adm-color-text)` |
+| --input-font-size | Font size of input. | `var(--adm-font-size-main)` |
+| --input-width | Width of the input. | `44px` |
diff --git a/src/components/stepper/index.zh.md b/src/components/stepper/index.zh.md
index 6685bc2e5..fae09efa9 100644
--- a/src/components/stepper/index.zh.md
+++ b/src/components/stepper/index.zh.md
@@ -16,37 +16,39 @@
 
 ### 属性
 
-| 参数          | 说明                                                  | 类型                                              | 默认值  |
-| ------------- | ----------------------------------------------------- | ------------------------------------------------- | ------- |
-| allowEmpty    | 是否允许内容为空                                      | `boolean`                                         | `false` |
-| defaultValue  | 默认值                                                | `number \| null`                                  | `0`     |
-| digits        | 格式化到小数点后固定位数，设置为 `0` 表示格式化到整数 | `number`                                          | -       |
-| disabled      | 是否禁用步进器                                        | `boolean`                                         | `false` |
-| inputReadOnly | 输入框是否只读                                        | `boolean`                                         | `false` |
-| max           | 最大值                                                | `number`                                          | -       |
-| min           | 最小值                                                | `number`                                          | -       |
-| onBlur        | 输入框失去焦点时触发                                  | `(e: React.FocusEvent<HTMLInputElement>) => void` | -       |
-| onChange      | 变化时的回调                                          | `(value: number \| null) => void`                 | -       |
-| onFocus       | 输入框获得焦点时触发                                  | `(e: React.FocusEvent<HTMLInputElement>) => void` | -       |
-| step          | 每次改变步数，可以为小数                              | `number`                                          | `1`     |
-| value         | 当前数，受控值                                        | `number \| null`                                  | -       |
+| 参数 | 说明 | 类型 | 默认值 | 版本 |
+| --- | --- | --- | --- | --- |
+| allowEmpty | 是否允许内容为空 | `boolean` | `false` |
+| defaultValue | 默认值 | `number \| null` | `0` |
+| digits | 格式化到小数点后固定位数，设置为 `0` 表示格式化到整数。配置 `formatter` 时展示会以 `formatter` 为准 | `number` | - |
+| disabled | 是否禁用步进器 | `boolean` | `false` |
+| formatter | 格式化展示数值 | `(value?: number) => string` | - | 5.26.0 |
+| inputReadOnly | 输入框是否只读 | `boolean` | `false` |
+| max | 最大值 | `number` | - |
+| min | 最小值 | `number` | - |
+| onBlur | 输入框失去焦点时触发 | `(e: React.FocusEvent<HTMLInputElement>) => void` | - |
+| onChange | 变化时的回调 | `(value: number \| null) => void` | - |
+| onFocus | 输入框获得焦点时触发 | `(e: React.FocusEvent<HTMLInputElement>) => void` | - |
+| parser | 将输入解析为对应数字，一般配合 `formatter` 使用 | `(text: string) => number` | - | 5.26.0 |
+| step | 每次改变步数，可以为小数 | `number` | `1` |
+| value | 当前数，受控值 | `number \| null` | - |
 
 当 `allowEmpty` 为 `true` 时，`onChange` 的 `value` 参数可能会为 `null`，在使用时请留意。
 
 ### CSS 变量
 
-| 属性                      | 说明                              | 默认值                      |
-| ------------------------- | --------------------------------- | --------------------------- |
-| --active-border           | 输入框 Focus 状态下，四周边框样式 | `var(--border)`             |
-| --border                  | 组件四周边框的样式                | `none`                      |
-| --border-inner            | 组件内部边框的样式                | `solid 2px transparent`     |
-| --border-radius           | 组件整体的圆角                    | `2px`                       |
-| --button-background-color | 左右两侧按钮背景颜色              | `#f5f5f5`                   |
-| --button-font-size        | 左右两侧按钮文字大小              | `15px`                      |
-| --button-text-color       | 左右两侧按钮文字颜色              | `var(--adm-color-primary)`  |
-| --button-width            | 左右两侧按钮的宽度                | `var(--height)`             |
-| --height                  | 组件整体高度                      | `28px`                      |
-| --input-background-color  | 输入框的背景颜色                  | `#f5f5f5`                   |
-| --input-font-color        | 输入框文字颜色                    | `var(--adm-color-text)`     |
-| --input-font-size         | 输入框文字大小                    | `var(--adm-font-size-main)` |
-| --input-width             | 仅输入框的宽度                    | `44px`                      |
+| 属性 | 说明 | 默认值 |
+| --- | --- | --- |
+| --active-border | 输入框 Focus 状态下，四周边框样式 | `var(--border)` |
+| --border | 组件四周边框的样式 | `none` |
+| --border-inner | 组件内部边框的样式 | `solid 2px transparent` |
+| --border-radius | 组件整体的圆角 | `2px` |
+| --button-background-color | 左右两侧按钮背景颜色 | `#f5f5f5` |
+| --button-font-size | 左右两侧按钮文字大小 | `15px` |
+| --button-text-color | 左右两侧按钮文字颜色 | `var(--adm-color-primary)` |
+| --button-width | 左右两侧按钮的宽度 | `var(--height)` |
+| --height | 组件整体高度 | `28px` |
+| --input-background-color | 输入框的背景颜色 | `#f5f5f5` |
+| --input-font-color | 输入框文字颜色 | `var(--adm-color-text)` |
+| --input-font-size | 输入框文字大小 | `var(--adm-font-size-main)` |
+| --input-width | 仅输入框的宽度 | `44px` |
diff --git a/src/components/stepper/stepper.tsx b/src/components/stepper/stepper.tsx
index 481780759..b0fc94fc4 100644
--- a/src/components/stepper/stepper.tsx
+++ b/src/components/stepper/stepper.tsx
@@ -5,27 +5,13 @@ import { NativeProps, withNativeProps } from '../../utils/native-props'
 import { usePropsValue } from '../../utils/use-props-value'
 import { mergeProps } from '../../utils/with-default-props'
 import { bound } from '../../utils/bound'
-import Input, { InputProps } from '../input'
+import Input, { InputProps, InputRef } from '../input'
 import Button from '../button'
 import Big from 'big.js'
 import { useConfig } from '../config-provider'
 
 const classPrefix = `adm-stepper`
 
-function convertValueToText(value: number | null, digits?: number) {
-  if (value === null) return ''
-  if (digits !== undefined) {
-    return value.toFixed(digits)
-  } else {
-    return value.toString()
-  }
-}
-
-function convertTextToValue(text: string) {
-  if (text === '') return null
-  return parseFloat(text)
-}
-
 type ValueProps = {
   allowEmpty: true
   value?: number | null
@@ -48,6 +34,10 @@ export type StepperProps = Pick<InputProps, 'onFocus' | 'onBlur'> &
     digits?: number
     disabled?: boolean
     inputReadOnly?: boolean
+
+    // Format & Parse
+    parser?: (text: string) => number
+    formatter?: (value?: number) => string
   } & NativeProps<
     | '--height'
     | '--input-width'
@@ -73,24 +63,38 @@ const defaultProps = {
 
 export const Stepper: FC<StepperProps> = p => {
   const props = mergeProps(defaultProps, p)
-  const { disabled, step, max, min, inputReadOnly } = props
+  const { disabled, step, max, min, inputReadOnly, digits, formatter, parser } =
+    props
   const { locale } = useConfig()
 
-  // ============================== Focus ===============================
-  const [hasFocus, setHasFocus] = useState(false)
+  // ========================== Parse / Format ==========================
+  const parseValue = (text: string) => {
+    if (text === '') return null
+    return parser ? parser(text) : parseFloat(text)
+  }
+
+  const formatValue = (value: number | null) => {
+    if (value === null) return ''
+
+    if (formatter) {
+      return formatter(value)
+    } else if (digits !== undefined) {
+      return value.toFixed(digits)
+    } else {
+      return value.toString()
+    }
+  }
 
   // ======================== Value & InputValue ========================
   const [value, setValue] = usePropsValue<number | null>(props as any)
-  const [inputValue, setInputValue] = useState(() =>
-    convertValueToText(value, props.digits)
-  )
+  const [inputValue, setInputValue] = useState(() => formatValue(value))
 
   // >>>>> Value
   function setValueWithCheck(v: number) {
     if (isNaN(v)) return
     let target = bound(v, props.min, props.max)
-    if (props.digits !== undefined) {
-      target = parseFloat(target.toFixed(props.digits))
+    if (digits !== undefined) {
+      target = parseFloat(target.toFixed(digits))
     }
     setValue(target)
   }
@@ -98,7 +102,7 @@ export const Stepper: FC<StepperProps> = p => {
   // >>>>> Input
   const handleInputChange = (v: string) => {
     setInputValue(v)
-    const value = convertTextToValue(v)
+    const value = parseValue(v)
     if (value === null) {
       if (props.allowEmpty) {
         setValue(null)
@@ -110,6 +114,32 @@ export const Stepper: FC<StepperProps> = p => {
     }
   }
 
+  // ============================== Focus ===============================
+  const [focused, setFocused] = useState(false)
+  const inputRef = React.useRef<InputRef>(null)
+
+  function triggerFocus(nextFocus: boolean) {
+    setFocused(nextFocus)
+
+    // We will convert value to original text when focus
+    if (nextFocus) {
+      setInputValue(typeof value === 'number' ? String(value) : '')
+    }
+  }
+
+  useEffect(() => {
+    if (focused) {
+      inputRef.current?.nativeElement?.select?.()
+    }
+  }, [focused])
+
+  // Focus change to format value
+  useEffect(() => {
+    if (!focused) {
+      setInputValue(formatValue(value))
+    }
+  }, [focused, value, digits])
+
   // ============================ Operations ============================
   const handleMinus = () => {
     setValueWithCheck(
@@ -145,25 +175,12 @@ export const Stepper: FC<StepperProps> = p => {
     return false
   }
 
-  // ============================== Effect ==============================
-  useEffect(() => {
-    if (!hasFocus) {
-      setInputValue(convertValueToText(value, props.digits))
-    }
-  }, [hasFocus])
-
-  useEffect(() => {
-    if (!hasFocus) {
-      setInputValue(convertValueToText(value, props.digits))
-    }
-  }, [value, props.digits])
-
   // ============================== Render ==============================
   return withNativeProps(
     props,
     <div
       className={classNames(classPrefix, {
-        [`${classPrefix}-active`]: hasFocus,
+        [`${classPrefix}-active`]: focused,
       })}
     >
       <Button
@@ -179,9 +196,10 @@ export const Stepper: FC<StepperProps> = p => {
       </Button>
       <div className={`${classPrefix}-middle`}>
         <Input
+          ref={inputRef}
           className={`${classPrefix}-input`}
           onFocus={e => {
-            setHasFocus(true)
+            triggerFocus(true)
             props.onFocus?.(e)
           }}
           value={inputValue}
@@ -190,7 +208,7 @@ export const Stepper: FC<StepperProps> = p => {
           }}
           disabled={disabled}
           onBlur={e => {
-            setHasFocus(false)
+            triggerFocus(false)
             props.onBlur?.(e)
           }}
           readOnly={inputReadOnly}
diff --git a/src/components/water-mark/demos/demo1.tsx b/src/components/water-mark/demos/demo1.tsx
index a461c32b3..d98004d43 100644
--- a/src/components/water-mark/demos/demo1.tsx
+++ b/src/components/water-mark/demos/demo1.tsx
@@ -7,6 +7,10 @@ const textProps = {
   content: 'Ant Design Mobile',
 }
 
+const rowsTextProps = {
+  content: ['Ant Design Mobile', 'Ant Design Mobile Pro'],
+}
+
 const imageProps = {
   image:
     'https://gw.alipayobjects.com/zos/bmw-prod/59a18171-ae17-4fc5-93a0-2645f64a3aca.svg',
@@ -23,6 +27,8 @@ export default () => {
     <div className='water-mark-overlay'>
       <Button onClick={() => setProps(textProps)}>普通水印</Button>
       <br />
+      <Button onClick={() => setProps(rowsTextProps)}>多行文字水印</Button>
+      <br />
       <Button onClick={() => setProps(imageProps)}>图片水印</Button>
       <WaterMark {...props} />
     </div>
diff --git a/src/components/water-mark/index.en.md b/src/components/water-mark/index.en.md
index 06007f2e3..011b4d950 100644
--- a/src/components/water-mark/index.en.md
+++ b/src/components/water-mark/index.en.md
@@ -16,24 +16,24 @@ Suitable for preventing information theft.
 
 ### Props
 
-| Name        | Description                                                                                          | Type               | Default              |
-| ----------- | ---------------------------------------------------------------------------------------------------- | ------------------ | -------------------- |
-| content     | WaterMark text content                                                                               | `string`           | -                    |
-| fontColor   | WaterMark text color                                                                                 | `string`           | `rgba(0, 0, 0, .15)` |
-| fontSize    | WaterMark text font size                                                                             | `string \| number` | `16`                 |
-| fullPage    | Whether to cover the whole page                                                                      | `boolean`          | `true`               |
-| gapX        | Horizontal spacing between watermarks                                                                | `number`           | `24`                 |
-| gapY        | Vertical spacing between watermarks                                                                  | `number`           | `48`                 |
-| height      | The height of the WaterMark                                                                          | `number`           | `64`                 |
-| image       | Image source, it is recommended to export 2x or 3x image, give priority to image rendering watermark | `string`           | -                    |
-| imageHeight | Image height                                                                                         | `number`           | `64`                 |
-| imageWidth  | Image width                                                                                          | `number`           | `120`                |
-| rotate      | When the watermark is drawn, the angle of rotation, in °                                             | `number`           | `-22`                |
-| width       | The width of the WaterMark                                                                           | `number`           | `120`                |
-| zIndex      | Z-index of the added watermark element                                                               | `number`           | `2000`               |
+| Name | Description | Type | Default |
+| --- | --- | --- | --- |
+| content | WaterMark text content | `string \| string[]` | - |
+| fontColor | WaterMark text color | `string` | `rgba(0, 0, 0, .15)` |
+| fontSize | WaterMark text font size | `string \| number` | `16` |
+| fullPage | Whether to cover the whole page | `boolean` | `true` |
+| gapX | Horizontal spacing between watermarks | `number` | `24` |
+| gapY | Vertical spacing between watermarks | `number` | `48` |
+| height | The height of the WaterMark | `number` | `64` |
+| image | Image source, it is recommended to export 2x or 3x image, give priority to image rendering watermark | `string` | - |
+| imageHeight | Image height | `number` | `64` |
+| imageWidth | Image width | `number` | `120` |
+| rotate | When the watermark is drawn, the angle of rotation, in ° | `number` | `-22` |
+| width | The width of the WaterMark | `number` | `120` |
+| zIndex | Z-index of the added watermark element | `number` | `2000` |
 
 ### CSS Variables
 
-| Name      | Description                       | Default | Global                     |
-| --------- | --------------------------------- | ------- | -------------------------- |
-| --z-index | `z-index` of the water mark layer | `2000`  | `--adm-water-mark-z-index` |
+| Name | Description | Default | Global |
+| --- | --- | --- | --- |
+| --z-index | `z-index` of the water mark layer | `2000` | `--adm-water-mark-z-index` |
diff --git a/src/components/water-mark/index.zh.md b/src/components/water-mark/index.zh.md
index a0ae03dc0..62835fe31 100644
--- a/src/components/water-mark/index.zh.md
+++ b/src/components/water-mark/index.zh.md
@@ -16,21 +16,21 @@
 
 ### 属性
 
-| 参数        | 说明                                                 | 类型               | 默认值               |
-| ----------- | ---------------------------------------------------- | ------------------ | -------------------- |
-| content     | 水印文字内容                                         | `string`           | -                    |
-| fontColor   | 水印文字颜色                                         | `string`           | `rgba(0, 0, 0, .15)` |
-| fontSize    | 文字大小                                             | `string \| number` | `16`                 |
-| fullPage    | 是否覆盖整个页面                                     | `boolean`          | `true`               |
-| gapX        | 水印之间的水平间距                                   | `number`           | `24`                 |
-| gapY        | 水印之间的垂直间距                                   | `number`           | `48`                 |
-| height      | 水印的高度                                           | `number`           | `64`                 |
-| image       | 图片源，建议导出 2 倍或 3 倍图，优先使用图片渲染水印 | `string`           | -                    |
-| imageHeight | 图片高度                                             | `number`           | `64`                 |
-| imageWidth  | 图片宽度                                             | `number`           | `120`                |
-| rotate      | 水印绘制时，旋转的角度，单位 °                       | `number`           | `-22`                |
-| width       | 水印的宽度                                           | `number`           | `120`                |
-| zIndex      | 追加的水印元素的 z-index                             | `number`           | `2000`               |
+| 参数 | 说明 | 类型 | 默认值 |
+| --- | --- | --- | --- |
+| content | 水印文字内容 | `string \| string[]` | - |
+| fontColor | 水印文字颜色 | `string` | `rgba(0, 0, 0, .15)` |
+| fontSize | 文字大小 | `string \| number` | `16` |
+| fullPage | 是否覆盖整个页面 | `boolean` | `true` |
+| gapX | 水印之间的水平间距 | `number` | `24` |
+| gapY | 水印之间的垂直间距 | `number` | `48` |
+| height | 水印的高度 | `number` | `64` |
+| image | 图片源，建议导出 2 倍或 3 倍图，优先使用图片渲染水印 | `string` | - |
+| imageHeight | 图片高度 | `number` | `64` |
+| imageWidth | 图片宽度 | `number` | `120` |
+| rotate | 水印绘制时，旋转的角度，单位 ° | `number` | `-22` |
+| width | 水印的宽度 | `number` | `120` |
+| zIndex | 追加的水印元素的 z-index | `number` | `2000` |
 
 ### CSS 变量
 
diff --git a/src/components/water-mark/water-mark.tsx b/src/components/water-mark/water-mark.tsx
index 411d41d1a..2733e8804 100644
--- a/src/components/water-mark/water-mark.tsx
+++ b/src/components/water-mark/water-mark.tsx
@@ -15,7 +15,7 @@ export type WaterMarkProps = {
   image?: string
   imageWidth?: number
   imageHeight?: number
-  content?: string
+  content?: string | string[]
   fontColor?: string
   fontStyle?: 'none' | 'normal' | 'italic' | 'oblique'
   fontFamily?: string
@@ -53,7 +53,6 @@ export const WaterMark: FC<WaterMarkProps> = p => {
   useEffect(() => {
     const canvas = document.createElement('canvas')
     const ratio = window.devicePixelRatio
-
     const ctx = canvas.getContext('2d')
 
     const canvasWidth = `${(gapX + width) * ratio}px`
@@ -95,8 +94,13 @@ export const WaterMark: FC<WaterMarkProps> = p => {
         const markSize = Number(fontSize) * ratio
         ctx.font = `${fontStyle} normal ${fontWeight} ${markSize}px/${markHeight}px ${fontFamily}`
         ctx.fillStyle = fontColor
-
-        ctx.fillText(content, 0, 0)
+        if (Array.isArray(content)) {
+          content.forEach((item: string, index: number) =>
+            ctx.fillText(item, 0, index * markSize)
+          )
+        } else {
+          ctx.fillText(content, 0, 0)
+        }
         ctx.restore()
         setBase64Url(canvas.toDataURL())
       }


Test patch:
diff --git a/src/components/calendar/tests/__snapshots__/calendar.test.tsx.snap b/src/components/calendar/tests/__snapshots__/calendar.test.tsx.snap
index 9f74d9420..c33e4205d 100644
--- a/src/components/calendar/tests/__snapshots__/calendar.test.tsx.snap
+++ b/src/components/calendar/tests/__snapshots__/calendar.test.tsx.snap
@@ -375,7 +375,7 @@ exports[`Calendar controlled mode 1`] = `
         />
       </div>
       <div
-        class="adm-calendar-cell adm-calendar-cell-selected"
+        class="adm-calendar-cell adm-calendar-cell-selected adm-calendar-cell-selected-row-end"
       >
         <div
           class="adm-calendar-cell-top"
@@ -387,7 +387,7 @@ exports[`Calendar controlled mode 1`] = `
         />
       </div>
       <div
-        class="adm-calendar-cell adm-calendar-cell-selected"
+        class="adm-calendar-cell adm-calendar-cell-selected adm-calendar-cell-selected-row-begin"
       >
         <div
           class="adm-calendar-cell-top"
@@ -3321,7 +3321,7 @@ exports[`Calendar range mode 1`] = `
         />
       </div>
       <div
-        class="adm-calendar-cell adm-calendar-cell-selected"
+        class="adm-calendar-cell adm-calendar-cell-selected adm-calendar-cell-selected-row-end"
       >
         <div
           class="adm-calendar-cell-top"
@@ -3333,7 +3333,7 @@ exports[`Calendar range mode 1`] = `
         />
       </div>
       <div
-        class="adm-calendar-cell adm-calendar-cell-selected"
+        class="adm-calendar-cell adm-calendar-cell-selected adm-calendar-cell-selected-row-begin"
       >
         <div
           class="adm-calendar-cell-top"
@@ -3405,7 +3405,7 @@ exports[`Calendar range mode 1`] = `
         />
       </div>
       <div
-        class="adm-calendar-cell adm-calendar-cell-selected"
+        class="adm-calendar-cell adm-calendar-cell-selected adm-calendar-cell-selected-row-end"
       >
         <div
           class="adm-calendar-cell-top"
@@ -3417,7 +3417,7 @@ exports[`Calendar range mode 1`] = `
         />
       </div>
       <div
-        class="adm-calendar-cell adm-calendar-cell-selected"
+        class="adm-calendar-cell adm-calendar-cell-selected adm-calendar-cell-selected-row-begin"
       >
         <div
           class="adm-calendar-cell-top"
diff --git a/src/components/floating-bubble/tests/floating-bubble.test.tsx b/src/components/floating-bubble/tests/floating-bubble.test.tsx
index 140da43fc..3cd9e1ceb 100644
--- a/src/components/floating-bubble/tests/floating-bubble.test.tsx
+++ b/src/components/floating-bubble/tests/floating-bubble.test.tsx
@@ -1,4 +1,4 @@
-import React from 'react'
+import React, { useState } from 'react'
 import { render, fireEvent, waitFor, sleep } from 'testing'
 import FloatingBubble from '..'
 
@@ -146,4 +146,37 @@ describe('FloatingBubble', () => {
     await sleep(0)
     expect(btn).toHaveStyle('transform: translate(0px, 0px)')
   })
+
+  test('controlled mode', async () => {
+    const App = () => {
+      const [offset, setOffset] = useState({ x: -24, y: -24 })
+      return (
+        <FloatingBubble
+          onOffsetChange={offset => {
+            setOffset(offset)
+          }}
+          offset={offset}
+          axis='xy'
+        />
+      )
+    }
+    render(<App />)
+    const btn = document.querySelectorAll(`.${classPrefix}-button`)[0]
+    expect(btn).toHaveStyle('transform: translate(-24px, -24px)')
+
+    mockBoundaryRect()
+    fireEvent.mouseDown(btn, {
+      buttons: 1,
+      clientX: -27,
+      clientY: -27,
+    })
+    fireEvent.mouseMove(btn, {
+      buttons: 1,
+      clientX: 0,
+      clientY: 0,
+    })
+    mockButtonRect({ x: 0, y: 0 })
+    fireEvent.mouseUp(btn)
+    expect(btn).toHaveStyle('transform: translate(0px, 0px)')
+  })
 })
diff --git a/src/components/mask/tests/mask.test.tsx b/src/components/mask/tests/mask.test.tsx
index 996953542..d9675142a 100644
--- a/src/components/mask/tests/mask.test.tsx
+++ b/src/components/mask/tests/mask.test.tsx
@@ -73,6 +73,16 @@ describe('Mask', () => {
     )
   })
 
+  test('renders with custom color', () => {
+    const { getByTestId } = render(
+      <Mask visible color='rgba(125, 125, 125, 0.5)' data-testid='mask' />
+    )
+
+    expect(getByTestId('mask').style.background).toEqual(
+      `rgba(125, 125, 125, 0.5)`
+    )
+  })
+
   test('afterClose should be called when close', async () => {
     const afterClose = jest.fn()
 
diff --git a/src/components/notice-bar/tests/notice-bar.test.tsx b/src/components/notice-bar/tests/notice-bar.test.tsx
index 66673f0e7..75e5b1611 100644
--- a/src/components/notice-bar/tests/notice-bar.test.tsx
+++ b/src/components/notice-bar/tests/notice-bar.test.tsx
@@ -53,7 +53,7 @@ describe('NoticeBar', () => {
   })
 
   test('long content', async () => {
-    const { container, debug } = render(
+    const { container } = render(
       <NoticeBar
         delay={100}
         speed={500}
@@ -78,4 +78,21 @@ describe('NoticeBar', () => {
     await sleep(200)
     expect(container).toMatchSnapshot()
   })
+
+  test('`wrap` prop', () => {
+    render(
+      <NoticeBar
+        content={
+          <>
+            <div>1</div>
+            <div>2</div>
+          </>
+        }
+        wrap
+      />
+    )
+    expect(document.querySelector(`.${classPrefix}`)).toHaveClass(
+      `${classPrefix}-wrap`
+    )
+  })
 })
diff --git a/src/components/stepper/tests/stepper.test.tsx b/src/components/stepper/tests/stepper.test.tsx
index dd446835b..0e2023ec2 100644
--- a/src/components/stepper/tests/stepper.test.tsx
+++ b/src/components/stepper/tests/stepper.test.tsx
@@ -207,4 +207,29 @@ describe('stepper', () => {
       expect(input.value).toBe('1')
     })
   })
+
+  test('formatter & parser', () => {
+    const formatter = jest.fn((val?: number) => `$ ${val}`)
+    const parser = jest.fn((text: string) => parseFloat(text))
+
+    const { container } = render(
+      <Stepper formatter={formatter} parser={parser} />
+    )
+
+    const inputEle = container.querySelector('input') as HTMLInputElement
+    expect(inputEle.value).toEqual('$ 0')
+
+    fireEvent.focus(inputEle)
+    expect(inputEle.value).toEqual('0')
+
+    fireEvent.change(inputEle, {
+      target: {
+        value: 93,
+      },
+    })
+    expect(inputEle.value).toEqual('93')
+
+    fireEvent.blur(inputEle)
+    expect(inputEle.value).toEqual('$ 93')
+  })
 })
diff --git a/src/components/water-mark/tests/__snapshots__/water-mark.test.tsx.snap b/src/components/water-mark/tests/__snapshots__/water-mark.test.tsx.snap
index 468aa90e6..cd70dd038 100644
--- a/src/components/water-mark/tests/__snapshots__/water-mark.test.tsx.snap
+++ b/src/components/water-mark/tests/__snapshots__/water-mark.test.tsx.snap
@@ -17,3 +17,12 @@ exports[`WaterMark image watermark 1`] = `
   />
 </div>
 `;
+
+exports[`WaterMark more rows content watermark 1`] = `
+<div>
+  <div
+    class="adm-water-mark adm-water-mark-full-page"
+    style="z-index: 2000; background-size: 144px; background-image: url(data:image/png;base64,00);"
+  />
+</div>
+`;
diff --git a/src/components/water-mark/tests/water-mark.test.tsx b/src/components/water-mark/tests/water-mark.test.tsx
index a4927d866..5c3ffde47 100644
--- a/src/components/water-mark/tests/water-mark.test.tsx
+++ b/src/components/water-mark/tests/water-mark.test.tsx
@@ -24,6 +24,13 @@ describe('WaterMark', () => {
     expect(container).toMatchSnapshot()
   })
 
+  test('more rows content watermark', () => {
+    const { container } = render(
+      <WaterMark content={['Ant Design Mobile', 'Ant Design']} />
+    )
+    expect(container).toMatchSnapshot()
+  })
+
   test('image watermark', () => {
     const image =
       'https://gw.alipayobjects.com/zos/bmw-prod/59a18171-ae17-4fc5-93a0-2645f64a3aca.svg'