diff --git a/.dumi/theme/builtins/preview-default/Previewer.tsx b/.dumi/theme/builtins/preview-default/Previewer.tsx index 202df0fd04..1e63eae3db 100644 --- a/.dumi/theme/builtins/preview-default/Previewer.tsx +++ b/.dumi/theme/builtins/preview-default/Previewer.tsx @@ -5,7 +5,6 @@ import { history } from 'dumi' import type { IPreviewerComponentProps } from 'dumi/theme' import { context, - // useRiddle, useMotions, useCopy, useLocaleProps, @@ -66,9 +65,7 @@ const Previewer: React.FC = oProps => { const openCSB = useCodeSandbox( props.hideActions?.includes('CSB') ? null : props ) - // const openRiddle = useRiddle( - // props.hideActions?.includes('RIDDLE') ? null : props - // ) + const [execMotions, isMotionRunning] = useMotions( props.motions || [], demoRef.current @@ -119,14 +116,6 @@ const Previewer: React.FC = oProps => { onClick={openCSB} /> )} - {/*{openRiddle && (*/} - {/* */} - {/*)}*/} {props.motions && ( + + ) diff --git a/src/components/cascader/cascader.tsx b/src/components/cascader/cascader.tsx index 65e4bed8d3..3d4a8a0e49 100644 --- a/src/components/cascader/cascader.tsx +++ b/src/components/cascader/cascader.tsx @@ -40,6 +40,7 @@ export type CascaderProps = { title?: ReactNode confirmText?: ReactNode cancelText?: ReactNode + loading?: boolean children?: ( items: (CascaderOption | null)[], actions: CascaderActions diff --git a/src/components/cascader/index.en.md b/src/components/cascader/index.en.md index fab8b14e0b..78100ec70f 100644 --- a/src/components/cascader/index.en.md +++ b/src/components/cascader/index.en.md @@ -39,7 +39,7 @@ type CascaderValueExtend = { | children | Render function of the selected options | `(items: CascaderOption[], actions: CascaderActions) => ReactNode` | - | | confirmText | Text of the ok button | `ReactNode` | `'确定'` | | defaultValue | Default selected options | `CascaderValue[]` | `[]` | -| destroyOnClose | Unmount content when not visible | `boolean` | `true` | +| destroyOnClose | Destroy `dom` when not visible | `boolean` | `true` | | forceRender | Render content forcely | `boolean` | `false` | | onCancel | Triggered when cancelling | `() => void` | - | | onClose | Triggered when confirming or cancelling | `() => void` | - | @@ -51,6 +51,7 @@ type CascaderValueExtend = { | title | Title | `ReactNode` | - | | value | Selected options | `CascaderValue[]` | - | | visible | Whether to show or hide the Picker | `boolean` | `false` | +| loading | Open the skeleton screen | `boolean` | `false` | Please pay attention to the `children` property of `CascaderOption`. If the `children` of an `option` is `[]`, then when the user selects this `option`, the Cascader component will automatically jump to the next level, even if There are currently no options at this level (because Cascader has no way to determine whether this empty array will become an array with content in subsequent updates). Therefore, please make sure that the `children` property of the last level option (aka "leaf node") does not exist or has the value `undefined`, so that the Cascader component can correctly recognize it. diff --git a/src/components/cascader/index.zh.md b/src/components/cascader/index.zh.md index 294e77dc3f..9310cb7bd1 100644 --- a/src/components/cascader/index.zh.md +++ b/src/components/cascader/index.zh.md @@ -39,7 +39,7 @@ type CascaderValueExtend = { | children | 所选项的渲染函数 | `(items: CascaderOption[], actions: CascaderActions) => ReactNode` | - | | confirmText | 确定按钮的文字 | `ReactNode` | `'确定'` | | defaultValue | 默认选中项 | `CascaderValue[]` | `[]` | -| destroyOnClose | 不可见时卸载内容 | `boolean` | `true` | +| destroyOnClose | 不可见时是否销毁 `DOM` 结构 | `boolean` | `true` | | forceRender | 强制渲染内容 | `boolean` | `false` | | onCancel | 取消时触发 | `() => void` | - | | onClose | 确认和取消时都会触发关闭事件 | `() => void` | - | @@ -51,6 +51,7 @@ type CascaderValueExtend = { | title | 标题 | `ReactNode` | - | | value | 选中项 | `CascaderValue[]` | - | | visible | 是否显示级联选择 | `boolean` | `false` | +| loading | 开启骨架屏 | `boolean` | `false` | 请留意 `CascaderOption` 的 `children` 属性,如果某个 `option` 的 `children` 为 `[]`,那当用户选择了这个 `option` 时,Cascader 组件会自动跳转到下一级,即便这一级当前是没有任何选项的(因为 Cascader 没有办法判断,在后续的更新中,这个空数组会不会变为一个有内容的数组)。因此,请确保最末一级的 option(也就是"叶子节点")的 `children` 属性不存在或者值为 `undefined`,这样 Cascader 组件才能将其正确地识别。 diff --git a/src/components/collapse/index.en.md b/src/components/collapse/index.en.md index bd8105441e..fb6dd655c7 100644 --- a/src/components/collapse/index.en.md +++ b/src/components/collapse/index.en.md @@ -32,7 +32,7 @@ A content area that can be collapsed/expanded. | Name | Description | Type | Default | | --- | --- | --- | --- | | arrow | Custom arrow | `React.ReactNode \| ((active: boolean) => React.ReactNode)` | - | -| destroyOnClose | Unmount content when not visible | `boolean` | `false` | +| 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` | | key | The unique identifier | `string` | - | diff --git a/src/components/collapse/index.zh.md b/src/components/collapse/index.zh.md index 74671e77f1..44b5ef8779 100644 --- a/src/components/collapse/index.zh.md +++ b/src/components/collapse/index.zh.md @@ -32,7 +32,7 @@ | 属性 | 说明 | 类型 | 默认值 | | --- | --- | --- | --- | | arrow | 自定义箭头 | `ReactNode \| ((active: boolean) => React.ReactNode)` | - | -| destroyOnClose | 不可见时卸载内容 | `boolean` | `false` | +| destroyOnClose | 不可见时是否销毁 `DOM` 结构 | `boolean` | `false` | | disabled | 是否为禁用状态 | `boolean` | `false` | | forceRender | 被隐藏时是否渲染 `DOM` 结构 | `boolean` | `false` | | key | 唯一标识符 | `string` | - | diff --git a/src/components/dialog/index.en.md b/src/components/dialog/index.en.md index d534e76692..4a2375f800 100644 --- a/src/components/dialog/index.en.md +++ b/src/components/dialog/index.en.md @@ -28,7 +28,7 @@ When users need to process transactions, but do not want to jump to pages to int | closeOnAction | Whether to close after clicking the operation button | `boolean` | `false` | | closeOnMaskClick | Whether to support clicking the mask to close the dialog box | `boolean` | `false` | | content | The content of the Dialog | `React.ReactNode` | - | -| destroyOnClose | Unmount content when not visible | `boolean` | `false` | +| destroyOnClose | Destroy `dom` when not visible | `boolean` | `false` | | disableBodyScroll | Whether to disable `body` scrolling | `boolean` | `true` | | forceRender | Whether to render the `DOM` structure when hidden | `boolean` | `false` | | getContainer | The parent container of the custom dialog | `HTMLElement \| (() => HTMLElement) \| null` | `null` | diff --git a/src/components/dialog/index.zh.md b/src/components/dialog/index.zh.md index 641d7cea24..c22ecbd005 100644 --- a/src/components/dialog/index.zh.md +++ b/src/components/dialog/index.zh.md @@ -28,7 +28,7 @@ | closeOnAction | 点击操作按钮后后是否关闭 | `boolean` | `false` | | closeOnMaskClick | 是否支持点击遮罩关闭对话框 | `boolean` | `false` | | content | 对话框内容 | `React.ReactNode` | - | -| destroyOnClose | 不可见时卸载内容 | `boolean` | `false` | +| destroyOnClose | 不可见时是否销毁 `DOM` 结构 | `boolean` | `false` | | disableBodyScroll | 是否禁用 `body` 滚动 | `boolean` | `true` | | forceRender | 被隐藏时是否渲染 `DOM` 结构 | `boolean` | `false` | | getContainer | 自定义对话框的父容器 | `HTMLElement \| (() => HTMLElement) \| null` | `null` | diff --git a/src/components/dropdown/index.en.md b/src/components/dropdown/index.en.md index 0bcefd80eb..28f817a3db 100644 --- a/src/components/dropdown/index.en.md +++ b/src/components/dropdown/index.en.md @@ -37,7 +37,7 @@ It is suitable for filtering, sorting and changing the display range or order of | Name | Description | Type | Default | | --- | --- | --- | --- | | arrow | Custom arrow | `React.ReactNode` | - | -| destroyOnClose | Unmount content when not visible | `boolean` | `false` | +| 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` | | key | The unique value | `string` | - | diff --git a/src/components/dropdown/index.zh.md b/src/components/dropdown/index.zh.md index e0bbe9608d..6558b1b360 100644 --- a/src/components/dropdown/index.zh.md +++ b/src/components/dropdown/index.zh.md @@ -37,7 +37,7 @@ | 属性 | 说明 | 类型 | 默认值 | | -------------- | --------------------------- | ----------------- | ------- | | arrow | 自定义 arrow | `React.ReactNode` | - | -| destroyOnClose | 不可见时卸载内容 | `boolean` | `false` | +| destroyOnClose | 不可见时是否销毁 `DOM` 结构 | `boolean` | `false` | | forceRender | 被隐藏时是否渲染 `DOM` 结构 | `boolean` | `false` | | highlight | 高亮 | `boolean` | `false` | | key | 唯一值 | `string` | - | diff --git a/src/components/form/form-subscribe.tsx b/src/components/form/form-subscribe.tsx index 3b3a77de19..8f74fb6d92 100644 --- a/src/components/form/form-subscribe.tsx +++ b/src/components/form/form-subscribe.tsx @@ -25,7 +25,7 @@ export const FormSubscribe: FC = props => { // Memo to avoid useless render const childNode = React.useMemo( () => props.children(value, form), - [JSON.stringify(value)] + [JSON.stringify(value), props.children] ) return ( diff --git a/src/components/image-viewer/tests/image-viewer.test.tsx b/src/components/image-viewer/tests/image-viewer.test.tsx index a655cd8a99..0fa411ac04 100644 --- a/src/components/image-viewer/tests/image-viewer.test.tsx +++ b/src/components/image-viewer/tests/image-viewer.test.tsx @@ -166,7 +166,6 @@ describe('ImageViewer.Multi', () => { expect(renderer.getByText('3 / 4')).not.toBeNull() expect(renderer.container).toMatchSnapshot() }) - test('rendering with footer', () => { function App() { return ( @@ -180,7 +179,6 @@ describe('ImageViewer.Multi', () => { render() expect(screen.getByText('查看原图')).toBeInTheDocument() }) - test('`ImageViewer.Multi.show` should be work', async () => { render( <> @@ -196,16 +194,16 @@ describe('ImageViewer.Multi', () => { fireEvent.click(screen.getByText('show')) const imgs = await screen.findAllByRole('img') expect(imgs[0]).toBeVisible() - await userEvent.click(imgs[0]) + await act(async () => { + await userEvent.click(imgs[0]) + }) await waitFor(() => expect(imgs[0]).not.toBeVisible()) }) - test('slide should be work', async () => { Object.defineProperty(window, 'innerWidth', { value: 300, }) const onIndexChange = jest.fn() - render( ) - fireEvent.click(screen.getByText('show')) await screen.findAllByRole('img') const slides = document.querySelectorAll(`.${classPrefix}-slides`)[0] expect(screen.getByText('1 / 4')).toBeInTheDocument() - mockDrag(slides, [ - { - clientX: 300, - }, - { - clientX: 200, - }, - { - clientX: 100, - }, - ]) - await waitFor(() => expect(onIndexChange).toBeCalledWith(1)) + await act(async () => { + await mockDrag( + slides, + [ + { + clientX: 300, + }, + { + clientX: 200, + }, + { + clientX: 100, + }, + ], + 5 + ) + }) + + expect(onIndexChange).toBeCalledWith(1) expect(screen.getByText('2 / 4')).toBeInTheDocument() }) }) diff --git a/src/components/infinite-scroll/index.en.md b/src/components/infinite-scroll/index.en.md index bca49131af..4dfe7cee0b 100644 --- a/src/components/infinite-scroll/index.en.md +++ b/src/components/infinite-scroll/index.en.md @@ -45,7 +45,7 @@ function loadMore() { // ok ### Customized Content -If necessary, `` allows custom display content, this content can contain any element, including svg and elements with css animation. +If necessary, `InfiniteScroll` allows custom display content, this content can contain any element, including svg and elements with css animation. @@ -93,7 +93,9 @@ But in some scenarios (for example, when used with the `Tabs` component), you ma ``` -Problem description: The `Tabs` component displays the content of the first `Tab` item by default, so the content of the second `Tab` item `InfiniteScroll` is not visible. But the second `Tab` has a `forceRender` property added, so its content is rendered even if it is not visible. When the `InfiniteScroll` component is rendered this time, since the component is not visible, the `loadMore` function will not be called, which is normal and as expected. _However, when we switch to the second `Tab` to display the `InfiniteScroll` component, we find that the `InfiniteScroll` component does not call the `loadMore` function, which is different from what we expected. We hope that the `loadMore` function will be called at this time_. +Problem description: The `Tabs` component displays the content of the first `Tab` item by default, so the content of the second `Tab` item `InfiniteScroll` is not visible. But the second `Tab` has a `forceRender` property added, so its content is rendered even if it is not visible. When the `InfiniteScroll` component is rendered this time, since the component is not visible, the `loadMore` function will not be called, which is normal and as expected. + +_However, when we switch to the second `Tab` to display the `InfiniteScroll` component, we find that the `InfiniteScroll` component does not call the `loadMore` function, which is different from what we expected. We hope that the `loadMore` function will be called at this time_. Reason: When you click to switch the `Tab` item of the `Tabs` component, the highlight state of the `Tabs` component will be modified. At this time, the `Tabs` component will be re-rendered. However, it should be noted that **only the content of the `Tabs` component itself will be re-rendered, and the `InfiniteScroll` component is outside the `Tabs` component, not the `Tabs` component's own content**. So, when switching `Tab`, the `InfiniteScroll` component does not re-render, and it does not trigger its checking mechanism again. diff --git a/src/components/infinite-scroll/index.zh.md b/src/components/infinite-scroll/index.zh.md index 826240a932..47bf6d02e7 100644 --- a/src/components/infinite-scroll/index.zh.md +++ b/src/components/infinite-scroll/index.zh.md @@ -45,7 +45,7 @@ function loadMore() { // 正确 ### 自定义 Content -如果需要的话,`` 允许自定义展示内容,这个内容可以包含任何元素,包括 svg 和带有 css 动画的元素。 +如果需要的话,`InfiniteScroll` 允许自定义展示内容,这个内容可以包含任何元素,包括 svg 和带有 css 动画的元素。 @@ -93,7 +93,9 @@ InfiniteScroll 本身已经包含了防止并发的重复请求的逻辑,所 ``` -问题描述:`Tabs` 组件默认展示第一个 `Tab` 项的内容,所以,第二个 `Tab` 项的内容 `InfiniteScroll` 是不可见的。但第二个 `Tab` 添加了 `forceRender` 属性,所以即使不可见,其内容也会渲染。本次渲染 `InfiniteScroll`组件时,由于该组件不可见,所以,不会调用 `loadMore` 函数,这是正常的,跟我们的预期相同。_但是,当我们切换到第二个 `Tab` 展示 `InfiniteScroll` 组件时,发现 `InfiniteScroll` 组件并没有调用 `loadMore` 函数,这一点跟我们预期不同,我们希望此时 `loadMore` 函数被调用_。 +问题描述:`Tabs` 组件默认展示第一个 `Tab` 项的内容,所以,第二个 `Tab` 项的内容 `InfiniteScroll` 是不可见的。但第二个 `Tab` 添加了 `forceRender` 属性,所以即使不可见,其内容也会渲染。本次渲染 `InfiniteScroll`组件时,由于该组件不可见,所以,不会调用 `loadMore` 函数,这是正常的,跟我们的预期相同。 + +_但是,当我们切换到第二个 `Tab` 展示 `InfiniteScroll` 组件时,发现 `InfiniteScroll` 组件并没有调用 `loadMore` 函数,这一点跟我们预期不同,我们希望此时 `loadMore` 函数被调用_。 原因说明:点击切换 `Tabs` 组件的 `Tab` 项时,会修改 `Tabs` 组件的高亮状态,此时,`Tabs` 组件会重新渲染。但是,要注意的是**只有在 `Tabs` 组件自身的内容才会被重新渲染,而 `InfiniteScroll` 组件是在 `Tabs` 组件外部的,并非 `Tabs` 组件自身内容**。所以,切换 `Tab` 时,`InfiniteScroll` 组件并不会重新渲染,也就没有再次触发它的检查机制。 diff --git a/src/components/input/input.tsx b/src/components/input/input.tsx index 9d2a9a3159..0ab19f3d06 100644 --- a/src/components/input/input.tsx +++ b/src/components/input/input.tsx @@ -121,9 +121,13 @@ export const Input = forwardRef((p, ref) => { function checkValue() { let nextValue = value if (props.type === 'number') { - nextValue = + const boundValue = nextValue && bound(parseFloat(nextValue), props.min, props.max).toString() + // fix the display issue of numbers starting with 0 + if (Number(nextValue) !== Number(boundValue)) { + nextValue = boundValue + } } if (nextValue !== value) { setValue(nextValue) diff --git a/src/components/input/tests/input.test.tsx b/src/components/input/tests/input.test.tsx index c6373bee4b..46fee22293 100644 --- a/src/components/input/tests/input.test.tsx +++ b/src/components/input/tests/input.test.tsx @@ -190,4 +190,22 @@ describe('Input', () => { }) expect(ref.current?.nativeElement?.value).toBe('') }) + + test('numbers that start with 0 should be work', () => { + const ref = createRef() + render() + const input = document.querySelector('input')! + fireEvent.change(input, { + target: { value: '012' }, + }) + // input.blur() + act(() => { + input.focus() + }) + act(() => { + input.blur() + }) + + expect(input.value).toBe('012') + }) }) diff --git a/src/components/jumbo-tabs/index.en.md b/src/components/jumbo-tabs/index.en.md index d4b1e09e62..f96feea0bd 100644 --- a/src/components/jumbo-tabs/index.en.md +++ b/src/components/jumbo-tabs/index.en.md @@ -25,7 +25,7 @@ Used in lists or modules in presentational interfaces when options require furth | Name | Description | Type | Default | | --- | --- | --- | --- | | description | The description text | `ReactNode` | - | -| destroyOnClose | Unmount content when not visible | `boolean` | `false` | +| destroyOnClose | Destroy `dom` when not visible | `boolean` | `false` | | disabled | Whether to disable the tab | `boolean` | `false` | | forceRender | Whether to render the `DOM` structure when hidden | `boolean` | `false` | | key | Corresponding to `activeKey` | `string` | - | diff --git a/src/components/jumbo-tabs/index.zh.md b/src/components/jumbo-tabs/index.zh.md index c3d329e3ea..9a762cadb2 100644 --- a/src/components/jumbo-tabs/index.zh.md +++ b/src/components/jumbo-tabs/index.zh.md @@ -25,7 +25,7 @@ | 属性 | 说明 | 类型 | 默认值 | | -------------- | --------------------------- | ----------- | ------- | | description | 选项卡描述 | `ReactNode` | - | -| destroyOnClose | 不可见时卸载内容 | `boolean` | `false` | +| destroyOnClose | 不可见时是否销毁 `DOM` 结构 | `boolean` | `false` | | disabled | 是否禁用 | `boolean` | `false` | | forceRender | 被隐藏时是否渲染 `DOM` 结构 | `boolean` | `false` | | key | 对应 `activeKey` | `string` | - | diff --git a/src/components/list/list-item.tsx b/src/components/list/list-item.tsx index 9a6cc4b668..f1501e4dde 100644 --- a/src/components/list/list-item.tsx +++ b/src/components/list/list-item.tsx @@ -15,7 +15,7 @@ export type ListItemProps = { clickable?: boolean arrow?: boolean | ReactNode disabled?: boolean - onClick?: (e: React.MouseEvent) => void + onClick?: (e: React.MouseEvent) => void } & NativeProps< '--prefix-width' | '--align-items' | '--active-background-color' > diff --git a/src/components/mask/index.en.md b/src/components/mask/index.en.md index bae4f6a1f4..97a8fcf1a3 100644 --- a/src/components/mask/index.en.md +++ b/src/components/mask/index.en.md @@ -19,7 +19,7 @@ Often used in the background layer of a modal window to make the visual focus st | 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` | +| destroyOnClose | Destroy `dom` when not visible | `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` | diff --git a/src/components/mask/index.zh.md b/src/components/mask/index.zh.md index 63b9223601..1ec0542b76 100644 --- a/src/components/mask/index.zh.md +++ b/src/components/mask/index.zh.md @@ -19,7 +19,7 @@ | afterClose | 完全关闭后触发 | `() => void` | - | | afterShow | 完全展示后触发 | `() => void` | - | | color | 背景蒙层的颜色 | `'black' \| 'white' \| string` | `'black'` | -| destroyOnClose | 不可见时卸载内容 | `boolean` | `false` | +| destroyOnClose | 不可见时是否销毁 `DOM` 结构 | `boolean` | `false` | | disableBodyScroll | 是否禁用 `body` 滚动 | `boolean` | `true` | | forceRender | 强制渲染内容 | `boolean` | `false` | | getContainer | 指定挂载的 `HTML` 节点,如果为 `null` 的话,会渲染到当前节点 | `HTMLElement \| () => HTMLElement \| null` | `null` | diff --git a/src/components/modal/index.en.md b/src/components/modal/index.en.md index ea442e86a4..47549a6e3c 100644 --- a/src/components/modal/index.en.md +++ b/src/components/modal/index.en.md @@ -28,7 +28,7 @@ When users need to process transactions, but do not want to jump to pages to int | closeOnAction | Whether to close after clicking the operation button | `boolean` | `false` | | closeOnMaskClick | Whether to support clicking the mask to close the modal box | `boolean` | `false` | | content | The content of the Modal | `React.ReactNode` | - | -| destroyOnClose | Unmount content when not visible | `boolean` | `false` | +| destroyOnClose | Destroy `dom` when not visible | `boolean` | `false` | | disableBodyScroll | Mask Whether to disable `body` scrolling | `boolean` | `true` | | forceRender | Whether to render the `DOM` structure when hidden | `boolean` | `false` | | getContainer | The parent container of the custom modal | `HTMLElement \| (() => HTMLElement) \| null` | `null` | diff --git a/src/components/modal/index.zh.md b/src/components/modal/index.zh.md index d20e63583b..75724b9e85 100644 --- a/src/components/modal/index.zh.md +++ b/src/components/modal/index.zh.md @@ -28,7 +28,7 @@ | closeOnAction | 点击操作按钮后后是否关闭 | `boolean` | `false` | | closeOnMaskClick | 是否支持点击遮罩关闭弹窗 | `boolean` | `false` | | content | 弹窗内容 | `React.ReactNode` | - | -| destroyOnClose | 不可见时卸载内容 | `boolean` | `false` | +| destroyOnClose | 不可见时是否销毁 `DOM` 结构 | `boolean` | `false` | | disableBodyScroll | 背景蒙层是否禁用 `body` 滚动 | `boolean` | `true` | | forceRender | 被隐藏时是否渲染 `DOM` 结构 | `boolean` | `false` | | getContainer | 自定义弹窗的父容器 | `HTMLElement \| (() => HTMLElement) \| null` | `null` | diff --git a/src/components/notice-bar/notice-bar.less b/src/components/notice-bar/notice-bar.less index f34e1ae0b4..bb2082e372 100644 --- a/src/components/notice-bar/notice-bar.less +++ b/src/components/notice-bar/notice-bar.less @@ -66,6 +66,8 @@ } & .@{class-prefix-notice-bar}-right { + display: flex; + align-items: center; flex-shrink: 0; margin-left: 12px; } diff --git a/src/components/number-keyboard/index.en.md b/src/components/number-keyboard/index.en.md index f306d4394d..b778566a90 100644 --- a/src/components/number-keyboard/index.en.md +++ b/src/components/number-keyboard/index.en.md @@ -27,7 +27,7 @@ If possible, we recommend using the native keyboard provided by the system or cl | closeOnConfirm | Whether to automatically close when the ok button is clicked | `boolean` | `true` | | confirmText | The text of the confirm button, if `null` is set, it would be shown | `string \| null` | `null` | | customKey | Customized button | `string \| [string, string]` | - | -| destroyOnClose | Unmount content when not visible | `boolean` | `false` | +| 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` | | onClose | Triggered when it is clicked | `() => void` | - | diff --git a/src/components/number-keyboard/index.zh.md b/src/components/number-keyboard/index.zh.md index 37c3c43493..71464df5b9 100644 --- a/src/components/number-keyboard/index.zh.md +++ b/src/components/number-keyboard/index.zh.md @@ -27,7 +27,7 @@ | closeOnConfirm | 是否在点击确定按钮时自动关闭 | `boolean` | `true` | | confirmText | 完成按钮文案,`null` 不展示 | `string \| null` | `null` | | customKey | 自定义按钮 | `string \| [string, string]` | - | -| destroyOnClose | 不可见时卸载内容 | `boolean` | `false` | +| destroyOnClose | 不可见时是否销毁 `DOM` 结构 | `boolean` | `false` | | forceRender | 强制渲染内容 | `boolean` | `false` | | getContainer | 指定挂载的 HTML 节点,默认为 `body`,如果为 `null` 的话,会渲染到当前节点 | `HTMLElement \| () => HTMLElement \| null` | `() => document.body` | | onClose | 点击关闭时触发 | `() => void` | - | diff --git a/src/components/passcode-input/tests/passcode-input.test.tsx b/src/components/passcode-input/tests/passcode-input.test.tsx index 31261ca825..ccd8e66020 100644 --- a/src/components/passcode-input/tests/passcode-input.test.tsx +++ b/src/components/passcode-input/tests/passcode-input.test.tsx @@ -69,7 +69,9 @@ describe('PasscodeInput', () => { render() const input = screen.getByRole('button', { name: '密码输入框' }) fireEvent.focus(input) - await userEvent.keyboard('abc') + await act(async () => { + await userEvent.keyboard('abc') + }) expect(input).toHaveTextContent('abc') }) @@ -91,7 +93,9 @@ describe('PasscodeInput', () => { const input = screen.getByRole('button', { name: '密码输入框' }) fireEvent.focus(input) expect(onFocus).toBeCalled() - await userEvent.keyboard('abcde') + await act(async () => { + await userEvent.keyboard('abcde') + }) expect(onFill).toBeCalled() expect(onChange).toBeCalledTimes(4) fireEvent.blur(input) diff --git a/src/components/picker/index.en.md b/src/components/picker/index.en.md index 4c982fd9b6..6089776dda 100644 --- a/src/components/picker/index.en.md +++ b/src/components/picker/index.en.md @@ -43,7 +43,7 @@ type PickerValueExtend = { | columns | Options to configure each column | `PickerColumn[] \| ((value: PickerValue[]) => PickerColumn[])` | - | | confirmText | Text of the ok button | `ReactNode` | `'确定'` | | defaultValue | Default selected options | `PickerValue[]` | `[]` | -| destroyOnClose | Unmount content when not visible | `boolean` | `false` | +| destroyOnClose | Destroy `dom` when not visible | `boolean` | `false` | | forceRender | Render content forcely | `boolean` | `false` | | mouseWheel | Whether to allow interact with mouse wheel | `boolean` | `false` | | onCancel | Triggered when cancelling | `() => void` | - | @@ -154,7 +154,7 @@ type PickerDate = Date & { | --- | --- | --- | --- | --- | | children | The rendering function of the selected items | `(value: PickerDate, actions: PickerActions) => ReactNode` | - | | defaultValue | Default selected value | `PickerDate` | - | -| destroyOnClose | Unmount content when not visible | `boolean` | `false` | +| destroyOnClose | Destroy `dom` when not visible | `boolean` | `false` | | filter | Filter available time | `DatePickerFilter` | - | | forceRender | Render content forcely | `boolean` | `false` | | max | Max value | `PickerDate` | ten years later | diff --git a/src/components/picker/index.zh.md b/src/components/picker/index.zh.md index 948b1dd6cb..4b1b100d1b 100644 --- a/src/components/picker/index.zh.md +++ b/src/components/picker/index.zh.md @@ -43,7 +43,7 @@ type PickerValueExtend = { | columns | 配置每一列的选项 | `PickerColumn[] \| ((value: PickerValue[]) => PickerColumn[])` | - | | confirmText | 确定按钮的文字 | `ReactNode` | `'确定'` | | defaultValue | 默认选中项 | `PickerValue[]` | `[]` | -| destroyOnClose | 不可见时卸载内容 | `boolean` | `false` | +| destroyOnClose | 不可见时是否销毁 `DOM` 结构 | `boolean` | `false` | | forceRender | 强制渲染内容 | `boolean` | `false` | | mouseWheel | 是否允许通过鼠标滚轮进行选择 | `boolean` | `false` | | onCancel | 取消时触发 | `() => void` | - | @@ -156,7 +156,7 @@ type PickerDate = Date & { | --- | --- | --- | --- | --- | | children | 所选项的渲染函数 | `(value: PickerDate, actions: PickerActions) => ReactNode` | - | | defaultValue | 选中值 | `PickerDate` | - | -| destroyOnClose | 不可见时卸载内容 | `boolean` | `false` | +| destroyOnClose | 不可见时是否销毁 `DOM` 结构 | `boolean` | `false` | | filter | 过滤可供选择的时间 | `DatePickerFilter` | - | | forceRender | 强制渲染内容 | `boolean` | `false` | | max | 最大值 | `PickerDate` | 十年后 | diff --git a/src/components/popup/index.en.md b/src/components/popup/index.en.md index cc9ba46403..33bc6e6cdd 100644 --- a/src/components/popup/index.en.md +++ b/src/components/popup/index.en.md @@ -24,7 +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` | -| destroyOnClose | Unmount content when not visible | `boolean` | `false` | +| 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` | | mask | Whether to display Mask | `boolean` | `true` | @@ -38,6 +38,7 @@ It is suitable for displaying pop-up windows, information prompts, selection inp | stopPropagation | Stop the propagation of some events. | `PropagationEvent[]` | `['click']` | | style | Container style | `React.CSSProperties` | - | | visible | Whether visible | `boolean` | `false` | +| closeOnSwipe | Whether to support closing by swiping up/down | `boolean` | `false` | ### CSS Variables diff --git a/src/components/popup/index.zh.md b/src/components/popup/index.zh.md index 6c21b72e8b..6c699b774c 100644 --- a/src/components/popup/index.zh.md +++ b/src/components/popup/index.zh.md @@ -24,7 +24,7 @@ | bodyStyle | 内容区域样式 | `React.CSSProperties` | - | | className | 容器类名 | `string` | - | | closeOnMaskClick | 点击背景蒙层后是否关闭 | `boolean` | `false` | -| destroyOnClose | 不可见时卸载内容 | `boolean` | `false` | +| destroyOnClose | 不可见时是否销毁 `DOM` 结构 | `boolean` | `false` | | forceRender | 强制渲染内容 | `boolean` | `false` | | getContainer | 指定挂载的 `HTML` 节点,默认为 `body`,如果为 `null` 的话,会渲染到当前节点 | `HTMLElement \| () => HTMLElement \| null` | `() => document.body` | | mask | 是否展示蒙层 | `boolean` | `true` | @@ -38,6 +38,7 @@ | stopPropagation | 阻止某些事件的冒泡 | `PropagationEvent[]` | `['click']` | | style | 容器样式 | `React.CSSProperties` | - | | visible | 是否可见 | `boolean` | `false` | +| closeOnSwipe | 是否支持向上/下滑动关闭 | `boolean` | `false` | ### CSS 变量 diff --git a/src/components/popup/popup.tsx b/src/components/popup/popup.tsx index ab6a067945..ab3e7a1b16 100644 --- a/src/components/popup/popup.tsx +++ b/src/components/popup/popup.tsx @@ -20,11 +20,13 @@ const classPrefix = `adm-popup` export type PopupProps = PopupBaseProps & PropsWithChildren<{ position?: 'bottom' | 'top' | 'left' | 'right' + closeOnSwipe?: boolean }> & NativeProps<'--z-index'> const defaultProps = { ...defaultPopupBaseProps, + closeOnSwipe: false, position: 'bottom', } @@ -70,6 +72,7 @@ export const Popup: FC = p => { const bind = useDrag( ({ swipe: [, swipeY] }) => { + if (!props.closeOnSwipe) return if ( (swipeY === 1 && props.position === 'bottom') || (swipeY === -1 && props.position === 'top') diff --git a/src/components/popup/tests/popup.test.tsx b/src/components/popup/tests/popup.test.tsx index a6d4d55dd4..6565beb1f5 100644 --- a/src/components/popup/tests/popup.test.tsx +++ b/src/components/popup/tests/popup.test.tsx @@ -1,45 +1,45 @@ import * as React from 'react' -import { render, cleanup, fireEvent, mockDrag } from 'testing' +import { render, mockDrag, act, waitFor } from 'testing' import Popup from '..' describe('Popup', () => { - test('top swipe should be closed', () => { + test('top swipe should be closed', async () => { const onClose = jest.fn() render( - +
) - mockDrag( + await mockDrag( document.querySelector('.adm-popup') as Element, - new Array(8).fill(0).map((_, i) => { + new Array(4).fill(0).map((_, i) => { return { clientY: 400 - 50 * i, } - }) + }), + 5 ) - expect(onClose).toBeCalledTimes(1) }) - test('bottom swipe should be closed', () => { + test('bottom swipe should be closed', async () => { const onClose = jest.fn() render( - +
) - mockDrag( + await mockDrag( document.querySelector('.adm-popup') as Element, - new Array(8).fill(0).map((_, i) => { + new Array(6).fill(0).map((_, i) => { return { clientY: 50 * i, } - }) + }), + 5 ) - expect(onClose).toBeCalledTimes(1) }) }) diff --git a/src/components/pull-to-refresh/pull-to-refresh.tsx b/src/components/pull-to-refresh/pull-to-refresh.tsx index 9a02691a8a..fc63f1d209 100644 --- a/src/components/pull-to-refresh/pull-to-refresh.tsx +++ b/src/components/pull-to-refresh/pull-to-refresh.tsx @@ -59,6 +59,7 @@ export const PullToRefresh: FC = p => { config: { tension: 300, friction: 30, + round: true, clamp: true, }, })) @@ -119,7 +120,9 @@ export const PullToRefresh: FC = p => { } const [, y] = state.movement - if (state.first && y > 0) { + const parsedY = Math.ceil(y) + + if (state.first && parsedY > 0) { const target = state.event.target if (!target || !(target instanceof Element)) return let scrollParent = getScrollParent(target) @@ -147,7 +150,7 @@ export const PullToRefresh: FC = p => { } event.stopPropagation() const height = Math.max( - rubberbandIfOutOfBounds(y, 0, 0, headHeight * 5, 0.5), + rubberbandIfOutOfBounds(parsedY, 0, 0, headHeight * 5, 0.5), 0 ) api.start({ height }) diff --git a/src/components/radio/tests/radio.test.tsx b/src/components/radio/tests/radio.test.tsx index 40e8d99920..fa3b56fed1 100644 --- a/src/components/radio/tests/radio.test.tsx +++ b/src/components/radio/tests/radio.test.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { fireEvent, render, testA11y, userEvent, screen } from 'testing' +import { fireEvent, render, testA11y, userEvent, screen, act } from 'testing' import Radio from '../' import { RadioGroupProps } from '../group' @@ -30,7 +30,10 @@ describe('Radio', () => { 1 ) - await userEvent.tripleClick(screen.getByRole('radio')) + + await act(async () => { + await userEvent.tripleClick(screen.getByRole('radio')) + }) expect(onChange).toBeCalledTimes(1) }) }) @@ -125,7 +128,9 @@ describe('Radio.Group', () => { 2 ) - await userEvent.tripleClick(screen.getByRole('radio', { name: '1' })) + await act(async () => { + await userEvent.tripleClick(screen.getByRole('radio', { name: '1' })) + }) expect(onChange).toBeCalledTimes(1) }) }) diff --git a/src/components/search-bar/tests/search-bar.test.tsx b/src/components/search-bar/tests/search-bar.test.tsx index f68cf4e11e..53543ed29c 100644 --- a/src/components/search-bar/tests/search-bar.test.tsx +++ b/src/components/search-bar/tests/search-bar.test.tsx @@ -53,7 +53,9 @@ describe('adm-search-bar', () => { render() const input = screen.getByRole('searchbox') fireEvent.focus(input) - await userEvent.type(input, '12') + await act(async () => { + await userEvent.type(input, '12') + }) fireEvent.click(screen.getByText('取消')) expect(input).toHaveValue('') }) @@ -62,7 +64,9 @@ describe('adm-search-bar', () => { const onSearch = jest.fn() render() const input = screen.getByRole('searchbox') - await userEvent.type(input, '12{enter}') + await act(async () => { + await userEvent.type(input, '12{enter}') + }) expect(onSearch).toBeCalledWith('12') }) @@ -80,7 +84,10 @@ describe('adm-search-bar', () => { expect(input).toHaveFocus() expect(onFocus).toBeCalled() - await userEvent.type(input, '12') + await act(async () => { + await userEvent.type(input, '12') + }) + act(() => { ref.current?.clear() }) diff --git a/src/components/slider/slider.tsx b/src/components/slider/slider.tsx index 59227af690..69c4ada271 100644 --- a/src/components/slider/slider.tsx +++ b/src/components/slider/slider.tsx @@ -111,7 +111,7 @@ export const Slider: FC = p => { return Object.keys(marks) .map(parseFloat) .sort((a, b) => a - b) - } else { + } else if (ticks) { const points: number[] = [] for ( let i = getMiniDecimal(min); @@ -122,6 +122,8 @@ export const Slider: FC = p => { } return points } + + return [] }, [marks, ticks, step, min, max]) function getValueByPosition(position: number) { @@ -133,9 +135,10 @@ export const Slider: FC = p => { if (pointList.length) { value = nearest(pointList, newPosition) } else { - const lengthPerStep = 100 / ((max - min) / step) - const steps = Math.round(newPosition / lengthPerStep) - value = steps * lengthPerStep * (max - min) * 0.01 + min + // 使用 MiniDecimal 避免精度问题 + const cell = Math.round((newPosition - min) / step) + const nextVal = getMiniDecimal(cell).multi(step) + value = getMiniDecimal(min).add(nextVal.toString()).toNumber() } return value } diff --git a/src/components/swiper/demos/demo1.tsx b/src/components/swiper/demos/demo1.tsx index ca3e2bae70..f550fb17ad 100644 --- a/src/components/swiper/demos/demo1.tsx +++ b/src/components/swiper/demos/demo1.tsx @@ -34,7 +34,15 @@ export default () => { - {items} + { + console.log(i, 'onIndexChange1') + }} + > + {items} + diff --git a/src/components/swiper/swiper.tsx b/src/components/swiper/swiper.tsx index 8f61a296a3..acb4d9f039 100644 --- a/src/components/swiper/swiper.tsx +++ b/src/components/swiper/swiper.tsx @@ -20,7 +20,7 @@ import PageIndicator, { PageIndicatorProps } from '../page-indicator' import { staged } from 'staged-components' import { useRefState } from '../../utils/use-ref-state' import { bound } from '../../utils/bound' -import { useIsomorphicLayoutEffect } from 'ahooks' +import { useIsomorphicLayoutEffect, useGetState } from 'ahooks' import { mergeFuncProps } from '../../utils/with-func-props' const classPrefix = `adm-swiper` @@ -81,6 +81,7 @@ export const Swiper = forwardRef( staged((p, ref) => { const props = mergeProps(defaultProps, p) const [uid] = useState({}) + const timeoutRef = useRef(null) const isVertical = props.direction === 'vertical' const slideRatio = props.slideSize / 100 @@ -125,7 +126,7 @@ export const Swiper = forwardRef( return (trackPixels * props.slideSize) / 100 } - const [current, setCurrent] = useState(props.defaultIndex) + const [current, setCurrent, getCurrent] = useGetState(props.defaultIndex) const [dragging, setDragging, draggingRef] = useRefState(false) @@ -237,10 +238,13 @@ export const Swiper = forwardRef( const targetIndex = loop ? modulus(roundedIndex, count) : bound(roundedIndex, 0, count - 1) - setCurrent(targetIndex) - if (targetIndex !== current) { + + if (targetIndex !== getCurrent()) { props.onIndexChange?.(targetIndex) } + + setCurrent(targetIndex) + api.start({ position: (loop ? roundedIndex : boundIndex(roundedIndex)) * 100, immediate, @@ -269,19 +273,20 @@ export const Swiper = forwardRef( }) const { autoplay, autoplayInterval } = props - useEffect(() => { - if (!autoplay || dragging) return - let interval: number - function tick() { - interval = window.setTimeout(tick, autoplayInterval) + const runTimeSwiper = () => { + timeoutRef.current = window.setTimeout(() => { swipeNext() - } + runTimeSwiper() + }, autoplayInterval) + } + useEffect(() => { + if (!autoplay || dragging) return - interval = window.setTimeout(tick, autoplayInterval) + runTimeSwiper() return () => { - if (interval) window.clearTimeout(interval) + if (timeoutRef.current) window.clearTimeout(timeoutRef.current) } }, [autoplay, autoplayInterval, dragging, count]) @@ -324,8 +329,16 @@ export const Swiper = forwardRef( ), }} > - {React.Children.map(validChildren, child => { - return
{child}
+ {React.Children.map(validChildren, (child, index) => { + return ( +
+ {child} +
+ ) })} ) diff --git a/src/components/swiper/tests/swiper.test.tsx b/src/components/swiper/tests/swiper.test.tsx index a9b3298bdb..ded1fa445b 100644 --- a/src/components/swiper/tests/swiper.test.tsx +++ b/src/components/swiper/tests/swiper.test.tsx @@ -237,17 +237,21 @@ describe('Swiper', () => { ) const el = $$(`.${classPrefix}-track`)[0] - mockDrag(el, [ - { clientX: 50, clientY: 300 }, - { - clientX: 50, - clientY: 200, - }, - { - clientX: 60, - clientY: 50, - }, - ]) + await mockDrag( + el, + [ + { clientX: 50, clientY: 300 }, + { + clientX: 50, + clientY: 200, + }, + { + clientX: 60, + clientY: 50, + }, + ], + 5 + ) expect($$(`.${classPrefix}-track-inner`)[0]).toHaveStyle( 'transform: translate3d(0,-100%,0)' diff --git a/src/components/tab-bar/index.en.md b/src/components/tab-bar/index.en.md index 7c4fff5d4c..22990e7dc9 100644 --- a/src/components/tab-bar/index.en.md +++ b/src/components/tab-bar/index.en.md @@ -19,7 +19,7 @@ Useful for switching between different pages. | Name | Description | Type | Default | | --- | --- | --- | --- | | activeKey | `key` of currently active `item` | `string \| null` | - | -| defaultActiveKey | The initialized `key` of the selected `item`, if the `activeKey` is not set | `string \| null` | `key` of the 1st `Tab` | +| defaultActiveKey | The initialized `key` of the selected `item`, if the `activeKey` is not set | `string \| null` | `key` of the first `TabBar.Item` | | onChange | Callback when switching panel | `(key: string) => void` | - | | safeArea | Whether to enable safe area padding | `boolean` | `false` | diff --git a/src/components/tab-bar/index.zh.md b/src/components/tab-bar/index.zh.md index 90112ec71c..54032233bb 100644 --- a/src/components/tab-bar/index.zh.md +++ b/src/components/tab-bar/index.zh.md @@ -19,7 +19,7 @@ | 属性 | 说明 | 类型 | 默认值 | | --- | --- | --- | --- | | activeKey | 当前激活 `item` 的 `key` | `string \| null` | - | -| defaultActiveKey | 初始化选中 `item` 的 `key`,如果没有设置 `activeKey` | `string \| null` | 第一个 `Tab` 的 `key` | +| defaultActiveKey | 初始化选中 `item` 的 `key`,如果没有设置 `activeKey` | `string \| null` | 第一个 `TabBar.Item` 的 `key` | | onChange | 切换面板的回调 | `(key: string) => void` | - | | safeArea | 是否开启安全区适配 | `boolean` | `false` | diff --git a/src/components/tabs/index.en.md b/src/components/tabs/index.en.md index 56cf56bb2a..d6e34ab092 100644 --- a/src/components/tabs/index.en.md +++ b/src/components/tabs/index.en.md @@ -45,7 +45,7 @@ The current content needs to be divided into groups of the same hierarchical str | Name | Description | Type | Default | | --- | --- | --- | --- | -| destroyOnClose | Unmount content when not visible | `boolean` | `false` | +| destroyOnClose | Destroy `dom` when not visible | `boolean` | `false` | | disabled | Whether to disable the tab | `boolean` | `false` | | forceRender | Whether to render the `DOM` structure when hidden | `boolean` | `false` | | key | Corresponding to `activeKey` | `string` | - | diff --git a/src/components/tabs/index.zh.md b/src/components/tabs/index.zh.md index f335a9c583..df3f840232 100644 --- a/src/components/tabs/index.zh.md +++ b/src/components/tabs/index.zh.md @@ -44,7 +44,7 @@ | 属性 | 说明 | 类型 | 默认值 | | -------------- | --------------------------- | ----------- | ------- | -| destroyOnClose | 不可见时卸载内容 | `boolean` | `false` | +| destroyOnClose | 不可见时是否销毁 `DOM` 结构 | `boolean` | `false` | | disabled | 是否禁用 | `boolean` | `false` | | forceRender | 被隐藏时是否渲染 `DOM` 结构 | `boolean` | `false` | | key | 对应 `activeKey` | `string` | - | diff --git a/src/tests/testing.tsx b/src/tests/testing.tsx index d3d59cac12..06c5916e7c 100644 --- a/src/tests/testing.tsx +++ b/src/tests/testing.tsx @@ -104,7 +104,7 @@ export { customRender as render } export const testA11y = async (ui: UI | Element) => { const container = React.isValidElement(ui) ? customRender(ui).container : ui - const results = await axe(container) + const results = await axe(container as Element) expect(results).toHaveNoViolations() } @@ -116,7 +116,7 @@ export const actSleep = (time: number) => { return act(() => sleep(time)) } -export const mockDrag = (el: Element, options: any[]) => { +export const mockDrag = async (el: Element, options: any[], time?: number) => { const [downOptions, ...moveOptions] = options fireEvent.mouseDown(el, { buttons: 1, @@ -127,6 +127,10 @@ export const mockDrag = (el: Element, options: any[]) => { buttons: 1, ...item, }) + + if (time) { + await sleep(time) + } } fireEvent.mouseUp(el) } diff --git a/src/utils/reduce-and-restore-motion.ts b/src/utils/reduce-and-restore-motion.ts index 6f2bc6aec5..08d7ca5206 100644 --- a/src/utils/reduce-and-restore-motion.ts +++ b/src/utils/reduce-and-restore-motion.ts @@ -39,5 +39,5 @@ function subscribe(onStoreChange: () => void) { } export function useMotionReduced() { - return useSyncExternalStore(subscribe, isMotionReduced) + return useSyncExternalStore(subscribe, isMotionReduced, isMotionReduced) }