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

View File

@ -3,7 +3,23 @@ import React from 'react';
import CloseOutlined from '@ant-design/icons/CloseOutlined'; import CloseOutlined from '@ant-design/icons/CloseOutlined';
import pickAttrs from 'rc-util/lib/pickAttrs'; 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 = { export type UseClosableParams = {
closable?: ClosableType; closable?: ClosableType;
@ -11,63 +27,150 @@ export type UseClosableParams = {
defaultClosable?: boolean; defaultClosable?: boolean;
defaultCloseIcon?: ReactNode; defaultCloseIcon?: ReactNode;
customCloseIconRender?: (closeIcon: ReactNode) => ReactNode; customCloseIconRender?: (closeIcon: ReactNode) => ReactNode;
context?: ContextClosable;
}; };
function useInnerClosable( /** Convert `closable` and `closeIcon` to config object */
closable?: UseClosableParams['closable'], function useClosableConfig(closableCollection?: ClosableCollection | null) {
closeIcon?: ReactNode, const { closable, closeIcon } = closableCollection || {};
defaultClosable?: boolean,
) { return React.useMemo(() => {
if (typeof closable === 'boolean') { if (
return closable; // If `closable`, whatever rest be should be true
} !closable &&
if (typeof closable === 'object') { (closable === false || closeIcon === false || closeIcon === null)
return true; ) {
} return false;
if (closeIcon === undefined) { }
return !!defaultClosable;
} if (closable === undefined && closeIcon === undefined) {
return closeIcon !== false && closeIcon !== null; 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, * Assign object without `undefined` field. Will skip if is `false`.
closeIcon, * This helps to handle both closableConfig or false
customCloseIconRender, */
defaultCloseIcon = <CloseOutlined />, function assignWithoutUndefined<T extends object>(
defaultClosable = false, ...objList: (Partial<T> | false | null | undefined)[]
}: UseClosableParams): [closable: boolean, closeIcon: React.ReactNode | null] { ): Partial<T> {
const mergedClosable = useInnerClosable(closable, closeIcon, defaultClosable); const target: Partial<T> = {};
if (!mergedClosable) { objList.forEach((obj) => {
return [false, null]; if (obj) {
} (Object.keys(obj) as (keyof T)[]).forEach((key) => {
const { closeIcon: closableIcon, ...restProps } = if (obj[key] !== undefined) {
typeof closable === 'object' target[key] = obj[key];
? closable }
: ({} as { closeIcon: React.ReactNode } & React.AriaAttributes); });
// Priority: closable.closeIcon > closeIcon > defaultCloseIcon
const mergedCloseIcon: ReactNode = (() => {
if (typeof closable === 'object' && closableIcon !== undefined) {
return closableIcon;
} }
return typeof closeIcon === 'boolean' || closeIcon === undefined || closeIcon === null });
? defaultCloseIcon
: closeIcon;
})();
const ariaProps = pickAttrs(restProps, true);
const plainCloseIcon = customCloseIconRender return target;
? customCloseIconRender(mergedCloseIcon) }
: mergedCloseIcon;
const closeIconWithAria = React.isValidElement(plainCloseIcon) ? ( /** Collection contains the all the props related with closable. e.g. `closable`, `closeIcon` */
React.cloneElement(plainCloseIcon, ariaProps) interface ClosableCollection {
) : ( closable?: ClosableType;
<span {...ariaProps}>{plainCloseIcon}</span> 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(); 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', () => { it('Should Table className & style works', () => {
const { container } = render( const { container } = render(
<ConfigProvider <ConfigProvider

View File

@ -125,7 +125,7 @@ export type MenuConfig = ComponentStyleConfig & Pick<MenuProps, 'expandIcon'>;
export type TourConfig = Pick<TourProps, 'closeIcon'>; export type TourConfig = Pick<TourProps, 'closeIcon'>;
export type ModalConfig = ComponentStyleConfig & export type ModalConfig = ComponentStyleConfig &
Pick<ModalProps, 'classNames' | 'styles' | 'closeIcon'>; Pick<ModalProps, 'classNames' | 'styles' | 'closeIcon' | 'closable'>;
export type TabsConfig = ComponentStyleConfig & export type TabsConfig = ComponentStyleConfig &
Pick<TabsProps, 'indicator' | 'indicatorSize' | 'moreIcon' | 'addIcon' | 'removeIcon'>; 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 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'>; export type CardConfig = ComponentStyleConfig & Pick<CardProps, 'classNames' | 'styles'>;

View File

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

View File

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

View File

@ -178,28 +178,25 @@ Array [
> >
Tag 2 Tag 2
<span <span
class="ant-tag-close-icon" aria-label="close-circle"
class="anticon anticon-close-circle ant-tag-close-icon"
role="img"
tabindex="-1"
> >
<span <svg
aria-label="close-circle" aria-hidden="true"
class="anticon anticon-close-circle" data-icon="close-circle"
role="img" fill="currentColor"
fill-rule="evenodd"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
> >
<svg <path
aria-hidden="true" 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"
data-icon="close-circle" />
fill="currentColor" </svg>
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>
</span> </span>
</span>, </span>,
] ]
@ -845,28 +842,25 @@ exports[`renders components/tag/demo/customize.tsx extend context correctly 1`]
> >
Tag2 Tag2
<span <span
class="ant-tag-close-icon" aria-label="close-circle"
class="anticon anticon-close-circle ant-tag-close-icon"
role="img"
tabindex="-1"
> >
<span <svg
aria-label="close-circle" aria-hidden="true"
class="anticon anticon-close-circle" data-icon="close-circle"
role="img" fill="currentColor"
fill-rule="evenodd"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
> >
<svg <path
aria-hidden="true" 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"
data-icon="close-circle" />
fill="currentColor" </svg>
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>
</span> </span>
</span> </span>
</div> </div>

View File

@ -176,28 +176,25 @@ Array [
> >
Tag 2 Tag 2
<span <span
class="ant-tag-close-icon" aria-label="close-circle"
class="anticon anticon-close-circle ant-tag-close-icon"
role="img"
tabindex="-1"
> >
<span <svg
aria-label="close-circle" aria-hidden="true"
class="anticon anticon-close-circle" data-icon="close-circle"
role="img" fill="currentColor"
fill-rule="evenodd"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
> >
<svg <path
aria-hidden="true" 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"
data-icon="close-circle" />
fill="currentColor" </svg>
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>
</span> </span>
</span>, </span>,
] ]
@ -829,28 +826,25 @@ exports[`renders components/tag/demo/customize.tsx correctly 1`] = `
> >
Tag2 Tag2
<span <span
class="ant-tag-close-icon" aria-label="close-circle"
class="anticon anticon-close-circle ant-tag-close-icon"
role="img"
tabindex="-1"
> >
<span <svg
aria-label="close-circle" aria-hidden="true"
class="anticon anticon-close-circle" data-icon="close-circle"
role="img" fill="currentColor"
fill-rule="evenodd"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
> >
<svg <path
aria-hidden="true" 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"
data-icon="close-circle" />
fill="currentColor" </svg>
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>
</span> </span>
</span> </span>
</div> </div>

View File

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

View File

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