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: Drawer support aria in closable #47543

Merged
merged 13 commits into from
Feb 28, 2024
65 changes: 38 additions & 27 deletions components/_util/__tests__/hooks.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { useEffect } from 'react';
import { CloseOutlined } from '@ant-design/icons';
import { render } from '@testing-library/react';
import React, { useEffect } from 'react';

import type { UseClosableParams } from '../hooks/useClosable';
import useClosable from '../hooks/useClosable';

Expand All @@ -19,7 +20,7 @@ describe('hooks test', () => {
},
{
params: [undefined, undefined, true],
res: [true, 'anticon-close'],
res: [true, '.anticon-close'],
},
{
params: [undefined, undefined, false],
Expand All @@ -33,11 +34,11 @@ describe('hooks test', () => {
},
{
params: [true, undefined, true],
res: [true, 'anticon-close'],
res: [true, '.anticon-close'],
},
{
params: [true, undefined, false],
res: [true, 'anticon-close'],
res: [true, '.anticon-close'],
},

// test case like: <Component closable={false | true} closeIcon={null | false | element} />
Expand All @@ -51,19 +52,19 @@ describe('hooks test', () => {
},
{
params: [true, null, true],
res: [true, 'anticon-close'],
res: [true, '.anticon-close'],
},
{
params: [true, false, true],
res: [true, 'anticon-close'],
res: [true, '.anticon-close'],
},
{
params: [true, null, false],
res: [true, 'anticon-close'],
res: [true, '.anticon-close'],
},
{
params: [true, false, false],
res: [true, 'anticon-close'],
res: [true, '.anticon-close'],
},
{
params: [
Expand All @@ -73,7 +74,7 @@ describe('hooks test', () => {
</div>,
false,
],
res: [true, 'custom-close'],
res: [true, '.custom-close'],
},
{
params: [false, <div key="close">close</div>, false],
Expand All @@ -97,7 +98,7 @@ describe('hooks test', () => {
</div>,
undefined,
],
res: [true, 'custom-close'],
res: [true, '.custom-close'],
},
{
params: [
Expand All @@ -107,7 +108,7 @@ describe('hooks test', () => {
</div>,
true,
],
res: [true, 'custom-close'],
res: [true, '.custom-close'],
},
{
params: [
Expand All @@ -117,7 +118,18 @@ describe('hooks test', () => {
</div>,
false,
],
res: [true, 'custom-close'],
res: [true, '.custom-close'],
},
{
params: [
{
closeIcon: 'Close',
'aria-label': 'Close Btn',
},
undefined,
false,
],
res: [true, '*[aria-label="Close Btn"]'],
},
];

Expand All @@ -126,13 +138,11 @@ describe('hooks test', () => {
React.isValidElement(params[1]) ? 'element' : params[1]
},defaultClosable=${params[2]}. the result should be ${res}`, () => {
const App = () => {
const [closable, closeIcon] = useClosable(
params[0],
params[1],
undefined,
undefined,
params[2],
);
const [closable, closeIcon] = useClosable({
closable: params[0],
closeIcon: params[1],
defaultClosable: params[2],
});
useEffect(() => {
expect(closable).toBe(res[0]);
}, [closable]);
Expand All @@ -142,19 +152,17 @@ describe('hooks test', () => {
if (res[1] === '') {
expect(container.querySelector('.anticon-close')).toBeFalsy();
} else {
expect(container.querySelector(`.${res[1]}`)).toBeTruthy();
expect(container.querySelector(`${res[1]}`)).toBeTruthy();
}
});
});

it('useClosable with defaultCloseIcon', () => {
const App = () => {
const [closable, closeIcon] = useClosable(
true,
undefined,
undefined,
<CloseOutlined className="custom-close-icon" />,
);
const [closable, closeIcon] = useClosable({
closable: true,
defaultCloseIcon: <CloseOutlined className="custom-close-icon" />,
});
useEffect(() => {
expect(closable).toBe(true);
}, [closable]);
Expand All @@ -169,7 +177,10 @@ describe('hooks test', () => {
const customCloseIconRender = (icon: React.ReactNode) => (
<span className="custom-close-wrapper">{icon}</span>
);
const [closable, closeIcon] = useClosable(true, undefined, customCloseIconRender);
const [closable, closeIcon] = useClosable({
closable: true,
customCloseIconRender,
});
useEffect(() => {
expect(closable).toBe(true);
}, [closable]);
Expand Down
66 changes: 48 additions & 18 deletions components/_util/hooks/useClosable.tsx
Original file line number Diff line number Diff line change
@@ -1,41 +1,71 @@
import type { ReactNode } from 'react';
import React from 'react';
import CloseOutlined from '@ant-design/icons/CloseOutlined';
import pickAttrs from 'rc-util/lib/pickAttrs';

function useInnerClosable(closable?: boolean, closeIcon?: ReactNode, defaultClosable?: boolean) {
export type UseClosableParams = {
closable?: boolean | ({ closeIcon?: React.ReactNode } & React.AriaAttributes);
closeIcon?: ReactNode;
defaultClosable?: boolean;
defaultCloseIcon?: ReactNode;
customCloseIconRender?: (closeIcon: ReactNode) => ReactNode;
};

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;
}

export type UseClosableParams = {
closable?: boolean;
closeIcon?: ReactNode;
defaultClosable?: boolean;
defaultCloseIcon?: ReactNode;
customCloseIconRender?: (closeIcon: ReactNode) => ReactNode;
};

function useClosable(
closable?: boolean,
closeIcon?: ReactNode,
customCloseIconRender?: (closeIcon: ReactNode) => ReactNode,
defaultCloseIcon: ReactNode = <CloseOutlined />,
function useClosable({
closable,
kiner-tang marked this conversation as resolved.
Show resolved Hide resolved
closeIcon,
customCloseIconRender,
defaultCloseIcon = <CloseOutlined />,
defaultClosable = false,
): [closable: boolean, closeIcon: React.ReactNode | null] {
}: UseClosableParams): [closable: boolean, closeIcon: React.ReactNode | null] {
const mergedClosable = useInnerClosable(closable, closeIcon, defaultClosable);

if (!mergedClosable) {
return [false, null];
}
const mergedCloseIcon =
typeof closeIcon === 'boolean' || closeIcon === undefined || closeIcon === 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;
}
return typeof closeIcon === 'boolean' || closeIcon === undefined || closeIcon === null
? defaultCloseIcon
: closeIcon;
return [true, customCloseIconRender ? customCloseIconRender(mergedCloseIcon) : mergedCloseIcon];
})();
const ariaProps = pickAttrs(restProps, true);

const plainCloseIcon = customCloseIconRender
? customCloseIconRender(mergedCloseIcon)
: mergedCloseIcon;

const closeIconWithAria = React.isValidElement(plainCloseIcon) ? (
React.cloneElement(plainCloseIcon, ariaProps)
kiner-tang marked this conversation as resolved.
Show resolved Hide resolved
) : (
<span {...ariaProps}>{plainCloseIcon}</span>
);

return [true, closeIconWithAria];
}

export default useClosable;
19 changes: 19 additions & 0 deletions components/config-provider/__tests__/style.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,25 @@ describe('ConfigProvider support style and className props', () => {
expect(document.querySelector<HTMLSpanElement>(selectors)).toBeTruthy();
});

it('Should support closable', () => {
render(
<ConfigProvider
drawer={{
closable: {
closeIcon: <span className="cp-test-close-icon">close</span>,
'aria-label': 'Close Btn',
},
}}
>
<Drawer title="Test Drawer" open />
</ConfigProvider>,
);

const selectors = '.ant-drawer-content .ant-drawer-close .cp-test-close-icon';
expect(document.querySelector<HTMLSpanElement>(selectors)).toBeTruthy();
expect(document.querySelector('*[aria-label="Close Btn"]')).toBeTruthy();
});

it('Should Drawer style works', () => {
render(
<ConfigProvider
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 @@ -109,7 +109,7 @@ export interface CardConfig extends ComponentStyleConfig {
}

export type DrawerConfig = ComponentStyleConfig &
Pick<DrawerProps, 'classNames' | 'styles' | 'closeIcon'>;
Pick<DrawerProps, 'classNames' | 'styles' | 'closeIcon' | 'closable'>;

export type FlexConfig = ComponentStyleConfig & Pick<FlexProps, 'vertical'>;

Expand Down
21 changes: 13 additions & 8 deletions components/drawer/DrawerPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import * as React from 'react';
import classNames from 'classnames';
import type { DrawerProps as RCDrawerProps } from 'rc-drawer';

import useClosable from '../_util/hooks/useClosable';
import { ConfigContext } from '../config-provider';

Expand Down Expand Up @@ -30,7 +29,7 @@ export interface DrawerPanelProps {
*
* `<Drawer closeIcon={false} />`
*/
closable?: boolean;
closable?: boolean | ({ closeIcon?: React.ReactNode } & React.AriaAttributes);
closeIcon?: React.ReactNode;
onClose?: RCDrawerProps['onClose'];

Expand Down Expand Up @@ -79,13 +78,19 @@ const DrawerPanel: React.FC<DrawerPanelProps> = (props) => {
[onClose],
);

const [mergedClosable, mergedCloseIcon] = useClosable(
closable,
typeof closeIcon !== 'undefined' ? closeIcon : drawerContext?.closeIcon,
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,
undefined,
true,
);
defaultClosable: true,
});

const headerNode = React.useMemo<React.ReactNode>(() => {
if (!title && !mergedClosable) {
Expand Down
16 changes: 16 additions & 0 deletions components/drawer/__tests__/Drawer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -361,4 +361,20 @@ describe('Drawer', () => {
expect(container1.outerHTML).toEqual(container2.outerHTML);
});
});
it('should support aria-* and closeIcon by closable', async () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

async 好像是多余的

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const { baseElement } = render(
<Drawer
open
closable={{
'aria-label': 'Close',
closeIcon: <span className="custom-close">Close</span>,
}}
>
Here is content of Drawer
</Drawer>,
);
expect(baseElement.querySelector('.ant-drawer-close')).not.toBeNull();
expect(baseElement.querySelector('.custom-close')).not.toBeNull();
expect(baseElement.querySelector('*[aria-label="Close"]')).not.toBeNull();
});
});
12 changes: 6 additions & 6 deletions components/modal/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,13 +106,13 @@ const Modal: React.FC<ModalProps> = (props) => {
<Footer {...props} onOk={handleOk} onCancel={handleCancel} />
);

const [mergedClosable, mergedCloseIcon] = useClosable(
const [mergedClosable, mergedCloseIcon] = useClosable({
closable,
typeof closeIcon !== 'undefined' ? closeIcon : modal?.closeIcon,
(icon) => renderCloseIcon(prefixCls, icon),
<CloseOutlined className={`${prefixCls}-close-icon`} />,
true,
);
closeIcon: typeof closeIcon !== 'undefined' ? closeIcon : modal?.closeIcon,
customCloseIconRender: (icon) => renderCloseIcon(prefixCls, icon),
defaultCloseIcon: <CloseOutlined className={`${prefixCls}-close-icon`} />,
defaultClosable: true,
});

// ============================ Refs ============================
// Select `ant-modal-content` by `panelRef`
Expand Down
12 changes: 6 additions & 6 deletions components/tag/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,20 +108,20 @@ const InternalTag: React.ForwardRefRenderFunction<HTMLSpanElement, TagProps> = (
setVisible(false);
};

const [, mergedCloseIcon] = useClosable(
const [, mergedCloseIcon] = useClosable({
closable,
closeIcon ?? tag?.closeIcon,
(iconNode: React.ReactNode) =>
closeIcon: closeIcon ?? tag?.closeIcon,
customCloseIconRender: (iconNode: React.ReactNode) =>
iconNode === null ? (
<CloseOutlined className={`${prefixCls}-close-icon`} onClick={handleCloseClick} />
) : (
<span className={`${prefixCls}-close-icon`} onClick={handleCloseClick}>
{iconNode}
</span>
),
null,
false,
);
defaultCloseIcon: null,
defaultClosable: false,
});

const isNeedWave =
typeof props.onClick === 'function' ||
Expand Down