Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Alert support aria-* in closable #47474

Merged
merged 3 commits into from
Feb 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
65 changes: 54 additions & 11 deletions components/alert/Alert.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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>;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -144,15 +155,20 @@ 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;
}
if (typeof closable === 'boolean') {
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;
Expand All @@ -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}
Expand Down Expand Up @@ -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>
)}
Expand Down
51 changes: 51 additions & 0 deletions components/alert/__tests__/__snapshots__/demo-extend.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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>
`;

Expand Down
51 changes: 51 additions & 0 deletions components/alert/__tests__/__snapshots__/demo.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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>
`;

Expand Down
41 changes: 40 additions & 1 deletion components/alert/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -154,14 +154,53 @@ 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(() => {});

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');
Expand Down
11 changes: 11 additions & 0 deletions components/alert/demo/closable.tsx
Original file line number Diff line number Diff line change
@@ -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.');
Expand All @@ -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>
);

Expand Down
2 changes: 1 addition & 1 deletion components/alert/index.en-US.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 | - | |
Expand Down
2 changes: 1 addition & 1 deletion components/alert/index.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 | - | |
Expand Down
23 changes: 21 additions & 2 deletions components/config-provider/__tests__/style.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
2 changes: 1 addition & 1 deletion components/config-provider/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export type ModalConfig = ComponentStyleConfig &
export type TabsConfig = ComponentStyleConfig &
Pick<TabsProps, 'indicator' | 'indicatorSize' | 'moreIcon' | 'addIcon' | 'removeIcon'>;

export type AlertConfig = ComponentStyleConfig & Pick<AlertProps, 'closeIcon'>;
export type AlertConfig = ComponentStyleConfig & Pick<AlertProps, 'closable' | 'closeIcon'>;

export type BadgeConfig = ComponentStyleConfig & Pick<BadgeProps, 'classNames' | 'styles'>;

Expand Down