Skip to content

Commit 70241a5

Browse files
committed
refactor(mentions): optimize mention item components
1 parent 8bc5ceb commit 70241a5

21 files changed

Lines changed: 345 additions & 589 deletions

src/components/ListItemLayout/ListItemLayout.tsx

Lines changed: 34 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import clsx from 'clsx';
2-
import type { ComponentProps, ComponentType, HTMLAttributes, ReactNode } from 'react';
2+
import type {
3+
ComponentProps,
4+
ComponentType,
5+
ElementType,
6+
HTMLAttributes,
7+
ReactNode,
8+
} from 'react';
39
import React from 'react';
410

511
export type ListItemLayoutRootElement = Extract<
@@ -48,7 +54,9 @@ export const ListItemLayout = <RootElement extends ListItemLayoutRootElement = '
4854
TrailingIcon,
4955
TrailingSlot,
5056
}: ListItemLayoutProps<RootElement>) => {
51-
const RootComponent = RootElement ?? 'div';
57+
const RootComponent = (RootElement ?? 'div') as ElementType<
58+
HTMLAttributes<HTMLElement>
59+
>;
5260
const resolvedRootProps = {
5361
...(RootComponent === 'button' ? { type: 'button' } : undefined),
5462
...rootProps,
@@ -60,32 +68,30 @@ export const ListItemLayout = <RootElement extends ListItemLayoutRootElement = '
6068
),
6169
} as HTMLAttributes<HTMLElement>;
6270

63-
// JSX cannot type-check a generic intrinsic element with generic root props here.
64-
// Call sites still get RootElement-specific rootProps; createElement keeps rendering simple internally.
65-
return React.createElement(
66-
RootComponent,
67-
resolvedRootProps,
68-
LeadingIcon && (
69-
<span className='str-chat__list-item-layout__leading-icon'>
70-
<LeadingIcon />
71-
</span>
72-
),
73-
LeadingSlot && <LeadingSlot />,
74-
<ContentSlot
75-
className={contentClassName}
76-
description={description}
77-
descriptionClassName={descriptionClassName}
78-
subtitle={subtitle}
79-
subtitleClassName={subtitleClassName}
80-
title={title}
81-
titleClassName={titleClassName}
82-
/>,
83-
TrailingIcon && (
84-
<span className='str-chat__list-item-layout__trailing-icon'>
85-
<TrailingIcon />
86-
</span>
87-
),
88-
TrailingSlot && <TrailingSlot />,
71+
return (
72+
<RootComponent {...resolvedRootProps}>
73+
{LeadingIcon && (
74+
<div className='str-chat__list-item-layout__leading-icon'>
75+
<LeadingIcon />
76+
</div>
77+
)}
78+
{LeadingSlot && <LeadingSlot />}
79+
<ContentSlot
80+
className={contentClassName}
81+
description={description}
82+
descriptionClassName={descriptionClassName}
83+
subtitle={subtitle}
84+
subtitleClassName={subtitleClassName}
85+
title={title}
86+
titleClassName={titleClassName}
87+
/>
88+
{TrailingIcon && (
89+
<div className='str-chat__list-item-layout__trailing-icon'>
90+
<TrailingIcon />
91+
</div>
92+
)}
93+
{TrailingSlot && <TrailingSlot />}
94+
</RootComponent>
8995
);
9096
};
9197

src/components/ListItemLayout/styling/ListItemLayout.scss

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -77,14 +77,6 @@
7777
.str-chat__list-item-layout__trailing-icon {
7878
display: flex;
7979
flex-shrink: 0;
80-
width: var(--str-chat__icon-size-sm);
81-
height: var(--str-chat__icon-size-sm);
82-
83-
svg {
84-
stroke: currentColor;
85-
width: 100%;
86-
height: 100%;
87-
}
8880
}
8981

9082
.str-chat__list-item-layout__description,

src/components/MessageComposer/__tests__/MessageInput.test.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1297,11 +1297,10 @@ describe(`MessageInputFlat`, () => {
12971297
);
12981298
expect(
12991299
container.querySelectorAll('.str-chat__suggestion-list-item').length,
1300-
).toBeGreaterThanOrEqual(4);
1300+
).toBeGreaterThanOrEqual(3);
13011301
expect(screen.getByText(mentionName)).toBeInTheDocument();
13021302
expect(screen.getByText('@channel')).toBeInTheDocument();
13031303
expect(screen.getByText('@here')).toBeInTheDocument();
1304-
expect(screen.getByText('admin')).toBeInTheDocument();
13051304
});
13061305

13071306
const results = await axe(container);

src/components/TextareaComposer/SuggestionList/BroadcastMentionItem.tsx

Lines changed: 0 additions & 86 deletions
This file was deleted.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import clsx from 'clsx';
2+
import React from 'react';
3+
import type { ChannelMentionSuggestion, HereMentionSuggestion } from 'stream-chat';
4+
import { IconMegaphone } from '../../../Icons';
5+
import { ListItemLayout } from '../../../ListItemLayout';
6+
import { useTranslationContext } from '../../../../context';
7+
import { MentionSuggestionTitle } from './MentionSuggestionTitle';
8+
import type { MentionItemComponentProps } from './types';
9+
10+
export type BroadcastMentionItemProps = MentionItemComponentProps<
11+
ChannelMentionSuggestion | HereMentionSuggestion
12+
>;
13+
14+
export const BroadcastMentionItem = ({
15+
entity,
16+
focused,
17+
...buttonProps
18+
}: BroadcastMentionItemProps) => {
19+
const { t } = useTranslationContext();
20+
const description =
21+
entity.mentionType === 'channel'
22+
? t('mention/Channel Description')
23+
: t('mention/Here Description');
24+
25+
return (
26+
<ListItemLayout
27+
contentClassName='str-chat__suggestion-list__item-content'
28+
description={description}
29+
descriptionClassName='str-chat__suggestion-list__item-details'
30+
LeadingIcon={IconMegaphone}
31+
RootElement='button'
32+
rootProps={{
33+
...buttonProps,
34+
className: clsx(
35+
'str-chat__context-menu__button',
36+
buttonProps.className,
37+
'str-chat__suggestion-list__mention',
38+
),
39+
role: buttonProps.role ?? 'menuitem',
40+
title: `@${entity.name}`,
41+
}}
42+
selected={focused}
43+
title={<MentionSuggestionTitle>{entity.name}</MentionSuggestionTitle>}
44+
titleClassName='str-chat__suggestion-list__item-title str-chat__suggestion-list__mention-item-title'
45+
/>
46+
);
47+
};

src/components/TextareaComposer/SuggestionList/MentionItem.tsx renamed to src/components/TextareaComposer/SuggestionList/MentionItem/MentionItem.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import type { ComponentProps } from 'react';
21
import React from 'react';
32
import type { MentionSuggestion } from 'stream-chat';
43
import {
@@ -15,16 +14,15 @@ import {
1514
type UserGroupItemProps,
1615
} from './UserGroupItem';
1716
import { UserItem as DefaultUserItem, type UserItemProps } from './UserItem';
17+
import type { MentionItemComponentProps } from './types';
1818

19-
export type MentionItemProps = {
19+
export type MentionItemProps = MentionItemComponentProps<MentionSuggestion> & {
2020
BroadcastMentionItem?: React.ComponentType<BroadcastMentionItemProps>;
21-
entity: MentionSuggestion;
22-
focused?: boolean;
2321
RoleItem?: React.ComponentType<RoleItemProps>;
2422
SpecialMentionItem?: React.ComponentType<SpecialMentionItemProps>;
2523
UserGroupItem?: React.ComponentType<UserGroupItemProps>;
2624
UserItem?: React.ComponentType<UserItemProps>;
27-
} & ComponentProps<'button'>;
25+
};
2826

2927
export const MentionItem = ({
3028
BroadcastMentionItem = DefaultBroadcastMentionItem,
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import type { ReactNode } from 'react';
2+
3+
export type MentionSuggestionTitleProps = {
4+
children: ReactNode;
5+
};
6+
7+
export const MentionSuggestionTitle = ({ children }: MentionSuggestionTitleProps) => (
8+
<>@{children}</>
9+
);
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import clsx from 'clsx';
2+
import React from 'react';
3+
import type { RoleMentionSuggestion } from 'stream-chat';
4+
import { IconShield } from '../../../Icons';
5+
import { ListItemLayout } from '../../../ListItemLayout';
6+
import { useTranslationContext } from '../../../../context';
7+
import { MentionSuggestionTitle } from './MentionSuggestionTitle';
8+
import type { MentionItemComponentProps } from './types';
9+
import { TokenizedSuggestionParts } from '../TokenizedSuggestionParts';
10+
11+
export type RoleItemProps = MentionItemComponentProps<RoleMentionSuggestion>;
12+
13+
export const RoleItem = ({ entity, focused, ...buttonProps }: RoleItemProps) => {
14+
void focused;
15+
const { t } = useTranslationContext();
16+
const role = entity.name;
17+
18+
return (
19+
<ListItemLayout
20+
contentClassName='str-chat__suggestion-list__item-content'
21+
LeadingIcon={IconShield}
22+
RootElement='button'
23+
rootProps={{
24+
...buttonProps,
25+
className: clsx(
26+
'str-chat__context-menu__button',
27+
buttonProps.className,
28+
'str-chat__suggestion-list__mention',
29+
),
30+
role: buttonProps.role ?? 'menuitem',
31+
title: `@${role}`,
32+
}}
33+
selected={focused}
34+
subtitle={t('Notify all {{ role }} members', { role })}
35+
subtitleClassName='str-chat__suggestion-list__item-details'
36+
title={
37+
<MentionSuggestionTitle>
38+
<TokenizedSuggestionParts tokenizedDisplayName={entity.tokenizedDisplayName} />
39+
</MentionSuggestionTitle>
40+
}
41+
titleClassName='str-chat__suggestion-list__item-title str-chat__suggestion-list__mention-item-title'
42+
/>
43+
);
44+
};
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import type { MentionItemComponentProps } from './types';
2+
3+
type MinimalMentionEntity = {
4+
id: string;
5+
mentionType: string;
6+
name?: string;
7+
};
8+
9+
export type SpecialMentionItemProps = MentionItemComponentProps<MinimalMentionEntity>;
10+
11+
export const SpecialMentionItem = () => null;
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import clsx from 'clsx';
2+
import React from 'react';
3+
import type { UserGroupMentionSuggestion } from 'stream-chat';
4+
import { IconUsers } from '../../../Icons';
5+
import { ListItemLayout } from '../../../ListItemLayout';
6+
import { MentionSuggestionTitle } from './MentionSuggestionTitle';
7+
import type { MentionItemComponentProps } from './types';
8+
import { TokenizedSuggestionParts } from '../TokenizedSuggestionParts';
9+
10+
export type UserGroupItemProps = MentionItemComponentProps<UserGroupMentionSuggestion>;
11+
12+
export const UserGroupItem = ({
13+
entity,
14+
focused,
15+
...buttonProps
16+
}: UserGroupItemProps) => {
17+
void focused;
18+
19+
return (
20+
<ListItemLayout
21+
contentClassName='str-chat__suggestion-list__item-content'
22+
LeadingIcon={IconUsers}
23+
RootElement='button'
24+
rootProps={{
25+
...buttonProps,
26+
className: clsx(
27+
'str-chat__context-menu__button',
28+
buttonProps.className,
29+
'str-chat__suggestion-list__mention',
30+
),
31+
role: buttonProps.role ?? 'menuitem',
32+
title: `@${entity.name}`,
33+
}}
34+
selected={focused}
35+
subtitle={entity.description}
36+
subtitleClassName='str-chat__suggestion-list__item-details'
37+
title={
38+
<MentionSuggestionTitle>
39+
<TokenizedSuggestionParts tokenizedDisplayName={entity.tokenizedDisplayName} />
40+
</MentionSuggestionTitle>
41+
}
42+
titleClassName='str-chat__suggestion-list__item-title str-chat__suggestion-list__mention-item-title'
43+
/>
44+
);
45+
};

0 commit comments

Comments
 (0)