mirror of
https://github.com/ant-design/ant-design.git
synced 2024-12-11 23:00:20 +08:00
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
This commit is contained in:
parent
bb59275276
commit
9d3211757c
@ -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<HTMLDivElement>;
|
||||
@ -77,19 +80,26 @@ const IconNode: React.FC<IconNodeProps> = (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<CloseIconProps> = (props) => {
|
||||
const { isClosable, prefixCls, closeIcon, handleClose } = props;
|
||||
const { isClosable, prefixCls, closeIcon, handleClose, ariaProps } = props;
|
||||
const mergedCloseIcon =
|
||||
closeIcon === true || closeIcon === undefined ? <CloseOutlined /> : closeIcon;
|
||||
return isClosable ? (
|
||||
<button type="button" onClick={handleClose} className={`${prefixCls}-close-icon`} tabIndex={0}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClose}
|
||||
className={`${prefixCls}-close-icon`}
|
||||
tabIndex={0}
|
||||
{...ariaProps}
|
||||
>
|
||||
{mergedCloseIcon}
|
||||
</button>
|
||||
) : null;
|
||||
@ -120,7 +130,8 @@ const Alert: React.FC<AlertProps> = (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<HTMLDivElement>(null);
|
||||
@ -144,6 +155,7 @@ const Alert: React.FC<AlertProps> = (props) => {
|
||||
|
||||
// closeable when closeText or closeIcon is assigned
|
||||
const isClosable = React.useMemo<boolean>(() => {
|
||||
if (typeof closable === 'object' && closable.closeIcon) return true;
|
||||
if (closeText) {
|
||||
return true;
|
||||
}
|
||||
@ -151,8 +163,12 @@ const Alert: React.FC<AlertProps> = (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<AlertProps> = (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(
|
||||
<CSSMotion
|
||||
visible={!closed}
|
||||
@ -212,8 +254,9 @@ const Alert: React.FC<AlertProps> = (props) => {
|
||||
<CloseIconNode
|
||||
isClosable={isClosable}
|
||||
prefixCls={prefixCls}
|
||||
closeIcon={closeText || (closeIcon ?? alert?.closeIcon)}
|
||||
closeIcon={mergedCloseIcon}
|
||||
handleClose={handleClose}
|
||||
ariaProps={mergeAriaProps}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
@ -583,6 +583,57 @@ exports[`renders components/alert/demo/closable.tsx extend context correctly 1`]
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-space-item"
|
||||
>
|
||||
<div
|
||||
class="ant-alert ant-alert-error ant-alert-with-description ant-alert-no-icon"
|
||||
data-show="true"
|
||||
role="alert"
|
||||
>
|
||||
<div
|
||||
class="ant-alert-content"
|
||||
>
|
||||
<div
|
||||
class="ant-alert-message"
|
||||
>
|
||||
Error Text
|
||||
</div>
|
||||
<div
|
||||
class="ant-alert-description"
|
||||
>
|
||||
Error Description Error Description Error Description Error Description Error Description Error Description
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
aria-label="close"
|
||||
class="ant-alert-close-icon"
|
||||
tabindex="0"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-label="close-square"
|
||||
class="anticon anticon-close-square"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="close-square"
|
||||
fill="currentColor"
|
||||
fill-rule="evenodd"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M880 112c17.7 0 32 14.3 32 32v736c0 17.7-14.3 32-32 32H144c-17.7 0-32-14.3-32-32V144c0-17.7 14.3-32 32-32zM639.98 338.82h-.04l-.08.06L512 466.75 384.14 338.88c-.04-.05-.06-.06-.08-.06a.12.12 0 00-.07 0c-.03 0-.05.01-.09.05l-45.02 45.02a.2.2 0 00-.05.09.12.12 0 000 .07v.02a.27.27 0 00.06.06L466.75 512 338.88 639.86c-.05.04-.06.06-.06.08a.12.12 0 000 .07c0 .03.01.05.05.09l45.02 45.02a.2.2 0 00.09.05.12.12 0 00.07 0c.02 0 .04-.01.08-.05L512 557.25l127.86 127.87c.04.04.06.05.08.05a.12.12 0 00.07 0c.03 0 .05-.01.09-.05l45.02-45.02a.2.2 0 00.05-.09.12.12 0 000-.07v-.02a.27.27 0 00-.05-.06L557.25 512l127.87-127.86c.04-.04.05-.06.05-.08a.12.12 0 000-.07c0-.03-.01-.05-.05-.09l-45.02-45.02a.2.2 0 00-.09-.05.12.12 0 00-.07 0z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
@ -573,6 +573,57 @@ exports[`renders components/alert/demo/closable.tsx correctly 1`] = `
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-space-item"
|
||||
>
|
||||
<div
|
||||
class="ant-alert ant-alert-error ant-alert-with-description ant-alert-no-icon"
|
||||
data-show="true"
|
||||
role="alert"
|
||||
>
|
||||
<div
|
||||
class="ant-alert-content"
|
||||
>
|
||||
<div
|
||||
class="ant-alert-message"
|
||||
>
|
||||
Error Text
|
||||
</div>
|
||||
<div
|
||||
class="ant-alert-description"
|
||||
>
|
||||
Error Description Error Description Error Description Error Description Error Description Error Description
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
aria-label="close"
|
||||
class="ant-alert-close-icon"
|
||||
tabindex="0"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-label="close-square"
|
||||
class="anticon anticon-close-square"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="close-square"
|
||||
fill="currentColor"
|
||||
fill-rule="evenodd"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M880 112c17.7 0 32 14.3 32 32v736c0 17.7-14.3 32-32 32H144c-17.7 0-32-14.3-32-32V144c0-17.7 14.3-32 32-32zM639.98 338.82h-.04l-.08.06L512 466.75 384.14 338.88c-.04-.05-.06-.06-.08-.06a.12.12 0 00-.07 0c-.03 0-.05.01-.09.05l-45.02 45.02a.2.2 0 00-.05.09.12.12 0 000 .07v.02a.27.27 0 00.06.06L466.75 512 338.88 639.86c-.05.04-.06.06-.06.08a.12.12 0 000 .07c0 .03.01.05.05.09l45.02 45.02a.2.2 0 00.09.05.12.12 0 00.07 0c.02 0 .04-.01.08-.05L512 557.25l127.86 127.87c.04.04.06.05.08.05a.12.12 0 00.07 0c.03 0 .05-.01.09-.05l45.02-45.02a.2.2 0 00.05-.09.12.12 0 000-.07v-.02a.27.27 0 00-.05-.06L557.25 512l127.87-127.86c.04-.04.05-.06.05-.08a.12.12 0 000-.07c0-.03-.01-.05-.05-.09l-45.02-45.02a.2.2 0 00-.09-.05.12.12 0 00-.07 0z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
@ -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(<Alert />);
|
||||
expect(container.querySelector('*[aria-label]')).toBeFalsy();
|
||||
rerender(<Alert closable={{ 'aria-label': 'Close' }} closeIcon="CloseIcon" />);
|
||||
expect(container.querySelector('[aria-label="Close"]')).toBeTruthy();
|
||||
rerender(<Alert closable={{ 'aria-label': 'Close' }} closeText="CloseText" />);
|
||||
expect(container.querySelector('[aria-label="Close"]')).toBeTruthy();
|
||||
rerender(<Alert closable={{ 'aria-label': 'Close', closeIcon: 'CloseIconProp' }} />);
|
||||
expect(container.querySelector('[aria-label="Close"]')).toBeTruthy();
|
||||
});
|
||||
it('close button should be support custom icon by closable', () => {
|
||||
const { container, rerender } = render(<Alert />);
|
||||
expect(container.querySelector('.ant-alert-close-icon')).toBeFalsy();
|
||||
rerender(<Alert closable={{ closeIcon: 'CloseBtn' }} />);
|
||||
expect(container.querySelector('.ant-alert-close-icon')?.textContent).toBe('CloseBtn');
|
||||
rerender(<Alert closable={{ closeIcon: 'CloseBtn' }} closeIcon="CloseBtn2" />);
|
||||
expect(container.querySelector('.ant-alert-close-icon')?.textContent).toBe('CloseBtn');
|
||||
rerender(<Alert closable={{ closeIcon: 'CloseBtn' }} closeText="CloseBtn3" />);
|
||||
expect(container.querySelector('.ant-alert-close-icon')?.textContent).toBe('CloseBtn');
|
||||
rerender(<Alert closeText="CloseBtn2" />);
|
||||
expect(container.querySelector('.ant-alert-close-icon')?.textContent).toBe('CloseBtn2');
|
||||
rerender(<Alert closeIcon="CloseBtn3" />);
|
||||
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(<Alert closeText="close" />);
|
||||
|
||||
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(<Alert closeIcon="close" />);
|
||||
|
||||
expect(warnSpy).toHaveBeenCalledWith(
|
||||
`Warning: [antd: Alert] \`closeIcon\` is deprecated. Please use \`closable.closeIcon\` instead.`,
|
||||
);
|
||||
|
||||
expect(container.querySelector('.ant-alert-close-icon')?.textContent).toBe('close');
|
||||
|
@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Alert, Space } from 'antd';
|
||||
import { CloseSquareFilled } from '@ant-design/icons';
|
||||
|
||||
const onClose = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
console.log(e, 'I was closed.');
|
||||
@ -20,6 +21,16 @@ const App: React.FC = () => (
|
||||
closable
|
||||
onClose={onClose}
|
||||
/>
|
||||
<Alert
|
||||
message="Error Text"
|
||||
description="Error Description Error Description Error Description Error Description Error Description Error Description"
|
||||
type="error"
|
||||
closable={{
|
||||
'aria-label': 'close',
|
||||
closeIcon: <CloseSquareFilled />,
|
||||
}}
|
||||
onClose={onClose}
|
||||
/>
|
||||
</Space>
|
||||
);
|
||||
|
||||
|
@ -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 | `<CloseOutlined />` | |
|
||||
| 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 | - | |
|
||||
|
@ -43,7 +43,7 @@ group:
|
||||
| action | 自定义操作项 | ReactNode | - | 4.9.0 |
|
||||
| afterClose | 关闭动画结束后触发的回调函数 | () => void | - | |
|
||||
| banner | 是否用作顶部公告 | boolean | false | |
|
||||
| closeIcon | 自定义关闭 Icon,>=5.7.0: 设置为 `null` 或 `false` 时隐藏关闭按钮 | ReactNode | `<CloseOutlined />` | |
|
||||
| closable | 可关闭配置,>=5.15.0: 支持 `aria-*` | boolean \| ({ closeIcon?: React.ReactNode } & React.AriaAttributes) | `false` | |
|
||||
| description | 警告提示的辅助性文字介绍 | ReactNode | - | |
|
||||
| icon | 自定义图标,`showIcon` 为 true 时有效 | ReactNode | - | |
|
||||
| message | 警告提示内容 | ReactNode | - | |
|
||||
|
@ -708,18 +708,37 @@ describe('ConfigProvider support style and className props', () => {
|
||||
});
|
||||
|
||||
it('Should Alert className works', () => {
|
||||
const { container } = render(
|
||||
const { container, rerender } = render(
|
||||
<ConfigProvider
|
||||
alert={{
|
||||
className: 'test-class',
|
||||
closeIcon: <span className="cp-test-icon">cp-test-icon</span>,
|
||||
closable: { 'aria-label': 'close' },
|
||||
}}
|
||||
>
|
||||
<Alert closable message="Test Message" />
|
||||
<Alert message="Test Message" />
|
||||
</ConfigProvider>,
|
||||
);
|
||||
expect(container.querySelector<HTMLDivElement>('.ant-alert')).toHaveClass('test-class');
|
||||
expect(container.querySelector<HTMLSpanElement>('.ant-alert .cp-test-icon')).toBeTruthy();
|
||||
expect(container.querySelectorAll('*[aria-label="close"]')).toBeTruthy();
|
||||
rerender(
|
||||
<ConfigProvider
|
||||
alert={{
|
||||
className: 'test-class',
|
||||
closable: {
|
||||
'aria-label': 'close',
|
||||
closeIcon: <span className="cp-test-icon">cp-test-icon</span>,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Alert message="Test Message" />
|
||||
</ConfigProvider>,
|
||||
);
|
||||
|
||||
expect(container.querySelector<HTMLDivElement>('.ant-alert')).toHaveClass('test-class');
|
||||
expect(container.querySelector<HTMLSpanElement>('.ant-alert .cp-test-icon')).toBeTruthy();
|
||||
expect(container.querySelectorAll('*[aria-label="close"]')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('Should Alert style works', () => {
|
||||
|
@ -90,7 +90,7 @@ export type ModalConfig = ComponentStyleConfig &
|
||||
export type TabsConfig = ComponentStyleConfig &
|
||||
Pick<TabsProps, 'indicator' | 'indicatorSize' | 'moreIcon' | 'addIcon'>;
|
||||
|
||||
export type AlertConfig = ComponentStyleConfig & Pick<AlertProps, 'closeIcon'>;
|
||||
export type AlertConfig = ComponentStyleConfig & Pick<AlertProps, 'closable' | 'closeIcon'>;
|
||||
|
||||
export type BadgeConfig = ComponentStyleConfig & Pick<BadgeProps, 'classNames' | 'styles'>;
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user