Files
ant-design/components/tag/CheckableTagGroup.tsx
lijianan 5c52fea0bf refactor(types): derive SemanticName from semantic maps (#56389)
* refactor(types): derive SemanticName from semantic maps

* update type
2025-12-27 18:12:44 +08:00

233 lines
6.4 KiB
TypeScript

import React, { useImperativeHandle, useMemo } from 'react';
import type { ReactNode } from 'react';
import { useControlledState } from '@rc-component/util';
import pickAttrs from '@rc-component/util/lib/pickAttrs';
import { clsx } from 'clsx';
import type { SemanticClassNamesType, SemanticStylesType } from '../_util/hooks';
import { useMergeSemantic } from '../_util/hooks';
import { useComponentConfig } from '../config-provider/context';
import useCSSVarCls from '../config-provider/hooks/useCSSVarCls';
import CheckableTag from './CheckableTag';
import useStyle from './style';
export type CheckableTagOption<CheckableTagValue> = {
value: CheckableTagValue;
label: ReactNode;
};
interface CheckableTagGroupSingleProps<CheckableTagValue> {
multiple?: false;
value?: CheckableTagValue | null;
defaultValue?: CheckableTagValue | null;
onChange?: (value: CheckableTagValue | null) => void;
}
interface CheckableTagGroupMultipleProps<CheckableTagValue> {
multiple: true;
value?: CheckableTagValue[];
defaultValue?: CheckableTagValue[];
onChange?: (value: CheckableTagValue[]) => void;
}
export type SemanticName = keyof TagGroupSemanticClassNames & keyof TagGroupSemanticStyles;
export type TagGroupSemanticClassNames = {
root?: string;
item?: string;
};
export type TagGroupSemanticStyles = {
root?: React.CSSProperties;
item?: React.CSSProperties;
};
type CheckableTagGroupBaseProps<CheckableTagValue> = {
// style
prefixCls?: string;
rootClassName?: string;
options?: (CheckableTagOption<CheckableTagValue> | CheckableTagValue)[];
disabled?: boolean;
} & (
| CheckableTagGroupSingleProps<CheckableTagValue>
| CheckableTagGroupMultipleProps<CheckableTagValue>
) &
Pick<React.HTMLAttributes<HTMLDivElement>, 'className' | 'style' | 'id' | 'role'> & {
[key: `data-${string}`]: any;
[key: `aria-${string}`]: any;
};
export type CheckableTagGroupClassNamesType = SemanticClassNamesType<
CheckableTagGroupBaseProps<any>,
TagGroupSemanticClassNames
>;
export type CheckableTagGroupStylesType = SemanticStylesType<
CheckableTagGroupBaseProps<any>,
TagGroupSemanticStyles
>;
export type CheckableTagGroupProps<CheckableTagValue> =
CheckableTagGroupBaseProps<CheckableTagValue> & {
classNames?: CheckableTagGroupClassNamesType;
styles?: CheckableTagGroupStylesType;
};
export interface CheckableTagGroupRef {
nativeElement: HTMLDivElement;
}
function CheckableTagGroup<CheckableTagValue extends string | number>(
props: CheckableTagGroupProps<CheckableTagValue>,
ref: React.Ref<CheckableTagGroupRef>,
) {
const {
id,
prefixCls: customizePrefixCls,
rootClassName,
className,
style,
classNames,
styles,
disabled,
options,
value,
defaultValue,
onChange,
multiple,
...restProps
} = props;
const {
getPrefixCls,
direction,
className: contextClassName,
style: contextStyle,
classNames: contextClassNames,
styles: contextStyles,
} = useComponentConfig('tag');
const prefixCls = getPrefixCls('tag', customizePrefixCls);
const groupPrefixCls = `${prefixCls}-checkable-group`;
const rootCls = useCSSVarCls(prefixCls);
const [hashId, cssVarCls] = useStyle(prefixCls, rootCls);
// ====================== Styles ======================
const [mergedClassNames, mergedStyles] = useMergeSemantic<
CheckableTagGroupClassNamesType,
CheckableTagGroupStylesType,
typeof props
>([contextClassNames, classNames], [contextStyles, styles], {
props,
});
// =============================== Option ===============================
const parsedOptions = useMemo(
() =>
(options || []).map((option) => {
if (option && typeof option === 'object') {
return option;
}
return {
value: option,
label: option,
};
}),
[options],
);
// =============================== Values ===============================
const [mergedValue, setMergedValue] = useControlledState(defaultValue, value);
const handleChange = (checked: boolean, option: CheckableTagOption<CheckableTagValue>) => {
let newValue: CheckableTagValue | CheckableTagValue[] | null = null;
if (multiple) {
const valueList = (mergedValue || []) as CheckableTagValue[];
newValue = checked
? [...valueList, option.value]
: valueList.filter((item) => item !== option.value);
} else {
newValue = checked ? option.value : null;
}
setMergedValue(newValue);
onChange?.(newValue as any); // TS not support generic type in function call
};
// ================================ Refs ================================
const divRef = React.useRef<HTMLDivElement>(null);
useImperativeHandle(ref, () => ({
nativeElement: divRef.current!,
}));
// ================================ ARIA ================================
const ariaProps = pickAttrs(restProps, {
aria: true,
data: true,
});
// =============================== Render ===============================
return (
<div
{...ariaProps}
className={clsx(
groupPrefixCls,
contextClassName,
rootClassName,
{
[`${groupPrefixCls}-disabled`]: disabled,
[`${groupPrefixCls}-rtl`]: direction === 'rtl',
},
hashId,
cssVarCls,
className,
mergedClassNames.root,
)}
style={{
...contextStyle,
...mergedStyles.root,
...style,
}}
id={id}
ref={divRef}
>
{parsedOptions.map((option) => (
<CheckableTag
key={option.value}
className={clsx(`${groupPrefixCls}-item`, mergedClassNames.item)}
style={mergedStyles.item}
checked={
multiple
? ((mergedValue as CheckableTagValue[]) || []).includes(option.value)
: mergedValue === option.value
}
onChange={(checked) => handleChange(checked, option)}
disabled={disabled}
>
{option.label}
</CheckableTag>
))}
</div>
);
}
const ForwardCheckableTagGroup = React.forwardRef(CheckableTagGroup) as (<
CheckableTagValue extends string | number,
>(
props: CheckableTagGroupProps<CheckableTagValue> & { ref?: React.Ref<CheckableTagGroupRef> },
) => React.ReactElement) & {
displayName?: string;
};
if (process.env.NODE_ENV !== 'production') {
ForwardCheckableTagGroup.displayName = 'CheckableTagGroup';
}
export default ForwardCheckableTagGroup;