Skip to content

Commit

Permalink
feat: popover component & new topic actions layout (#580)
Browse files Browse the repository at this point in the history
  • Loading branch information
y-young authored May 14, 2023
1 parent 982593c commit 7ce2f19
Show file tree
Hide file tree
Showing 24 changed files with 1,309 additions and 276 deletions.
23 changes: 23 additions & 0 deletions packages/design/components/Popover/Popover.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { Meta, StoryFn } from '@storybook/react';
import React from 'react';

import Button from '../Button';
import type { PopoverProps } from '.';
import Popover from '.';

const storyMeta: Meta<typeof Popover> = {
title: 'modern/Popover',
component: Popover,
};

export default storyMeta;

const Template: StoryFn<PopoverProps> = (args) => {
return <Popover {...args} />;
};

export const Default = Template.bind({});
Default.args = {
content: <div style={{ padding: 30 }}>Popover content</div>,
children: <Button>Popover</Button>,
};
24 changes: 24 additions & 0 deletions packages/design/components/Popover/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import './style/index.less';

import classNames from 'classnames';
import React from 'react';

export interface PopoverProps {
content: React.ReactNode;
children: React.ReactNode;
className?: string;
}

const Popover = ({ children, content, className }: PopoverProps) => {
return (
<div className={classNames('bgm-popover', className)}>
{children}
{/* 添加一个wrapper使绝对定位元素能够水平居中 */}
<div className='bgm-popover__container'>
<div className='bgm-popover__content'>{content}</div>
</div>
</div>
);
};

export default Popover;
29 changes: 29 additions & 0 deletions packages/design/components/Popover/style/index.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
@import '../../../theme/base.less';

.bgm-popover {
display: inline-block;

&__container {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
}

&__content {
border: 1px solid @gray-10;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.05);
background-color: white;
border-radius: 17px;
position: absolute;
visibility: hidden;
opacity: 0;
transition: visibility 0s, opacity 0.15s linear;
z-index: 99;
}

&:hover &__content {
visibility: visible;
opacity: 1;
}
}
75 changes: 32 additions & 43 deletions packages/design/components/Topic/Comment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ import { Friend, OriginalPoster, TopicClosed, TopicReopen, TopicSilent } from '@
import { getUserProfileLink } from '@bangumi/utils/pages';

import Avatar from '../../components/Avatar';
import Button from '../../components/Button';
import RichContent from '../../components/RichContent';
import Typography from '../../components/Typography';
import { toast } from '../Toast';
import CommentActions from './CommentActions';
import CommentInfo from './CommentInfo';
import ReplyForm from './ReplyForm';

Expand Down Expand Up @@ -161,46 +161,6 @@ const Comment: FC<CommentProps> = ({
}
};

const commentActions = user && !isDeleted && (
<div className='bgm-comment__opinions'>
{showReplyEditor ? (
<ReplyForm
autoFocus
topicId={topicId}
replyTo={props.id}
placeholder={`回复 @${creator.nickname}:`}
content={replyContent}
onChange={setReplyContent}
onCancel={() => {
setShowReplyEditor(false);
}}
onSuccess={handleReplySuccess}
/>
) : (
<>
<Button type='secondary' size='small' onClick={startReply}>
回复
</Button>
<Button type='secondary' size='small'>
+1
</Button>
{user.id === creator.id && (
<>
{!replies?.length && (
<Button.Link type='text' size='small' to={`/group/reply/${props.id}/edit`}>
编辑
</Button.Link>
)}
<Button type='text' size='small' onClick={handleDeleteReply}>
删除
</Button>
</>
)}
</>
)}
</div>
);

return (
<div>
<div className={headerClassName} id={`post_${props.id}`}>
Expand All @@ -219,11 +179,40 @@ const Comment: FC<CommentProps> = ({
{isFriend ? <Friend /> : null}
{!isReply && creator.sign ? <span>{`(${creator.sign})`}</span> : null}
</div>
<CommentInfo createdAt={createdAt} floor={floor} id={`post_${props.id}`} />
<div className='comment-info'>
<CommentInfo createdAt={createdAt} floor={floor} id={props.id} />
{user && !isDeleted && (
<>
&nbsp;&nbsp;|&nbsp;&nbsp;
<CommentActions
id={props.id}
onReply={startReply}
onDelete={handleDeleteReply}
isAuthor={user?.id === creator.id}
editable={!replies?.length}
/>
</>
)}
</div>
</span>
<RenderContent state={state} text={text} />
</div>
{commentActions}
{showReplyEditor && (
<div className='bgm-comment__opinions'>
<ReplyForm
autoFocus
topicId={topicId}
replyTo={props.id}
placeholder={`回复 @${creator.nickname}:`}
content={replyContent}
onChange={setReplyContent}
onCancel={() => {
setShowReplyEditor(false);
}}
onSuccess={handleReplySuccess}
/>
</div>
)}
</div>
</div>
{replies?.map((reply, idx) => (
Expand Down
51 changes: 51 additions & 0 deletions packages/design/components/Topic/CommentActions.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import type { StoryFn } from '@storybook/react';
import React from 'react';
import { BrowserRouter } from 'react-router-dom';

import type { CommentActionsProps } from './CommentActions';
import CommentActions from './CommentActions';

export default {
title: 'Topic/CommentActions',
component: CommentActions,
};

const Template: StoryFn<CommentActionsProps> = (args) => {
return (
<BrowserRouter>
<CommentActions {...args} id={375793} />
</BrowserRouter>
);
};

export const Basic = Template.bind({});

export const WithText = Template.bind({});
WithText.args = {
showText: true,
};

export const IsAuthor = Template.bind({});
IsAuthor.args = {
isAuthor: true,
};
IsAuthor.parameters = {
docs: {
description: {
story: '显示编辑和删除按钮',
},
},
};

export const NonEditable = Template.bind({});
NonEditable.args = {
isAuthor: true,
editable: false,
};
NonEditable.parameters = {
docs: {
description: {
story: '显示删除按钮,但隐藏编辑按钮,例如有子回复时',
},
},
};
62 changes: 62 additions & 0 deletions packages/design/components/Topic/CommentActions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import React from 'react';

import { Comment as CommentIcon, More } from '@bangumi/icons';

import Button from '../../components/Button';
import Popover from '../Popover';

export interface CommentActionsProps {
id: number;
onReply?: () => void;
onDelete?: () => void;
isAuthor?: boolean;
editable?: boolean;
showText?: boolean;
}

const CommentActions = ({
id,
onReply,
onDelete,
isAuthor = false,
editable = true,
showText = false,
}: CommentActionsProps) => {
return (
<div className='bgm-comment-actions'>
<Button type='plain' size='small' onClick={onReply} title='回复'>
<CommentIcon />
{showText && '回复'}
</Button>
{/* TODO: 实现贴贴功能 */}
<Popover
content={
<div className='bgm-comment-actions__popover'>
{isAuthor && (
<>
{editable && (
<Button.Link type='text' size='small' to={`/group/reply/${id}/edit`}>
编辑
</Button.Link>
)}
<Button type='text' size='small' onClick={onDelete}>
删除
</Button>
</>
)}
{/* TODO: 实现绝交和报告疑虑功能 */}
<Button type='text'>绝交</Button>
<Button type='text'>报告疑虑</Button>
</div>
}
>
<Button type='plain' size='small' className='bgm-comment-actions__more' title='其他'>
<More />
{showText && '其他'}
</Button>
</Popover>
</div>
);
};

export default CommentActions;
6 changes: 2 additions & 4 deletions packages/design/components/Topic/CommentInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,11 @@ export interface CommentInfoProps {
floor: string | number;
isSpecial?: boolean;
createdAt: number | string | Date;
id?: string;
id?: number;
}

const spaces = '\u00A0'.repeat(2);

// Todo: report
const CommentInfo: FC<CommentInfoProps> = ({ floor, createdAt, isSpecial = false, id = '' }) => {
let date: string;
if (typeof createdAt === 'number') {
Expand All @@ -24,10 +23,9 @@ const CommentInfo: FC<CommentInfoProps> = ({ floor, createdAt, isSpecial = false

return !isSpecial ? (
<span className='bgm-topic__commentInfo'>
<a href={`#${id}`}>#{floor}</a>
<a href={`#post_${id}`}>#{floor}</a>
{spaces}|{spaces}
{date}
{spaces}|{spaces}!
</span>
) : (
<span className='bgm-topic__commentInfo'>{date}</span>
Expand Down
12 changes: 5 additions & 7 deletions packages/design/components/Topic/__test__/Comment.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,9 +104,7 @@ describe('Normal Comment', () => {
const props = buildProps(false, repliesComment, '233', 233, user);
const { container } = render(<Comment {...props} />);
// 选取主评论的操作区域
const actions = container.querySelector(
'.bgm-comment__box > .bgm-comment__opinions',
)?.textContent;
const actions = container.querySelector('.bgm-comment__box .bgm-comment-actions')?.textContent;
expect(actions?.includes('编辑')).toBeFalsy();
expect(actions?.includes('删除')).toBeTruthy();
});
Expand All @@ -119,10 +117,10 @@ describe('Normal Comment', () => {

it('click reply button should show editor form', () => {
const props = buildProps(false);
const { getByText, container } = render(<Comment {...props} />);
const { getByText, container, getByTitle } = render(<Comment {...props} />);
expect(container.getElementsByClassName('bgm-editor__form').length).toBe(0);

fireEvent.click(getByText('回复'));
fireEvent.click(getByTitle('回复'));
expect(container.getElementsByClassName('bgm-editor__form').length).toBe(1);

fireEvent.click(getByText('取消'));
Expand All @@ -141,11 +139,11 @@ describe('Normal Comment', () => {

const onSuccess = vi.fn();
const props = buildProps(false);
const { getByText, container } = render(
const { getByText, container, getByTitle } = render(
<Comment {...props} onCommentUpdate={onSuccess} topicId={1} />,
);
const fillAndSubmit = () => {
fireEvent.click(getByText('回复'));
fireEvent.click(getByTitle('回复'));
fireEvent.change(container.querySelector('textarea')!, { target: { value: '233' } });
fireEvent.click(getByText('写好了'));
};
Expand Down
37 changes: 37 additions & 0 deletions packages/design/components/Topic/__test__/CommentActions.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { render as _render } from '@testing-library/react';
import React from 'react';
import { BrowserRouter } from 'react-router-dom';

import type { CommentActionsProps } from '../CommentActions';
import CommentActions from '../CommentActions';

const render = (props: CommentActionsProps) =>
_render(
<BrowserRouter>
<CommentActions {...props} />
</BrowserRouter>,
);

describe('Basic Usage', () => {
it('should render', () => {
expect(render({ id: 233 })).toMatchSnapshot();
});
});

describe('With Text', () => {
it('should render', () => {
expect(render({ id: 233, showText: true })).toMatchSnapshot();
});
});

describe('Is Author', () => {
it('should render', () => {
expect(render({ id: 233, isAuthor: true })).toMatchSnapshot();
});
});

describe('Non-editable', () => {
it('should render', () => {
expect(render({ id: 233, isAuthor: true, editable: false })).toMatchSnapshot();
});
});
Loading

0 comments on commit 7ce2f19

Please sign in to comment.