Skip to content

Commit

Permalink
feat: Alert support aria-* in closable (ant-design#47474)
Browse files Browse the repository at this point in the history
* feat: Alert support aria-* in closable

* feat: Alert support aria-* in closable

* feat: Alert support aria-* in closable
  • Loading branch information
kiner-tang authored and tanzhenyun committed Mar 29, 2024
1 parent 414e1a7 commit fa40d28
Show file tree
Hide file tree
Showing 9 changed files with 231 additions and 17 deletions.
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

0 comments on commit fa40d28

Please sign in to comment.