Skip to content

Commit

Permalink
feat: Drawer support aria in closable (ant-design#47543)
Browse files Browse the repository at this point in the history
* feat: drawer support aria in closable prop

* 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
  • Loading branch information
kiner-tang authored and tanzhenyun committed Mar 29, 2024
1 parent 3af16a9 commit b80b6f0
Show file tree
Hide file tree
Showing 10 changed files with 156 additions and 74 deletions.
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,
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)
) : (
<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 @@ -176,6 +176,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 @@ -115,7 +115,7 @@ export type TagConfig = ComponentStyleConfig & Pick<TagProps, 'closeIcon'>;
export type CardConfig = ComponentStyleConfig & Pick<CardProps, 'classNames' | 'styles'>;

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 () => {
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

0 comments on commit b80b6f0

Please sign in to comment.