From 9d3211757c69018068ecc9095bc527f593af9f40 Mon Sep 17 00:00:00 2001 From: kiner-tang <1127031143@qq.com> Date: Sun, 18 Feb 2024 18:08:02 +0800 Subject: [PATCH] feat: Alert support aria-* in closable (#47446) * feat: support aria-* in closeable * feat: support aria-* in closeable * feat: optimize code * feat: optimize code * feat: optimize code * feat: optimize code --- components/alert/Alert.tsx | 65 +++++++++++++++---- .../__snapshots__/demo-extend.test.ts.snap | 51 +++++++++++++++ .../__tests__/__snapshots__/demo.test.ts.snap | 51 +++++++++++++++ components/alert/__tests__/index.test.tsx | 41 +++++++++++- components/alert/demo/closable.tsx | 11 ++++ components/alert/index.en-US.md | 2 +- components/alert/index.zh-CN.md | 2 +- .../config-provider/__tests__/style.test.tsx | 23 ++++++- components/config-provider/context.ts | 2 +- 9 files changed, 231 insertions(+), 17 deletions(-) diff --git a/components/alert/Alert.tsx b/components/alert/Alert.tsx index 9c72731004..1b190dc7da 100644 --- a/components/alert/Alert.tsx +++ b/components/alert/Alert.tsx @@ -18,9 +18,9 @@ export interface AlertProps { /** Type of Alert styles, options:`success`, `info`, `warning`, `error` */ type?: 'success' | 'info' | 'warning' | 'error'; /** Whether Alert can be closed */ - closable?: boolean; + closable?: boolean | ({ closeIcon?: React.ReactNode } & React.AriaAttributes); /** - * @deprecated please use `closeIcon` instead. + * @deprecated please use `closable.closeIcon` instead. * Close text to show */ closeText?: React.ReactNode; @@ -42,7 +42,10 @@ export interface AlertProps { rootClassName?: string; banner?: boolean; icon?: React.ReactNode; - /** Custom closeIcon */ + /** + * Custom closeIcon + * @deprecated please use `closable.closeIcon` instead. + */ closeIcon?: React.ReactNode; action?: React.ReactNode; onMouseEnter?: React.MouseEventHandler; @@ -77,19 +80,26 @@ const IconNode: React.FC = (props) => { return React.createElement(iconType, { className: `${prefixCls}-icon` }); }; -interface CloseIconProps { +type CloseIconProps = { isClosable: boolean; prefixCls: AlertProps['prefixCls']; closeIcon: AlertProps['closeIcon']; handleClose: AlertProps['onClose']; -} + ariaProps: React.AriaAttributes; +}; const CloseIconNode: React.FC = (props) => { - const { isClosable, prefixCls, closeIcon, handleClose } = props; + const { isClosable, prefixCls, closeIcon, handleClose, ariaProps } = props; const mergedCloseIcon = closeIcon === true || closeIcon === undefined ? : closeIcon; return isClosable ? ( - ) : null; @@ -120,7 +130,8 @@ const Alert: React.FC = (props) => { if (process.env.NODE_ENV !== 'production') { const warning = devUseWarning('Alert'); - warning.deprecated(!closeText, 'closeText', 'closeIcon'); + warning.deprecated(!closeText, 'closeText', 'closable.closeIcon'); + warning.deprecated(!closeIcon, 'closeIcon', 'closable.closeIcon'); } const ref = React.useRef(null); @@ -144,6 +155,7 @@ const Alert: React.FC = (props) => { // closeable when closeText or closeIcon is assigned const isClosable = React.useMemo(() => { + if (typeof closable === 'object' && closable.closeIcon) return true; if (closeText) { return true; } @@ -151,8 +163,12 @@ const Alert: React.FC = (props) => { return closable; } // should be true when closeIcon is 0 or '' - return closeIcon !== false && closeIcon !== null && closeIcon !== undefined; - }, [closeText, closeIcon, closable]); + if (closeIcon !== false && closeIcon !== null && closeIcon !== undefined) { + return true; + } + + return !!alert?.closable; + }, [closeText, closeIcon, closable, alert?.closable]); // banner mode defaults to Icon const isShowIcon = banner && showIcon === undefined ? true : showIcon; @@ -175,6 +191,32 @@ const Alert: React.FC = (props) => { const restProps = pickAttrs(otherProps, { aria: true, data: true }); + const mergedCloseIcon = React.useMemo(() => { + if (typeof closable === 'object' && closable.closeIcon) { + return closable.closeIcon; + } + if (closeText) { + return closeText; + } + if (closeIcon !== undefined) { + return closeIcon; + } + if (typeof alert?.closable === 'object' && alert?.closable?.closeIcon) { + return alert?.closable?.closeIcon; + } + return alert?.closeIcon; + }, [closeIcon, closable, closeText, alert?.closeIcon]); + + const mergeAriaProps = React.useMemo(() => { + const merged = closable ?? alert?.closable; + if (typeof merged === 'object') { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { closeIcon: _, ...ariaProps } = merged; + return ariaProps; + } + return {}; + }, [closable, alert?.closable]); + return wrapCSSVar( = (props) => { )} diff --git a/components/alert/__tests__/__snapshots__/demo-extend.test.ts.snap b/components/alert/__tests__/__snapshots__/demo-extend.test.ts.snap index b399d10d4e..5973c04e40 100644 --- a/components/alert/__tests__/__snapshots__/demo-extend.test.ts.snap +++ b/components/alert/__tests__/__snapshots__/demo-extend.test.ts.snap @@ -583,6 +583,57 @@ exports[`renders components/alert/demo/closable.tsx extend context correctly 1`] +
+ +
`; diff --git a/components/alert/__tests__/__snapshots__/demo.test.ts.snap b/components/alert/__tests__/__snapshots__/demo.test.ts.snap index 01a258b133..45e372512b 100644 --- a/components/alert/__tests__/__snapshots__/demo.test.ts.snap +++ b/components/alert/__tests__/__snapshots__/demo.test.ts.snap @@ -573,6 +573,57 @@ exports[`renders components/alert/demo/closable.tsx correctly 1`] = ` +
+ +
`; diff --git a/components/alert/__tests__/index.test.tsx b/components/alert/__tests__/index.test.tsx index 7965d4ba6f..72a29c287c 100644 --- a/components/alert/__tests__/index.test.tsx +++ b/components/alert/__tests__/index.test.tsx @@ -154,6 +154,31 @@ describe('Alert', () => { expect(container.querySelector('.ant-alert-close-icon')).toBeFalsy(); }); + it('close button should be support aria-* by closable', () => { + const { container, rerender } = render(); + expect(container.querySelector('*[aria-label]')).toBeFalsy(); + rerender(); + expect(container.querySelector('[aria-label="Close"]')).toBeTruthy(); + rerender(); + expect(container.querySelector('[aria-label="Close"]')).toBeTruthy(); + rerender(); + expect(container.querySelector('[aria-label="Close"]')).toBeTruthy(); + }); + it('close button should be support custom icon by closable', () => { + const { container, rerender } = render(); + expect(container.querySelector('.ant-alert-close-icon')).toBeFalsy(); + rerender(); + expect(container.querySelector('.ant-alert-close-icon')?.textContent).toBe('CloseBtn'); + rerender(); + expect(container.querySelector('.ant-alert-close-icon')?.textContent).toBe('CloseBtn'); + rerender(); + expect(container.querySelector('.ant-alert-close-icon')?.textContent).toBe('CloseBtn'); + rerender(); + expect(container.querySelector('.ant-alert-close-icon')?.textContent).toBe('CloseBtn2'); + rerender(); + expect(container.querySelector('.ant-alert-close-icon')?.textContent).toBe('CloseBtn3'); + }); + it('should warning when using closeText', () => { resetWarned(); const warnSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); @@ -161,7 +186,21 @@ describe('Alert', () => { const { container } = render(); expect(warnSpy).toHaveBeenCalledWith( - `Warning: [antd: Alert] \`closeText\` is deprecated. Please use \`closeIcon\` instead.`, + `Warning: [antd: Alert] \`closeText\` is deprecated. Please use \`closable.closeIcon\` instead.`, + ); + + expect(container.querySelector('.ant-alert-close-icon')?.textContent).toBe('close'); + + warnSpy.mockRestore(); + }); + it('should warning when using closeIcon', () => { + resetWarned(); + const warnSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + + const { container } = render(); + + expect(warnSpy).toHaveBeenCalledWith( + `Warning: [antd: Alert] \`closeIcon\` is deprecated. Please use \`closable.closeIcon\` instead.`, ); expect(container.querySelector('.ant-alert-close-icon')?.textContent).toBe('close'); diff --git a/components/alert/demo/closable.tsx b/components/alert/demo/closable.tsx index 2d04f4ebaf..d88ac700ce 100644 --- a/components/alert/demo/closable.tsx +++ b/components/alert/demo/closable.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { Alert, Space } from 'antd'; +import { CloseSquareFilled } from '@ant-design/icons'; const onClose = (e: React.MouseEvent) => { console.log(e, 'I was closed.'); @@ -20,6 +21,16 @@ const App: React.FC = () => ( closable onClose={onClose} /> + , + }} + onClose={onClose} + /> ); diff --git a/components/alert/index.en-US.md b/components/alert/index.en-US.md index 21df5a2522..9812a873b9 100644 --- a/components/alert/index.en-US.md +++ b/components/alert/index.en-US.md @@ -42,7 +42,7 @@ Common props ref:[Common props](/docs/react/common-props) | action | The action of Alert | ReactNode | - | 4.9.0 | | afterClose | Called when close animation is finished | () => void | - | | | banner | Whether to show as banner | boolean | false | | -| closeIcon | Custom close icon, >=5.7.0: close button will be hidden when setting to `null` or `false` | ReactNode | `` | | +| closable | The config of closable, >=5.15.0: support `aria-*` | boolean \| ({ closeIcon?: React.ReactNode } & React.AriaAttributes) | `false` | | | description | Additional content of Alert | ReactNode | - | | | icon | Custom icon, effective when `showIcon` is true | ReactNode | - | | | message | Content of Alert | ReactNode | - | | diff --git a/components/alert/index.zh-CN.md b/components/alert/index.zh-CN.md index 5e5bd40f7c..d1be592b0c 100644 --- a/components/alert/index.zh-CN.md +++ b/components/alert/index.zh-CN.md @@ -43,7 +43,7 @@ group: | action | 自定义操作项 | ReactNode | - | 4.9.0 | | afterClose | 关闭动画结束后触发的回调函数 | () => void | - | | | banner | 是否用作顶部公告 | boolean | false | | -| closeIcon | 自定义关闭 Icon,>=5.7.0: 设置为 `null` 或 `false` 时隐藏关闭按钮 | ReactNode | `` | | +| closable | 可关闭配置,>=5.15.0: 支持 `aria-*` | boolean \| ({ closeIcon?: React.ReactNode } & React.AriaAttributes) | `false` | | | description | 警告提示的辅助性文字介绍 | ReactNode | - | | | icon | 自定义图标,`showIcon` 为 true 时有效 | ReactNode | - | | | message | 警告提示内容 | ReactNode | - | | diff --git a/components/config-provider/__tests__/style.test.tsx b/components/config-provider/__tests__/style.test.tsx index 92bc3b0e7d..124d0d970f 100644 --- a/components/config-provider/__tests__/style.test.tsx +++ b/components/config-provider/__tests__/style.test.tsx @@ -708,18 +708,37 @@ describe('ConfigProvider support style and className props', () => { }); it('Should Alert className works', () => { - const { container } = render( + const { container, rerender } = render( cp-test-icon, + closable: { 'aria-label': 'close' }, }} > - + , ); expect(container.querySelector('.ant-alert')).toHaveClass('test-class'); expect(container.querySelector('.ant-alert .cp-test-icon')).toBeTruthy(); + expect(container.querySelectorAll('*[aria-label="close"]')).toBeTruthy(); + rerender( + cp-test-icon, + }, + }} + > + + , + ); + + expect(container.querySelector('.ant-alert')).toHaveClass('test-class'); + expect(container.querySelector('.ant-alert .cp-test-icon')).toBeTruthy(); + expect(container.querySelectorAll('*[aria-label="close"]')).toBeTruthy(); }); it('Should Alert style works', () => { diff --git a/components/config-provider/context.ts b/components/config-provider/context.ts index d3904fef78..04a454e0c9 100644 --- a/components/config-provider/context.ts +++ b/components/config-provider/context.ts @@ -90,7 +90,7 @@ export type ModalConfig = ComponentStyleConfig & export type TabsConfig = ComponentStyleConfig & Pick; -export type AlertConfig = ComponentStyleConfig & Pick; +export type AlertConfig = ComponentStyleConfig & Pick; export type BadgeConfig = ComponentStyleConfig & Pick;