Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
0ef131a
feat(comment): import comment type & desc
slatejack Sep 10, 2026
6a28681
feat(comment): update comment type, create comment component, import …
slatejack Sep 23, 2026
5f78e8e
feat(comment): add onActions and onFolds operations
slatejack Sep 23, 2026
d21bfbd
feat(comment): add comment demo page
slatejack Sep 23, 2026
d424582
style(comment): update submodule branch
slatejack Sep 23, 2026
5e2789f
test(comment): update test snapshots
slatejack Sep 23, 2026
90462bb
fix(comment): fix comment folds position
slatejack Sep 28, 2026
3497b84
feat(comment): update comment demo, align figma
slatejack Sep 28, 2026
312075f
feat(comment): update submodule
slatejack Sep 28, 2026
9008233
test(comment): update snapshots
slatejack Sep 28, 2026
c450f83
docs(comment): update comment component overview style in overview page
slatejack Oct 8, 2026
bd27bf0
fix(comment): update comment component style, fix the issue where def…
slatejack Oct 8, 2026
f20bde5
fix(comment): fix comment reply style, fix hidden reply in collapse s…
slatejack Oct 8, 2026
ee1bdb7
feat(comment): update comment demo, enable reply in demo
slatejack Oct 8, 2026
2a9441b
test(comment): update snapshots
slatejack Oct 8, 2026
41e2378
chore: update common
github-actions[bot] Oct 8, 2026
5a18327
chore: merge develop
github-actions[bot] Oct 8, 2026
e7b5eba
chore: update snapshot
github-actions[bot] Oct 8, 2026
62f5de3
fix(comment): fix comment style, alignment figma page
slatejack Oct 10, 2026
48abd5f
fix(comment): fix comment component style, alignment figma page
slatejack Oct 12, 2026
01956f9
fix(comment): fix comment demo style
slatejack Oct 12, 2026
5acf95c
fix(comment): fix comment demo style, use css token
slatejack Oct 12, 2026
88d79a7
chore: update common
github-actions[bot] Oct 12, 2026
92fd253
fix: fix cr
anlyyao Oct 13, 2026
00963a1
fix(comment): fix demo style
slatejack Oct 13, 2026
ed30c61
fix(comment): update submodule comment style
slatejack Oct 13, 2026
c93c8e0
chore: update common
github-actions[bot] Oct 13, 2026
9e6fb73
chore: merge develop
github-actions[bot] Oct 13, 2026
0d21408
test: update snapshots
slatejack Oct 13, 2026
53c3388
Merge branch 'develop' into feature/comment
anlyyao Oct 14, 2026
5509735
Merge branch 'develop' into feature/comment
anlyyao Oct 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion site/docs/overview.en-US.md
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,13 @@ spline: explain
<p class="name">CountDown</p>
</a>
</div>
<div class="image-wrapper">
<a class="item" href="./components/comment">
<img class="__light__" src="https://tdesign.gtimg.com/site/mobile/doc-comment.png" />
<img class="__dark__" src="https://tdesign.gtimg.com/site/mobile/doc-comment-dark.png" />
<p class="name">Comment</p>
</a>
</div>
<div class="image-wrapper">
<a class="item" href="./components/empty-en">
<img class="__light__" src="https://tdesign.gtimg.com/site/mobile/doc-empty.png" />
Expand Down Expand Up @@ -484,4 +491,4 @@ spline: explain
<p class="name">Toast</p>
</a>
</div>
</section>
</section>
9 changes: 8 additions & 1 deletion site/docs/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,13 @@ spline: explain
<p class="name">CountDown 倒计时</p>
</a>
</div>
<div class="image-wrapper">
<a class="item" href="./components/comment">
<img class="__light__" src="https://tdesign.gtimg.com/site/mobile/doc-comment.png" />
<img class="__dark__" src="https://tdesign.gtimg.com/site/mobile/doc-comment-dark.png" />
<p class="name">Comment 评论</p>
</a>
</div>
<div class="image-wrapper">
<a class="item" href="./components/empty">
<img class="__light__" src="https://tdesign.gtimg.com/site/mobile/doc-empty.png" />
Expand Down Expand Up @@ -484,4 +491,4 @@ spline: explain
<p class="name">Toast 轻提示</p>
</a>
</div>
</section>
</section>
6 changes: 6 additions & 0 deletions site/mobile/mobile.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -428,5 +428,11 @@ export default {
name: 'typography',
component: () => import('tdesign-mobile-react/typography/_example/index.tsx'),
},
{
title: 'Comment 评论',
titleEn: 'Comment',
name: 'comment',
component: () => import('tdesign-mobile-react/comment/_example/index.tsx'),
},
],
};
8 changes: 8 additions & 0 deletions site/web/site.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,14 @@ export const docs = [
component: () => import('tdesign-mobile-react/count-down/count-down.md'),
componentEn: () => import('tdesign-mobile-react/count-down/count-down.en-US.md'),
},
{
title: 'Comment 评论',
titleEn: 'Comment',
name: 'comment',
path: '/mobile-react/components/comment',
component: () => import('tdesign-mobile-react/comment/comment.md'),
componentEn: () => import('tdesign-mobile-react/comment/comment.en-US.md'),
},
{
title: 'Empty 空状态',
titleEn: 'Empty',
Expand Down
234 changes: 234 additions & 0 deletions src/comment/Comment.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
import React, { ReactNode, useEffect, useLayoutEffect, useRef, useState } from 'react';
import { isObject } from 'lodash-es';
import { ChevronDownSIcon, ChevronUpSIcon } from 'tdesign-icons-react';
import { CommentActionItem, CommentFold, CommentFoldState, TdCommentProps } from './type';
import { usePrefixClass } from '../hooks/useClass';
import Avatar, { AvatarProps } from '../avatar';
import parseTNode, { parseContentTNode } from '../_util/parseTNode';
import { StyledProps, TNode } from '../common';

export interface CommentProps extends TdCommentProps, StyledProps {}

const DEFAULT_COMMENT_FOLDS: CommentFold = {
state: 'collapsed',
total: 0,
step: 3,
content: {
collapsed: '展开回复',
partial: ['展开更多回复', '收起'], // [主文案, 副文案]
expanded: '收起',
},
};
const Comment: React.FC<TdCommentProps> = (props) => {
const { actions, author, avatar, content, datetime, folds, defaultFolds, reply, children, onActions, onFolds } =
props;
const [innerFoldsState, setInnerFoldsState] = useState<CommentFoldState>(defaultFolds?.state || 'collapsed');
const foldSteps = useRef(0);
const replyBodyRef = useRef<HTMLDivElement>(null);
const replyInnerRef = useRef<HTMLDivElement>(null);
const rootClassName = usePrefixClass('comment');

const isControlled = folds !== undefined;
const commentFolds = folds || defaultFolds || DEFAULT_COMMENT_FOLDS;

// 受控模式下,当外部 folds.state 变化时同步 foldSteps
useEffect(() => {
if (!isControlled) {
return;
}
if (folds.state === 'collapsed') {
foldSteps.current = 0;
} else if (folds.state === 'expanded') {
foldSteps.current = (commentFolds.step || 3) - 1;
}
}, [isControlled, folds?.state, commentFolds.step]);

// 当前实际使用的折叠状态
const curFoldsState: CommentFoldState = isControlled ? folds.state || 'collapsed' : innerFoldsState;

// 首次渲染前同步设置高度,避免闪烁(无动画)
useLayoutEffect(() => {
const body = replyBodyRef.current;
const inner = replyInnerRef.current;
if (body && inner) {
body.style.height = `${inner.scrollHeight}px`;
}
}, []);

// 监听回复区内容高度变化,驱动过渡动画
useEffect(() => {
const body = replyBodyRef.current;
const inner = replyInnerRef.current;
if (!body || !inner) return;

const observer = new ResizeObserver(() => {
body.style.height = `${inner.scrollHeight}px`;
});

observer.observe(inner);
return () => observer.disconnect();
}, []);

const handleClickActions = (event: MouseEvent, action: CommentActionItem | TNode) => {
onActions?.({ action, e: event });
};

const handleClickFold = (event: MouseEvent, isExpand: boolean) => {
if (!isExpand) {
// 收起:重置为 collapsed
foldSteps.current = 0;
if (!isControlled) {
setInnerFoldsState('collapsed');
}
onFolds?.({
fold: { ...commentFolds, state: 'collapsed' },
e: event,
});
return;
}

// 展开:递增步进并计算下一状态
foldSteps.current += 1;
const nextState: CommentFoldState = foldSteps.current >= (commentFolds.step || 3) - 1 ? 'expanded' : 'partial';

if (!isControlled) {
setInnerFoldsState(nextState);
}
onFolds?.({
fold: { ...commentFolds, state: nextState },
e: event,
});
};

const renderAvatar = () => {
if (isObject(avatar) && !React.isValidElement(avatar) && typeof avatar !== 'function') {
return <Avatar className={`${rootClassName}__avatar`} {...(avatar as AvatarProps)} />;
}
return parseTNode(avatar);
};

const renderAuthor = () => <div className={`${rootClassName}__author`}>{parseTNode(author)}</div>;
const renderContent = () => <div className={`${rootClassName}__content`}>{parseTNode(content)}</div>;
const renderFooter = () => {
const renderActions = () => {
if (!actions) {
return null;
}
if (!Array.isArray(actions)) {
return (
<div onClick={(e) => handleClickActions(e as unknown as MouseEvent, actions)}>{parseTNode(actions)}</div>
);
}
const renderLeftPlacement = () =>
actions
.filter((item) => item.placement === 'start')
.map((item) => (
<div key={item.key} onClick={(e) => handleClickActions(e as unknown as MouseEvent, item)}>
{parseContentTNode(item.content, { disabled: item.disabled })}
</div>
));
const renderRightPlacement = () =>
actions
.filter((item) => item.placement === 'end')
.map((item) => (
<div key={item.key} onClick={(e) => handleClickActions(e as unknown as MouseEvent, item)}>
{parseContentTNode(item.content, { disabled: item.disabled })}
</div>
));
return (
<>
<div className={`${rootClassName}-block`}>{renderLeftPlacement()}</div>
<div className={`${rootClassName}-block`}>{renderRightPlacement()}</div>
</>
);
};
return (
<div className={`${rootClassName}__bottom`}>
<div className={`${rootClassName}__date`}>{parseTNode(datetime)}</div>
<div className={`${rootClassName}__actions`}>{renderActions()}</div>
</div>
);
};

const renderFolds = () => {
const renderFoldsContent = () => {
if (curFoldsState === 'collapsed') {
Comment thread
slatejack marked this conversation as resolved.
// 全收起
return (
<div
className={`${rootClassName}__folds-item`}
onClick={(e) => handleClickFold(e as unknown as MouseEvent, true)}
>
{commentFolds.content.collapsed as ReactNode}
<ChevronDownSIcon size="16px" />
</div>
);
}
if (curFoldsState === 'partial') {
// 半展开
return (
<>
<div
className={`${rootClassName}__folds-item`}
onClick={(e) => handleClickFold(e as unknown as MouseEvent, true)}
>
{commentFolds.content.partial[0] as ReactNode}
<ChevronDownSIcon size="16px" />
</div>
<div
className={`${rootClassName}__folds-item`}
onClick={(e) => handleClickFold(e as unknown as MouseEvent, false)}
>
{commentFolds.content.partial[1] as ReactNode}
<ChevronUpSIcon size="16px" />
</div>
</>
);
}
// 全展开
return (
<div
className={`${rootClassName}__folds-item`}
onClick={(e) => handleClickFold(e as unknown as MouseEvent, false)}
>
{commentFolds.content.expanded as ReactNode}
<ChevronUpSIcon size="16px" />
</div>
);
};
if (!commentFolds.total || commentFolds.total <= 1) {
return null;
}
return <div className={`${rootClassName}__folds`}>{renderFoldsContent()}</div>;
};

const renderReply = () => {
const replyContent = reply || children;
return (
<div className={`${rootClassName}__reply`}>
<div ref={replyBodyRef} className={`${rootClassName}__reply-body`}>
<div ref={replyInnerRef} className={`${rootClassName}__reply-inner`}>
{parseTNode(replyContent)}
</div>
</div>
{renderFolds()}
</div>
);
};

return (
<div className={`${rootClassName}`}>
Comment thread
slatejack marked this conversation as resolved.
<div className={`${rootClassName}__inner`}>
{renderAvatar()}
<div className={`${rootClassName}__detail`}>
{renderAuthor()}
{renderContent()}
{renderFooter()}
</div>
</div>
{renderReply()}
</div>
);
};

export default Comment;
42 changes: 42 additions & 0 deletions src/comment/_example/base.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React from 'react';
import { Button, Comment, Tag } from 'tdesign-mobile-react';
import { ThumbUpIcon, Uncomfortable1Icon } from 'tdesign-icons-react';

export default function () {
return (
<div className="mobile-comment">
<Comment
avatar={{ shape: 'circle', size: '32px', image: 'https://tdesign.gtimg.com/mobile/demos/avatar1.png' }}
author={
<div className="mobile-comment__author">
<div>Name1 名称</div>
<Tag className="mobile-comment__author-tag" variant="light" theme="primary" shape="round">
作者
</Tag>
</div>
}
actions={[
{ placement: 'start', content: <div className="mobile-comment__reply-button">回复</div>, key: 'reply' },
{
placement: 'end',
content: (
<div className="mobile-comment__actions">
<Button className="mobile-comment__actions-button" icon={<ThumbUpIcon size="14px" />} variant="text">
Comment thread
slatejack marked this conversation as resolved.
6
</Button>
<Button
icon={<Uncomfortable1Icon size="14px" />}
className="mobile-comment__actions-button"
variant="text"
/>
</div>
),
key: 'actions',
},
]}
content="这是一段很长很长很长很长很长很长的评论内容。"
datetime="今天16:38·广东"
/>
</div>
);
}
Loading
Loading