Skip to content

Commit c390d5c

Browse files
fix: User Autocomplete's avatar being misaligned (RocketChat#37486)
1 parent dbb8824 commit c390d5c

7 files changed

Lines changed: 263 additions & 18 deletions

File tree

.changeset/neat-rats-grab.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@rocket.chat/meteor": patch
3+
---
4+
5+
Fixes the User Autocomplete's selected option being misaligned

apps/meteor/client/components/UserAutoCompleteMultiple/UserAutoCompleteMultiple.tsx

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1-
import { AutoComplete, Box, OptionAvatar, Option, OptionContent, Chip, OptionDescription } from '@rocket.chat/fuselage';
1+
import { AutoComplete, OptionAvatar, Option, OptionContent, OptionDescription } from '@rocket.chat/fuselage';
22
import { useDebouncedValue } from '@rocket.chat/fuselage-hooks';
33
import { UserAvatar } from '@rocket.chat/ui-avatar';
44
import { useEndpoint } from '@rocket.chat/ui-contexts';
55
import { useQuery } from '@tanstack/react-query';
66
import type { ComponentProps, ReactElement } from 'react';
77
import { memo, useMemo, useState } from 'react';
88

9+
import UserAvatarChip from './UserAvatarChip';
10+
911
const query = (
1012
term = '',
1113
): {
@@ -33,13 +35,8 @@ const UserAutoCompleteMultiple = ({ onChange, ...props }: UserAutoCompleteMultip
3335
setFilter={setFilter}
3436
onChange={onChange}
3537
multiple
36-
renderSelected={({ selected: { value, label }, onRemove, ...props }): ReactElement => (
37-
<Chip {...props} height='x20' value={value} onClick={onRemove} mie={4}>
38-
<UserAvatar size='x20' username={value} />
39-
<Box is='span' margin='none' mis={4}>
40-
{label}
41-
</Box>
42-
</Chip>
38+
renderSelected={({ selected: { value: username, label }, onRemove, ...props }): ReactElement => (
39+
<UserAvatarChip {...props} username={username} name={label} mie={4} onClick={onRemove} />
4340
)}
4441
renderItem={({ value, label, ...props }): ReactElement => (
4542
<Option data-qa-type='autocomplete-user-option' key={value} {...props}>

apps/meteor/client/components/UserAutoCompleteMultiple/UserAutoCompleteMultipleFederated.tsx

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import type { OptionType } from '@rocket.chat/fuselage';
2-
import { MultiSelectFiltered, Icon, Box, Chip } from '@rocket.chat/fuselage';
2+
import { MultiSelectFiltered } from '@rocket.chat/fuselage';
33
import { useDebouncedValue } from '@rocket.chat/fuselage-hooks';
4-
import { UserAvatar } from '@rocket.chat/ui-avatar';
54
import { useEndpoint } from '@rocket.chat/ui-contexts';
65
import { keepPreviousData, useQuery } from '@tanstack/react-query';
76
import type { ReactElement, AllHTMLAttributes } from 'react';
87
import { memo, useState, useCallback, useMemo } from 'react';
98

109
import AutocompleteOptions, { OptionsContext } from './UserAutoCompleteMultipleOptions';
10+
import UserAvatarChip from './UserAvatarChip';
1111

1212
type UserAutoCompleteMultipleFederatedProps = {
1313
onChange: (value: Array<string>) => void;
@@ -103,16 +103,19 @@ const UserAutoCompleteMultipleFederated = ({
103103
onChange={handleOnChange}
104104
filter={filter}
105105
setFilter={setFilter}
106-
renderSelected={({ value, onMouseDown }: { value: string; onMouseDown: () => void }) => {
107-
const currentCachedOption = selectedCache[value] || {};
106+
renderSelected={({ value: username, onMouseDown }: { value: string; onMouseDown: () => void }) => {
107+
const currentCachedOption = selectedCache[username] || {};
108108

109109
return (
110-
<Chip key={value} height='x20' onMouseDown={onMouseDown} mie={4} mb={2}>
111-
{currentCachedOption._federated ? <Icon size='x20' name='globe' /> : <UserAvatar size='x20' username={value} />}
112-
<Box is='span' margin='none' mis={4}>
113-
{currentCachedOption.name || currentCachedOption.username || value}
114-
</Box>
115-
</Chip>
110+
<UserAvatarChip
111+
mie={4}
112+
mb={2}
113+
key={username}
114+
federated={currentCachedOption._federated}
115+
name={currentCachedOption.name}
116+
username={currentCachedOption.username || username}
117+
onMouseDown={onMouseDown}
118+
/>
116119
);
117120
}}
118121
renderOptions={AutocompleteOptions}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { composeStories } from '@storybook/react';
2+
import { render, screen } from '@testing-library/react';
3+
import { axe } from 'jest-axe';
4+
5+
import UserAvatarChip from './UserAvatarChip';
6+
import * as stories from './UserAvatarChip.stories';
7+
8+
describe('UserAvatarChip', () => {
9+
const testCases = Object.values(composeStories(stories)).map((Story) => [Story.storyName || 'Story', Story]);
10+
11+
test.each(testCases)(`renders %s without crashing`, async (_storyname, Story) => {
12+
const { baseElement } = render(<Story />);
13+
expect(baseElement).toMatchSnapshot();
14+
});
15+
16+
test.each(testCases)('%s should have no a11y violations', async (_storyname, Story) => {
17+
const { container } = render(<Story />);
18+
19+
const results = await axe(container);
20+
expect(results).toHaveNoViolations();
21+
});
22+
23+
it('should pass extra props to the Chip component', () => {
24+
const handleClick = jest.fn();
25+
render(<UserAvatarChip username='testuser' onClick={handleClick} />);
26+
screen.getByRole('button').click();
27+
expect(handleClick).toHaveBeenCalled();
28+
});
29+
});
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { action } from '@storybook/addon-actions';
2+
import type { Meta } from '@storybook/react';
3+
4+
import UserAvatarChip from './UserAvatarChip';
5+
6+
const meta = {
7+
component: UserAvatarChip,
8+
parameters: {
9+
layout: 'centered',
10+
},
11+
} satisfies Meta<typeof UserAvatarChip>;
12+
13+
export default meta;
14+
15+
export const Default = {
16+
args: {
17+
onClick: action('onClick'),
18+
name: 'John Doe',
19+
username: 'johndoe',
20+
},
21+
};
22+
23+
export const Federated = {
24+
args: {
25+
onClick: action('onClick'),
26+
name: 'John Doe',
27+
username: 'johndoe',
28+
federated: true,
29+
},
30+
};
31+
32+
export const WithoutName = {
33+
args: {
34+
onClick: action('onClick'),
35+
username: 'johndoe',
36+
},
37+
};
38+
39+
export const WithoutClickEvent = {
40+
args: {
41+
username: 'johndoe',
42+
},
43+
};
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { Box, Chip, Icon } from '@rocket.chat/fuselage';
2+
import { UserAvatar } from '@rocket.chat/ui-avatar';
3+
import type { ComponentProps } from 'react';
4+
5+
type UserAvatarChipProps = ComponentProps<typeof Chip> & {
6+
federated?: boolean;
7+
username: string;
8+
name?: string;
9+
};
10+
11+
const UserAvatarChip = ({ federated, username, name, ...props }: UserAvatarChipProps) => {
12+
return (
13+
<Chip height='x20' {...props}>
14+
{federated ? <Icon size='x20' name='globe' verticalAlign='middle' /> : <UserAvatar size='x20' username={username} />}
15+
<Box is='span' margin='none' mis={4} verticalAlign='middle'>
16+
{name ?? username}
17+
</Box>
18+
</Chip>
19+
);
20+
};
21+
22+
export default UserAvatarChip;
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
2+
3+
exports[`UserAvatarChip renders Default without crashing 1`] = `
4+
<body>
5+
<div>
6+
<button
7+
class="rcx-box rcx-chip rcx-css-u2ekhj"
8+
type="button"
9+
>
10+
<span
11+
class="rcx-box rcx-chip__text rcx-css-trljwa"
12+
>
13+
<figure
14+
class="rcx-box rcx-box--full rcx-avatar rcx-avatar--x20"
15+
>
16+
<img
17+
alt=""
18+
aria-hidden="true"
19+
class="rcx-avatar__element rcx-avatar__element--x20"
20+
data-username="johndoe"
21+
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQYV2Oora39DwAFaQJ3y3rKeAAAAABJRU5ErkJggg=="
22+
title="johndoe"
23+
/>
24+
</figure>
25+
<span
26+
class="rcx-box rcx-box--full rcx-css-1wsppvb"
27+
>
28+
John Doe
29+
</span>
30+
</span>
31+
<i
32+
aria-hidden="true"
33+
class="rcx-box rcx-box--full rcx-icon--name-cross rcx-icon rcx-css-trljwa rcx-css-1wv1vf9"
34+
>
35+
36+
</i>
37+
</button>
38+
</div>
39+
</body>
40+
`;
41+
42+
exports[`UserAvatarChip renders Federated without crashing 1`] = `
43+
<body>
44+
<div>
45+
<button
46+
class="rcx-box rcx-chip rcx-css-u2ekhj"
47+
type="button"
48+
>
49+
<span
50+
class="rcx-box rcx-chip__text rcx-css-trljwa"
51+
>
52+
<i
53+
aria-hidden="true"
54+
class="rcx-box rcx-box--full rcx-icon--name-globe rcx-icon rcx-css-s0bbgk"
55+
>
56+
57+
</i>
58+
<span
59+
class="rcx-box rcx-box--full rcx-css-1wsppvb"
60+
>
61+
John Doe
62+
</span>
63+
</span>
64+
<i
65+
aria-hidden="true"
66+
class="rcx-box rcx-box--full rcx-icon--name-cross rcx-icon rcx-css-trljwa rcx-css-1wv1vf9"
67+
>
68+
69+
</i>
70+
</button>
71+
</div>
72+
</body>
73+
`;
74+
75+
exports[`UserAvatarChip renders WithoutClickEvent without crashing 1`] = `
76+
<body>
77+
<div>
78+
<button
79+
class="rcx-box rcx-chip rcx-css-u2ekhj"
80+
disabled=""
81+
type="button"
82+
>
83+
<span
84+
class="rcx-box rcx-chip__text rcx-css-trljwa"
85+
>
86+
<figure
87+
class="rcx-box rcx-box--full rcx-avatar rcx-avatar--x20"
88+
>
89+
<img
90+
alt=""
91+
aria-hidden="true"
92+
class="rcx-avatar__element rcx-avatar__element--x20"
93+
data-username="johndoe"
94+
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQYV2Oora39DwAFaQJ3y3rKeAAAAABJRU5ErkJggg=="
95+
title="johndoe"
96+
/>
97+
</figure>
98+
<span
99+
class="rcx-box rcx-box--full rcx-css-1wsppvb"
100+
>
101+
johndoe
102+
</span>
103+
</span>
104+
</button>
105+
</div>
106+
</body>
107+
`;
108+
109+
exports[`UserAvatarChip renders WithoutName without crashing 1`] = `
110+
<body>
111+
<div>
112+
<button
113+
class="rcx-box rcx-chip rcx-css-u2ekhj"
114+
type="button"
115+
>
116+
<span
117+
class="rcx-box rcx-chip__text rcx-css-trljwa"
118+
>
119+
<figure
120+
class="rcx-box rcx-box--full rcx-avatar rcx-avatar--x20"
121+
>
122+
<img
123+
alt=""
124+
aria-hidden="true"
125+
class="rcx-avatar__element rcx-avatar__element--x20"
126+
data-username="johndoe"
127+
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQYV2Oora39DwAFaQJ3y3rKeAAAAABJRU5ErkJggg=="
128+
title="johndoe"
129+
/>
130+
</figure>
131+
<span
132+
class="rcx-box rcx-box--full rcx-css-1wsppvb"
133+
>
134+
johndoe
135+
</span>
136+
</span>
137+
<i
138+
aria-hidden="true"
139+
class="rcx-box rcx-box--full rcx-icon--name-cross rcx-icon rcx-css-trljwa rcx-css-1wv1vf9"
140+
>
141+
142+
</i>
143+
</button>
144+
</div>
145+
</body>
146+
`;

0 commit comments

Comments
 (0)