Skip to content

Commit 4b06f64

Browse files
committed
fix(react): migrate runtime component checks to markers
1 parent de881dc commit 4b06f64

38 files changed

Lines changed: 394 additions & 45 deletions
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@tiny-design/react": patch
3+
---
4+
5+
Replace runtime displayName checks with component markers for React component composition and keep displayName for debugging only.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
export const INPUT_GROUP_CONTROL_MARK = Symbol('tiny-design.input-group-control');
2+
export const SELECT_MARK = Symbol('tiny-design.select');
3+
export const SELECT_OPTION_MARK = Symbol('tiny-design.select-option');
4+
export const SELECT_OPT_GROUP_MARK = Symbol('tiny-design.select-opt-group');
5+
export const MENU_MARK = Symbol('tiny-design.menu');
6+
export const MENU_ITEM_MARK = Symbol('tiny-design.menu-item');
7+
export const SUB_MENU_MARK = Symbol('tiny-design.sub-menu');
8+
export const MENU_ITEM_GROUP_MARK = Symbol('tiny-design.menu-item-group');
9+
export const MENU_DIVIDER_MARK = Symbol('tiny-design.menu-divider');
10+
export const ANCHOR_LINK_MARK = Symbol('tiny-design.anchor-link');
11+
export const STEPS_ITEM_MARK = Symbol('tiny-design.steps-item');
12+
export const TIMELINE_ITEM_MARK = Symbol('tiny-design.timeline-item');
13+
export const CARD_CONTENT_MARK = Symbol('tiny-design.card-content');
14+
export const FLIP_ITEM_MARK = Symbol('tiny-design.flip-item');
15+
export const AVATAR_MARK = Symbol('tiny-design.avatar');
16+
17+
type Marker = symbol;
18+
type Markable = Record<PropertyKey, unknown>;
19+
20+
export function markComponent<T extends object>(component: T, marker: Marker): T {
21+
(component as T & Markable)[marker] = true;
22+
return component;
23+
}
24+
25+
export function hasMarker(type: unknown, marker: Marker): boolean {
26+
if (!type || (typeof type !== 'function' && typeof type !== 'object')) {
27+
return false;
28+
}
29+
30+
return Boolean((type as Markable)[marker]);
31+
}

packages/react/src/anchor/__tests__/anchor.test.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React from 'react';
22
import { render } from '@testing-library/react';
3+
import { ANCHOR_LINK_MARK, markComponent } from '../../_utils/component-markers';
34
import Anchor from '../index';
45

56
describe('<Anchor />', () => {
@@ -33,6 +34,21 @@ describe('<Anchor />', () => {
3334
expect(getByText('Link 2')).toBeInTheDocument();
3435
});
3536

37+
it('should recognize marker-based link wrappers', () => {
38+
const WrappedLink = markComponent(
39+
(props: React.ComponentProps<typeof Anchor.Link>) => <Anchor.Link {...props} />,
40+
ANCHOR_LINK_MARK
41+
);
42+
43+
const { getByText } = render(
44+
<Anchor>
45+
<WrappedLink href="#s1" title="Wrapped Link" />
46+
</Anchor>
47+
);
48+
49+
expect(getByText('Wrapped Link')).toBeInTheDocument();
50+
});
51+
3652
it('should forward ref to root list element', () => {
3753
const ref = React.createRef<HTMLUListElement>();
3854

packages/react/src/anchor/anchor-link.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React, { useContext, useEffect } from 'react';
22
import classNames from 'classnames';
3+
import { ANCHOR_LINK_MARK, markComponent } from '../_utils/component-markers';
34
import { AnchorLinkProps } from './types';
45
import { AnchorContext } from './anchor-context';
56

@@ -50,5 +51,6 @@ const AnchorLink = React.forwardRef<HTMLAnchorElement, AnchorLinkProps>(
5051
);
5152

5253
AnchorLink.displayName = 'AnchorLink';
54+
markComponent(AnchorLink, ANCHOR_LINK_MARK);
5355

5456
export default AnchorLink;

packages/react/src/anchor/anchor.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React, { useContext, useState, useCallback, useEffect, useRef, useMemo } from 'react';
22
import classNames from 'classnames';
3+
import { ANCHOR_LINK_MARK, hasMarker } from '../_utils/component-markers';
34
import { ConfigContext } from '../config-provider/config-context';
45
import { resolveTargetContainer } from '../config-provider/container-utils';
56
import { getPrefixCls } from '../_utils/general';
@@ -228,7 +229,7 @@ const Anchor = React.forwardRef<HTMLUListElement, AnchorProps>((props, ref): JSX
228229
</div>
229230
{React.Children.map(children, (child) => {
230231
const childElement = child as React.FunctionComponentElement<AnchorLinkProps>;
231-
if (childElement.type.displayName === 'AnchorLink') {
232+
if (hasMarker(childElement.type, ANCHOR_LINK_MARK)) {
232233
const childProps: Partial<AnchorLinkProps> = {
233234
prefixCls,
234235
};

packages/react/src/avatar/__tests__/avatar.test.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React from 'react';
22
import { render } from '@testing-library/react';
3+
import { AVATAR_MARK, markComponent } from '../../_utils/component-markers';
34
import Avatar from '../index';
45

56
describe('<Avatar />', () => {
@@ -37,4 +38,22 @@ describe('<Avatar />', () => {
3738
const { container } = render(<Avatar size={50}>A</Avatar>);
3839
expect(container.firstChild).toHaveStyle({ width: '50px', height: '50px' });
3940
});
41+
42+
it('should recognize marker-based avatar wrappers in Avatar.Group', () => {
43+
const WrappedAvatar = markComponent(
44+
(props: React.ComponentProps<typeof Avatar>) => <Avatar {...props} />,
45+
AVATAR_MARK
46+
);
47+
48+
const { container } = render(
49+
<Avatar.Group gap={-10}>
50+
<WrappedAvatar>A</WrappedAvatar>
51+
<WrappedAvatar>B</WrappedAvatar>
52+
</Avatar.Group>
53+
);
54+
55+
const avatars = container.querySelectorAll('.ty-avatar');
56+
expect(avatars).toHaveLength(2);
57+
expect(avatars[1]).toHaveStyle({ marginLeft: '-10px' });
58+
});
4059
});

packages/react/src/avatar/avatar-group.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React, { useContext } from 'react';
22
import classNames from 'classnames';
3+
import { AVATAR_MARK, hasMarker } from '../_utils/component-markers';
34
import { ConfigContext } from '../config-provider/config-context';
45
import { getPrefixCls } from '../_utils/general';
56
import { AvatarProps, AvatarGroupProps } from './types';
@@ -14,7 +15,7 @@ const AvatarGroup = (props: AvatarGroupProps): JSX.Element => {
1415
<span {...otherProps} className={cls} style={style}>
1516
{React.Children.map(children, (child, idx) => {
1617
const childElement = child as React.FunctionComponentElement<AvatarProps>;
17-
if (childElement.type.displayName === 'Avatar') {
18+
if (hasMarker(childElement.type, AVATAR_MARK)) {
1819
const childProps: Partial<AvatarProps> = {
1920
style: {
2021
...childElement.props.style,

packages/react/src/avatar/avatar.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React, { useRef, useEffect, useState, useContext } from 'react';
22
import classNames from 'classnames';
3+
import { AVATAR_MARK, markComponent } from '../_utils/component-markers';
34
import { ConfigContext } from '../config-provider/config-context';
45
import { getPrefixCls } from '../_utils/general';
56
import { AvatarProps } from './types';
@@ -98,5 +99,6 @@ const Avatar = React.forwardRef<HTMLSpanElement, AvatarProps>((props, ref) => {
9899
});
99100

100101
Avatar.displayName = 'Avatar';
102+
markComponent(Avatar, AVATAR_MARK);
101103

102104
export default Avatar;

packages/react/src/button/button.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React, { useContext } from 'react';
22
import classNames from 'classnames';
3+
import { INPUT_GROUP_CONTROL_MARK, markComponent } from '../_utils/component-markers';
34
import { ConfigContext } from '../config-provider/config-context';
45
import { getPrefixCls } from '../_utils/general';
56
import { ButtonProps } from './types';
@@ -93,6 +94,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>((props: ButtonPr
9394
});
9495

9596
Button.displayName = 'Button';
96-
(Button as typeof Button & { [BUTTON_MARK]?: boolean })[BUTTON_MARK] = true;
97+
markComponent(Button, BUTTON_MARK);
98+
markComponent(Button, INPUT_GROUP_CONTROL_MARK);
9799

98100
export default Button;

packages/react/src/card/__tests__/card.test.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React from 'react';
22
import { render } from '@testing-library/react';
3+
import { CARD_CONTENT_MARK, markComponent } from '../../_utils/component-markers';
34
import Card from '../index';
45

56
describe('<Card />', () => {
@@ -65,4 +66,19 @@ describe('<Card />', () => {
6566
const { getByText } = render(<Card footer={<div>Footer</div>}>Content</Card>);
6667
expect(getByText('Footer')).toBeInTheDocument();
6768
});
69+
70+
it('should recognize marker-based card content wrappers', () => {
71+
const WrappedContent = markComponent(
72+
(props: React.ComponentProps<typeof Card.Content>) => <Card.Content {...props} />,
73+
CARD_CONTENT_MARK
74+
);
75+
76+
const { container } = render(
77+
<Card>
78+
<WrappedContent>Wrapped body</WrappedContent>
79+
</Card>
80+
);
81+
82+
expect(container.querySelector('.ty-card__body')).toHaveTextContent('Wrapped body');
83+
});
6884
});

0 commit comments

Comments
 (0)