ant-design/components/_util/hooks/useClosable.tsx
EmilyyyLiu 62c9814ba7
feat: Replace 'rc-notification' with '@rc-component/notification' (#53319)
* feat:依赖新增@rc-component/notification

* feat: 对notification的closable改造,并将rc-notification使用修改为@rc-component/notification

* @rc-component/notification已经删除closeIcon,use Message删除closeIcon的配置

* reactivate PR

* feat: 修改notification mergedCloseIcon 的获取方式,并增加useClosable 非hook的方法

* 补充computeClosable方法的单测

* 补充computeClosable 方法单测

* test: 补充行覆盖率

* 提高useClosable代码复用

* feat: Add missing type definitions due to merging

* test: Supplement the row coverage of computeClassable method

* test: Supplement the row coverage of computeClassable method(2)

* test: Supplement the row coverage of computeClassable method(3)

---------

Co-authored-by: 刘欢 <lh01217311@antgroup.com>
2025-05-15 11:53:38 +08:00

176 lines
5.3 KiB
TypeScript

import type { ReactNode } from 'react';
import React from 'react';
import CloseOutlined from '@ant-design/icons/CloseOutlined';
import type { DialogProps } from '@rc-component/dialog';
import pickAttrs from '@rc-component/util/lib/pickAttrs';
import { useLocale } from '../../locale';
import defaultLocale from '../../locale/en_US';
import type { HTMLAriaDataAttributes } from '../aria-data-attrs';
import extendsObject from '../extendsObject';
export type ClosableType = DialogProps['closable'];
export type BaseContextClosable = { closable?: ClosableType; closeIcon?: ReactNode };
export type ContextClosable<T extends BaseContextClosable = any> = Partial<
Pick<T, 'closable' | 'closeIcon'>
>;
export function pickClosable<T extends BaseContextClosable>(
context?: ContextClosable<T>,
): ContextClosable<T> | undefined {
if (!context) {
return undefined;
}
return {
closable: context.closable,
closeIcon: context.closeIcon,
};
}
/** Collection contains the all the props related with closable. e.g. `closable`, `closeIcon` */
interface ClosableCollection {
closable?: ClosableType;
closeIcon?: ReactNode;
disabled?: boolean;
}
interface FallbackCloseCollection extends ClosableCollection {
/**
* Some components need to wrap CloseIcon twice,
* this method will be executed once after the final CloseIcon is calculated
*/
closeIconRender?: (closeIcon: ReactNode) => ReactNode;
}
const EmptyFallbackCloseCollection: ClosableCollection = {};
type DataAttributes = {
[key: `data-${string}`]: string;
};
function computeClosableConfig(
closable?: ClosableType,
closeIcon?: ReactNode,
): ClosableType | boolean | null {
if (!closable && (closable === false || closeIcon === false || closeIcon === null)) {
return false;
}
if (closable === undefined && closeIcon === undefined) {
return null;
}
let closableConfig: ClosableType = {
closeIcon: typeof closeIcon !== 'boolean' && closeIcon !== null ? closeIcon : undefined,
};
if (closable && typeof closable === 'object') {
closableConfig = {
...closableConfig,
...closable,
};
}
return closableConfig;
}
function mergeClosableConfigs(
propConfig: ReturnType<typeof computeClosableConfig>,
contextConfig: ReturnType<typeof computeClosableConfig>,
fallbackConfig: ClosableCollection & { closeIconRender?: (icon: ReactNode) => ReactNode },
) {
if (propConfig === false) return false;
if (propConfig) return extendsObject(fallbackConfig, contextConfig, propConfig);
if (contextConfig === false) return false;
if (contextConfig) return extendsObject(fallbackConfig, contextConfig);
return fallbackConfig.closable ? fallbackConfig : false;
}
function computeCloseIcon(
mergedConfig: ClosableCollection,
fallbackCloseCollection: FallbackCloseCollection,
closeLabel: string,
): [ReactNode, React.AriaAttributes & DataAttributes] {
const { closeIconRender } = fallbackCloseCollection;
const { closeIcon, ...restConfig } = mergedConfig;
let finalCloseIcon = closeIcon;
const ariaOrDataProps = pickAttrs(restConfig, true);
if (finalCloseIcon !== null && finalCloseIcon !== undefined) {
if (closeIconRender) {
finalCloseIcon = closeIconRender(finalCloseIcon);
}
finalCloseIcon = React.isValidElement(finalCloseIcon) ? (
React.cloneElement(finalCloseIcon, {
'aria-label': closeLabel,
...ariaOrDataProps,
} as HTMLAriaDataAttributes)
) : (
<span aria-label={closeLabel} {...ariaOrDataProps}>
{finalCloseIcon}
</span>
);
}
return [finalCloseIcon, ariaOrDataProps];
}
export function computeClosable(
propCloseCollection?: ClosableCollection,
contextCloseCollection?: ClosableCollection | null,
fallbackCloseCollection: FallbackCloseCollection = EmptyFallbackCloseCollection,
closeLabel = 'Close',
): [
closable: boolean,
closeIcon: React.ReactNode,
closeBtnIsDisabled: boolean,
ariaOrDataProps: React.AriaAttributes & DataAttributes,
] {
const propConfig = computeClosableConfig(
propCloseCollection?.closable,
propCloseCollection?.closeIcon,
);
const contextConfig = computeClosableConfig(
contextCloseCollection?.closable,
contextCloseCollection?.closeIcon,
);
const mergedFallback = {
closeIcon: <CloseOutlined />,
...fallbackCloseCollection,
};
const mergedConfig = mergeClosableConfigs(propConfig, contextConfig, mergedFallback);
const closeBtnIsDisabled = typeof mergedConfig !== 'boolean' ? !!mergedConfig?.disabled : false;
if (mergedConfig === false) {
return [false, null, closeBtnIsDisabled, {}];
}
const [closeIcon, ariaProps] = computeCloseIcon(mergedConfig, mergedFallback, closeLabel);
return [true, closeIcon, closeBtnIsDisabled, ariaProps];
}
export default function useClosable(
propCloseCollection?: ClosableCollection,
contextCloseCollection?: ClosableCollection | null,
fallbackCloseCollection: FallbackCloseCollection = EmptyFallbackCloseCollection,
) {
const [contextLocale] = useLocale('global', defaultLocale.global);
return React.useMemo(() => {
return computeClosable(
propCloseCollection,
contextCloseCollection,
{
closeIcon: <CloseOutlined />,
...fallbackCloseCollection,
},
contextLocale.close,
);
}, [propCloseCollection, contextCloseCollection, fallbackCloseCollection, contextLocale.close]);
}