feat: Tag support aria-* in closable (#47678)

* feat: Tag support aria-* in closable

* feat: optimize code

* feat: optimize code

* feat: optimize code

* feat: optimize code

* feat: optimize code

* feat: optimize code

* feat: optimize code

* feat: optimize code

* feat: optimize code

* feat: optimize code

* feat: optimize code

* feat: optimize code

* feat: optimize code

* feat: optimize code

* feat: optimize code

* feat: optimize code

* feat: optimize code

* feat: optimize code

* feat: optimize code

* feat: optimize code

* feat: optimize code

* feat: optimize code

* feat: optimize code

* feat: optimize code

* feat: optimize code

* feat: optimize code

* feat: optimize code

* refactor: useClosable

* chore: modal

* fix: check logic

* chore: clean up

* feat: optimize code

* feat: optimize code

---------

Signed-off-by: kiner-tang <1127031143@qq.com>
Co-authored-by: 二货机器人 <smith3816@gmail.com>
This commit is contained in:
kiner-tang 2024-03-31 11:56:55 +08:00 committed by GitHub
parent 1b36fd22e5
commit 506753c3ef
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 392 additions and 212 deletions

View File

@ -6,9 +6,9 @@ import type { UseClosableParams } from '../hooks/useClosable';
import useClosable from '../hooks/useClosable';
type ParamsOfUseClosable = [
UseClosableParams['closable'],
UseClosableParams['closeIcon'],
UseClosableParams['defaultClosable'],
closable: UseClosableParams['closable'],
closeIcon: UseClosableParams['closeIcon'],
defaultClosable: UseClosableParams['defaultClosable'],
];
describe('hooks test', () => {
@ -81,7 +81,7 @@ describe('hooks test', () => {
res: [false, ''],
},
// test case like: <Component closeIcon={null | false | element} />
// test case like: <Component closeIcon={null | false | element | true} />
{
params: [undefined, null, undefined],
res: [false, ''],
@ -90,6 +90,10 @@ describe('hooks test', () => {
params: [undefined, false, undefined],
res: [false, ''],
},
{
params: [undefined, true, undefined],
res: [true, '.anticon-close'],
},
{
params: [
undefined,
@ -138,11 +142,16 @@ describe('hooks test', () => {
React.isValidElement(params[1]) ? 'element' : params[1]
},defaultClosable=${params[2]}. the result should be ${res}`, () => {
const App = () => {
const [closable, closeIcon] = useClosable({
closable: params[0],
closeIcon: params[1],
defaultClosable: params[2],
});
const [closable, closeIcon] = useClosable(
{
closable: params[0],
closeIcon: params[1],
},
null,
{
closable: params[2],
},
);
useEffect(() => {
expect(closable).toBe(res[0]);
}, [closable]);
@ -159,10 +168,15 @@ describe('hooks test', () => {
it('useClosable with defaultCloseIcon', () => {
const App = () => {
const [closable, closeIcon] = useClosable({
closable: true,
defaultCloseIcon: <CloseOutlined className="custom-close-icon" />,
});
const [closable, closeIcon] = useClosable(
{
closable: true,
},
null,
{
closeIcon: <CloseOutlined className="custom-close-icon" />,
},
);
useEffect(() => {
expect(closable).toBe(true);
}, [closable]);
@ -171,16 +185,37 @@ describe('hooks test', () => {
const { container } = render(<App />);
expect(container.querySelector('.custom-close-icon')).toBeTruthy();
});
it('useClosable without defaultCloseIcon', () => {
const App = () => {
const [closable, closeIcon] = useClosable(
{
closable: true,
},
null,
);
useEffect(() => {
expect(closable).toBe(true);
}, [closable]);
return <div>hooks test {closeIcon}</div>;
};
const { container } = render(<App />);
expect(container.querySelector('.anticon-close')).toBeTruthy();
});
it('useClosable with customCloseIconRender', () => {
const App = () => {
const customCloseIconRender = (icon: React.ReactNode) => (
<span className="custom-close-wrapper">{icon}</span>
);
const [closable, closeIcon] = useClosable({
closable: true,
customCloseIconRender,
});
const [closable, closeIcon] = useClosable(
{
closable: true,
},
null,
{
closeIconRender: customCloseIconRender,
},
);
useEffect(() => {
expect(closable).toBe(true);
}, [closable]);

View File

@ -3,7 +3,23 @@ import React from 'react';
import CloseOutlined from '@ant-design/icons/CloseOutlined';
import pickAttrs from 'rc-util/lib/pickAttrs';
export type ClosableType = boolean | ({ closeIcon?: React.ReactNode } & React.AriaAttributes);
export type BaseClosableType = { closeIcon?: React.ReactNode } & React.AriaAttributes;
export type ClosableType = boolean | BaseClosableType;
export type ContextClosable<T extends { closable?: ClosableType; closeIcon?: ReactNode } = any> =
Partial<Pick<T, 'closable' | 'closeIcon'>>;
export function pickClosable<T extends { closable?: ClosableType; closeIcon?: ReactNode }>(
context?: ContextClosable<T>,
): ContextClosable<T> | undefined {
if (!context) {
return undefined;
}
return {
closable: context.closable,
closeIcon: context.closeIcon,
};
}
export type UseClosableParams = {
closable?: ClosableType;
@ -11,63 +27,150 @@ export type UseClosableParams = {
defaultClosable?: boolean;
defaultCloseIcon?: ReactNode;
customCloseIconRender?: (closeIcon: ReactNode) => ReactNode;
context?: ContextClosable;
};
function useInnerClosable(
closable?: UseClosableParams['closable'],
closeIcon?: ReactNode,
defaultClosable?: boolean,
) {
if (typeof closable === 'boolean') {
return closable;
}
if (typeof closable === 'object') {
return true;
}
if (closeIcon === undefined) {
return !!defaultClosable;
}
return closeIcon !== false && closeIcon !== null;
/** Convert `closable` and `closeIcon` to config object */
function useClosableConfig(closableCollection?: ClosableCollection | null) {
const { closable, closeIcon } = closableCollection || {};
return React.useMemo(() => {
if (
// If `closable`, whatever rest be should be true
!closable &&
(closable === false || closeIcon === false || closeIcon === null)
) {
return false;
}
if (closable === undefined && closeIcon === undefined) {
return null;
}
let closableConfig: BaseClosableType = {
closeIcon: typeof closeIcon !== 'boolean' && closeIcon !== null ? closeIcon : undefined,
};
if (closable && typeof closable === 'object') {
closableConfig = {
...closableConfig,
...closable,
};
}
return closableConfig;
}, [closable, closeIcon]);
}
function useClosable({
closable,
closeIcon,
customCloseIconRender,
defaultCloseIcon = <CloseOutlined />,
defaultClosable = false,
}: UseClosableParams): [closable: boolean, closeIcon: React.ReactNode | null] {
const mergedClosable = useInnerClosable(closable, closeIcon, defaultClosable);
/**
* Assign object without `undefined` field. Will skip if is `false`.
* This helps to handle both closableConfig or false
*/
function assignWithoutUndefined<T extends object>(
...objList: (Partial<T> | false | null | undefined)[]
): Partial<T> {
const target: Partial<T> = {};
if (!mergedClosable) {
return [false, null];
}
const { closeIcon: closableIcon, ...restProps } =
typeof closable === 'object'
? closable
: ({} as { closeIcon: React.ReactNode } & React.AriaAttributes);
// Priority: closable.closeIcon > closeIcon > defaultCloseIcon
const mergedCloseIcon: ReactNode = (() => {
if (typeof closable === 'object' && closableIcon !== undefined) {
return closableIcon;
objList.forEach((obj) => {
if (obj) {
(Object.keys(obj) as (keyof T)[]).forEach((key) => {
if (obj[key] !== undefined) {
target[key] = obj[key];
}
});
}
return typeof closeIcon === 'boolean' || closeIcon === undefined || closeIcon === null
? defaultCloseIcon
: closeIcon;
})();
const ariaProps = pickAttrs(restProps, true);
});
const plainCloseIcon = customCloseIconRender
? customCloseIconRender(mergedCloseIcon)
: mergedCloseIcon;
return target;
}
const closeIconWithAria = React.isValidElement(plainCloseIcon) ? (
React.cloneElement(plainCloseIcon, ariaProps)
) : (
<span {...ariaProps}>{plainCloseIcon}</span>
/** Collection contains the all the props related with closable. e.g. `closable`, `closeIcon` */
interface ClosableCollection {
closable?: ClosableType;
closeIcon?: ReactNode;
}
/** Use same object to support `useMemo` optimization */
const EmptyFallbackCloseCollection: ClosableCollection = {};
export default function useClosable(
propCloseCollection?: ClosableCollection,
contextCloseCollection?: ClosableCollection | null,
fallbackCloseCollection: ClosableCollection & {
/**
* Some components need to wrap CloseIcon twice,
* this method will be executed once after the final CloseIcon is calculated
*/
closeIconRender?: (closeIcon: ReactNode) => ReactNode;
} = EmptyFallbackCloseCollection,
): [closable: boolean, closeIcon: React.ReactNode | null] {
// Align the `props`, `context` `fallback` to config object first
const propCloseConfig = useClosableConfig(propCloseCollection);
const contextCloseConfig = useClosableConfig(contextCloseCollection);
const mergedFallbackCloseCollection = React.useMemo(
() => ({
closeIcon: <CloseOutlined />,
...fallbackCloseCollection,
}),
[fallbackCloseCollection],
);
return [true, closeIconWithAria];
}
// Use fallback logic to fill the config
const mergedClosableConfig = React.useMemo(() => {
// ================ Props First ================
// Skip if prop is disabled
if (propCloseConfig === false) {
return false;
}
export default useClosable;
if (propCloseConfig) {
return assignWithoutUndefined(
mergedFallbackCloseCollection,
contextCloseConfig,
propCloseConfig,
);
}
// =============== Context Second ==============
// Skip if context is disabled
if (contextCloseConfig === false) {
return false;
}
if (contextCloseConfig) {
return assignWithoutUndefined(mergedFallbackCloseCollection, contextCloseConfig);
}
// ============= Fallback Default ==============
return !mergedFallbackCloseCollection.closable ? false : mergedFallbackCloseCollection;
}, [propCloseConfig, contextCloseConfig, mergedFallbackCloseCollection]);
// Calculate the final closeIcon
return React.useMemo(() => {
if (mergedClosableConfig === false) {
return [false, null];
}
const { closeIconRender } = mergedFallbackCloseCollection;
const { closeIcon } = mergedClosableConfig;
let mergedCloseIcon: ReactNode = closeIcon;
if (mergedCloseIcon !== null && mergedCloseIcon !== undefined) {
// Wrap the closeIcon if needed
if (closeIconRender) {
mergedCloseIcon = closeIconRender(closeIcon);
}
// Wrap the closeIcon with aria props
const ariaProps = pickAttrs(mergedClosableConfig, true);
if (Object.keys(ariaProps).length) {
mergedCloseIcon = React.isValidElement(mergedCloseIcon) ? (
React.cloneElement(mergedCloseIcon, ariaProps)
) : (
<span {...ariaProps}>{mergedCloseIcon}</span>
);
}
}
return [true, mergedCloseIcon];
}, [mergedClosableConfig, mergedFallbackCloseCollection]);
}

View File

@ -1086,6 +1086,59 @@ describe('ConfigProvider support style and className props', () => {
expect(element?.querySelector<HTMLSpanElement>('.cp-test-closeIcon')).toBeTruthy();
});
it('Should Tag support aria-* in closable', () => {
const { container } = render(
<ConfigProvider
tag={{
closable: {
closeIcon: <span className="cp-test-closeIcon">cp-test-closeIcon</span>,
'aria-label': 'Close Tag',
},
}}
>
<Tag>Test</Tag>
<Tag.CheckableTag checked>CheckableTag</Tag.CheckableTag>
</ConfigProvider>,
);
const element = container.querySelector<HTMLSpanElement>('.ant-tag');
expect(element?.querySelector('.ant-tag-close-icon')).toBeTruthy();
expect(element?.querySelector('.ant-tag-close-icon')?.getAttribute('aria-label')).toBe(
'Close Tag',
);
expect(element?.querySelector('.cp-test-closeIcon')).toBeTruthy();
});
it('Should Tag hide closeIcon when closeIcon=false', () => {
const { container } = render(
<ConfigProvider
tag={{
closeIcon: false,
}}
>
<Tag>Test</Tag>
<Tag.CheckableTag checked>CheckableTag</Tag.CheckableTag>
</ConfigProvider>,
);
const element = container.querySelector<HTMLSpanElement>('.ant-tag');
expect(element?.querySelector('.ant-tag-close-icon')).toBeFalsy();
});
it('Should Tag show default closeIcon when closeIcon=true', () => {
const { container } = render(
<ConfigProvider
tag={{
closeIcon: true,
}}
>
<Tag>Test</Tag>
<Tag.CheckableTag checked>CheckableTag</Tag.CheckableTag>
</ConfigProvider>,
);
const element = container.querySelector<HTMLSpanElement>('.ant-tag');
expect(element?.querySelector('.ant-tag-close-icon')).toBeTruthy();
expect(element?.querySelector('.anticon-close')).toBeTruthy();
});
it('Should Table className & style works', () => {
const { container } = render(
<ConfigProvider

View File

@ -125,7 +125,7 @@ export type MenuConfig = ComponentStyleConfig & Pick<MenuProps, 'expandIcon'>;
export type TourConfig = Pick<TourProps, 'closeIcon'>;
export type ModalConfig = ComponentStyleConfig &
Pick<ModalProps, 'classNames' | 'styles' | 'closeIcon'>;
Pick<ModalProps, 'classNames' | 'styles' | 'closeIcon' | 'closable'>;
export type TabsConfig = ComponentStyleConfig &
Pick<TabsProps, 'indicator' | 'indicatorSize' | 'moreIcon' | 'addIcon' | 'removeIcon'>;
@ -144,7 +144,7 @@ export type ButtonConfig = ComponentStyleConfig & Pick<ButtonProps, 'classNames'
export type NotificationConfig = ComponentStyleConfig & Pick<ArgsProps, 'closeIcon'>;
export type TagConfig = ComponentStyleConfig & Pick<TagProps, 'closeIcon'>;
export type TagConfig = ComponentStyleConfig & Pick<TagProps, 'closeIcon' | 'closable'>;
export type CardConfig = ComponentStyleConfig & Pick<CardProps, 'classNames' | 'styles'>;

View File

@ -1,7 +1,8 @@
import * as React from 'react';
import classNames from 'classnames';
import type { DrawerProps as RCDrawerProps } from 'rc-drawer';
import useClosable, { type ClosableType } from '../_util/hooks/useClosable';
import useClosable, { pickClosable, type ClosableType } from '../_util/hooks/useClosable';
import { ConfigContext } from '../config-provider';
export interface DrawerClassNames extends NonNullable<RCDrawerProps['classNames']> {
@ -57,8 +58,6 @@ const DrawerPanel: React.FC<DrawerPanelProps> = (props) => {
title,
footer,
extra,
closeIcon,
closable,
onClose,
headerStyle,
bodyStyle,
@ -78,19 +77,14 @@ const DrawerPanel: React.FC<DrawerPanelProps> = (props) => {
[onClose],
);
const mergedContextCloseIcon = React.useMemo(() => {
if (typeof drawerContext?.closable === 'object' && drawerContext.closable.closeIcon) {
return drawerContext.closable.closeIcon;
}
return drawerContext?.closeIcon;
}, [drawerContext?.closable, drawerContext?.closeIcon]);
const [mergedClosable, mergedCloseIcon] = useClosable({
closable: closable ?? drawerContext?.closable,
closeIcon: typeof closeIcon !== 'undefined' ? closeIcon : mergedContextCloseIcon,
customCloseIconRender,
defaultClosable: true,
});
const [mergedClosable, mergedCloseIcon] = useClosable(
pickClosable(props),
pickClosable(drawerContext),
{
closable: true,
closeIconRender: customCloseIconRender,
},
);
const headerNode = React.useMemo<React.ReactNode>(() => {
if (!title && !mergedClosable) {

View File

@ -3,7 +3,7 @@ import CloseOutlined from '@ant-design/icons/CloseOutlined';
import classNames from 'classnames';
import Dialog from 'rc-dialog';
import useClosable from '../_util/hooks/useClosable';
import useClosable, { pickClosable } from '../_util/hooks/useClosable';
import { useZIndex } from '../_util/hooks/useZIndex';
import { getTransitionName } from '../_util/motion';
import { canUseDocElement } from '../_util/styleChecker';
@ -44,7 +44,7 @@ const Modal: React.FC<ModalProps> = (props) => {
getPopupContainer: getContextPopupContainer,
getPrefixCls,
direction,
modal,
modal: modalContext,
} = React.useContext(ConfigContext);
const handleCancel = (e: React.MouseEvent<HTMLButtonElement>) => {
@ -77,8 +77,6 @@ const Modal: React.FC<ModalProps> = (props) => {
wrapClassName,
centered,
getContainer,
closeIcon,
closable,
focusTriggerAfterClose = true,
style,
// Deprecated
@ -106,13 +104,15 @@ const Modal: React.FC<ModalProps> = (props) => {
<Footer {...props} onOk={handleOk} onCancel={handleCancel} />
);
const [mergedClosable, mergedCloseIcon] = useClosable({
closable,
closeIcon: typeof closeIcon !== 'undefined' ? closeIcon : modal?.closeIcon,
customCloseIconRender: (icon) => renderCloseIcon(prefixCls, icon),
defaultCloseIcon: <CloseOutlined className={`${prefixCls}-close-icon`} />,
defaultClosable: true,
});
const [mergedClosable, mergedCloseIcon] = useClosable(
pickClosable(props),
pickClosable(modalContext),
{
closable: true,
closeIcon: <CloseOutlined className={`${prefixCls}-close-icon`} />,
closeIconRender: (icon) => renderCloseIcon(prefixCls, icon),
},
);
// ============================ Refs ============================
// Select `ant-modal-content` by `panelRef`
@ -142,15 +142,15 @@ const Modal: React.FC<ModalProps> = (props) => {
focusTriggerAfterClose={focusTriggerAfterClose}
transitionName={getTransitionName(rootPrefixCls, 'zoom', props.transitionName)}
maskTransitionName={getTransitionName(rootPrefixCls, 'fade', props.maskTransitionName)}
className={classNames(hashId, className, modal?.className)}
style={{ ...modal?.style, ...style }}
className={classNames(hashId, className, modalContext?.className)}
style={{ ...modalContext?.style, ...style }}
classNames={{
...modal?.classNames,
...modalContext?.classNames,
...modalClassNames,
wrapper: classNames(wrapClassNameExtended, modalClassNames?.wrapper),
}}
styles={{
...modal?.styles,
...modalContext?.styles,
...modalStyles,
}}
panelRef={panelRef}

View File

@ -178,28 +178,25 @@ Array [
>
Tag 2
<span
class="ant-tag-close-icon"
aria-label="close-circle"
class="anticon anticon-close-circle ant-tag-close-icon"
role="img"
tabindex="-1"
>
<span
aria-label="close-circle"
class="anticon anticon-close-circle"
role="img"
<svg
aria-hidden="true"
data-icon="close-circle"
fill="currentColor"
fill-rule="evenodd"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<svg
aria-hidden="true"
data-icon="close-circle"
fill="currentColor"
fill-rule="evenodd"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M512 64c247.4 0 448 200.6 448 448S759.4 960 512 960 64 759.4 64 512 264.6 64 512 64zm0 76c-205.4 0-372 166.6-372 372s166.6 372 372 372 372-166.6 372-372-166.6-372-372-372zm128.01 198.83c.03 0 .05.01.09.06l45.02 45.01a.2.2 0 01.05.09.12.12 0 010 .07c0 .02-.01.04-.05.08L557.25 512l127.87 127.86a.27.27 0 01.05.06v.02a.12.12 0 010 .07c0 .03-.01.05-.05.09l-45.02 45.02a.2.2 0 01-.09.05.12.12 0 01-.07 0c-.02 0-.04-.01-.08-.05L512 557.25 384.14 685.12c-.04.04-.06.05-.08.05a.12.12 0 01-.07 0c-.03 0-.05-.01-.09-.05l-45.02-45.02a.2.2 0 01-.05-.09.12.12 0 010-.07c0-.02.01-.04.06-.08L466.75 512 338.88 384.14a.27.27 0 01-.05-.06l-.01-.02a.12.12 0 010-.07c0-.03.01-.05.05-.09l45.02-45.02a.2.2 0 01.09-.05.12.12 0 01.07 0c.02 0 .04.01.08.06L512 466.75l127.86-127.86c.04-.05.06-.06.08-.06a.12.12 0 01.07 0z"
/>
</svg>
</span>
<path
d="M512 64c247.4 0 448 200.6 448 448S759.4 960 512 960 64 759.4 64 512 264.6 64 512 64zm0 76c-205.4 0-372 166.6-372 372s166.6 372 372 372 372-166.6 372-372-166.6-372-372-372zm128.01 198.83c.03 0 .05.01.09.06l45.02 45.01a.2.2 0 01.05.09.12.12 0 010 .07c0 .02-.01.04-.05.08L557.25 512l127.87 127.86a.27.27 0 01.05.06v.02a.12.12 0 010 .07c0 .03-.01.05-.05.09l-45.02 45.02a.2.2 0 01-.09.05.12.12 0 01-.07 0c-.02 0-.04-.01-.08-.05L512 557.25 384.14 685.12c-.04.04-.06.05-.08.05a.12.12 0 01-.07 0c-.03 0-.05-.01-.09-.05l-45.02-45.02a.2.2 0 01-.05-.09.12.12 0 010-.07c0-.02.01-.04.06-.08L466.75 512 338.88 384.14a.27.27 0 01-.05-.06l-.01-.02a.12.12 0 010-.07c0-.03.01-.05.05-.09l45.02-45.02a.2.2 0 01.09-.05.12.12 0 01.07 0c.02 0 .04.01.08.06L512 466.75l127.86-127.86c.04-.05.06-.06.08-.06a.12.12 0 01.07 0z"
/>
</svg>
</span>
</span>,
]
@ -845,28 +842,25 @@ exports[`renders components/tag/demo/customize.tsx extend context correctly 1`]
>
Tag2
<span
class="ant-tag-close-icon"
aria-label="close-circle"
class="anticon anticon-close-circle ant-tag-close-icon"
role="img"
tabindex="-1"
>
<span
aria-label="close-circle"
class="anticon anticon-close-circle"
role="img"
<svg
aria-hidden="true"
data-icon="close-circle"
fill="currentColor"
fill-rule="evenodd"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<svg
aria-hidden="true"
data-icon="close-circle"
fill="currentColor"
fill-rule="evenodd"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M512 64c247.4 0 448 200.6 448 448S759.4 960 512 960 64 759.4 64 512 264.6 64 512 64zm0 76c-205.4 0-372 166.6-372 372s166.6 372 372 372 372-166.6 372-372-166.6-372-372-372zm128.01 198.83c.03 0 .05.01.09.06l45.02 45.01a.2.2 0 01.05.09.12.12 0 010 .07c0 .02-.01.04-.05.08L557.25 512l127.87 127.86a.27.27 0 01.05.06v.02a.12.12 0 010 .07c0 .03-.01.05-.05.09l-45.02 45.02a.2.2 0 01-.09.05.12.12 0 01-.07 0c-.02 0-.04-.01-.08-.05L512 557.25 384.14 685.12c-.04.04-.06.05-.08.05a.12.12 0 01-.07 0c-.03 0-.05-.01-.09-.05l-45.02-45.02a.2.2 0 01-.05-.09.12.12 0 010-.07c0-.02.01-.04.06-.08L466.75 512 338.88 384.14a.27.27 0 01-.05-.06l-.01-.02a.12.12 0 010-.07c0-.03.01-.05.05-.09l45.02-45.02a.2.2 0 01.09-.05.12.12 0 01.07 0c.02 0 .04.01.08.06L512 466.75l127.86-127.86c.04-.05.06-.06.08-.06a.12.12 0 01.07 0z"
/>
</svg>
</span>
<path
d="M512 64c247.4 0 448 200.6 448 448S759.4 960 512 960 64 759.4 64 512 264.6 64 512 64zm0 76c-205.4 0-372 166.6-372 372s166.6 372 372 372 372-166.6 372-372-166.6-372-372-372zm128.01 198.83c.03 0 .05.01.09.06l45.02 45.01a.2.2 0 01.05.09.12.12 0 010 .07c0 .02-.01.04-.05.08L557.25 512l127.87 127.86a.27.27 0 01.05.06v.02a.12.12 0 010 .07c0 .03-.01.05-.05.09l-45.02 45.02a.2.2 0 01-.09.05.12.12 0 01-.07 0c-.02 0-.04-.01-.08-.05L512 557.25 384.14 685.12c-.04.04-.06.05-.08.05a.12.12 0 01-.07 0c-.03 0-.05-.01-.09-.05l-45.02-45.02a.2.2 0 01-.05-.09.12.12 0 010-.07c0-.02.01-.04.06-.08L466.75 512 338.88 384.14a.27.27 0 01-.05-.06l-.01-.02a.12.12 0 010-.07c0-.03.01-.05.05-.09l45.02-45.02a.2.2 0 01.09-.05.12.12 0 01.07 0c.02 0 .04.01.08.06L512 466.75l127.86-127.86c.04-.05.06-.06.08-.06a.12.12 0 01.07 0z"
/>
</svg>
</span>
</span>
</div>

View File

@ -176,28 +176,25 @@ Array [
>
Tag 2
<span
class="ant-tag-close-icon"
aria-label="close-circle"
class="anticon anticon-close-circle ant-tag-close-icon"
role="img"
tabindex="-1"
>
<span
aria-label="close-circle"
class="anticon anticon-close-circle"
role="img"
<svg
aria-hidden="true"
data-icon="close-circle"
fill="currentColor"
fill-rule="evenodd"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<svg
aria-hidden="true"
data-icon="close-circle"
fill="currentColor"
fill-rule="evenodd"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M512 64c247.4 0 448 200.6 448 448S759.4 960 512 960 64 759.4 64 512 264.6 64 512 64zm0 76c-205.4 0-372 166.6-372 372s166.6 372 372 372 372-166.6 372-372-166.6-372-372-372zm128.01 198.83c.03 0 .05.01.09.06l45.02 45.01a.2.2 0 01.05.09.12.12 0 010 .07c0 .02-.01.04-.05.08L557.25 512l127.87 127.86a.27.27 0 01.05.06v.02a.12.12 0 010 .07c0 .03-.01.05-.05.09l-45.02 45.02a.2.2 0 01-.09.05.12.12 0 01-.07 0c-.02 0-.04-.01-.08-.05L512 557.25 384.14 685.12c-.04.04-.06.05-.08.05a.12.12 0 01-.07 0c-.03 0-.05-.01-.09-.05l-45.02-45.02a.2.2 0 01-.05-.09.12.12 0 010-.07c0-.02.01-.04.06-.08L466.75 512 338.88 384.14a.27.27 0 01-.05-.06l-.01-.02a.12.12 0 010-.07c0-.03.01-.05.05-.09l45.02-45.02a.2.2 0 01.09-.05.12.12 0 01.07 0c.02 0 .04.01.08.06L512 466.75l127.86-127.86c.04-.05.06-.06.08-.06a.12.12 0 01.07 0z"
/>
</svg>
</span>
<path
d="M512 64c247.4 0 448 200.6 448 448S759.4 960 512 960 64 759.4 64 512 264.6 64 512 64zm0 76c-205.4 0-372 166.6-372 372s166.6 372 372 372 372-166.6 372-372-166.6-372-372-372zm128.01 198.83c.03 0 .05.01.09.06l45.02 45.01a.2.2 0 01.05.09.12.12 0 010 .07c0 .02-.01.04-.05.08L557.25 512l127.87 127.86a.27.27 0 01.05.06v.02a.12.12 0 010 .07c0 .03-.01.05-.05.09l-45.02 45.02a.2.2 0 01-.09.05.12.12 0 01-.07 0c-.02 0-.04-.01-.08-.05L512 557.25 384.14 685.12c-.04.04-.06.05-.08.05a.12.12 0 01-.07 0c-.03 0-.05-.01-.09-.05l-45.02-45.02a.2.2 0 01-.05-.09.12.12 0 010-.07c0-.02.01-.04.06-.08L466.75 512 338.88 384.14a.27.27 0 01-.05-.06l-.01-.02a.12.12 0 010-.07c0-.03.01-.05.05-.09l45.02-45.02a.2.2 0 01.09-.05.12.12 0 01.07 0c.02 0 .04.01.08.06L512 466.75l127.86-127.86c.04-.05.06-.06.08-.06a.12.12 0 01.07 0z"
/>
</svg>
</span>
</span>,
]
@ -829,28 +826,25 @@ exports[`renders components/tag/demo/customize.tsx correctly 1`] = `
>
Tag2
<span
class="ant-tag-close-icon"
aria-label="close-circle"
class="anticon anticon-close-circle ant-tag-close-icon"
role="img"
tabindex="-1"
>
<span
aria-label="close-circle"
class="anticon anticon-close-circle"
role="img"
<svg
aria-hidden="true"
data-icon="close-circle"
fill="currentColor"
fill-rule="evenodd"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<svg
aria-hidden="true"
data-icon="close-circle"
fill="currentColor"
fill-rule="evenodd"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M512 64c247.4 0 448 200.6 448 448S759.4 960 512 960 64 759.4 64 512 264.6 64 512 64zm0 76c-205.4 0-372 166.6-372 372s166.6 372 372 372 372-166.6 372-372-166.6-372-372-372zm128.01 198.83c.03 0 .05.01.09.06l45.02 45.01a.2.2 0 01.05.09.12.12 0 010 .07c0 .02-.01.04-.05.08L557.25 512l127.87 127.86a.27.27 0 01.05.06v.02a.12.12 0 010 .07c0 .03-.01.05-.05.09l-45.02 45.02a.2.2 0 01-.09.05.12.12 0 01-.07 0c-.02 0-.04-.01-.08-.05L512 557.25 384.14 685.12c-.04.04-.06.05-.08.05a.12.12 0 01-.07 0c-.03 0-.05-.01-.09-.05l-45.02-45.02a.2.2 0 01-.05-.09.12.12 0 010-.07c0-.02.01-.04.06-.08L466.75 512 338.88 384.14a.27.27 0 01-.05-.06l-.01-.02a.12.12 0 010-.07c0-.03.01-.05.05-.09l45.02-45.02a.2.2 0 01.09-.05.12.12 0 01.07 0c.02 0 .04.01.08.06L512 466.75l127.86-127.86c.04-.05.06-.06.08-.06a.12.12 0 01.07 0z"
/>
</svg>
</span>
<path
d="M512 64c247.4 0 448 200.6 448 448S759.4 960 512 960 64 759.4 64 512 264.6 64 512 64zm0 76c-205.4 0-372 166.6-372 372s166.6 372 372 372 372-166.6 372-372-166.6-372-372-372zm128.01 198.83c.03 0 .05.01.09.06l45.02 45.01a.2.2 0 01.05.09.12.12 0 010 .07c0 .02-.01.04-.05.08L557.25 512l127.87 127.86a.27.27 0 01.05.06v.02a.12.12 0 010 .07c0 .03-.01.05-.05.09l-45.02 45.02a.2.2 0 01-.09.05.12.12 0 01-.07 0c-.02 0-.04-.01-.08-.05L512 557.25 384.14 685.12c-.04.04-.06.05-.08.05a.12.12 0 01-.07 0c-.03 0-.05-.01-.09-.05l-45.02-45.02a.2.2 0 01-.05-.09.12.12 0 010-.07c0-.02.01-.04.06-.08L466.75 512 338.88 384.14a.27.27 0 01-.05-.06l-.01-.02a.12.12 0 010-.07c0-.03.01-.05.05-.09l45.02-45.02a.2.2 0 01.09-.05.12.12 0 01.07 0c.02 0 .04.01.08.06L512 466.75l127.86-127.86c.04-.05.06-.06.08-.06a.12.12 0 01.07 0z"
/>
</svg>
</span>
</span>
</div>

View File

@ -1,10 +1,9 @@
import React from 'react';
import { CheckCircleOutlined } from '@ant-design/icons';
import { Simulate } from 'react-dom/test-utils';
import { CheckCircleOutlined } from '@ant-design/icons';
import Tag from '..';
import { resetWarned } from '../../_util/warning';
import mountTest from '../../../tests/shared/mountTest';
import rtlTest from '../../../tests/shared/rtlTest';
import { act, fireEvent, render } from '../../../tests/utils';
@ -219,4 +218,11 @@ describe('Tag', () => {
waitRaf();
expect(document.querySelector('.ant-wave')).toBeFalsy();
});
it('should support aria-* in closable', () => {
const { container } = render(<Tag closable={{ closeIcon: 'X', 'aria-label': 'CloseBtn' }} />);
expect(container.querySelector('.ant-tag-close-icon')?.getAttribute('aria-label')).toEqual(
'CloseBtn',
);
expect(container.querySelector('.ant-tag-close-icon')?.textContent).toEqual('X');
});
});

View File

@ -1,10 +1,11 @@
import * as React from 'react';
import CloseOutlined from '@ant-design/icons/CloseOutlined';
import classNames from 'classnames';
import omit from 'rc-util/lib/omit';
import type { PresetColorType, PresetStatusColorType } from '../_util/colors';
import { isPresetColor, isPresetStatusColor } from '../_util/colors';
import useClosable from '../_util/hooks/useClosable';
import useClosable, { pickClosable } from '../_util/hooks/useClosable';
import { replaceElement } from '../_util/reactNode';
import type { LiteralUnion } from '../_util/type';
import { devUseWarning } from '../_util/warning';
import Wave from '../_util/wave';
@ -21,7 +22,7 @@ export interface TagProps extends React.HTMLAttributes<HTMLSpanElement> {
className?: string;
rootClassName?: string;
color?: LiteralUnion<PresetColorType | PresetStatusColorType>;
closable?: boolean;
closable?: boolean | ({ closeIcon?: React.ReactNode } & React.AriaAttributes);
/** Advised to use closeIcon instead. */
closeIcon?: React.ReactNode;
/** @deprecated `visible` will be removed in next major version. */
@ -47,26 +48,27 @@ const InternalTag: React.ForwardRefRenderFunction<HTMLSpanElement, TagProps> = (
icon,
color,
onClose,
closeIcon,
closable,
bordered = true,
visible: deprecatedVisible,
...props
} = tagProps;
const { getPrefixCls, direction, tag } = React.useContext(ConfigContext);
const { getPrefixCls, direction, tag: tagContext } = React.useContext(ConfigContext);
const [visible, setVisible] = React.useState(true);
const domProps = omit(props, ['closeIcon', 'closable']);
// Warning for deprecated usage
if (process.env.NODE_ENV !== 'production') {
const warning = devUseWarning('Tag');
warning.deprecated(!('visible' in props), 'visible', 'visible && <Tag />');
warning.deprecated(!('visible' in tagProps), 'visible', 'visible && <Tag />');
}
React.useEffect(() => {
if ('visible' in props) {
setVisible(props.visible!);
if (deprecatedVisible !== undefined) {
setVisible(deprecatedVisible!);
}
}, [props.visible]);
}, [deprecatedVisible]);
const isPreset = isPresetColor(color);
const isStatus = isPresetStatusColor(color);
@ -74,7 +76,7 @@ const InternalTag: React.ForwardRefRenderFunction<HTMLSpanElement, TagProps> = (
const tagStyle: React.CSSProperties = {
backgroundColor: color && !isInternalColor ? color : undefined,
...tag?.style,
...tagContext?.style,
...style,
};
@ -84,7 +86,7 @@ const InternalTag: React.ForwardRefRenderFunction<HTMLSpanElement, TagProps> = (
const tagClassName = classNames(
prefixCls,
tag?.className,
tagContext?.className,
{
[`${prefixCls}-${color}`]: isInternalColor,
[`${prefixCls}-has-color`]: color && !isInternalColor,
@ -108,19 +110,22 @@ const InternalTag: React.ForwardRefRenderFunction<HTMLSpanElement, TagProps> = (
setVisible(false);
};
const [, mergedCloseIcon] = useClosable({
closable,
closeIcon: closeIcon ?? tag?.closeIcon,
customCloseIconRender: (iconNode: React.ReactNode) =>
iconNode === null ? (
<CloseOutlined className={`${prefixCls}-close-icon`} onClick={handleCloseClick} />
) : (
const [, mergedCloseIcon] = useClosable(pickClosable(tagProps), pickClosable(tagContext), {
closable: false,
closeIconRender: (iconNode: React.ReactNode) => {
const replacement = (
<span className={`${prefixCls}-close-icon`} onClick={handleCloseClick}>
{iconNode}
</span>
),
defaultCloseIcon: null,
defaultClosable: false,
);
return replaceElement(iconNode, replacement, (originProps) => ({
onClick: (e: React.MouseEvent<HTMLElement>) => {
originProps?.onClick?.(e);
handleCloseClick(e);
},
className: classNames(originProps?.className, `${prefixCls}-close-icon`),
}));
},
});
const isNeedWave =
@ -139,7 +144,7 @@ const InternalTag: React.ForwardRefRenderFunction<HTMLSpanElement, TagProps> = (
);
const tagNode: React.ReactNode = (
<span {...props} ref={ref} className={tagClassName} style={tagStyle}>
<span {...domProps} ref={ref} className={tagClassName} style={tagStyle}>
{kids}
{mergedCloseIcon}
{isPreset && <PresetCmp key="preset" prefixCls={prefixCls} />}

View File

@ -1,5 +1,4 @@
import * as React from 'react';
import CloseOutlined from '@ant-design/icons/CloseOutlined';
import classNames from 'classnames';
import useClosable from '../_util/hooks/useClosable';
@ -31,17 +30,14 @@ const PurePanel: React.FC<PurePanelProps> = (props) => {
const [wrapCSSVar, hashId, cssVarCls] = useStyle(prefixCls);
const [mergedClosable, mergedCloseIcon] = useClosable({
closable,
closeIcon,
customCloseIconRender: (icon) =>
const [mergedClosable, mergedCloseIcon] = useClosable({ closable, closeIcon }, null, {
closable: true,
closeIconRender: (icon) =>
React.isValidElement(icon)
? cloneElement(icon, {
className: classNames(icon.props.className, `${prefixCls}-close-icon`),
})
: icon,
defaultCloseIcon: <CloseOutlined />,
defaultClosable: true,
});
return wrapCSSVar(