From bb9b8c49e34bc9068134e0cacfedc1c8e1c5160c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=B9=A4=E4=BB=99?= Date: Fri, 12 Apr 2024 17:23:15 +0800 Subject: [PATCH] feat(ConfigProvider): add icons config --- src/components/center-popup/center-popup.tsx | 7 +- src/components/check-list/check-list.tsx | 6 +- src/components/collapse/collapse.tsx | 52 +++++++----- src/components/collapse/index.en.md | 4 +- src/components/collapse/index.zh.md | 4 +- .../collapse/tests/collapse.test.tsx | 81 ++++++++++++++++++- .../config-provider/config-provider.tsx | 46 ++++++++++- src/components/config-provider/index.en.md | 29 +++++++ src/components/config-provider/index.zh.md | 29 +++++++ src/components/dropdown/dropdown.tsx | 37 +++++---- src/components/dropdown/index.en.md | 4 +- src/components/dropdown/index.zh.md | 4 +- src/components/dropdown/item.tsx | 32 ++++++-- src/components/form/form-item.tsx | 44 ++++++---- src/components/form/index.en.md | 1 + src/components/form/index.zh.md | 1 + src/components/input/index.en.md | 1 + src/components/input/index.zh.md | 1 + src/components/input/input.tsx | 16 +++- src/components/list/list-item.tsx | 20 +++-- src/components/nav-bar/demos/demo1.tsx | 8 +- src/components/nav-bar/index.en.md | 4 +- src/components/nav-bar/index.zh.md | 4 +- src/components/nav-bar/nav-bar.tsx | 27 +++++-- src/components/nav-bar/tests/nav-bar.test.tsx | 20 +++++ src/components/notice-bar/index.en.md | 1 + src/components/notice-bar/index.zh.md | 1 + src/components/notice-bar/notice-bar.less | 3 - src/components/notice-bar/notice-bar.tsx | 23 +++--- .../notice-bar/tests/notice-bar.test.tsx | 2 +- src/components/popup/index.en.md | 1 + src/components/popup/index.zh.md | 1 + ...pup-base-props.ts => popup-base-props.tsx} | 6 +- src/components/popup/popup.tsx | 7 +- src/components/result-page/result-page.tsx | 21 +---- src/components/result/result.tsx | 20 +---- .../result/tests/result-icon.test.tsx | 38 +++++++++ src/components/result/use-result-icon.tsx | 38 +++++++++ src/components/search-bar/index.en.md | 2 +- src/components/search-bar/index.zh.md | 2 +- src/components/search-bar/search-bar.tsx | 30 ++++--- src/components/toast/tests/toast.test.tsx | 8 +- src/components/toast/toast.less | 5 -- src/components/toast/toast.tsx | 24 +++--- .../virtual-input/virtual-input.tsx | 12 ++- src/utils/with-default-props.tsx | 1 + 46 files changed, 535 insertions(+), 193 deletions(-) create mode 100644 src/components/nav-bar/tests/nav-bar.test.tsx rename src/components/popup/{popup-base-props.ts => popup-base-props.tsx} (86%) create mode 100644 src/components/result/tests/result-icon.test.tsx create mode 100644 src/components/result/use-result-icon.tsx diff --git a/src/components/center-popup/center-popup.tsx b/src/components/center-popup/center-popup.tsx index 193600c029..4fcbf848d9 100644 --- a/src/components/center-popup/center-popup.tsx +++ b/src/components/center-popup/center-popup.tsx @@ -11,11 +11,11 @@ import classNames from 'classnames' import { NativeProps, withNativeProps } from '../../utils/native-props' import { ShouldRender } from '../../utils/should-render' import { useLockScroll } from '../../utils/use-lock-scroll' -import { CloseOutline } from 'antd-mobile-icons' import { defaultPopupBaseProps, PopupBaseProps, } from '../popup/popup-base-props' +import { useConfig } from '../config-provider' const classPrefix = 'adm-center-popup' @@ -38,7 +38,8 @@ const defaultProps = { } export const CenterPopup: FC = p => { - const props = mergeProps(defaultProps, p) + const { popup: componentConfig = {} } = useConfig() + const props = mergeProps(defaultProps, componentConfig, p) const unmountedRef = useUnmountedRef() const style = useSpring({ @@ -134,7 +135,7 @@ export const CenterPopup: FC = p => { props.onClose?.() }} > - + {props.closeIcon} )} {body} diff --git a/src/components/check-list/check-list.tsx b/src/components/check-list/check-list.tsx index c83efe1365..6052152125 100644 --- a/src/components/check-list/check-list.tsx +++ b/src/components/check-list/check-list.tsx @@ -1,11 +1,12 @@ import React from 'react' import type { FC, ReactNode } from 'react' +import { CheckOutline } from 'antd-mobile-icons' import { NativeProps, withNativeProps } from '../../utils/native-props' import List, { ListProps } from '../list' import { mergeProps } from '../../utils/with-default-props' import { CheckListContext } from './context' import { usePropsValue } from '../../utils/use-props-value' -import { CheckOutline } from 'antd-mobile-icons' +import { useConfig } from '../config-provider' const classPrefix = 'adm-check-list' @@ -30,7 +31,8 @@ const defaultProps = { } export const CheckList: FC = p => { - const props = mergeProps(defaultProps, p) + const { checkList: componentConfig = {} } = useConfig() + const props = mergeProps(defaultProps, componentConfig, p) const [value, setValue] = usePropsValue(props) diff --git a/src/components/collapse/collapse.tsx b/src/components/collapse/collapse.tsx index 569cb3dc4a..d6090103e1 100644 --- a/src/components/collapse/collapse.tsx +++ b/src/components/collapse/collapse.tsx @@ -1,15 +1,16 @@ +import { animated, useSpring } from '@react-spring/web' +import { useMount } from 'ahooks' +import classNames from 'classnames' +import type { FC, ReactElement, ReactNode } from 'react' import React, { isValidElement, useRef } from 'react' -import type { FC, ReactNode, ReactElement } from 'react' import { NativeProps, withNativeProps } from '../../utils/native-props' -import List from '../list' -import { DownOutline } from 'antd-mobile-icons' -import classNames from 'classnames' -import { useSpring, animated } from '@react-spring/web' -import { usePropsValue } from '../../utils/use-props-value' -import { useMount } from 'ahooks' import { useShouldRender } from '../../utils/should-render' -import { useIsomorphicUpdateLayoutEffect } from '../../utils/use-isomorphic-update-layout-effect' import { traverseReactNode } from '../../utils/traverse-react-node' +import { useIsomorphicUpdateLayoutEffect } from '../../utils/use-isomorphic-update-layout-effect' +import { usePropsValue } from '../../utils/use-props-value' +import { mergeProps } from '../../utils/with-default-props' +import { useConfig } from '../config-provider' +import List from '../list' const classPrefix = `adm-collapse` @@ -20,8 +21,12 @@ export type CollapsePanelProps = { forceRender?: boolean destroyOnClose?: boolean onClick?: (event: React.MouseEvent) => void - arrow?: ReactNode | ((active: boolean) => ReactNode) + arrowIcon?: ReactNode | ((active: boolean) => ReactNode) children?: ReactNode + /** + * @deprecated use `arrowIcon` instead + */ + arrow?: ReactNode | ((active: boolean) => ReactNode) } & NativeProps export const CollapsePanel: FC = () => { @@ -106,6 +111,10 @@ type ValueProps = { activeKey?: T defaultActiveKey?: T onChange?: (activeKey: T) => void + arrowIcon?: ReactNode | ((active: boolean) => ReactNode) + /** + * @deprecated use `arrowIcon` instead + */ arrow?: ReactNode | ((active: boolean) => ReactNode) } @@ -120,7 +129,11 @@ export type CollapseProps = ( children?: ReactNode } & NativeProps -export const Collapse: FC = props => { +const defaultProps = {} + +export const Collapse: FC = p => { + const { collapse: componentConfig = {} } = useConfig() + const props = mergeProps(defaultProps, componentConfig, p) const panels: ReactElement[] = [] traverseReactNode(props.children, child => { if (!isValidElement(child)) return @@ -196,15 +209,13 @@ export const Collapse: FC = props => { panel.props.onClick?.(event) } - const renderArrow = () => { - let arrow: CollapseProps['arrow'] = - if (props.arrow !== undefined) { - arrow = props.arrow - } - if (panel.props.arrow !== undefined) { - arrow = panel.props.arrow - } - return typeof arrow === 'function' ? ( + const arrow = + panel.props.arrow || + panel.props.arrowIcon || + props.arrow || + props.arrowIcon + const arrowIcon = + typeof arrow === 'function' ? ( arrow(active) ) : (
= props => { {arrow}
) - } return ( @@ -225,7 +235,7 @@ export const Collapse: FC = props => { className={`${classPrefix}-panel-header`} onClick={handleClick} disabled={panel.props.disabled} - arrow={renderArrow()} + arrowIcon={arrowIcon} > {panel.props.title} diff --git a/src/components/collapse/index.en.md b/src/components/collapse/index.en.md index fb6dd655c7..66e835d27f 100644 --- a/src/components/collapse/index.en.md +++ b/src/components/collapse/index.en.md @@ -21,7 +21,7 @@ A content area that can be collapsed/expanded. | --- | --- | --- | --- | | accordion | Whether to enable accordion mode | `boolean` | `false` | | activeKey | The `key` of the currently expanded panel | accordion mode: `string \| null`
non-accordion mode: `string[]` | - | -| arrow | Custom arrow. if you pass a ReactNode, antd-mobile will add a rotate animation for you. | `React.ReactNode \| ((active: boolean) => React.ReactNode)` | - | +| arrowIcon | Custom arrow icon. if you pass a ReactNode, antd-mobile will add a rotate animation for you. | `React.ReactNode \| ((active: boolean) => React.ReactNode)` | - | | defaultActiveKey | The `key` of the expanded panel by default | accordion mode: `string \| null`
non-accordion mode: `string[]` | - | | onChange | Triggered when the panel is switched | accordion mode: `(activeKey: string \| null) => void`
non-accordion mode: `(activeKey: string[]) => void` | - | @@ -31,7 +31,7 @@ A content area that can be collapsed/expanded. | Name | Description | Type | Default | | --- | --- | --- | --- | -| arrow | Custom arrow | `React.ReactNode \| ((active: boolean) => React.ReactNode)` | - | +| arrowIcon | Custom arrow icon | `React.ReactNode \| ((active: boolean) => React.ReactNode)` | - | | destroyOnClose | Destroy `dom` when not visible | `boolean` | `false` | | disabled | Whether disabled or not | `boolean` | `false` | | forceRender | Whether to render the `DOM` structure when hidden | `boolean` | `false` | diff --git a/src/components/collapse/index.zh.md b/src/components/collapse/index.zh.md index 44b5ef8779..dff5354cd2 100644 --- a/src/components/collapse/index.zh.md +++ b/src/components/collapse/index.zh.md @@ -21,7 +21,7 @@ | --- | --- | --- | --- | | accordion | 是否开启手风琴模式 | `boolean` | `false` | | activeKey | 当前展开面板的 `key` | 手风琴模式:`string \| null`
非手风琴模式:`string[]` | - | -| arrow | 自定义箭头,如果是 ReactNode,那么 antd-mobile 会自动为你增加旋转动画效果 | `ReactNode \| ((active: boolean) => React.ReactNode)` | - | +| arrowIcon | 自定义箭头图标,如果是 ReactNode,那么 antd-mobile 会自动为你增加旋转动画效果 | `ReactNode \| ((active: boolean) => React.ReactNode)` | - | | defaultActiveKey | 默认展开面板的 `key` | 手风琴模式:`string \| null`
非手风琴模式:`string[]` | - | | onChange | 切换面板时触发 | 手风琴模式:`(activeKey: string \| null) => void`
非手风琴模式:`(activeKey: string[]) => void` | - | @@ -31,7 +31,7 @@ | 属性 | 说明 | 类型 | 默认值 | | --- | --- | --- | --- | -| arrow | 自定义箭头 | `ReactNode \| ((active: boolean) => React.ReactNode)` | - | +| arrowIcon | 自定义箭头图标 | `ReactNode \| ((active: boolean) => React.ReactNode)` | - | | destroyOnClose | 不可见时是否销毁 `DOM` 结构 | `boolean` | `false` | | disabled | 是否为禁用状态 | `boolean` | `false` | | forceRender | 被隐藏时是否渲染 `DOM` 结构 | `boolean` | `false` | diff --git a/src/components/collapse/tests/collapse.test.tsx b/src/components/collapse/tests/collapse.test.tsx index 79321d614f..bbca16693a 100644 --- a/src/components/collapse/tests/collapse.test.tsx +++ b/src/components/collapse/tests/collapse.test.tsx @@ -1,5 +1,5 @@ -import { render, screen, testA11y, fireEvent, waitFor } from 'testing' import * as React from 'react' +import { fireEvent, render, screen, testA11y, waitFor } from 'testing' import Collapse from '../' const classPrefix = `adm-collapse` @@ -100,3 +100,82 @@ test('disabled', async () => { expect(screen.queryByTestId('first')).toBeVisible() expect(screen.queryByTestId('second')).toBe(null) }) + +describe('arrow', () => { + it('arrow (deprecated) of collapse', async () => { + render( + + +
这里是第一项的内容
+
+
+ ) + expect(screen.getByText('foobar')).toBeVisible() + }) + + it('arrow (deprecated) of panel', async () => { + render( + + +
这里是第一项的内容
+
+
+ ) + expect(screen.getByText('foobar')).toBeVisible() + }) + + it('arrow (deprecated) of both collapse and pannel', async () => { + render( + + +
这里是第一项的内容
+
+
+ ) + expect(screen.getByText('bar')).toBeVisible() + }) + + it('both arrow (deprecated) and arrowIcon of collapse', async () => { + render( + + +
这里是第一项的内容
+
+
+ ) + expect(screen.getByText('foo')).toBeVisible() + }) + + it('both (deprecated) arrow and arrowIcon of pannel', async () => { + render( + + +
这里是第一项的内容
+
+
+ ) + expect(screen.getByText('foo')).toBeVisible() + }) + + it('arrow (deprecated) of collapse and arrowIcon of panel', async () => { + render( + + +
这里是第一项的内容
+
+
+ ) + expect(screen.getByText('bar')).toBeVisible() + }) + + it('arrowIcon of collapse and arrow (deprecated) of panel', async () => { + render( + + +
这里是第一项的内容
+
+
+ ) + expect(screen.getByText('bar')).toBeVisible() + }) +}) diff --git a/src/components/config-provider/config-provider.tsx b/src/components/config-provider/config-provider.tsx index f608e45f5a..f66c425a73 100644 --- a/src/components/config-provider/config-provider.tsx +++ b/src/components/config-provider/config-provider.tsx @@ -1,10 +1,52 @@ -import React, { useContext } from 'react' -import type { FC, ReactNode } from 'react' +import React, { FC, ReactNode, useContext } from 'react' import { Locale } from '../../locales/base' import zhCN from '../../locales/zh-CN' type Config = { locale: Locale + checkList?: { + activeIcon?: ReactNode + } + collapse?: { + arrowIcon?: ReactNode | ((active: boolean) => ReactNode) + } + dropdown?: { + arrowIcon?: ReactNode + } + form?: { + helpIcon?: ReactNode + } + input?: { + clearIcon?: ReactNode + } + list?: { + arrowIcon?: ReactNode + } + navBar?: { + backIcon?: ReactNode + } + noticeBar?: { + icon?: ReactNode + closeIcon?: ReactNode + } + popup?: { + closeIcon?: ReactNode + } + result?: { + successIcon?: ReactNode + errorIcon?: ReactNode + infoIcon?: ReactNode + waitingIcon?: ReactNode + warningIcon?: ReactNode + } + searchBar?: { + searchIcon?: ReactNode + } + toast?: { + successIcon?: ReactNode + errorIcon?: ReactNode + loadingIcon?: ReactNode + } } export const defaultConfigRef: { diff --git a/src/components/config-provider/index.en.md b/src/components/config-provider/index.en.md index c03304a3fb..e435e5cfe1 100644 --- a/src/components/config-provider/index.en.md +++ b/src/components/config-provider/index.en.md @@ -1,7 +1,36 @@ # ConfigProvider +Configure locale messages and custom icons globally. + +## When to use + +- You want to use other languages than English +- You want to use your own icon set instead of `antd-mobile-icons` + ## Demos + +## ConfigProvider + +### Props + +| Name | Description | Type | Default | +| --- | --- | --- | --- | +| locale | Locale messages | `Locale` | [zh-CN] | +| checkList | CheckList config | `{ activeIcon?: ReactNode }` | - | +| collapse | Collapse config | `{ arrowIcon?: ReactNode \| ((active: boolean) => ReactNode) }` | - | +| dropdown | Dropdown config | `{ arrowIcon?: ReactNode }` | - | +| form | Form config | `{ helpIcon?: ReactNode }` | - | +| input | Input config | `{ clearIcon?: ReactNode }` | - | +| list | List config | `{ arrowIcon?: ReactNode }` | - | +| navBar | NavBar config | `{ backIcon?: ReactNode }` | - | +| noticeBar | NoticeBar config | `{ icon?: ReactNode, closeIcon?: ReactNode }` | - | +| popup | Popup config | `{ closeIcon?: ReactNode }` | - | +| result | Result config | `{ successIcon?: ReactNode, errorIcon?: ReactNode, infoIcon?: ReactNode, waitingIcon?: ReactNode, warningIcon?: ReactNode }` | - | +| searchBar | SearchBar config | `{ icon?: ReactNode }` | - | +| toast | Toast config | `{ successIcon?: ReactNode, failIcon?: ReactNode, loadingIcon?: ReactNode }` | - | + +[zh-CN]: https://github.com/ant-design/ant-design-mobile/blob/master/src/locales/zh-CN.ts diff --git a/src/components/config-provider/index.zh.md b/src/components/config-provider/index.zh.md index 84560f3eea..6011631fc5 100644 --- a/src/components/config-provider/index.zh.md +++ b/src/components/config-provider/index.zh.md @@ -1,7 +1,36 @@ # ConfigProvider 配置 +用于全局配置本地化文案和个性化图标。 + +## 何时使用 + +- 您想使用除英文之外的语言。 +- 您想使用自己的图标集,而不是内置的 `antd-mobile-icons`。 + ## 示例 + +## ConfigProvider + +### 属性 + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| locale | 本地化文案 | `Locale` | [zh-CN] | +| checkList | CheckList 配置 | `{ activeIcon?: ReactNode }` | - | +| collapse | Collapse 配置 | `{ arrowIcon?: ReactNode \| ((active: boolean) => ReactNode) }` | - | +| dropdown | Dropdown 配置 | `{ arrowIcon?: ReactNode }` | - | +| form | Form 配置 | `{ helpIcon?: ReactNode }` | - | +| input | Input 配置 | `{ clearIcon?: ReactNode }` | - | +| list | List 配置 | `{ arrowIcon?: ReactNode }` | - | +| navBar | NavBar 配置 | `{ backIcon?: ReactNode }` | - | +| noticeBar | NoticeBar 配置 | `{ icon?: ReactNode, closeIcon?: ReactNode }` | - | +| popup | Popup 配置 | `{ closeIcon?: ReactNode }` | - | +| result | Result 配置 | `{ successIcon?: ReactNode, errorIcon?: ReactNode, infoIcon?: ReactNode, waitingIcon?: ReactNode, warningIcon?: ReactNode }` | - | +| searchBar | SearchBar 配置 | `{ icon?: ReactNode }` | - | +| toast | Toast 配置 | `{ successIcon?: ReactNode, failIcon?: ReactNode, loadingIcon?: ReactNode }` | - | + +[zh-CN]: https://github.com/ant-design/ant-design-mobile/blob/master/src/locales/zh-CN.ts diff --git a/src/components/dropdown/dropdown.tsx b/src/components/dropdown/dropdown.tsx index 993ba22706..8ffd2db53b 100644 --- a/src/components/dropdown/dropdown.tsx +++ b/src/components/dropdown/dropdown.tsx @@ -1,26 +1,27 @@ import { useClickAway } from 'ahooks' import classNames from 'classnames' +import type { + ComponentProps, + PropsWithChildren, + ReactElement, + ReactNode, +} from 'react' import React, { cloneElement, + forwardRef, + isValidElement, useEffect, + useImperativeHandle, useRef, useState, - forwardRef, - useImperativeHandle, - isValidElement, } from 'react' -import type { - ReactNode, - ComponentProps, - PropsWithChildren, - ReactElement, -} from 'react' -import Popup, { PopupProps } from '../popup' -import Item, { ItemChildrenWrap } from './item' import { NativeProps, withNativeProps } from '../../utils/native-props' -import { mergeProps } from '../../utils/with-default-props' import { usePropsValue } from '../../utils/use-props-value' +import { mergeProps } from '../../utils/with-default-props' +import Popup, { PopupProps } from '../popup' import { defaultPopupBaseProps } from '../popup/popup-base-props' +import Item, { ItemChildrenWrap } from './item' +import { useConfig } from '../config-provider' const classPrefix = `adm-dropdown` @@ -30,6 +31,10 @@ export type DropdownProps = { closeOnMaskClick?: boolean closeOnClickAway?: boolean onChange?: (key: string | null) => void + arrowIcon?: ReactNode + /** + * @deprecated use `arrowIcon` instead + */ arrow?: ReactNode getContainer?: PopupProps['getContainer'] } & NativeProps @@ -47,7 +52,8 @@ export type DropdownRef = { const Dropdown = forwardRef>( (p, ref) => { - const props = mergeProps(defaultProps, p) + const { dropdown: componentConfig = {} } = useConfig() + const props = mergeProps(defaultProps, componentConfig, p) const [value, setValue] = usePropsValue({ value: props.activeKey, defaultValue: props.defaultActiveKey, @@ -95,7 +101,10 @@ const Dropdown = forwardRef>( }, active: child.key === value, arrow: - child.props.arrow === undefined ? props.arrow : child.props.arrow, + child.props.arrow || + child.props.arrowIcon || + props.arrow || + props.arrowIcon, } items.push(child) if (child.props.forceRender) popupForceRender = true diff --git a/src/components/dropdown/index.en.md b/src/components/dropdown/index.en.md index 915555728d..7d8efc45c0 100644 --- a/src/components/dropdown/index.en.md +++ b/src/components/dropdown/index.en.md @@ -17,7 +17,7 @@ It is suitable for filtering, sorting and changing the display range or order of | Name | Description | Type | Default | | --- | --- | --- | --- | | activeKey | Active `Item` key | `string \| null` | - | -| arrow | Custom arrow | `React.ReactNode` | - | +| arrowIcon | Custom arrow icon | `React.ReactNode` | - | | closeOnClickAway | Whether to automatically hide after clicking outside area | `boolean` | `false` | | closeOnMaskClick | Whether to automatically close after clicking on the mask | `boolean` | `true` | | defaultActiveKey | The default active `Item` key | `string \| null` | `null` | @@ -36,7 +36,7 @@ It is suitable for filtering, sorting and changing the display range or order of | Name | Description | Type | Default | | --- | --- | --- | --- | -| arrow | Custom arrow | `React.ReactNode` | - | +| arrowIcon | Custom arrow icon | `React.ReactNode` | - | | destroyOnClose | Destroy `dom` when not visible | `boolean` | `false` | | forceRender | Whether to render the content even if it is not active | `boolean` | `false` | | highlight | Highlight | `boolean` | `false` | diff --git a/src/components/dropdown/index.zh.md b/src/components/dropdown/index.zh.md index 8aba614b9a..629128911f 100644 --- a/src/components/dropdown/index.zh.md +++ b/src/components/dropdown/index.zh.md @@ -17,7 +17,7 @@ | 属性 | 说明 | 类型 | 默认值 | | --- | --- | --- | --- | | activeKey | 激活的 `Item` `key` | `string \| null` | - | -| arrow | 自定义 arrow | `React.ReactNode` | - | +| arrowIcon | 自定义箭头图标 | `React.ReactNode` | - | | closeOnClickAway | 是否在点击外部区域后自动隐藏 | `boolean` | `false` | | closeOnMaskClick | 是否在点击遮罩后自动隐藏 | `boolean` | `true` | | defaultActiveKey | 默认激活的 `Item` `key` | `string \| null` | `null` | @@ -36,7 +36,7 @@ | 属性 | 说明 | 类型 | 默认值 | | --- | --- | --- | --- | -| arrow | 自定义 arrow | `React.ReactNode` | - | +| arrowIcon | 自定义箭头图标 | `React.ReactNode` | - | | destroyOnClose | 不可见时是否销毁 `DOM` 结构 | `boolean` | `false` | | forceRender | 被隐藏时是否渲染 `DOM` 结构 | `boolean` | `false` | | highlight | 高亮 | `boolean` | `false` | diff --git a/src/components/dropdown/item.tsx b/src/components/dropdown/item.tsx index afa8c5ff16..1560272ceb 100644 --- a/src/components/dropdown/item.tsx +++ b/src/components/dropdown/item.tsx @@ -1,9 +1,11 @@ +import { DownFill } from 'antd-mobile-icons' import classNames from 'classnames' -import React from 'react' import type { FC, ReactNode } from 'react' +import React from 'react' import { NativeProps, withNativeProps } from '../../utils/native-props' import { useShouldRender } from '../../utils/should-render' -import { DownFill } from 'antd-mobile-icons' +import { mergeProps } from '../../utils/with-default-props' +import { useConfig } from '../config-provider' const classPrefix = `adm-dropdown-item` @@ -15,27 +17,41 @@ export type DropdownItemProps = { forceRender?: boolean destroyOnClose?: boolean onClick?: (event: React.MouseEvent) => void + arrowIcon?: ReactNode + /** + * @deprecated use `arrowIcon` instead + */ arrow?: ReactNode children?: ReactNode } & NativeProps +const defaultProps = { + arrowIcon: , +} + const Item: FC = props => { + const { dropdown: componentConfig = {} } = useConfig() + const { active, arrowIcon, arrow, highlight, onClick, title } = mergeProps( + defaultProps, + componentConfig, + props + ) const cls = classNames(classPrefix, { - [`${classPrefix}-active`]: props.active, - [`${classPrefix}-highlight`]: props.highlight ?? props.active, + [`${classPrefix}-active`]: active, + [`${classPrefix}-highlight`]: highlight ?? active, }) return withNativeProps( props, -
+
- {props.title} + {title} - {props.arrow === undefined ? : props.arrow} + {arrow || arrowIcon}
diff --git a/src/components/form/form-item.tsx b/src/components/form/form-item.tsx index 8a21f75027..c329db1a37 100644 --- a/src/components/form/form-item.tsx +++ b/src/components/form/form-item.tsx @@ -1,20 +1,21 @@ -import React, { useContext, useCallback, useState, useRef } from 'react' -import type { FC, ReactNode, MutableRefObject } from 'react' +import { QuestionCircleOutline } from 'antd-mobile-icons' import classNames from 'classnames' -import { NativeProps, withNativeProps } from '../../utils/native-props' import { Field, FormInstance } from 'rc-field-form' -import FieldContext from 'rc-field-form/lib/FieldContext' import type { FieldProps } from 'rc-field-form/lib/Field' -import type { Meta, InternalNamePath } from 'rc-field-form/lib/interface' -import type { FormLayout } from './index' +import FieldContext from 'rc-field-form/lib/FieldContext' +import type { InternalNamePath, Meta } from 'rc-field-form/lib/interface' +import type { FC, MutableRefObject, ReactNode } from 'react' +import React, { useCallback, useContext, useRef, useState } from 'react' import { devWarning } from '../../utils/dev-log' -import { FormContext, NoStyleItemContext } from './context' -import { toArray, isSafeSetRefComponent } from './utils' +import { NativeProps, withNativeProps } from '../../utils/native-props' +import { undefinedFallback } from '../../utils/undefined-fallback' +import { mergeProps } from '../../utils/with-default-props' +import { useConfig } from '../config-provider' import List, { ListItemProps } from '../list' import Popover from '../popover' -import { QuestionCircleOutline } from 'antd-mobile-icons' -import { useConfig } from '../config-provider' -import { undefinedFallback } from '../../utils/undefined-fallback' +import { FormContext, NoStyleItemContext } from './context' +import type { FormLayout } from './index' +import { isSafeSetRefComponent, toArray } from './utils' const NAME_SPLIT = '__SPLIT__' @@ -44,10 +45,11 @@ export type FormItemProps = Pick< > & Pick< ListItemProps, - 'style' | 'extra' | 'clickable' | 'arrow' | 'description' + 'style' | 'extra' | 'clickable' | 'arrow' | 'arrowIcon' | 'description' > & { label?: ReactNode help?: ReactNode + helpIcon?: ReactNode hasFeedback?: boolean required?: boolean noStyle?: boolean @@ -79,10 +81,12 @@ type FormItemLayoutProps = Pick< | 'disabled' | 'label' | 'help' + | 'helpIcon' | 'hidden' | 'layout' | 'extra' | 'clickable' + | 'arrowIcon' | 'arrow' | 'description' | 'childElementPosition' @@ -94,24 +98,30 @@ type FormItemLayoutProps = Pick< children: ReactNode } & NativeProps +const defaultProps = { + helpIcon: , +} + const FormItemLayout: FC = props => { + const { locale, form: componentConfig = {} } = useConfig() + const { style, extra, label, help, + helpIcon, required, children, htmlFor, hidden, + arrowIcon, arrow, childElementPosition = 'normal', - } = props + } = mergeProps(defaultProps, componentConfig, props) const context = useContext(FormContext) - const { locale } = useConfig() - const hasFeedback = props.hasFeedback !== undefined ? props.hasFeedback : context.hasFeedback const layout = props.layout || context.layout @@ -162,7 +172,7 @@ const FormItemLayout: FC = props => { e.preventDefault() }} > - + {helpIcon} )} @@ -210,7 +220,7 @@ const FormItemLayout: FC = props => { disabled={disabled} onClick={props.onClick} clickable={props.clickable} - arrow={arrow} + arrowIcon={arrow || arrowIcon} >
any` | - | | hasFeedback | Whether to show error feedback | `boolean` | `true` | | help | Prompt text | `ReactNode` | - | +| helpIcon | Prompt icon | `ReactNode` | `` | | hidden | Hide this field | `boolean` | `false` | | initialValue | Config sub default value. Form `initialValues` get higher priority when conflict. | `any` | - | | label | Label name | `ReactNode` | - | diff --git a/src/components/form/index.zh.md b/src/components/form/index.zh.md index c937dadfc1..2bc3bcf009 100644 --- a/src/components/form/index.zh.md +++ b/src/components/form/index.zh.md @@ -93,6 +93,7 @@ const validateMessages = { | getValueProps | 为子元素添加额外的属性 | `(value) => any` | - | | hasFeedback | 是否展示错误反馈 | `boolean` | `true` | | help | 提示文本 | `ReactNode` | - | +| helpIcon | 提示图标 | `ReactNode` | `` | | hidden | 是否隐藏整个字段 | `boolean` | `false` | | initialValue | 设置子元素默认值,如果与 Form 的 `initialValues` 冲突则以 Form 为准 | `any` | - | | label | 标签名 | `ReactNode` | - | diff --git a/src/components/input/index.en.md b/src/components/input/index.en.md index 5f75d954a0..4a3aa1bd35 100644 --- a/src/components/input/index.en.md +++ b/src/components/input/index.en.md @@ -21,6 +21,7 @@ The `Input` component is layout-independent. It only includes the most basic inp | Name | Description | Type | Default | | --- | --- | --- | --- | | clearable | Whether to enable the clear icon, the input box will be cleared after clicking the clear icon | `boolean` | `false` | +| clearIcon | Custom clear icon | `ReactNode` | `` | | defaultValue | The default value | `string` | - | | disabled | Whether it is disabled or not | `boolean` | `false` | | id | The `id` of the `input` element, usually used with `label` | `string` | - | diff --git a/src/components/input/index.zh.md b/src/components/input/index.zh.md index 72d69b76d1..4f3f937813 100644 --- a/src/components/input/index.zh.md +++ b/src/components/input/index.zh.md @@ -21,6 +21,7 @@ | 属性 | 说明 | 类型 | 默认值 | | --- | --- | --- | --- | | clearable | 是否启用清除图标,点击清除图标后会清空输入框 | `boolean` | `false` | +| clearIcon | 自定义清除图标 | `ReactNode` | `` | | defaultValue | 默认值 | `string` | - | | disabled | 是否禁用 | `boolean` | `false` | | id | `input` 元素的 `id`,常用来配合 `label` 使用 | `string` | - | diff --git a/src/components/input/input.tsx b/src/components/input/input.tsx index 94d8dfdc78..d4164c9927 100644 --- a/src/components/input/input.tsx +++ b/src/components/input/input.tsx @@ -1,4 +1,10 @@ -import React, { useState, forwardRef, useImperativeHandle, useRef } from 'react' +import React, { + useState, + forwardRef, + useImperativeHandle, + useRef, + ReactNode, +} from 'react' import { usePropsValue } from '../../utils/use-props-value' import { CloseCircleFill } from 'antd-mobile-icons' import { NativeProps, withNativeProps } from '../../utils/native-props' @@ -50,6 +56,7 @@ export type InputProps = Pick< defaultValue?: string onChange?: (val: string) => void clearable?: boolean + clearIcon?: ReactNode onlyShowClearWhenFocus?: boolean onClear?: () => void onEnterPress?: (e: React.KeyboardEvent) => void @@ -70,6 +77,7 @@ export type InputProps = Pick< const defaultProps = { defaultValue: '', + clearIcon: , onlyShowClearWhenFocus: true, } @@ -81,12 +89,12 @@ export type InputRef = { } export const Input = forwardRef((p, ref) => { - const props = mergeProps(defaultProps, p) + const { locale, input: componentConfig = {} } = useConfig() + const props = mergeProps(defaultProps, componentConfig, p) const [value, setValue] = usePropsValue(props) const [hasFocus, setHasFocus] = useState(false) const compositionStartRef = useRef(false) const nativeInputRef = useRef(null) - const { locale } = useConfig() useImperativeHandle(ref, () => ({ clear: () => { @@ -219,7 +227,7 @@ export const Input = forwardRef((p, ref) => { }} aria-label={locale.Input.clear} > - + {props.clearIcon}
)}
diff --git a/src/components/list/list-item.tsx b/src/components/list/list-item.tsx index d005c59ee1..a614bdb4e5 100644 --- a/src/components/list/list-item.tsx +++ b/src/components/list/list-item.tsx @@ -1,9 +1,10 @@ -import React from 'react' -import type { FC, ReactNode } from 'react' -import { NativeProps, withNativeProps } from '../../utils/native-props' import { RightOutline } from 'antd-mobile-icons' import classNames from 'classnames' +import type { FC, ReactNode } from 'react' +import React from 'react' import { isNodeWithContent } from '../../utils/is-node-with-content' +import { NativeProps, withNativeProps } from '../../utils/native-props' +import { useConfig } from '../config-provider' const classPrefix = `adm-list-item` @@ -14,16 +15,21 @@ export type ListItemProps = { prefix?: ReactNode extra?: ReactNode clickable?: boolean - arrow?: boolean | ReactNode + arrowIcon?: boolean | ReactNode disabled?: boolean onClick?: (e: React.MouseEvent) => void + /** + * @deprecated use `arrowIcon` instead + */ + arrow?: boolean | ReactNode } & NativeProps< '--prefix-width' | '--align-items' | '--active-background-color' > export const ListItem: FC = props => { + const { list: componentConfig = {} } = useConfig() const clickable = props.clickable ?? !!props.onClick - const arrow = props.arrow === undefined ? clickable : props.arrow + const arrow = props.arrow ?? props.arrowIcon ?? clickable const content = (
@@ -46,7 +52,9 @@ export const ListItem: FC = props => { )} {isNodeWithContent(arrow) && (
- {arrow === true ? : arrow} + {arrow === true + ? componentConfig?.arrowIcon || + : arrow}
)}
diff --git a/src/components/nav-bar/demos/demo1.tsx b/src/components/nav-bar/demos/demo1.tsx index ccd25f452e..d958efbbbe 100644 --- a/src/components/nav-bar/demos/demo1.tsx +++ b/src/components/nav-bar/demos/demo1.tsx @@ -1,7 +1,7 @@ -import React from 'react' import { NavBar, Space, Toast } from 'antd-mobile' +import { CloseOutline, MoreOutline, SearchOutline } from 'antd-mobile-icons' import { DemoBlock } from 'demos' -import { SearchOutline, MoreOutline, CloseOutline } from 'antd-mobile-icons' +import React from 'react' import './demo1.less' @@ -34,13 +34,13 @@ export default () => { - + 标题 - } onBack={back}> + } onBack={back}> 标题 diff --git a/src/components/nav-bar/index.en.md b/src/components/nav-bar/index.en.md index 4c3b2a88de..4690571dea 100644 --- a/src/components/nav-bar/index.en.md +++ b/src/components/nav-bar/index.en.md @@ -16,8 +16,8 @@ Need to display the title and action of the current page content. | Name | Description | Type | Default | | --- | --- | --- | --- | -| back | The returned text of the area, if `null` returned, `backArrow` would not be rendered | `ReacNode \| null` | `''` | -| backArrow | Whether to display the arrow of the return area, you can also pass in `ReactNode` for customization | `boolean \| ReactNode` | `true` | +| back | The returned text of the area, if `null` returned, `backIcon` would not be rendered | `ReacNode \| null` | `''` | +| backIcon | Whether to display the back arrow icon of the return area, you can also pass in `ReactNode` for customization | `boolean \| ReactNode` | `true` | | children | Title | `ReactNode` | - | | left | Content on the left, rendered on the right side of the return area | `ReactNode` | - | | onBack | Callback after clicking the return area | `() => void` | - | diff --git a/src/components/nav-bar/index.zh.md b/src/components/nav-bar/index.zh.md index 40f030c678..18ca1c4d25 100644 --- a/src/components/nav-bar/index.zh.md +++ b/src/components/nav-bar/index.zh.md @@ -16,8 +16,8 @@ | 属性 | 说明 | 类型 | 默认值 | | --- | --- | --- | --- | -| back | 返回区域的文字,如果为 `null` 的话,`backArrow` 也不会渲染 | `ReactNode \| null` | `''` | -| backArrow | 是否显示返回区域的箭头,也可以传入 `ReactNode` 进行自定义 | `boolean \| ReactNode` | `true` | +| back | 返回区域的文字,如果为 `null` 的话,`backIcon` 也不会渲染 | `ReactNode \| null` | `''` | +| backIcon | 是否显示返回区域的箭头,也可以传入 `ReactNode` 进行自定义 | `boolean \| ReactNode` | `true` | | children | 标题 | `ReactNode` | - | | left | 左侧内容,渲染在返回区域的右侧 | `ReactNode` | - | | onBack | 点击返回区域后的回调 | `() => void` | - | diff --git a/src/components/nav-bar/nav-bar.tsx b/src/components/nav-bar/nav-bar.tsx index 81ff6ef9c9..5060b3a0fe 100644 --- a/src/components/nav-bar/nav-bar.tsx +++ b/src/components/nav-bar/nav-bar.tsx @@ -1,14 +1,19 @@ -import React from 'react' -import type { FC, ReactNode } from 'react' -import classNames from 'classnames' import { LeftOutline } from 'antd-mobile-icons' +import classNames from 'classnames' +import type { FC, ReactNode } from 'react' +import React from 'react' import { NativeProps, withNativeProps } from '../../utils/native-props' import { mergeProps } from '../../utils/with-default-props' +import { useConfig } from '../config-provider' const classPrefix = `adm-nav-bar` export type NavBarProps = { back?: ReactNode + backIcon?: boolean | ReactNode + /** + * @deprecated use `backIcon` instead + */ backArrow?: boolean | ReactNode left?: ReactNode right?: ReactNode @@ -17,11 +22,13 @@ export type NavBarProps = { } & NativeProps<'--height' | '--border-bottom'> const defaultProps = { - backArrow: true, + backIcon: true, } + export const NavBar: FC = p => { - const props = mergeProps(defaultProps, p) - const { back, backArrow } = props + const { navBar: componentConfig = {} } = useConfig() + const props = mergeProps(defaultProps, componentConfig, p) + const { back, backIcon, backArrow } = props return withNativeProps( props, @@ -29,9 +36,13 @@ export const NavBar: FC = p => {
{back !== null && (
- {backArrow && ( + {(backIcon || backArrow) && ( - {backArrow === true ? : backArrow} + {backIcon === true || backArrow === true ? ( + + ) : ( + backIcon || backArrow + )} )} diff --git a/src/components/nav-bar/tests/nav-bar.test.tsx b/src/components/nav-bar/tests/nav-bar.test.tsx new file mode 100644 index 0000000000..3df9364009 --- /dev/null +++ b/src/components/nav-bar/tests/nav-bar.test.tsx @@ -0,0 +1,20 @@ +import React from 'react' +import { render, screen } from 'testing' +import NavBar from '..' + +const classPrefix = `adm-result` + +describe('NavBar', () => { + test('render title', () => { + render(Title) + expect(screen.getByText('Title')).toBeInTheDocument() + }) + test('render back arrow', () => { + render(Title) + expect(screen.getByText('Title')).toBeInTheDocument() + }) + test('render custom back arrow', () => { + render(Title) + expect(screen.getByText('Back')).toBeInTheDocument() + }) +}) diff --git a/src/components/notice-bar/index.en.md b/src/components/notice-bar/index.en.md index ab0c30fa87..4149d54876 100644 --- a/src/components/notice-bar/index.en.md +++ b/src/components/notice-bar/index.en.md @@ -17,6 +17,7 @@ It is applicable to the notification of information in the current page, which i | Name | Description | Type | Default | | --- | --- | --- | --- | | closeable | Whether it can be closed | `boolean` | `false` | +| closeIcon | Custom close button icon | `ReactNode` | `` | | 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` | diff --git a/src/components/notice-bar/index.zh.md b/src/components/notice-bar/index.zh.md index 65f5d0a240..14d5407e48 100644 --- a/src/components/notice-bar/index.zh.md +++ b/src/components/notice-bar/index.zh.md @@ -17,6 +17,7 @@ | 属性 | 说明 | 类型 | 默认值 | | --- | --- | --- | --- | | closeable | 是否可关闭 | `boolean` | `false` | +| closeIcon | 自定义关闭按钮图标 | `ReactNode` | `` | | color | 通告栏的类型 | `'default' \| 'alert' \| 'error' \| 'info'` | `'default'` | | content | 公告内容 | `React.ReactNode` | - | | delay | 开始滚动的延迟,单位 `ms` | `number` | `2000` | diff --git a/src/components/notice-bar/notice-bar.less b/src/components/notice-bar/notice-bar.less index d23d833844..13ef66513b 100644 --- a/src/components/notice-bar/notice-bar.less +++ b/src/components/notice-bar/notice-bar.less @@ -79,9 +79,6 @@ display: flex; align-items: center; justify-content: center; - } - - &-close-icon { font-size: var(--adm-font-size-10); } diff --git a/src/components/notice-bar/notice-bar.tsx b/src/components/notice-bar/notice-bar.tsx index 655cb462cd..cbc6480876 100644 --- a/src/components/notice-bar/notice-bar.tsx +++ b/src/components/notice-bar/notice-bar.tsx @@ -1,12 +1,13 @@ -import React, { useState, useRef, memo } from 'react' -import type { ReactNode } from 'react' -import classNames from 'classnames' -import { CloseOutline, SoundOutline } from 'antd-mobile-icons' import { useTimeout } from 'ahooks' -import { mergeProps } from '../../utils/with-default-props' +import { CloseOutline, SoundOutline } from 'antd-mobile-icons' +import classNames from 'classnames' +import type { ReactNode } from 'react' +import React, { memo, useRef, useState } from 'react' import { NativeProps, withNativeProps } from '../../utils/native-props' -import { useResizeEffect } from '../../utils/use-resize-effect' import { useMutationEffect } from '../../utils/use-mutation-effect' +import { useResizeEffect } from '../../utils/use-resize-effect' +import { mergeProps } from '../../utils/with-default-props' +import { useConfig } from '../config-provider' const classPrefix = `adm-notice-bar` @@ -21,6 +22,8 @@ export type NoticeBarProps = { content: ReactNode /** Whether it can be closed */ closeable?: boolean + /** Custom close icon */ + closeIcon?: ReactNode /** Callback when closed */ onClose?: () => void /** Event when click */ @@ -44,12 +47,14 @@ const defaultProps = { color: 'default', delay: 2000, speed: 50, - wrap: false, icon: , + closeIcon: , + wrap: false, } export const NoticeBar = memo(p => { - const props = mergeProps(defaultProps, p) + const { noticeBar: componentConfig = {} } = useConfig() + const props = mergeProps(defaultProps, componentConfig, p) const containerRef = useRef(null) const textRef = useRef(null) @@ -149,7 +154,7 @@ export const NoticeBar = memo(p => { props.onClose?.() }} > - + {props.closeIcon}
)} diff --git a/src/components/notice-bar/tests/notice-bar.test.tsx b/src/components/notice-bar/tests/notice-bar.test.tsx index 66490b704a..e0c1e542e6 100644 --- a/src/components/notice-bar/tests/notice-bar.test.tsx +++ b/src/components/notice-bar/tests/notice-bar.test.tsx @@ -30,7 +30,7 @@ describe('NoticeBar', () => { ) const el = getByTestId('notice') - const iconEl = el.querySelectorAll(`.${classPrefix}-close-icon`)[0] + const iconEl = el.querySelectorAll(`.${classPrefix}-close`)[0] expect(iconEl).toBeVisible() fireEvent.click(iconEl) diff --git a/src/components/popup/index.en.md b/src/components/popup/index.en.md index 33bc6e6cdd..35fc73e69e 100644 --- a/src/components/popup/index.en.md +++ b/src/components/popup/index.en.md @@ -24,6 +24,7 @@ It is suitable for displaying pop-up windows, information prompts, selection inp | bodyStyle | Content section style | `React.CSSProperties` | - | | className | Container class name | `string` | - | | closeOnMaskClick | Whether to close after clicking the mask layer | `boolean` | `false` | +| closeIcon | Custom close button icon | `ReactNode` | `` | | destroyOnClose | Destroy `dom` when not visible | `boolean` | `false` | | forceRender | Render content forcely | `boolean` | `false` | | getContainer | To get the specified mounted `HTML` node, the default is `body`, if `null` returned, it would be rendered to the current node | `HTMLElement \| () => HTMLElement \| null` | `() => document.body` | diff --git a/src/components/popup/index.zh.md b/src/components/popup/index.zh.md index 6c699b774c..88f9f7ad56 100644 --- a/src/components/popup/index.zh.md +++ b/src/components/popup/index.zh.md @@ -24,6 +24,7 @@ | bodyStyle | 内容区域样式 | `React.CSSProperties` | - | | className | 容器类名 | `string` | - | | closeOnMaskClick | 点击背景蒙层后是否关闭 | `boolean` | `false` | +| closeIcon | 自定义关闭按钮图标 | `ReactNode` | `` | | destroyOnClose | 不可见时是否销毁 `DOM` 结构 | `boolean` | `false` | | forceRender | 强制渲染内容 | `boolean` | `false` | | getContainer | 指定挂载的 `HTML` 节点,默认为 `body`,如果为 `null` 的话,会渲染到当前节点 | `HTMLElement \| () => HTMLElement \| null` | `() => document.body` | diff --git a/src/components/popup/popup-base-props.ts b/src/components/popup/popup-base-props.tsx similarity index 86% rename from src/components/popup/popup-base-props.ts rename to src/components/popup/popup-base-props.tsx index dc3817366a..5c47b61f39 100644 --- a/src/components/popup/popup-base-props.ts +++ b/src/components/popup/popup-base-props.tsx @@ -1,8 +1,8 @@ -import React from 'react' -import type { CSSProperties } from 'react' +import React, { CSSProperties, ReactNode } from 'react' import { GetContainer } from '../../utils/render-to-container' import { MaskProps } from '../mask' import { PropagationEvent } from '../../utils/with-stop-propagation' +import { CloseOutline } from 'antd-mobile-icons' export type PopupBaseProps = { afterClose?: () => void @@ -10,6 +10,7 @@ export type PopupBaseProps = { bodyClassName?: string bodyStyle?: CSSProperties closeOnMaskClick?: boolean + closeIcon?: ReactNode destroyOnClose?: boolean disableBodyScroll?: boolean forceRender?: boolean @@ -27,6 +28,7 @@ export type PopupBaseProps = { export const defaultPopupBaseProps = { closeOnMaskClick: false, + closeIcon: , destroyOnClose: false, disableBodyScroll: true, forceRender: false, diff --git a/src/components/popup/popup.tsx b/src/components/popup/popup.tsx index 45bb0570e9..c736cb0b08 100644 --- a/src/components/popup/popup.tsx +++ b/src/components/popup/popup.tsx @@ -10,7 +10,6 @@ import { renderToContainer } from '../../utils/render-to-container' import { useSpring, animated } from '@react-spring/web' import { withStopPropagation } from '../../utils/with-stop-propagation' import { ShouldRender } from '../../utils/should-render' -import { CloseOutline } from 'antd-mobile-icons' import { defaultPopupBaseProps, PopupBaseProps } from './popup-base-props' import { useInnerVisible } from '../../utils/use-inner-visible' import { useConfig } from '../config-provider' @@ -32,7 +31,8 @@ const defaultProps = { } export const Popup: FC = p => { - const props = mergeProps(defaultProps, p) + const { locale, popup: componentConfig = {} } = useConfig() + const props = mergeProps(defaultProps, componentConfig, p) const bodyCls = classNames( `${classPrefix}-body`, @@ -40,7 +40,6 @@ export const Popup: FC = p => { `${classPrefix}-body-position-${props.position}` ) - const { locale } = useConfig() const [active, setActive] = useState(props.visible) const ref = useRef(null) useLockScroll(ref, props.disableBodyScroll && active ? 'strict' : false) @@ -156,7 +155,7 @@ export const Popup: FC = p => { role='button' aria-label={locale.common.close} > - + {props.closeIcon} )} {props.children} diff --git a/src/components/result-page/result-page.tsx b/src/components/result-page/result-page.tsx index 34b8e4a703..71acddea86 100644 --- a/src/components/result-page/result-page.tsx +++ b/src/components/result-page/result-page.tsx @@ -1,29 +1,14 @@ import React, { useState } from 'react' import type { FC, ReactNode } from 'react' - -import { - CheckCircleFill, - CloseCircleFill, - InformationCircleFill, - ClockCircleFill, - ExclamationCircleFill, -} from 'antd-mobile-icons' import classNames from 'classnames' import { NativeProps, withNativeProps } from '../../utils/native-props' import { mergeProps } from '../../utils/with-default-props' import { isNodeWithContent } from '../../utils/is-node-with-content' import Button from '../button' +import { useResultIcon } from '../result/use-result-icon' const classPrefix = `adm-result-page` -const iconRecord = { - success: CheckCircleFill, - error: CloseCircleFill, - info: InformationCircleFill, - waiting: ClockCircleFill, - warning: ExclamationCircleFill, -} - interface ResultPageDetail { label: ReactNode value: ReactNode @@ -67,7 +52,7 @@ export const ResultPage: FC = p => { onPrimaryButtonClick, onSecondaryButtonClick, } = props - const resultIcon = icon || React.createElement(iconRecord[status]) + const fallbackIcon = useResultIcon(status) const [collapse, setCollapse] = useState(true) @@ -78,7 +63,7 @@ export const ResultPage: FC = p => { props,
-
{resultIcon}
+
{icon || fallbackIcon}
{title}
{isNodeWithContent(description) ? (
{description}
diff --git a/src/components/result/result.tsx b/src/components/result/result.tsx index 32041b9bef..ffe6da9d8c 100644 --- a/src/components/result/result.tsx +++ b/src/components/result/result.tsx @@ -1,26 +1,12 @@ import React from 'react' import type { FC, ReactNode } from 'react' import classNames from 'classnames' -import { - CheckCircleFill, - CloseCircleFill, - InformationCircleFill, - ClockCircleFill, - ExclamationCircleFill, -} from 'antd-mobile-icons' import { NativeProps, withNativeProps } from '../../utils/native-props' import { mergeProps } from '../../utils/with-default-props' +import { useResultIcon } from './use-result-icon' const classPrefix = `adm-result` -const iconRecord = { - success: CheckCircleFill, - error: CloseCircleFill, - info: InformationCircleFill, - waiting: ClockCircleFill, - warning: ExclamationCircleFill, -} - const defaultProps = { status: 'info', } @@ -35,13 +21,13 @@ export type ResultProps = { export const Result: FC = p => { const props = mergeProps(defaultProps, p) const { status, title, description, icon } = props + const fallbackIcon = useResultIcon(status) if (!status) return null - const resultIcon = icon || React.createElement(iconRecord[status]) return withNativeProps( props,
-
{resultIcon}
+
{icon || fallbackIcon}
{title}
{!!description && (
{description}
diff --git a/src/components/result/tests/result-icon.test.tsx b/src/components/result/tests/result-icon.test.tsx new file mode 100644 index 0000000000..ca7d63ab17 --- /dev/null +++ b/src/components/result/tests/result-icon.test.tsx @@ -0,0 +1,38 @@ +import React from 'react' +import { render, screen } from 'testing' +import Result from '..' +import { useResultIcon } from '../use-result-icon' + +const ResultIcon = (props: any) => { + const icon = useResultIcon(props.status) + return
{icon}
+} + +const classPrefix = `adm-result` + +describe('ResultIcon', () => { + test('renders with success status', () => { + render() + expect(document.querySelector('.antd-mobile-icon')).toBeTruthy() + }) + test('renders with error status', () => { + render() + expect(document.querySelector('.antd-mobile-icon')).toBeTruthy() + }) + test('renders with info status', () => { + render() + expect(document.querySelector('.antd-mobile-icon')).toBeTruthy() + }) + test('renders with waiting status', () => { + render() + expect(document.querySelector('.antd-mobile-icon')).toBeTruthy() + }) + test('renders with warning status', () => { + render() + expect(document.querySelector('.antd-mobile-icon')).toBeTruthy() + }) + test('renders with none status', () => { + render() + expect(document.querySelector('.antd-mobile-icon')).toBeFalsy() + }) +}) diff --git a/src/components/result/use-result-icon.tsx b/src/components/result/use-result-icon.tsx new file mode 100644 index 0000000000..c98e2ded9f --- /dev/null +++ b/src/components/result/use-result-icon.tsx @@ -0,0 +1,38 @@ +import React from 'react' +import { + CheckCircleFill, + CloseCircleFill, + InformationCircleFill, + ClockCircleFill, + ExclamationCircleFill, +} from 'antd-mobile-icons' +import { useConfig } from '../config-provider' + +export type ResultIconProps = {} + +export const useResultIcon = ( + status?: 'success' | 'error' | 'info' | 'waiting' | 'warning' +) => { + const { result: componentConfig = {} } = useConfig() + const { + successIcon = , + errorIcon = , + infoIcon = , + waitingIcon = , + warningIcon = , + } = componentConfig || {} + switch (status) { + case 'success': + return successIcon + case 'error': + return errorIcon + case 'info': + return infoIcon + case 'waiting': + return waitingIcon + case 'warning': + return warningIcon + default: + return null + } +} diff --git a/src/components/search-bar/index.en.md b/src/components/search-bar/index.en.md index dad981d8bf..ca7c5877c6 100644 --- a/src/components/search-bar/index.en.md +++ b/src/components/search-bar/index.en.md @@ -21,7 +21,7 @@ Narrow down the information pool to get targeted information quickly and easily. | cancelText | Text of the cancel button | `string` | `'取消'` | | clearOnCancel | Whether to clear the input after the cancel button is clicked | `boolean` | `true` | | clearable | Whether to enable the clear icon, the input would be cleared after the clear icon is clicked | `boolean` | `true` | -| icon | Icon | `ReactNode` | `` | +| searchIcon | Custom search icon | `ReactNode` | `` | | maxLength | The maximum number of characters the user can enter | `number` | - | | onBlur | Triggered when the input lose focus | `(e: React.FocusEvent) => void` | - | | onCancel | Triggered when the cancel button is clicked | `() => void` | - | diff --git a/src/components/search-bar/index.zh.md b/src/components/search-bar/index.zh.md index c0998f90bc..de6a440492 100644 --- a/src/components/search-bar/index.zh.md +++ b/src/components/search-bar/index.zh.md @@ -21,7 +21,7 @@ | cancelText | 取消按钮的文案 | `string` | `'取消'` | | clearOnCancel | 点击取消按钮后是否清空输入框 | `boolean` | `true` | | clearable | 是否启用清除图标,点击清除图标后会清空输入框 | `boolean` | `true` | -| icon | 图标 | `ReactNode` | `` | +| searchIcon | 自定义搜索图标 | `ReactNode` | `` | | maxLength | 输入的最大字符数 | `number` | - | | onBlur | 输入框失去焦点时触发 | `(e: React.FocusEvent) => void` | - | | onCancel | 点击取消按钮时触发 | `() => void` | - | diff --git a/src/components/search-bar/search-bar.tsx b/src/components/search-bar/search-bar.tsx index 0614f7207f..50b7e0e58e 100644 --- a/src/components/search-bar/search-bar.tsx +++ b/src/components/search-bar/search-bar.tsx @@ -1,13 +1,13 @@ -import React, { useState, useRef, forwardRef, useImperativeHandle } from 'react' -import type { ReactNode } from 'react' +import { SearchOutline } from 'antd-mobile-icons' import classNames from 'classnames' -import Input, { InputRef, InputProps } from '../input' -import Button from '../button' +import type { ReactNode } from 'react' +import React, { forwardRef, useImperativeHandle, useRef, useState } from 'react' import { NativeProps, withNativeProps } from '../../utils/native-props' -import { mergeProps } from '../../utils/with-default-props' -import { SearchOutline } from 'antd-mobile-icons' import { usePropsValue } from '../../utils/use-props-value' +import { mergeProps } from '../../utils/with-default-props' +import Button from '../button' import { useConfig } from '../config-provider' +import Input, { InputProps, InputRef } from '../input' const classPrefix = `adm-search-bar` @@ -25,6 +25,10 @@ export type SearchBarProps = Pick< onlyShowClearWhenFocus?: boolean showCancelButton?: boolean | ((focus: boolean, value: string) => boolean) cancelText?: string + searchIcon?: ReactNode + /** + * @deprecated use `searchIcon` instead + */ icon?: ReactNode clearOnCancel?: boolean onSearch?: (val: string) => void @@ -44,13 +48,14 @@ const defaultProps = { showCancelButton: false as NonNullable, defaultValue: '', clearOnCancel: true, - icon: , + searchIcon: , } export const SearchBar = forwardRef((p, ref) => { - const { locale } = useConfig() + const { locale, searchBar: componentConfig = {} } = useConfig() const props = mergeProps( defaultProps, + componentConfig, { cancelText: locale.common.cancel, }, @@ -111,13 +116,16 @@ export const SearchBar = forwardRef((p, ref) => { })} >
- {props.icon && ( -
{props.icon}
+ {(props.searchIcon || props.icon) && ( +
+ {props.searchIcon || props.icon} +
)} { fireEvent.click(getByText('success')) await waitForContentShow('content success') expect( - document.querySelectorAll(`.${classPrefix}-icon-success`)[0] + document.getElementById('CheckOutline-CheckOutline') ).toBeInTheDocument() fireEvent.click(getByText('fail')) await waitForContentShow('content fail') expect( - document.querySelectorAll(`.${classPrefix}-icon-fail`)[0] + document.getElementById('CloseOutline-CloseOutline') ).toBeInTheDocument() fireEvent.click(getByText('loading')) await waitForContentShow('content loading') - expect( - document.querySelectorAll(`.${classPrefix}-loading`)[0] - ).toBeInTheDocument() + expect(document.querySelector('.adm-spin-loading')).toBeInTheDocument() }) test('custom icon', async () => { diff --git a/src/components/toast/toast.less b/src/components/toast/toast.less index 7a23a018a7..d3d855f2ac 100644 --- a/src/components/toast/toast.less +++ b/src/components/toast/toast.less @@ -43,8 +43,3 @@ } } } - -.@{class-prefix-toast}-loading { - --size: 48px; - margin: 0 auto 8px; -} diff --git a/src/components/toast/toast.tsx b/src/components/toast/toast.tsx index b9e23ac7a9..fc045c1d76 100644 --- a/src/components/toast/toast.tsx +++ b/src/components/toast/toast.tsx @@ -1,13 +1,14 @@ -import React, { useMemo } from 'react' -import type { FC, ReactNode } from 'react' -import classNames from 'classnames' import { CheckOutline, CloseOutline } from 'antd-mobile-icons' -import Mask from '../mask' -import type { MaskProps } from '../mask' +import classNames from 'classnames' +import type { FC, ReactNode } from 'react' +import React, { useMemo } from 'react' +import { GetContainer } from '../../utils/render-to-container' import { mergeProps } from '../../utils/with-default-props' import { PropagationEvent } from '../../utils/with-stop-propagation' -import { GetContainer } from '../../utils/render-to-container' import AutoCenter from '../auto-center' +import { useConfig } from '../config-provider' +import type { MaskProps } from '../mask' +import Mask from '../mask' import SpinLoading from '../spin-loading' const classPrefix = `adm-toast` @@ -32,19 +33,22 @@ const defaultProps = { } export const InternalToast: FC = p => { - const props = mergeProps(defaultProps, p) + const { toast: componentConfig = {} } = useConfig() + const props = mergeProps(defaultProps, componentConfig, p) const { maskClickable, content, icon, position } = props const iconElement = useMemo(() => { if (icon === null || icon === undefined) return null switch (icon) { case 'success': - return + return componentConfig?.successIcon || case 'fail': - return + return componentConfig?.errorIcon || case 'loading': return ( - + componentConfig?.loadingIcon || ( + + ) ) default: return icon diff --git a/src/components/virtual-input/virtual-input.tsx b/src/components/virtual-input/virtual-input.tsx index 955496f4b0..bbdbacd183 100644 --- a/src/components/virtual-input/virtual-input.tsx +++ b/src/components/virtual-input/virtual-input.tsx @@ -25,7 +25,10 @@ export type VirtualInputProps = { keyboard?: ReactElement clearable?: boolean onClear?: () => void -} & Pick & +} & Pick< + InputProps, + 'value' | 'onChange' | 'placeholder' | 'disabled' | 'clearIcon' +> & NativeProps< | '--font-size' | '--color' @@ -38,6 +41,7 @@ export type VirtualInputProps = { const defaultProps = { defaultValue: '', + clearIcon: , } export type VirtualInputRef = { @@ -47,12 +51,12 @@ export type VirtualInputRef = { export const VirtualInput = forwardRef( (p, ref) => { - const props = mergeProps(defaultProps, p) + const { locale, input: componentConfig = {} } = useConfig() + const props = mergeProps(defaultProps, componentConfig, p) const [value, setValue] = usePropsValue(props) const rootRef = useRef(null) const contentRef = useRef(null) const [hasFocus, setHasFocus] = useState(false) - const { locale } = useConfig() function scrollToEnd() { const root = rootRef.current @@ -157,7 +161,7 @@ export const VirtualInput = forwardRef( role='button' aria-label={locale.Input.clear} > - + {props.clearIcon}
)} {[undefined, null, ''].includes(value) && ( diff --git a/src/utils/with-default-props.tsx b/src/utils/with-default-props.tsx index 3446d7dea6..84bbff841f 100644 --- a/src/utils/with-default-props.tsx +++ b/src/utils/with-default-props.tsx @@ -1,5 +1,6 @@ export function mergeProps(a: A, b: B): B & A export function mergeProps(a: A, b: B, c: C): C & B & A +export function mergeProps(a: A, b: B, c: C, d: D): D & C & B & A export function mergeProps(...items: any[]) { const ret: any = {} items.forEach(item => {