Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions .changeset/neat-rats-grab.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@rocket.chat/meteor": patch
---

Fixes the User Autocomplete's selected option being misaligned
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { AutoComplete, Box, OptionAvatar, Option, OptionContent, Chip, OptionDescription } from '@rocket.chat/fuselage';
import { AutoComplete, OptionAvatar, Option, OptionContent, OptionDescription } from '@rocket.chat/fuselage';
import { useDebouncedValue } from '@rocket.chat/fuselage-hooks';
import { UserAvatar } from '@rocket.chat/ui-avatar';
import { useEndpoint } from '@rocket.chat/ui-contexts';
import { useQuery } from '@tanstack/react-query';
import type { ComponentProps, ReactElement } from 'react';
import { memo, useMemo, useState } from 'react';

import UserAvatarChip from './UserAvatarChip';

const query = (
term = '',
): {
Expand Down Expand Up @@ -33,13 +35,8 @@ const UserAutoCompleteMultiple = ({ onChange, ...props }: UserAutoCompleteMultip
setFilter={setFilter}
onChange={onChange}
multiple
renderSelected={({ selected: { value, label }, onRemove, ...props }): ReactElement => (
<Chip {...props} height='x20' value={value} onClick={onRemove} mie={4}>
<UserAvatar size='x20' username={value} />
<Box is='span' margin='none' mis={4}>
{label}
</Box>
</Chip>
renderSelected={({ selected: { value: username, label }, onRemove, ...props }): ReactElement => (
<UserAvatarChip {...props} username={username} name={label} mie={4} onClick={onRemove} />
)}
renderItem={({ value, label, ...props }): ReactElement => (
<Option data-qa-type='autocomplete-user-option' key={value} {...props}>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import type { OptionType } from '@rocket.chat/fuselage';
import { MultiSelectFiltered, Icon, Box, Chip } from '@rocket.chat/fuselage';
import { MultiSelectFiltered } from '@rocket.chat/fuselage';
import { useDebouncedValue } from '@rocket.chat/fuselage-hooks';
import { UserAvatar } from '@rocket.chat/ui-avatar';
import { useEndpoint } from '@rocket.chat/ui-contexts';
import { keepPreviousData, useQuery } from '@tanstack/react-query';
import type { ReactElement, AllHTMLAttributes } from 'react';
import { memo, useState, useCallback, useMemo } from 'react';

import AutocompleteOptions, { OptionsContext } from './UserAutoCompleteMultipleOptions';
import UserAvatarChip from './UserAvatarChip';

type UserAutoCompleteMultipleFederatedProps = {
onChange: (value: Array<string>) => void;
Expand Down Expand Up @@ -103,16 +103,19 @@ const UserAutoCompleteMultipleFederated = ({
onChange={handleOnChange}
filter={filter}
setFilter={setFilter}
renderSelected={({ value, onMouseDown }: { value: string; onMouseDown: () => void }) => {
const currentCachedOption = selectedCache[value] || {};
renderSelected={({ value: username, onMouseDown }: { value: string; onMouseDown: () => void }) => {
const currentCachedOption = selectedCache[username] || {};

return (
<Chip key={value} height='x20' onMouseDown={onMouseDown} mie={4} mb={2}>
{currentCachedOption._federated ? <Icon size='x20' name='globe' /> : <UserAvatar size='x20' username={value} />}
<Box is='span' margin='none' mis={4}>
{currentCachedOption.name || currentCachedOption.username || value}
</Box>
</Chip>
<UserAvatarChip
mie={4}
mb={2}
key={username}
federated={currentCachedOption._federated}
name={currentCachedOption.name}
username={currentCachedOption.username || username}
onMouseDown={onMouseDown}
/>
);
}}
renderOptions={AutocompleteOptions}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { composeStories } from '@storybook/react';
import { render, screen } from '@testing-library/react';
import { axe } from 'jest-axe';

import UserAvatarChip from './UserAvatarChip';
import * as stories from './UserAvatarChip.stories';

describe('UserAvatarChip', () => {
const testCases = Object.values(composeStories(stories)).map((Story) => [Story.storyName || 'Story', Story]);

test.each(testCases)(`renders %s without crashing`, async (_storyname, Story) => {
const { baseElement } = render(<Story />);
expect(baseElement).toMatchSnapshot();
});

test.each(testCases)('%s should have no a11y violations', async (_storyname, Story) => {
const { container } = render(<Story />);

const results = await axe(container);
expect(results).toHaveNoViolations();
});

it('should pass extra props to the Chip component', () => {
const handleClick = jest.fn();
render(<UserAvatarChip username='testuser' onClick={handleClick} />);
screen.getByRole('button').click();
expect(handleClick).toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { action } from '@storybook/addon-actions';
import type { Meta } from '@storybook/react';

import UserAvatarChip from './UserAvatarChip';

const meta = {
component: UserAvatarChip,
parameters: {
layout: 'centered',
},
} satisfies Meta<typeof UserAvatarChip>;

export default meta;

export const Default = {
args: {
onClick: action('onClick'),
name: 'John Doe',
username: 'johndoe',
},
};

export const Federated = {
args: {
onClick: action('onClick'),
name: 'John Doe',
username: 'johndoe',
federated: true,
},
};

export const WithoutName = {
args: {
onClick: action('onClick'),
username: 'johndoe',
},
};

export const WithoutClickEvent = {
args: {
username: 'johndoe',
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Box, Chip, Icon } from '@rocket.chat/fuselage';
import { UserAvatar } from '@rocket.chat/ui-avatar';
import type { ComponentProps } from 'react';

type UserAvatarChipProps = ComponentProps<typeof Chip> & {
federated?: boolean;
username: string;
name?: string;
};

const UserAvatarChip = ({ federated, username, name, ...props }: UserAvatarChipProps) => {
return (
<Chip height='x20' {...props}>
{federated ? <Icon size='x20' name='globe' verticalAlign='middle' /> : <UserAvatar size='x20' username={username} />}
<Box is='span' margin='none' mis={4} verticalAlign='middle'>
{name ?? username}
</Box>
</Chip>
);
};

export default UserAvatarChip;
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing

exports[`UserAvatarChip renders Default without crashing 1`] = `
<body>
<div>
<button
class="rcx-box rcx-chip rcx-css-u2ekhj"
type="button"
>
<span
class="rcx-box rcx-chip__text rcx-css-trljwa"
>
<figure
class="rcx-box rcx-box--full rcx-avatar rcx-avatar--x20"
>
<img
alt=""
aria-hidden="true"
class="rcx-avatar__element rcx-avatar__element--x20"
data-username="johndoe"
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQYV2Oora39DwAFaQJ3y3rKeAAAAABJRU5ErkJggg=="
title="johndoe"
/>
</figure>
<span
class="rcx-box rcx-box--full rcx-css-1wsppvb"
>
John Doe
</span>
</span>
<i
aria-hidden="true"
class="rcx-box rcx-box--full rcx-icon--name-cross rcx-icon rcx-css-trljwa rcx-css-1wv1vf9"
>
</i>
</button>
</div>
</body>
`;

exports[`UserAvatarChip renders Federated without crashing 1`] = `
<body>
<div>
<button
class="rcx-box rcx-chip rcx-css-u2ekhj"
type="button"
>
<span
class="rcx-box rcx-chip__text rcx-css-trljwa"
>
<i
aria-hidden="true"
class="rcx-box rcx-box--full rcx-icon--name-globe rcx-icon rcx-css-s0bbgk"
>
</i>
<span
class="rcx-box rcx-box--full rcx-css-1wsppvb"
>
John Doe
</span>
</span>
<i
aria-hidden="true"
class="rcx-box rcx-box--full rcx-icon--name-cross rcx-icon rcx-css-trljwa rcx-css-1wv1vf9"
>
</i>
</button>
</div>
</body>
`;

exports[`UserAvatarChip renders WithoutClickEvent without crashing 1`] = `
<body>
<div>
<button
class="rcx-box rcx-chip rcx-css-u2ekhj"
disabled=""
type="button"
>
<span
class="rcx-box rcx-chip__text rcx-css-trljwa"
>
<figure
class="rcx-box rcx-box--full rcx-avatar rcx-avatar--x20"
>
<img
alt=""
aria-hidden="true"
class="rcx-avatar__element rcx-avatar__element--x20"
data-username="johndoe"
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQYV2Oora39DwAFaQJ3y3rKeAAAAABJRU5ErkJggg=="
title="johndoe"
/>
</figure>
<span
class="rcx-box rcx-box--full rcx-css-1wsppvb"
>
johndoe
</span>
</span>
</button>
</div>
</body>
`;

exports[`UserAvatarChip renders WithoutName without crashing 1`] = `
<body>
<div>
<button
class="rcx-box rcx-chip rcx-css-u2ekhj"
type="button"
>
<span
class="rcx-box rcx-chip__text rcx-css-trljwa"
>
<figure
class="rcx-box rcx-box--full rcx-avatar rcx-avatar--x20"
>
<img
alt=""
aria-hidden="true"
class="rcx-avatar__element rcx-avatar__element--x20"
data-username="johndoe"
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQYV2Oora39DwAFaQJ3y3rKeAAAAABJRU5ErkJggg=="
title="johndoe"
/>
</figure>
<span
class="rcx-box rcx-box--full rcx-css-1wsppvb"
>
johndoe
</span>
</span>
<i
aria-hidden="true"
class="rcx-box rcx-box--full rcx-icon--name-cross rcx-icon rcx-css-trljwa rcx-css-1wv1vf9"
>
</i>
</button>
</div>
</body>
`;
Loading