Compare commits

...

3 Commits

Author SHA1 Message Date
二货机器人
38f32d5cf7 feat(Typography): add semantic type support for classNames and styles
Add semantic type support to Typography component, allowing type-specific classNames and styles similar to Alert. This includes:

- Add BaseTypographyProps interface and semantic type definitions
- Add useMergeSemantic hook integration for merging semantic props
- Create useTypographySemantic hook for Typography component
- Add semantic demo and unit tests
- Export TypographyProps type from index

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 20:00:13 +08:00
二货机器人
065ff3d2ad Merge remote-tracking branch 'origin/feature' into typography-semantic 2026-02-12 14:35:24 +08:00
二货机器人
08a0e26973 chore: init 2026-02-12 11:45:53 +08:00
7 changed files with 354 additions and 36 deletions

View File

@@ -8,9 +8,13 @@ import useLayoutEffect from '@rc-component/util/lib/hooks/useLayoutEffect';
import { composeRef } from '@rc-component/util/lib/ref';
import { clsx } from 'clsx';
import { useMergeSemantic } from '../../_util/hooks';
import type { SemanticType } from '../../_util/hooks';
import isNonNullable from '../../_util/isNonNullable';
import { isStyleSupport } from '../../_util/styleChecker';
import type { DirectionType } from '../../config-provider';
import { ConfigContext } from '../../config-provider';
import { useComponentConfig } from '../../config-provider/context';
import useLocale from '../../locale/useLocale';
import type { TooltipProps } from '../../tooltip';
import Tooltip from '../../tooltip';
@@ -28,6 +32,41 @@ import { isEleEllipsis, isValidText } from './util';
export type BaseType = 'secondary' | 'success' | 'warning' | 'danger';
// Base typography props without generic parameter for semantic types
export interface BaseTypographyProps extends React.HTMLAttributes<HTMLElement> {
id?: string;
prefixCls?: string;
className?: string;
rootClassName?: string;
style?: React.CSSProperties;
classNames?: TypographyClassNamesType;
styles?: TypographyStylesType;
children?: React.ReactNode;
'aria-label'?: string;
direction?: DirectionType;
/** @private */
component?: keyof JSX.IntrinsicElements;
}
export type TypographySemanticClassNames = {
root?: string;
actions?: string;
action?: string;
};
export type TypographySemanticStyles = {
root?: React.CSSProperties;
actions?: React.CSSProperties;
action?: React.CSSProperties;
};
export type TypographyClassNamesType = SemanticType<
BaseTypographyProps,
TypographySemanticClassNames
>;
export type TypographyStylesType = SemanticType<BaseTypographyProps, TypographySemanticStyles>;
export interface CopyConfig {
text?: string | (() => string | Promise<string>);
onCopy?: (event?: React.MouseEvent<HTMLButtonElement>) => void;
@@ -65,9 +104,8 @@ export interface EllipsisConfig {
tooltip?: React.ReactNode | TooltipProps;
}
export interface BlockProps<
C extends keyof JSX.IntrinsicElements = keyof JSX.IntrinsicElements,
> extends TypographyProps<C> {
export interface BlockProps<C extends keyof JSX.IntrinsicElements = keyof JSX.IntrinsicElements>
extends TypographyProps<C> {
title?: string;
editable?: boolean | EditConfig;
copyable?: boolean | CopyConfig;
@@ -126,6 +164,8 @@ const Base = React.forwardRef<HTMLElement, BlockProps>((props, ref) => {
prefixCls: customizePrefixCls,
className,
style,
classNames,
styles,
type,
disabled,
children,
@@ -141,6 +181,8 @@ const Base = React.forwardRef<HTMLElement, BlockProps>((props, ref) => {
const { getPrefixCls, direction } = React.useContext(ConfigContext);
const [textLocale] = useLocale('Text');
const { classNames: contextClassNames, styles: contextStyles } = useComponentConfig('typography');
const typographyRef = React.useRef<HTMLElement>(null);
const editIconRef = React.useRef<HTMLButtonElement>(null);
@@ -149,6 +191,18 @@ const Base = React.forwardRef<HTMLElement, BlockProps>((props, ref) => {
const textProps = omit(restProps, DECORATION_PROPS);
// ============================ Semantic ============================
const [mergedClassNames, mergedStyles] = useMergeSemantic<
TypographyClassNamesType,
TypographyStylesType,
Partial<BlockProps>
>([contextClassNames, classNames], [contextStyles, styles], {
props: {
...props,
prefixCls,
},
});
// ========================== Editable ==========================
const [enableEdit, editConfig] = useMergedConfig<EditConfig>(editable);
const [editing, setEditing] = useControlledState(false, editConfig.editing);
@@ -424,7 +478,8 @@ const Base = React.forwardRef<HTMLElement, BlockProps>((props, ref) => {
return (
<span
key="operations"
className={`${prefixCls}-actions`}
className={clsx(`${prefixCls}-actions`, mergedClassNames.actions)}
style={mergedStyles.actions}
onMouseEnter={() => setIsHoveringOperations(true)}
onMouseLeave={() => setIsHoveringOperations(false)}
>
@@ -474,6 +529,8 @@ const Base = React.forwardRef<HTMLElement, BlockProps>((props, ref) => {
},
className,
)}
classNames={classNames}
styles={styles}
prefixCls={customizePrefixCls}
style={{
...style,

View File

@@ -3,53 +3,49 @@ import type { JSX } from 'react';
import { clsx } from 'clsx';
import type { DirectionType } from '../config-provider';
import { useComponentConfig } from '../config-provider/context';
import type { BaseTypographyProps } from './Base';
import { useTypographySemantic } from './hooks/useTypographySemantic';
import useStyle from './style';
export interface TypographyProps<C extends keyof JSX.IntrinsicElements>
extends React.HTMLAttributes<HTMLElement> {
id?: string;
prefixCls?: string;
extends BaseTypographyProps {
component?: C;
}
interface InternalProps {
className?: string;
rootClassName?: string;
style?: React.CSSProperties;
children?: React.ReactNode;
/** @internal */
component?: C;
'aria-label'?: string;
component?: keyof JSX.IntrinsicElements;
direction?: DirectionType;
mergedClassNames?: any;
mergedStyles?: any;
prefixCls?: string;
contextClassName?: string;
contextStyle?: React.CSSProperties;
hashId?: string;
cssVarCls?: string;
}
interface InternalTypographyProps<C extends keyof JSX.IntrinsicElements>
extends TypographyProps<C> {}
const Typography = React.forwardRef<
HTMLElement,
InternalTypographyProps<keyof JSX.IntrinsicElements>
>((props, ref) => {
const InternalTypography = React.forwardRef<HTMLElement, InternalProps>((props, ref) => {
const {
prefixCls: customizePrefixCls,
component: Component = 'article',
className,
rootClassName,
children,
direction: typographyDirection,
direction,
style,
mergedClassNames,
mergedStyles,
prefixCls,
contextClassName,
contextStyle,
hashId,
cssVarCls,
...restProps
} = props;
const {
getPrefixCls,
direction: contextDirection,
className: contextClassName,
style: contextStyle,
} = useComponentConfig('typography');
const direction = typographyDirection ?? contextDirection;
const prefixCls = getPrefixCls('typography', customizePrefixCls);
// Style
const [hashId, cssVarCls] = useStyle(prefixCls);
const componentClassName = clsx(
prefixCls,
contextClassName,
@@ -58,22 +54,74 @@ const Typography = React.forwardRef<
},
className,
rootClassName,
mergedClassNames?.root,
hashId,
cssVarCls,
);
const mergedStyle: React.CSSProperties = { ...contextStyle, ...style };
const mergedStyle: React.CSSProperties = {
...contextStyle,
...mergedStyles?.root,
...style,
};
return (
// @ts-expect-error: Expression produces a union type that is too complex to represent.
<Component className={componentClassName} style={mergedStyle} ref={ref} {...restProps}>
<Component {...restProps} className={componentClassName} style={mergedStyle} ref={ref}>
{children}
</Component>
);
});
if (process.env.NODE_ENV !== 'production') {
InternalTypography.displayName = 'InternalTypography';
}
const Typography = React.forwardRef<HTMLElement, TypographyProps<keyof JSX.IntrinsicElements>>(
(props, ref) => {
const {
prefixCls: customizePrefixCls,
className,
rootClassName,
children,
direction: typographyDirection,
style,
classNames,
styles,
...restProps
} = props;
const { mergedClassNames, mergedStyles, prefixCls, direction, contextClassName, contextStyle } =
useTypographySemantic(customizePrefixCls, classNames, styles, typographyDirection, props);
const [hashId, cssVarCls] = useStyle(prefixCls);
return (
<InternalTypography
ref={ref}
component="article"
className={className}
rootClassName={rootClassName}
direction={direction}
style={style}
mergedClassNames={mergedClassNames}
mergedStyles={mergedStyles}
prefixCls={prefixCls}
contextClassName={contextClassName}
contextStyle={contextStyle}
hashId={hashId}
cssVarCls={cssVarCls}
{...restProps}
>
{children}
</InternalTypography>
);
},
);
if (process.env.NODE_ENV !== 'production') {
Typography.displayName = 'Typography';
}
export default Typography;
export { InternalTypography };

View File

@@ -0,0 +1,3 @@
import { semanticDemoTest } from '../../../tests/shared/demoTest';
semanticDemoTest('typography');

View File

@@ -0,0 +1,109 @@
import React from 'react';
import { clsx } from 'clsx';
import Typography from '..';
import { render } from '../../../tests/utils';
import ConfigProvider from '../../config-provider';
import type { TypographyClassNamesType, TypographyStylesType } from '../Base';
describe('Typography.Semantic', () => {
it('should support classNames and styles as functions', () => {
const classNamesFn: TypographyClassNamesType = jest.fn(() => {
return { root: 'custom-typography' };
});
const stylesFn: TypographyStylesType = jest.fn(() => {
return { root: { color: '#1890ff' } };
});
const { rerender } = render(
<Typography classNames={classNamesFn} styles={stylesFn}>
Test Typography
</Typography>,
);
expect(classNamesFn).toHaveBeenCalled();
expect(stylesFn).toHaveBeenCalled();
const rootElement = document.querySelector<HTMLElement>('.ant-typography');
expect(rootElement).toHaveClass('custom-typography');
expect(rootElement).toHaveStyle({ color: 'rgb(24, 144, 255)' });
rerender(
<Typography classNames={classNamesFn} styles={stylesFn}>
Updated Typography
</Typography>,
);
const updatedRootElement = document.querySelector<HTMLElement>('.ant-typography');
expect(updatedRootElement).toHaveClass('custom-typography');
expect(updatedRootElement).toHaveStyle({ color: 'rgb(24, 144, 255)' });
});
it('should merge context and component classNames and styles', () => {
const contextClassNames: TypographyClassNamesType = {
root: 'context-root',
actions: 'context-actions',
};
const contextStyles: TypographyStylesType = {
root: { padding: '10px' },
actions: { margin: '5px' },
};
const componentClassNames: TypographyClassNamesType = {
root: 'component-root',
};
const componentStyles: TypographyStylesType = {
root: { fontSize: '14px' },
};
render(
<ConfigProvider>
<ConfigProvider
typography={
{
className: undefined,
style: undefined,
classNames: contextClassNames,
styles: contextStyles,
} as any
}
>
<Typography.Paragraph classNames={componentClassNames} styles={componentStyles} copyable>
Test Typography
</Typography.Paragraph>
</ConfigProvider>
</ConfigProvider>,
);
const rootElement = document.querySelector<HTMLElement>('.ant-typography');
const actionsElement = document.querySelector<HTMLElement>('.ant-typography-actions');
expect(rootElement).toHaveClass(clsx(contextClassNames.root, componentClassNames.root));
expect(actionsElement).toHaveClass(contextClassNames.actions!);
expect(rootElement).toHaveStyle({
padding: contextStyles.root?.padding,
fontSize: componentStyles.root?.fontSize,
});
expect(actionsElement).toHaveStyle({ margin: contextStyles.actions?.margin });
});
it('should support semantic className and style on actions', () => {
render(
<Typography.Paragraph
classNames={{ actions: 'custom-actions' }}
styles={{ actions: { backgroundColor: '#f0f0f0' } }}
copyable
>
Test Typography with Action
</Typography.Paragraph>,
);
const rootElement = document.querySelector<HTMLElement>('.ant-typography');
expect(rootElement).toBeInTheDocument();
const actionsElement = document.querySelector<HTMLElement>('.ant-typography-actions');
expect(actionsElement).toHaveClass('custom-actions');
expect(actionsElement).toHaveStyle({ backgroundColor: 'rgb(240, 240, 240)' });
});
});

View File

@@ -0,0 +1,39 @@
import React from 'react';
import { Typography } from 'antd';
import useLocale from '../../../.dumi/hooks/useLocale';
import SemanticPreview from '../../../.dumi/theme/common/SemanticPreview';
const locales = {
cn: {
root: '根元素,包含排版基础样式、布局和定位',
actions: '操作区域元素,包含复制、编辑、展开/收起等操作按钮的布局和间距样式',
},
en: {
root: 'Root element with base typography styles, layout, and positioning',
actions:
'Actions element with layout and spacing styles for copy, edit, expand/collapse buttons',
},
};
const App: React.FC = () => {
const [locale] = useLocale(locales);
return (
<SemanticPreview
componentName="Typography"
semantics={[
{ name: 'root', desc: locale.root, version: '6.0.0' },
{ name: 'actions', desc: locale.actions, version: '6.0.0' },
]}
>
<Typography.Paragraph copyable editable ellipsis={{ rows: 2, expandable: true }}>
Ant Design is a design language for background applications, refined by Ant UED Team. It
aims to uniform the user interface specs for internal background projects, lower the
unnecessary cost of design differences and implementation and liberate the resources of
design and front-end development.
</Typography.Paragraph>
</SemanticPreview>
);
};
export default App;

View File

@@ -0,0 +1,60 @@
import type { DirectionType } from '../../config-provider';
import { useComponentConfig } from '../../config-provider/context';
import { useMergeSemantic } from '../../_util/hooks';
import type { BaseTypographyProps, TypographyClassNamesType, TypographyStylesType } from '../Base';
interface UseTypographySemanticResult {
mergedClassNames: ReturnType<
typeof useMergeSemantic<TypographyClassNamesType, TypographyStylesType, BaseTypographyProps>
>[0];
mergedStyles: ReturnType<
typeof useMergeSemantic<TypographyClassNamesType, TypographyStylesType, BaseTypographyProps>
>[1];
prefixCls: string;
direction: DirectionType | undefined;
contextClassName: string | undefined;
contextStyle: React.CSSProperties | undefined;
}
export const useTypographySemantic = (
customizePrefixCls?: string,
classNames?: TypographyClassNamesType | undefined,
styles?: TypographyStylesType | undefined,
typographyDirection?: DirectionType,
props?: any,
): UseTypographySemanticResult => {
const {
getPrefixCls,
direction: contextDirection,
className: contextClassName,
style: contextStyle,
classNames: contextClassNames,
styles: contextStyles,
} = useComponentConfig('typography');
const direction = typographyDirection ?? contextDirection;
const prefixCls = getPrefixCls('typography', customizePrefixCls);
const mergedProps: BaseTypographyProps = {
...props,
prefixCls,
direction,
};
const [mergedClassNames, mergedStyles] = useMergeSemantic<
TypographyClassNamesType,
TypographyStylesType,
BaseTypographyProps
>([contextClassNames, classNames], [contextStyles, styles], {
props: mergedProps,
});
return {
mergedClassNames,
mergedStyles,
prefixCls,
direction,
contextClassName,
contextStyle,
};
};

View File

@@ -4,14 +4,16 @@ import Text from './Text';
import Title from './Title';
import OriginTypography from './Typography';
export type TypographyProps = typeof OriginTypography & {
export type { TypographyProps } from './Typography';
type CompoundedComponent = typeof OriginTypography & {
Text: typeof Text;
Link: typeof Link;
Title: typeof Title;
Paragraph: typeof Paragraph;
};
const Typography = OriginTypography as TypographyProps;
const Typography = OriginTypography as CompoundedComponent;
Typography.Text = Text;
Typography.Link = Link;
Typography.Title = Title;