MM-64868 Update accessible labels and descriptions for various autocompletes (#33553)

* Remove the word 'icon' from labels for status icons

* Remove unneeded alt text from at mention suggestion icons

* MM-64868 Update label and description of AtMentionSuggestion

* Update snapshots for other Suggestions

* Remove redundant alt text from EmoticonSuggestion

* Update label and description of ChannelMentionSuggestion

I removed the readout of the channel slug because it's likely redundant,
and I added the icon to it because whether or not the channel is
public/private or archived seems useful to non-sighted users.

* Remove icon from readout of GenericChannelSuggestion

It doesn't depend on anything about the channel, so it's unnecessary.

* Update label and description of GenericUserSuggestion

Same as the AtMentionProvider, the profile image no longer has alt text,
the mention is now the name, and the rest of the text is the
description.

* Update label and description of CommandSuggestion

* Update label and description of SearchChannelWithPermissionsSuggestion

Like ChannelMentionSuggestion, the name is the channel display name and
the description is the type of channel.

* Remove icon from readout of SearchUserSuggestion

* Fix aria-label for ExtensionSuggestion not being read out

* Update label and description of SearchChannelSuggestion

Unlike the ChannelMentionSuggestion, I didn't include the channel type
in this because, as of the previous PR, results are grouped by channel
type.

* Update label and description of SwitchChannelProvider

This one has a lot because of all the different things in that modal,
but I think everything relevant is read out.

* fixup Update label and description of SwitchChannelProvider

* Add some more unit tests

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
This commit is contained in:
Harrison Healey
2025-08-08 12:27:06 -04:00
committed by GitHub
co-authored by Mattermost Build
parent 3ab9a661c1
commit a03cc134bb
38 changed files with 992 additions and 179 deletions
@@ -13,9 +13,9 @@
import * as TIMEOUTS from '../../../../fixtures/timeouts';
describe('Integrations', () => {
const away = {name: 'away', ariaLabel: 'Away Icon', message: 'You are now away', className: 'icon-clock'};
const offline = {name: 'offline', ariaLabel: 'Offline Icon', message: 'You are now offline', className: 'icon-circle-outline'};
const online = {name: 'online', ariaLabel: 'Online Icon', message: 'You are now online', className: 'icon-check', profileClassName: 'icon-check-circle'};
const away = {name: 'away', ariaLabel: 'Away', message: 'You are now away', className: 'icon-clock'};
const offline = {name: 'offline', ariaLabel: 'Offline', message: 'You are now offline', className: 'icon-circle-outline'};
const online = {name: 'online', ariaLabel: 'Online', message: 'You are now online', className: 'icon-check', profileClassName: 'icon-check-circle'};
before(() => {
// # Login as test user and go to off-topic
@@ -17,8 +17,8 @@ describe('Multi-Team + DMs', () => {
let userB;
let testChannelUrl;
const away = {name: 'away', ariaLabel: 'Away Icon', message: 'You are now away', className: 'icon-clock'};
const online = {name: 'online', ariaLabel: 'Online Icon', message: 'You are now online', className: 'icon-check', profileClassName: 'icon-check-circle'};
const away = {name: 'away', ariaLabel: 'Away', message: 'You are now away', className: 'icon-clock'};
const online = {name: 'online', ariaLabel: 'Online', message: 'You are now online', className: 'icon-check', profileClassName: 'icon-check-circle'};
before(() => {
cy.apiInitSetup().then(({team, user}) => {
@@ -66,7 +66,7 @@ describe('Multi-Team + DMs', () => {
should('be.visible').
children('div.Avatar-IGMzc').
children().
find('svg').should('have.attr', 'aria-label', 'Away Icon');
find('svg').should('have.attr', 'aria-label', 'Away');
});
});
@@ -0,0 +1,88 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {renderHook} from '@testing-library/react-hooks';
import usePrefixedIds, {joinIds} from './usePrefixedIds';
describe('usePrefixedIds', () => {
test('should combine prefixes with suffixes and return the result', () => {
const {result} = renderHook((props) => usePrefixedIds(props.prefix, props.suffixes), {
initialProps: {
prefix: 'p1',
suffixes: {
s1: null,
s2: null,
s3: null,
},
},
});
expect(result.current).toEqual({
s1: 'p1-s1',
s2: 'p1-s2',
s3: 'p1-s3',
});
});
test('should recalculate the result only when the prefix changes', () => {
const {rerender, result} = renderHook((props) => usePrefixedIds(props.prefix, props.suffixes), {
initialProps: {
prefix: 'p1',
suffixes: {
s1: null,
s2: null,
s3: null,
},
},
});
expect(result.all.length).toBe(1);
// Re-rendering without changing the props should return the same result
rerender();
expect(result.all.length).toBe(2);
expect(result.all[0]).toBe(result.all[1]);
// Re-rendering without changing the prefix should return the same result, even if getSuffixes isn't memoized
rerender({
prefix: 'p1',
suffixes: {
s1: null,
s2: null,
s3: null,
},
});
expect(result.all.length).toBe(3);
expect(result.all[0]).toBe(result.all[1]);
expect(result.all[0]).toBe(result.all[2]);
// Changing the prefix should cause it to recalculate
rerender({
prefix: 'p2',
suffixes: {
s1: null,
s2: null,
s3: null,
},
});
expect(result.all.length).toBe(4);
expect(result.all[0]).toBe(result.all[1]);
expect(result.all[0]).toBe(result.all[2]);
expect(result.all[0]).not.toBe(result.all[3]);
expect(result.current).toEqual({
s1: 'p2-s1',
s2: 'p2-s2',
s3: 'p2-s3',
});
});
});
describe('joinIds', () => {
test('should concatenate together provided IDs without any falsy ones', () => {
expect(joinIds('aa', 'bb', '', 'dd', '', '', 'gg')).toEqual('aa bb dd gg');
});
});
@@ -0,0 +1,29 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {useMemo} from 'react';
/**
* Combines the given prefix with a number of suffixes to generate IDs for the children of a component.
*/
export default function usePrefixedIds<S extends Record<string, unknown>>(prefix: string, suffixes: S): {[K in keyof S]: string} {
// This hook assumes that suffixes never change, so use the original version unless prefix changes
// eslint-disable-next-line react-hooks/exhaustive-deps
const memoizedSuffixes = useMemo(() => suffixes, [prefix]);
return useMemo(() => {
const childIds = {
...memoizedSuffixes,
} as {[K in keyof S]: string};
for (const suffix of Object.keys(memoizedSuffixes)) {
childIds[suffix as keyof S] = `${prefix}-${suffix}`;
}
return childIds;
}, [prefix, memoizedSuffixes]);
}
export function joinIds(...ids: string[]): string {
return ids.filter(Boolean).join(' ');
}
@@ -96,7 +96,7 @@ exports[`components/drafts/panel/panel_body should have called handleFormattedTe
className="status "
>
<svg
aria-label="Offline Icon"
aria-label="Offline"
className="offline--icon"
height="100%"
role="img"
@@ -318,7 +318,7 @@ exports[`components/drafts/panel/panel_body should match snapshot 1`] = `
className="status "
>
<svg
aria-label="Offline Icon"
aria-label="Offline"
className="offline--icon"
height="100%"
role="img"
@@ -546,7 +546,7 @@ exports[`components/drafts/panel/panel_body should match snapshot for priority 1
className="status "
>
<svg
aria-label="Offline Icon"
aria-label="Offline"
className="offline--icon"
height="100%"
role="img"
@@ -834,7 +834,7 @@ exports[`components/drafts/panel/panel_body should match snapshot for requested_
className="status "
>
<svg
aria-label="Offline Icon"
aria-label="Offline"
className="offline--icon"
height="100%"
role="img"
@@ -12,6 +12,7 @@ import ExtensionSuggestion from './extension_suggestions';
describe('components/new_search/ExtensionSuggestion', () => {
const baseProps = {
id: 'test-suggestion',
item: {type: 'test-type', label: 'test-label', value: 'test-value'},
term: 'test',
matchedPretext: 'test',
@@ -103,7 +103,7 @@ const messages: Record<string, MessageDescriptor> =
const SearchFileExtensionSuggestion = React.forwardRef<
HTMLDivElement,
SuggestionProps<ExtensionItem>
>(({item, onClick, matchedPretext, isSelection}, ref) => {
>(({id, item, onClick, matchedPretext, isSelection}, ref) => {
const intl = useIntl();
const optionClicked = useCallback(() => {
@@ -132,6 +132,7 @@ SuggestionProps<ExtensionItem>
return (
<SearchFileExtensionSuggestionContainer
ref={ref}
id={id}
className={classNames({selected: isSelection})}
onClick={optionClicked}
onKeyDown={handleKeyDown}
@@ -35,10 +35,10 @@ describe('components/PostProfilePicture', () => {
<PostProfilePicture {...props}/>,
);
expect(screen.queryByLabelText('Online Icon')).not.toBeInTheDocument();
expect(screen.queryByLabelText('Online')).not.toBeInTheDocument();
// no status is given, 'Offline Icon' should be in the dom as a fallback
expect(screen.getByLabelText('Offline Icon')).toBeInTheDocument();
// no status is given, 'Offline' should be in the dom as a fallback
expect(screen.getByLabelText('Offline')).toBeInTheDocument();
});
test('status is specified, default props', () => {
@@ -50,12 +50,12 @@ describe('components/PostProfilePicture', () => {
<PostProfilePicture {...props}/>,
);
// status is given, 'Away Icon' should be in the dom
expect(screen.getByLabelText('Away Icon')).toBeInTheDocument();
// status is given, 'Away' should be in the dom
expect(screen.getByLabelText('Away')).toBeInTheDocument();
expect(screen.queryByLabelText('Online Icon')).not.toBeInTheDocument();
expect(screen.queryByLabelText('Online')).not.toBeInTheDocument();
expect(screen.queryByLabelText('Offline Icon')).not.toBeInTheDocument();
expect(screen.queryByLabelText('Offline')).not.toBeInTheDocument();
expect(screen.getAllByRole('img')).toHaveLength(2);
});
@@ -264,7 +264,7 @@ describe('components/ProfilePopover', () => {
(Client4.getCallsChannelState as jest.Mock).mockImplementation(async () => ({enabled: true}));
test('should correctly handle remote users based on connection status', async () => {
// Test 1: Verify shared user indicator is shown for any remote user
// Test 1: Verify shared user is shown for any remote user
{
const [props, initialState] = getBasePropsAndState();
initialState.entities!.users!.profiles!.user1!.remote_id = 'fakeuser';
@@ -274,7 +274,7 @@ describe('components/ProfilePopover', () => {
};
const {unmount} = renderWithPluginReducers(<ProfilePopover {...props}/>, initialState);
expect(await screen.findByLabelText('shared user indicator')).toBeInTheDocument();
expect(await screen.findByLabelText('shared user')).toBeInTheDocument();
unmount();
}
@@ -21,7 +21,7 @@ describe('components/SharedUserIndicator', () => {
return `From: ${descriptor.values?.remoteNames}`;
}
if (descriptor.id === 'shared_user_indicator.aria_label') {
return 'shared user indicator';
return 'shared user';
}
return descriptor.defaultMessage || '';
}),
@@ -30,7 +30,7 @@ const SharedUserIndicator = (props: Props) => {
<i
data-testid='SharedUserIcon'
className={classNames('icon icon-circle-multiple-outline', props.className)}
aria-label={props.ariaLabel || intl.formatMessage({id: 'shared_user_indicator.aria_label', defaultMessage: 'shared user indicator'})}
aria-label={props.ariaLabel || intl.formatMessage({id: 'shared_user_indicator.aria_label', defaultMessage: 'shared user'})}
role={props?.role}
/>
);
+23 -8
View File
@@ -9,12 +9,14 @@ import StatusOfflineIcon from 'components/widgets/icons/status_offline_icon';
import StatusOnlineIcon from 'components/widgets/icons/status_online_icon';
type Props = {
id?: string;
button?: boolean;
status?: string;
className?: string;
}
const StatusIcon = ({
id,
className = '',
button = false,
status,
@@ -29,14 +31,27 @@ const StatusIcon = ({
iconClassName = className || '';
}
if (status === 'online') {
return <StatusOnlineIcon className={iconClassName}/>;
} else if (status === 'away') {
return <StatusAwayIcon className={iconClassName}/>;
} else if (status === 'dnd') {
return <StatusDndIcon className={iconClassName}/>;
}
return <StatusOfflineIcon className={iconClassName}/>;
const Icon = getIcon(status);
return (
<Icon
id={id}
className={iconClassName}
/>
);
};
function getIcon(status?: string) {
switch (status) {
case 'online':
return StatusOnlineIcon;
case 'away':
return StatusAwayIcon;
case 'dnd':
return StatusDndIcon;
default:
return StatusOfflineIcon;
}
}
export default memo(StatusIcon);
@@ -1,7 +1,8 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`at mention suggestion Should display nick name of non signed in user 1`] = `
exports[`at mention suggestion should display nick name of non signed in user 1`] = `
<AtMentionSuggestion
id="test-suggestion-1"
isSelection={false}
item={
Object {
@@ -18,7 +19,10 @@ exports[`at mention suggestion Should display nick name of non signed in user 1`
term="@user"
>
<SuggestionContainer
aria-describedby="test-suggestion-1-description test-suggestion-1-youElement test-suggestion-1-status test-suggestion-1-botTag test-suggestion-1-sharedIcon test-suggestion-1-guestTag test-suggestion-1-groupMembers"
aria-labelledby="test-suggestion-1-atMention"
data-testid="mentionSuggestion_user2"
id="test-suggestion-1"
isSelection={false}
item={
Object {
@@ -35,8 +39,11 @@ exports[`at mention suggestion Should display nick name of non signed in user 1`
term="@user"
>
<li
aria-describedby="test-suggestion-1-description test-suggestion-1-youElement test-suggestion-1-status test-suggestion-1-botTag test-suggestion-1-sharedIcon test-suggestion-1-guestTag test-suggestion-1-groupMembers"
aria-labelledby="test-suggestion-1-atMention"
className="suggestion-list__item"
data-testid="mentionSuggestion_user2"
id="test-suggestion-1"
onClick={[Function]}
onMouseMove={[Function]}
role="option"
@@ -49,12 +56,13 @@ exports[`at mention suggestion Should display nick name of non signed in user 1`
className="profile-icon"
>
<Avatar
alt=""
size="sm"
url="/api/v4/users/userid2/image?_=0"
username="user2"
>
<img
alt="user2 profile image"
alt=""
className="Avatar Avatar-sm"
loading="lazy"
onError={[Function]}
@@ -62,18 +70,25 @@ exports[`at mention suggestion Should display nick name of non signed in user 1`
/>
</Avatar>
</span>
<Memo(StatusIcon) />
<Memo(StatusIcon)
id="test-suggestion-1-status"
/>
</span>
<span
className="suggestion-list__ellipsis"
>
<span
className="suggestion-list__main"
id="test-suggestion-1-atMention"
>
@user2
</span>
<span>
a b (c)
<span
id="test-suggestion-1-description"
>
<span>
a b (c)
</span>
</span>
<Component
emojiSize={15}
@@ -93,8 +108,9 @@ exports[`at mention suggestion Should display nick name of non signed in user 1`
</AtMentionSuggestion>
`;
exports[`at mention suggestion Should not display nick name of the signed in user 1`] = `
exports[`at mention suggestion should not display nick name of the signed in user 1`] = `
<AtMentionSuggestion
id="test-suggestion-1"
isSelection={false}
item={
Object {
@@ -112,7 +128,10 @@ exports[`at mention suggestion Should not display nick name of the signed in use
term="@user"
>
<SuggestionContainer
aria-describedby="test-suggestion-1-description test-suggestion-1-youElement test-suggestion-1-status test-suggestion-1-botTag test-suggestion-1-sharedIcon test-suggestion-1-guestTag test-suggestion-1-groupMembers"
aria-labelledby="test-suggestion-1-atMention"
data-testid="mentionSuggestion_user"
id="test-suggestion-1"
isSelection={false}
item={
Object {
@@ -130,8 +149,11 @@ exports[`at mention suggestion Should not display nick name of the signed in use
term="@user"
>
<li
aria-describedby="test-suggestion-1-description test-suggestion-1-youElement test-suggestion-1-status test-suggestion-1-botTag test-suggestion-1-sharedIcon test-suggestion-1-guestTag test-suggestion-1-groupMembers"
aria-labelledby="test-suggestion-1-atMention"
className="suggestion-list__item"
data-testid="mentionSuggestion_user"
id="test-suggestion-1"
onClick={[Function]}
onMouseMove={[Function]}
role="option"
@@ -144,12 +166,13 @@ exports[`at mention suggestion Should not display nick name of the signed in use
className="profile-icon"
>
<Avatar
alt=""
size="sm"
url="/api/v4/users/userid1/image?_=0"
username="user"
>
<img
alt="user profile image"
alt=""
className="Avatar Avatar-sm"
loading="lazy"
onError={[Function]}
@@ -157,27 +180,38 @@ exports[`at mention suggestion Should not display nick name of the signed in use
/>
</Avatar>
</span>
<Memo(StatusIcon) />
<Memo(StatusIcon)
id="test-suggestion-1-status"
/>
</span>
<span
className="suggestion-list__ellipsis"
>
<span
className="suggestion-list__main"
id="test-suggestion-1-atMention"
>
@user
</span>
<span>
a b
</span>
<FormattedMessage
defaultMessage="(you)"
id="suggestion.user.isCurrent"
<span
id="test-suggestion-1-description"
>
<span>
(you)
a b
</span>
</FormattedMessage>
</span>
<span
id="test-suggestion-1-youElement"
>
<FormattedMessage
defaultMessage="(you)"
id="suggestion.user.isCurrent"
>
<span>
(you)
</span>
</FormattedMessage>
</span>
<Component
emojiSize={15}
emojiStyle={
@@ -4,13 +4,13 @@
import React from 'react';
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
import * as Utils from 'utils/utils';
import {renderWithContext, screen} from 'tests/react_testing_utils';
import {TestHelper} from 'utils/test_helper';
import AtMentionSuggestion from './at_mention_suggestion';
import type {Item} from './at_mention_suggestion';
jest.mock('components/custom_status/custom_status_emoji', () => () => <div/>);
jest.spyOn(Utils, 'getFullName').mockReturnValue('a b');
describe('at mention suggestion', () => {
const userid1 = {
@@ -31,6 +31,7 @@ describe('at mention suggestion', () => {
} as Item;
const baseProps = {
id: 'test-suggestion-1',
matchedPretext: '@',
term: '@user',
isSelection: false,
@@ -38,7 +39,7 @@ describe('at mention suggestion', () => {
onMouseMove: jest.fn(),
};
it('Should not display nick name of the signed in user', () => {
test('should not display nick name of the signed in user', () => {
const wrapper = mountWithIntl(
<AtMentionSuggestion
{...baseProps}
@@ -52,7 +53,7 @@ describe('at mention suggestion', () => {
expect(wrapper.find('.suggestion-list__ellipsis').text()).not.toContain('a b (c)');
});
it('Should display nick name of non signed in user', () => {
test('should display nick name of non signed in user', () => {
const wrapper = mountWithIntl(
<AtMentionSuggestion
{...baseProps}
@@ -64,4 +65,75 @@ describe('at mention suggestion', () => {
expect(wrapper.find('.suggestion-list__ellipsis').text()).toContain('a b (c)');
});
describe('accessible text', () => {
const testCases = [
{
name: 'at-mention suggestions should be labeled with the user\'s username and described with other names',
term: '@test-user',
item: {...TestHelper.getUserMock({username: 'test-user', first_name: 'First', last_name: 'Last', nickname: 'Nickname'})},
expectedLabel: '@test-user',
expectedDescription: 'First Last (Nickname)',
},
{
name: 'at-mention suggestions should include status in the description',
term: '@test-user',
item: {...TestHelper.getUserMock({username: 'test-user', first_name: 'First', last_name: 'Last'}), status: 'online'},
expectedLabel: '@test-user',
expectedDescription: 'First Last Online',
},
{
name: 'at-mention suggestions should include if the user is the current user',
term: '@test-user',
item: {...TestHelper.getUserMock({username: 'test-user', first_name: 'First', last_name: 'Last'}), isCurrentUser: true},
expectedLabel: '@test-user',
expectedDescription: 'First Last (you)',
},
{
name: 'at-mention suggestions should include if the user is a bot',
term: '@test-user',
item: {...TestHelper.getUserMock({username: 'test-user', first_name: '', last_name: '', nickname: 'Nickname', is_bot: true})},
expectedLabel: '@test-user',
expectedDescription: '(Nickname) BOT',
},
{
name: 'at-mention suggestions should include if the user is a remote user',
term: '@test-user:remote',
item: {...TestHelper.getUserMock({username: 'test-user:remote', first_name: '', last_name: '', remote_id: 'remote1'})},
expectedLabel: '@test-user:remote',
expectedDescription: 'shared user',
},
{
name: 'group suggestions should be labeled with the group slug and described with the group name',
term: '@test-group',
item: TestHelper.getGroupMock({name: 'test-group', display_name: 'Test Group', member_count: 5}),
expectedLabel: '@test-group',
expectedDescription: '- Test Group 5 members',
},
{
name: 'special mention suggestions should be labeled with the at-mention and described properly',
term: '@channel',
item: {username: 'channel'},
expectedLabel: '@channel',
expectedDescription: 'Notifies everyone in this channel',
},
];
for (const testCase of testCases) {
test(testCase.name, () => {
renderWithContext(
<AtMentionSuggestion
{...baseProps}
term={testCase.term}
item={testCase.item as Item}
/>,
);
const suggestion = document.getElementById(baseProps.id);
expect(suggestion).toBe(screen.getByLabelText(testCase.expectedLabel));
expect(suggestion).toHaveAccessibleName(testCase.expectedLabel);
expect(suggestion).toHaveAccessibleDescription(testCase.expectedDescription);
});
}
});
});
@@ -3,12 +3,13 @@
import React from 'react';
import type {ReactNode} from 'react';
import {FormattedMessage, useIntl} from 'react-intl';
import {FormattedMessage} from 'react-intl';
import type {UserProfile} from '@mattermost/types/users';
import {isGuest} from 'mattermost-redux/utils/user_utils';
import usePrefixedIds, {joinIds} from 'components/common/hooks/usePrefixedIds';
import CustomStatusEmoji from 'components/custom_status/custom_status_emoji';
import SharedUserIndicator from 'components/shared_user_indicator';
import StatusIcon from 'components/status_icon';
@@ -37,9 +38,18 @@ function isGroup(o: unknown): o is Group {
}
const AtMentionSuggestion = React.forwardRef<HTMLLIElement, SuggestionProps<Item>>((props, ref) => {
const {item} = props;
const {id, item} = props;
const intl = useIntl();
const ids = usePrefixedIds(id, {
atMention: null,
description: null,
youElement: null,
status: null,
botTag: null,
sharedIcon: null,
guestTag: null,
groupMembers: null,
});
let itemname: string;
let description: ReactNode;
@@ -58,10 +68,7 @@ const AtMentionSuggestion = React.forwardRef<HTMLLIElement, SuggestionProps<Item
className='suggestion-list__icon suggestion-list__icon--large'
aria-hidden='true'
>
<i
className='icon icon-account-multiple-outline'
title={intl.formatMessage({id: 'generic_icons.member', defaultMessage: 'Member Icon'})}
/>
<i className='icon icon-account-multiple-outline'/>
</span>
);
} else if (item.username === 'channel') {
@@ -77,10 +84,7 @@ const AtMentionSuggestion = React.forwardRef<HTMLLIElement, SuggestionProps<Item
className='suggestion-list__icon suggestion-list__icon--large'
aria-hidden='true'
>
<i
className='icon icon-account-multiple-outline'
title={intl.formatMessage({id: 'generic_icons.member', defaultMessage: 'Member Icon'})}
/>
<i className='icon icon-account-multiple-outline'/>
</span>
);
} else if (item.username === 'here') {
@@ -96,10 +100,7 @@ const AtMentionSuggestion = React.forwardRef<HTMLLIElement, SuggestionProps<Item
className='suggestion-list__icon suggestion-list__icon--large'
aria-hidden='true'
>
<i
className='icon icon-account-multiple-outline'
title={intl.formatMessage({id: 'generic_icons.member', defaultMessage: 'Member Icon'})}
/>
<i className='icon icon-account-multiple-outline'/>
</span>
);
} else if (isGroup(item)) {
@@ -112,10 +113,7 @@ const AtMentionSuggestion = React.forwardRef<HTMLLIElement, SuggestionProps<Item
className='suggestion-list__icon suggestion-list__icon--large'
aria-hidden='true'
>
<i
className='icon icon-account-multiple-outline'
title={intl.formatMessage({id: 'generic_icons.member', defaultMessage: 'Member Icon'})}
/>
<i className='icon icon-account-multiple-outline'/>
</span>
);
} else {
@@ -136,9 +134,13 @@ const AtMentionSuggestion = React.forwardRef<HTMLLIElement, SuggestionProps<Item
username={item && item.username}
size='sm'
url={Utils.imageURLForUser(item.id, item.last_picture_update)}
alt=''
/>
</span>
<StatusIcon status={item && item.status}/>
<StatusIcon
id={ids.status}
status={item && item.status}
/>
</span>
);
@@ -155,29 +157,36 @@ const AtMentionSuggestion = React.forwardRef<HTMLLIElement, SuggestionProps<Item
}
const youElement = item.isCurrentUser ? (
<FormattedMessage
id='suggestion.user.isCurrent'
defaultMessage='(you)'
/>
<span id={ids.youElement}>
<FormattedMessage
id='suggestion.user.isCurrent'
defaultMessage='(you)'
/>
</span>
) : null;
const sharedIcon = item.remote_id ? (
<SharedUserIndicator
className='shared-user-icon'
/>
<span id={ids.sharedIcon}>
<SharedUserIndicator
className='shared-user-icon'
/>
</span>
) : null;
let countBadge;
if (isGroup(item)) {
countBadge = (
<span className='suggestion-list__group-count'>
<span
id={ids.groupMembers}
className='suggestion-list__group-count'
>
<Tag
text={
<FormattedMessage
id='suggestion.group.members'
defaultMessage='{member_count} {member_count, plural, one {member} other {members}}'
values={{
member_count: (item as Group).member_count,
member_count: item.member_count,
}}
/>
}
@@ -190,15 +199,20 @@ const AtMentionSuggestion = React.forwardRef<HTMLLIElement, SuggestionProps<Item
<SuggestionContainer
ref={ref}
{...props}
aria-labelledby={ids.atMention}
aria-describedby={joinIds(ids.description, ids.youElement, ids.status, ids.botTag, ids.sharedIcon, ids.guestTag, ids.groupMembers)}
data-testid={`mentionSuggestion_${itemname}`}
>
{icon}
<span className='suggestion-list__ellipsis'>
<span className='suggestion-list__main'>
<span
id={ids.atMention}
className='suggestion-list__main'
>
{'@' + itemname}
</span>
{item.is_bot && <BotTag/>}
{description}
{item.is_bot && <span id={ids.botTag}><BotTag/></span>}
{description && <span id={ids.description}>{description}</span>}
{youElement}
{customStatus}
{sharedIcon}
@@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import React from 'react';
import {defineMessage} from 'react-intl';
import {defineMessage, useIntl} from 'react-intl';
import type {Channel} from '@mattermost/types/channels';
@@ -12,6 +12,8 @@ import {sortChannelsByTypeAndDisplayName} from 'mattermost-redux/utils/channel_u
import store from 'stores/redux_store';
import usePrefixedIds from 'components/common/hooks/usePrefixedIds';
import {Constants} from 'utils/constants';
import Provider from './provider';
@@ -22,21 +24,58 @@ import type {SuggestionProps} from './suggestion';
export const MIN_CHANNEL_LINK_LENGTH = 2;
export const ChannelMentionSuggestion = React.forwardRef<HTMLLIElement, SuggestionProps<Channel>>((props, ref) => {
const {item: channel} = props;
const {formatMessage} = useIntl();
const {id, item: channel} = props;
const channelName = channel.display_name;
const channelIsArchived = channel && channel.delete_at && channel.delete_at !== 0;
const channelName = channel.display_name;
const ids = usePrefixedIds(id, {
channelType: null,
name: null,
});
let channelIcon;
if (channelIsArchived) {
channelIcon = (
<span className='suggestion-list__icon suggestion-list__icon--large'>
<i className='icon icon-archive-outline'/>
<span
id={ids.channelType}
className='suggestion-list__icon suggestion-list__icon--large'
aria-label={formatMessage({
id: 'suggestion.archived_channel',
defaultMessage: 'Archived channel',
})}
>
<i
className='icon icon-archive-outline'
role='presentation'
/>
</span>
);
} else {
let iconClass;
let iconLabel;
if (channel?.type === Constants.OPEN_CHANNEL) {
iconClass = 'icon-globe';
iconLabel = formatMessage({
id: 'suggestion.public_channel',
defaultMessage: 'Public channel',
});
} else {
iconClass = 'icon-lock-outline';
iconLabel = formatMessage({
id: 'suggestion.private_channel',
defaultMessage: 'Private channel',
});
}
channelIcon = (
<span className='suggestion-list__icon suggestion-list__icon--large'>
<i className={`icon icon--no-spacing icon-${channel?.type === Constants.OPEN_CHANNEL ? 'globe' : 'lock-outline'}`}/>
<span
id={ids.channelType}
className='suggestion-list__icon suggestion-list__icon--large'
aria-label={iconLabel}
>
<i className={`icon icon--no-spacing ${iconClass}`}/>
</span>
);
}
@@ -47,10 +86,15 @@ export const ChannelMentionSuggestion = React.forwardRef<HTMLLIElement, Suggesti
<SuggestionContainer
ref={ref}
{...props}
aria-labelledby={ids.name}
aria-describedby={ids.channelType}
>
{channelIcon}
<div className='suggestion-list__ellipsis'>
<span className='suggestion-list__main'>
<span
id={ids.name}
className='suggestion-list__main'
>
{channelName}
</span>
{description}
@@ -2,6 +2,9 @@
exports[`CommandSuggestion should match snapshot 1`] = `
<SuggestionContainer
aria-describedby="test-suggestion-description"
aria-labelledby="test-suggestion-label"
id="test-suggestion"
isSelection={true}
item={
Object {
@@ -29,11 +32,13 @@ exports[`CommandSuggestion should match snapshot 1`] = `
>
<div
className="slash-command__title"
id="test-suggestion-label"
>
invite @[username] ~[channel]
</div>
<div
className="slash-command__desc"
id="test-suggestion-description"
>
Invite a user to a channel
</div>
@@ -20,6 +20,7 @@ describe('CommandSuggestion', () => {
};
const baseProps = {
id: 'test-suggestion',
item: suggestion,
isSelection: true,
term: '/',
@@ -13,6 +13,8 @@ import {appsEnabled} from 'mattermost-redux/selectors/entities/apps';
import globalStore from 'stores/redux_store';
import usePrefixedIds from 'components/common/hooks/usePrefixedIds';
import {Constants} from 'utils/constants';
import * as UserAgent from 'utils/user_agent';
@@ -32,7 +34,7 @@ const OPEN_COMMAND_IN_MODAL_ITEM_ID = Constants.Integrations.OPEN_COMMAND_IN_MOD
const COMMAND_SUGGESTION_ERROR = Constants.Integrations.COMMAND_SUGGESTION_ERROR;
const CommandSuggestion = React.forwardRef<HTMLLIElement, SuggestionProps<AutocompleteSuggestion>>((props, ref) => {
const {item} = props;
const {id, item} = props;
let symbolSpan = <span>{'/'}</span>;
switch (item.IconData) {
@@ -57,21 +59,38 @@ const CommandSuggestion = React.forwardRef<HTMLLIElement, SuggestionProps<Autoco
className='slash-command__icon'
style={{backgroundColor: 'transparent'}}
>
<img src={item.IconData}/>
</div>);
<img
src={item.IconData}
alt=''
/>
</div>
);
}
const ids = usePrefixedIds(id, {
label: null,
description: null,
});
return (
<SuggestionContainer
ref={ref}
{...props}
aria-describedby={ids.description}
aria-labelledby={ids.label}
>
{icon}
<div className='slash-command__info'>
<div className='slash-command__title'>
<div
id={ids.label}
className='slash-command__title'
>
{item.Suggestion.substring(1) + ' ' + item.Hint}
</div>
<div className='slash-command__desc'>
<div
id={ids.description}
className='slash-command__desc'
>
{item.Description}
</div>
</div>
@@ -39,10 +39,9 @@ const EmoticonSuggestion = React.forwardRef<HTMLLIElement, SuggestionProps<Emoji
>
<div className='pull-left emoticon-suggestion__image-container'>
<img
alt={text}
className='emoticon-suggestion__image'
src={getEmojiImageUrl(emoji)}
title={text}
alt=''
/>
</div>
<div className='pull-left'>
@@ -22,7 +22,10 @@ const GenericChannelSuggestion = React.forwardRef<HTMLLIElement, SuggestionProps
const purpose = item.purpose;
const icon = (
<span className='suggestion-list__icon suggestion-list__icon--large'>
<span
className='suggestion-list__icon suggestion-list__icon--large'
aria-hidden={true}
>
<i className='icon icon--standard icon--no-spacing icon-globe'/>
</span>
);
@@ -6,6 +6,7 @@ import React from 'react';
import {Client4} from 'mattermost-redux/client';
import {isGuest} from 'mattermost-redux/utils/user_utils';
import usePrefixedIds, {joinIds} from 'components/common/hooks/usePrefixedIds';
import BotTag from 'components/widgets/tag/bot_tag';
import GuestTag from 'components/widgets/tag/guest_tag';
import Avatar from 'components/widgets/users/avatar';
@@ -19,7 +20,14 @@ import {SuggestionContainer} from './suggestion';
import type {SuggestionProps} from './suggestion';
const GenericUserSuggestion = React.forwardRef<HTMLLIElement, SuggestionProps<UserProfile>>((props, ref) => {
const {item} = props;
const {id, item} = props;
const ids = usePrefixedIds(id, {
name: null,
description: null,
botTag: null,
guestTag: null,
});
const username = item.username;
let description = '';
@@ -36,20 +44,26 @@ const GenericUserSuggestion = React.forwardRef<HTMLLIElement, SuggestionProps<Us
<SuggestionContainer
ref={ref}
{...props}
aria-labelledby={ids.name}
aria-describedby={joinIds(ids.description, ids.botTag, ids.guestTag)}
>
<Avatar
size='xxs'
username={username}
url={Client4.getUsersRoute() + '/' + item.id + '/image?_=' + (item.last_picture_update || 0)}
alt=''
/>
<div className='suggestion-list__ellipsis'>
<span className='suggestion-list__main'>
<span
id={ids.name}
className='suggestion-list__main'
>
{'@' + username}
</span>
{description}
<span id={ids.description}>{description}</span>
</div>
{item.is_bot && <BotTag/>}
{isGuest(item.roles) && <GuestTag/>}
{item.is_bot && <span id={ids.botTag}><BotTag/></span>}
{isGuest(item.roles) && <span id={ids.guestTag}><GuestTag/></span>}
</SuggestionContainer>
);
});
@@ -2,7 +2,9 @@
exports[`components/suggestion/search_channel_suggestion should match snapshot 1`] = `
<SuggestionContainer
currentUserId="userid1"
aria-describedby="test-suggestion-botTag"
aria-labelledby="test-suggestion-name"
id="test-suggestion"
isSelection={false}
item={
Object {
@@ -26,7 +28,6 @@ exports[`components/suggestion/search_channel_suggestion should match snapshot 1
matchedPretext=""
onClick={[MockFunction]}
onMouseMove={[MockFunction]}
teammateIsBot={false}
term=""
>
<span
@@ -41,6 +42,7 @@ exports[`components/suggestion/search_channel_suggestion should match snapshot 1
>
<span
className="suggestion-list__main"
id="test-suggestion-name"
>
name
</span>
@@ -51,7 +53,9 @@ exports[`components/suggestion/search_channel_suggestion should match snapshot 1
exports[`components/suggestion/search_channel_suggestion should match snapshot, channel type DM_CHANNEL 1`] = `
<SuggestionContainer
currentUserId="userid1"
aria-describedby="test-suggestion-botTag"
aria-labelledby="test-suggestion-name"
id="test-suggestion"
isSelection={true}
item={
Object {
@@ -75,10 +79,10 @@ exports[`components/suggestion/search_channel_suggestion should match snapshot,
matchedPretext=""
onClick={[MockFunction]}
onMouseMove={[MockFunction]}
teammateIsBot={false}
term=""
>
<Memo(Avatar)
alt=""
size="sm"
url="/api/v4/users/DN/image?_=0"
/>
@@ -87,6 +91,7 @@ exports[`components/suggestion/search_channel_suggestion should match snapshot,
>
<span
className="suggestion-list__main"
id="test-suggestion-name"
>
@name
</span>
@@ -96,7 +101,9 @@ exports[`components/suggestion/search_channel_suggestion should match snapshot,
exports[`components/suggestion/search_channel_suggestion should match snapshot, channel type GM_CHANNEL 1`] = `
<SuggestionContainer
currentUserId="userid1"
aria-describedby="test-suggestion-botTag"
aria-labelledby="test-suggestion-name"
id="test-suggestion"
isSelection={true}
item={
Object {
@@ -120,7 +127,6 @@ exports[`components/suggestion/search_channel_suggestion should match snapshot,
matchedPretext=""
onClick={[MockFunction]}
onMouseMove={[MockFunction]}
teammateIsBot={false}
term=""
>
<span
@@ -137,6 +143,7 @@ exports[`components/suggestion/search_channel_suggestion should match snapshot,
>
<span
className="suggestion-list__main"
id="test-suggestion-name"
>
@name
</span>
@@ -146,7 +153,9 @@ exports[`components/suggestion/search_channel_suggestion should match snapshot,
exports[`components/suggestion/search_channel_suggestion should match snapshot, channel type OPEN_CHANNEL 1`] = `
<SuggestionContainer
currentUserId="userid1"
aria-describedby="test-suggestion-botTag"
aria-labelledby="test-suggestion-name"
id="test-suggestion"
isSelection={true}
item={
Object {
@@ -170,7 +179,6 @@ exports[`components/suggestion/search_channel_suggestion should match snapshot,
matchedPretext=""
onClick={[MockFunction]}
onMouseMove={[MockFunction]}
teammateIsBot={false}
term=""
>
<span
@@ -185,6 +193,7 @@ exports[`components/suggestion/search_channel_suggestion should match snapshot,
>
<span
className="suggestion-list__main"
id="test-suggestion-name"
>
name
</span>
@@ -195,7 +204,9 @@ exports[`components/suggestion/search_channel_suggestion should match snapshot,
exports[`components/suggestion/search_channel_suggestion should match snapshot, channel type PRIVATE_CHANNEL 1`] = `
<SuggestionContainer
currentUserId="userid1"
aria-describedby="test-suggestion-botTag"
aria-labelledby="test-suggestion-name"
id="test-suggestion"
isSelection={true}
item={
Object {
@@ -219,7 +230,6 @@ exports[`components/suggestion/search_channel_suggestion should match snapshot,
matchedPretext=""
onClick={[MockFunction]}
onMouseMove={[MockFunction]}
teammateIsBot={false}
term=""
>
<span
@@ -234,6 +244,7 @@ exports[`components/suggestion/search_channel_suggestion should match snapshot,
>
<span
className="suggestion-list__main"
id="test-suggestion-name"
>
name
</span>
@@ -244,7 +255,9 @@ exports[`components/suggestion/search_channel_suggestion should match snapshot,
exports[`components/suggestion/search_channel_suggestion should match snapshot, isSelection is false 1`] = `
<SuggestionContainer
currentUserId="userid1"
aria-describedby="test-suggestion-botTag"
aria-labelledby="test-suggestion-name"
id="test-suggestion"
isSelection={false}
item={
Object {
@@ -268,7 +281,6 @@ exports[`components/suggestion/search_channel_suggestion should match snapshot,
matchedPretext=""
onClick={[MockFunction]}
onMouseMove={[MockFunction]}
teammateIsBot={false}
term=""
>
<span
@@ -283,6 +295,7 @@ exports[`components/suggestion/search_channel_suggestion should match snapshot,
>
<span
className="suggestion-list__main"
id="test-suggestion-name"
>
name
</span>
@@ -293,7 +306,9 @@ exports[`components/suggestion/search_channel_suggestion should match snapshot,
exports[`components/suggestion/search_channel_suggestion should match snapshot, isSelection is true 1`] = `
<SuggestionContainer
currentUserId="userid1"
aria-describedby="test-suggestion-botTag"
aria-labelledby="test-suggestion-name"
id="test-suggestion"
isSelection={true}
item={
Object {
@@ -317,7 +332,6 @@ exports[`components/suggestion/search_channel_suggestion should match snapshot,
matchedPretext=""
onClick={[MockFunction]}
onMouseMove={[MockFunction]}
teammateIsBot={false}
term=""
>
<span
@@ -332,6 +346,7 @@ exports[`components/suggestion/search_channel_suggestion should match snapshot,
>
<span
className="suggestion-list__main"
id="test-suggestion-name"
>
name
</span>
@@ -12,6 +12,7 @@ describe('components/suggestion/search_channel_suggestion', () => {
const mockChannel = TestHelper.getChannelMock();
const baseProps = {
id: 'test-suggestion',
item: mockChannel,
isSelection: false,
currentUserId: 'userid1',
@@ -7,6 +7,7 @@ import type {Channel} from '@mattermost/types/channels';
import {getUserIdFromChannelName} from 'mattermost-redux/utils/channel_utils';
import usePrefixedIds from 'components/common/hooks/usePrefixedIds';
import BotTag from 'components/widgets/tag/bot_tag';
import Avatar from 'components/widgets/users/avatar';
@@ -20,6 +21,7 @@ function itemToName(item: Channel, currentUserId: string): {icon: React.ReactEle
if (item.type === Constants.DM_CHANNEL) {
const profilePicture = (
<Avatar
alt=''
url={imageURLForUser(getUserIdFromChannelName(currentUserId, item.name))}
size='sm'
/>
@@ -76,8 +78,18 @@ type Props = SuggestionProps<Channel> & {
teammateIsBot: boolean;
}
const SearchChannelSuggestion = React.forwardRef<HTMLLIElement, Props>((props, ref) => {
const {item, teammateIsBot, currentUserId} = props;
const SearchChannelSuggestion = React.forwardRef<HTMLLIElement, Props>(({
id,
item,
teammateIsBot,
currentUserId,
...otherProps
}, ref) => {
const ids = usePrefixedIds(id, {
name: null,
channelType: null,
botTag: null,
});
const nameObject = itemToName(item, currentUserId);
if (!nameObject) {
@@ -91,16 +103,23 @@ const SearchChannelSuggestion = React.forwardRef<HTMLLIElement, Props>((props, r
return (
<SuggestionContainer
ref={ref}
{...props}
id={id}
item={item}
{...otherProps}
aria-labelledby={ids.name}
aria-describedby={ids.botTag}
>
{icon}
<div className='suggestion-list__ellipsis'>
<span className='suggestion-list__main'>
<span
id={ids.name}
className='suggestion-list__main'
>
{name}
</span>
{description}
</div>
{tag}
{tag && <span id={ids.botTag}>{tag}</span>}
</SuggestionContainer>
);
});
@@ -2,6 +2,7 @@
// See LICENSE.txt for license information.
import React from 'react';
import {useIntl} from 'react-intl';
import type {Channel} from '@mattermost/types/channels';
@@ -20,6 +21,8 @@ import {sortChannelsByTypeAndDisplayName} from 'mattermost-redux/utils/channel_u
import store from 'stores/redux_store';
import usePrefixedIds from 'components/common/hooks/usePrefixedIds';
import {Constants} from 'utils/constants';
import Provider from './provider';
@@ -37,7 +40,9 @@ interface WrappedChannel {
type ChannelSearchFunction = (teamId: string, channelPrefix: string) => Promise<ActionResult>
const SearchChannelWithPermissionsSuggestion = React.forwardRef<HTMLLIElement, SuggestionProps<WrappedChannel>>((props, ref) => {
const {item} = props;
const {formatMessage} = useIntl();
const {id, item} = props;
const channel = item.channel;
const channelIsArchived = channel.delete_at && channel.delete_at !== 0;
@@ -45,26 +50,61 @@ const SearchChannelWithPermissionsSuggestion = React.forwardRef<HTMLLIElement, S
let icon = null;
if (channelIsArchived) {
icon = (
<i className='icon icon--no-spacing icon-archive-outline'/>
<i
className='icon icon--no-spacing icon-archive-outline'
aria-label={formatMessage({
id: 'suggestion.archived_channel',
defaultMessage: 'Archived channel',
})}
/>
);
} else if (channel.type === Constants.OPEN_CHANNEL) {
icon = (
<i className='icon icon--no-spacing icon-globe'/>
<i
className='icon icon--no-spacing icon-globe'
aria-label={formatMessage({
id: 'suggestion.public_channel',
defaultMessage: 'Public channel',
})}
/>
);
} else if (channel.type === Constants.PRIVATE_CHANNEL) {
icon = (
<i className='icon icon--no-spacing icon-lock-outline'/>
<i
className='icon icon--no-spacing icon-lock-outline'
aria-label={formatMessage({
id: 'suggestion.private_channel',
defaultMessage: 'Private channel',
})}
/>
);
}
const ids = usePrefixedIds(id, {
name: null,
channelType: null,
});
return (
<SuggestionContainer
ref={ref}
{...props}
aria-labelledby={ids.name}
aria-describedby={ids.channelType}
>
<span className='suggestion-list__icon suggestion-list__icon--large'>{icon}</span>
<span
id={ids.channelType}
className='suggestion-list__icon suggestion-list__icon--large'
>
{icon}
</span>
<div className='suggestion-list__ellipsis'>
<span className='suggestion-list__main'>{displayName}</span>
<span
id={ids.name}
className='suggestion-list__main'
>
{displayName}
</span>
</div>
</SuggestionContainer>
);
@@ -50,6 +50,7 @@ export const SearchUserSuggestion = React.forwardRef<HTMLLIElement, SuggestionPr
size='sm'
username={username}
url={Utils.imageURLForUser(item.id, item.last_picture_update)}
alt=''
/>
<div className='suggestion-list__ellipsis'>
<span className='suggestion-list__main'>
@@ -4,7 +4,9 @@
import classNames from 'classnames';
import React, {useCallback} from 'react';
export interface SuggestionProps<Item> extends Omit<React.HTMLAttributes<HTMLLIElement>, 'onClick' | 'onMouseMove' | 'role'> {
export interface SuggestionProps<Item> extends Omit<React.HTMLAttributes<HTMLLIElement>, 'id' | 'onClick' | 'onMouseMove' | 'role'> {
id: string;
// eslint-disable-next-line react/no-unused-prop-types
item: Item;
@@ -1,15 +1,21 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import type {Channel} from '@mattermost/types/channels';
import {CollapsedThreads} from '@mattermost/types/config';
import type {Team} from '@mattermost/types/teams';
import type {UserProfile} from '@mattermost/types/users';
import {Preferences} from 'mattermost-redux/constants';
import {General, Preferences} from 'mattermost-redux/constants';
import {renderWithContext, screen} from 'tests/react_testing_utils';
import mockStore from 'tests/test_store';
import {StoragePrefixes} from 'utils/constants';
import {TestHelper} from 'utils/test_helper';
import SwitchChannelProvider from './switch_channel_provider';
import SwitchChannelProvider, {ConnectedSwitchChannelSuggestion} from './switch_channel_provider';
const latestPost = TestHelper.getPostMock({
id: 'latest_post_id',
@@ -990,3 +996,261 @@ describe('components/SwitchChannelProvider', () => {
}));
});
});
describe('SwitchChannelSuggestion', () => {
const baseProps = {
id: 'test-suggestion',
matchedPretext: '',
isSelection: false,
onClick: jest.fn(),
onMouseMove: jest.fn(),
};
const currentUserId = 'currentUser';
const team1 = TestHelper.getTeamMock({id: 'team1', display_name: 'Team One'});
const team2 = TestHelper.getTeamMock({id: 'team2', display_name: 'Team Two'});
function getBaseState(teams: Team[], channels: Channel[]): any {
return {
entities: {
channels: {
channels: channels.reduce((channelsMap, channel) => ({...channelsMap, [channel.id]: channel}), {}),
myMembers: channels.reduce((membersMap, channel) => ({
...membersMap,
[channel.id]: TestHelper.getChannelMembershipMock({channel_id: channel.id, user_id: currentUserId}),
}), {}),
},
teams: {
teams: teams.reduce((teamsMap, team) => ({...teamsMap, [team.id]: team}), {}),
myMembers: teams.reduce((membersMap, team) => ({
...membersMap,
[team.id]: TestHelper.getTeamMembershipMock({team_id: team.id, user_id: currentUserId}),
}), {}),
},
},
};
}
test('should show the team name for channels if the user is on multiple teams', () => {
const channel1 = TestHelper.getChannelMock({id: 'channel1', team_id: 'team1', name: 'channel_one', display_name: 'Channel One'});
const {replaceStoreState} = renderWithContext(
<ConnectedSwitchChannelSuggestion
{...baseProps}
term={channel1.name}
item={{
channel: channel1,
name: channel1.name,
deactivated: false,
}}
/>,
getBaseState([team1], [channel1]),
);
const suggestion = document.getElementById(baseProps.id);
// When the user is on only a single team, the channel's URL name is displayed
expect(screen.getByText(`~${channel1.name}`)).toBeInTheDocument();
expect(suggestion).toHaveAccessibleName(channel1.display_name);
expect(suggestion).toHaveAccessibleDescription(`~${channel1.name} Public channel`);
replaceStoreState(getBaseState([team1, team2], [channel1]));
// When the user is on multiple teams, we show the team's display name instead
expect(screen.getByText(team1.display_name)).toBeInTheDocument();
expect(suggestion).toHaveAccessibleName(channel1.display_name);
expect(suggestion).toHaveAccessibleDescription(`${team1.display_name} Public channel`);
});
test('should show the type of channel', () => {
const channel1 = TestHelper.getChannelMock({id: 'channel1', team_id: 'team1', name: 'channel_one', display_name: 'Channel One', type: General.OPEN_CHANNEL});
const channel2 = TestHelper.getChannelMock({id: 'channel2', team_id: 'team1', name: 'channel_two', display_name: 'Channel Two', type: General.PRIVATE_CHANNEL});
const {rerender} = renderWithContext(
<ConnectedSwitchChannelSuggestion
{...baseProps}
term={channel1.name}
item={{
channel: channel1,
name: channel1.name,
deactivated: false,
}}
/>,
getBaseState([team1], [channel1, channel2]),
);
const suggestion = document.getElementById(baseProps.id);
expect(screen.getByLabelText('Public channel')).toBeInTheDocument();
expect(suggestion).toHaveAccessibleName(channel1.display_name);
expect(suggestion).toHaveAccessibleDescription(`~${channel1.name} Public channel`);
rerender(
<ConnectedSwitchChannelSuggestion
{...baseProps}
term={channel2.name}
item={{
channel: channel2,
name: channel2.name,
deactivated: false,
}}
/>,
);
expect(screen.getByLabelText('Private channel')).toBeInTheDocument();
expect(suggestion).toHaveAccessibleName(channel2.display_name);
expect(suggestion).toHaveAccessibleDescription(`~${channel2.name} Private channel`);
});
test('should show if the channel has a draft instead of the channel type', () => {
const channel1 = TestHelper.getChannelMock({id: 'channel1', team_id: 'team1', name: 'channel_one', display_name: 'Channel One'});
const channel2 = TestHelper.getChannelMock({id: 'channel2', team_id: 'team1', name: 'channel_two', display_name: 'Channel Two'});
const testState = getBaseState([team1], [channel1, channel2]);
testState.storage = {
storage: {
[`${StoragePrefixes.DRAFT}${channel2.id}`]: {
value: TestHelper.getPostDraftMock({message: 'post draft'}),
},
},
};
const {rerender} = renderWithContext(
<ConnectedSwitchChannelSuggestion
{...baseProps}
term={channel1.name}
item={{
channel: channel1,
name: channel1.name,
deactivated: false,
}}
/>,
testState,
);
const suggestion = document.getElementById(baseProps.id);
expect(screen.queryByLabelText('Has draft')).not.toBeInTheDocument();
expect(suggestion).toHaveAccessibleName(channel1.display_name);
expect(suggestion).toHaveAccessibleDescription(`~${channel1.name} Public channel`);
rerender(
<ConnectedSwitchChannelSuggestion
{...baseProps}
term={channel2.name}
item={{
channel: channel2,
name: channel2.name,
deactivated: false,
}}
/>,
);
expect(screen.queryByLabelText('Has draft')).toBeInTheDocument();
expect(suggestion).toHaveAccessibleName(channel2.display_name);
expect(suggestion).toHaveAccessibleDescription(`~${channel2.name} Has draft`);
});
test('should show if the channel is archived instead of the channel type', () => {
const channel1 = TestHelper.getChannelMock({id: 'channel1', team_id: 'team1', name: 'channel_one', display_name: 'Channel One'});
const channel2 = TestHelper.getChannelMock({id: 'channel2', team_id: 'team1', name: 'channel_two', display_name: 'Channel Two', delete_at: 1});
const {rerender} = renderWithContext(
<ConnectedSwitchChannelSuggestion
{...baseProps}
term={channel1.name}
item={{
channel: channel1,
name: channel1.name,
deactivated: false,
}}
/>,
getBaseState([team1], [channel1, channel2]),
);
const suggestion = document.getElementById(baseProps.id);
expect(screen.queryByLabelText('Archved channel')).not.toBeInTheDocument();
expect(suggestion).toHaveAccessibleName(channel1.display_name);
expect(suggestion).toHaveAccessibleDescription(`~${channel1.name} Public channel`);
rerender(
<ConnectedSwitchChannelSuggestion
{...baseProps}
term={channel2.name}
item={{
channel: channel2,
name: channel2.name,
deactivated: false,
}}
/>,
);
expect(screen.queryByLabelText('Archived channel')).toBeInTheDocument();
expect(suggestion).toHaveAccessibleName(channel2.display_name);
expect(suggestion).toHaveAccessibleDescription(`~${channel2.name} Archived channel`);
});
test('should show if the channel has unread mentions', () => {
const channel1 = TestHelper.getChannelMock({id: 'channel1', team_id: 'team1', name: 'channel_one', display_name: 'Channel One'});
const channel2 = TestHelper.getChannelMock({id: 'channel2', team_id: 'team1', name: 'channel_two', display_name: 'Channel Two'});
const channel3 = TestHelper.getChannelMock({id: 'channel3', team_id: 'team1', name: 'channel_three', display_name: 'Channel Three'});
const testState = getBaseState([team1], [channel1, channel2, channel3]);
testState.entities.channels.myMembers[channel1.id].mention_count = 0;
testState.entities.channels.myMembers[channel2.id].mention_count = 1;
testState.entities.channels.myMembers[channel3.id].mention_count = 5;
const {rerender} = renderWithContext(
<ConnectedSwitchChannelSuggestion
{...baseProps}
term={channel1.name}
item={{
channel: channel1,
name: channel1.name,
deactivated: false,
}}
/>,
testState,
);
const suggestion = document.getElementById(baseProps.id);
expect(screen.queryByLabelText(/unread/, {exact: false})).not.toBeInTheDocument();
expect(suggestion).toHaveAccessibleName(channel1.display_name);
expect(suggestion).toHaveAccessibleDescription(`~${channel1.name} Public channel`);
rerender(
<ConnectedSwitchChannelSuggestion
{...baseProps}
term={channel2.name}
item={{
channel: channel2,
name: channel2.name,
deactivated: false,
}}
/>,
);
expect(screen.queryByLabelText('1 unread notification')).toBeInTheDocument();
expect(suggestion).toHaveAccessibleName(channel2.display_name);
expect(suggestion).toHaveAccessibleDescription(`1 unread notification ~${channel2.name} Public channel`);
rerender(
<ConnectedSwitchChannelSuggestion
{...baseProps}
term={channel3.name}
item={{
channel: channel3,
name: channel3.name,
deactivated: false,
}}
/>,
);
expect(screen.queryByLabelText('5 unread notifications')).toBeInTheDocument();
expect(suggestion).toHaveAccessibleName(channel3.display_name);
expect(suggestion).toHaveAccessibleDescription(`5 unread notifications ~${channel3.name} Public channel`);
});
});
@@ -3,8 +3,7 @@
import classNames from 'classnames';
import React from 'react';
import {defineMessage, injectIntl} from 'react-intl';
import type {WrappedComponentProps} from 'react-intl';
import {defineMessage, useIntl} from 'react-intl';
import {connect, useSelector} from 'react-redux';
import type {Channel, ChannelMembership} from '@mattermost/types/channels';
@@ -53,6 +52,7 @@ import {isGuest} from 'mattermost-redux/utils/user_utils';
import {getPostDraft} from 'selectors/rhs';
import globalStore from 'stores/redux_store';
import usePrefixedIds, {joinIds} from 'components/common/hooks/usePrefixedIds';
import CustomStatusEmoji from 'components/custom_status/custom_status_emoji';
import ProfilePicture from 'components/profile_picture';
import SharedChannelIndicator from 'components/shared_channel_indicator';
@@ -114,7 +114,8 @@ export interface WrappedChannel {
unread_mentions?: number;
}
type Props = SuggestionProps<WrappedChannel> & WrappedComponentProps & {
type Props = SuggestionProps<WrappedChannel> & {
id: string;
channelMember: ChannelMembership;
collapsedThreads: boolean;
dmChannelTeammate?: UserProfile;
@@ -122,20 +123,38 @@ type Props = SuggestionProps<WrappedChannel> & WrappedComponentProps & {
isPartOfOnlyOneTeam: boolean;
status?: string;
team?: Team;
id: string;
}
const SwitchChannelSuggestion = React.forwardRef<HTMLLIElement, Props>((props, ref) => {
const {item, status, collapsedThreads, team, isPartOfOnlyOneTeam} = props;
export const SwitchChannelSuggestion = React.forwardRef<HTMLLIElement, Props>(({
id,
item,
channelMember: member,
collapsedThreads,
dmChannelTeammate: teammate,
hasDraft,
isPartOfOnlyOneTeam,
status,
team,
...otherProps
}, ref) => {
const {formatMessage} = useIntl();
const channel = item.channel;
const channelIsArchived = channel.delete_at && channel.delete_at !== 0;
const currentUserId = useSelector(getCurrentUserId);
const member = props.channelMember;
const teammate = props.dmChannelTeammate;
let badge = null;
const ids = usePrefixedIds(id, {
name: null,
channelType: null,
description: null,
sharedIcon: null,
tag: null,
teamName: null,
unreadBadge: null,
});
let badge = null;
if ((member && member.notify_props) || item.unread_mentions) {
let unreadMentions;
if (item.unread_mentions) {
@@ -145,7 +164,16 @@ const SwitchChannelSuggestion = React.forwardRef<HTMLLIElement, Props>((props, r
}
if (unreadMentions > 0 && !channelIsArchived) {
badge = (
<div className={classNames('suggestion-list_unread-mentions', (isPartOfOnlyOneTeam ? 'position-end' : ''))}>
<div
id={ids.unreadBadge}
className={classNames('suggestion-list_unread-mentions', (isPartOfOnlyOneTeam ? 'position-end' : ''))}
aria-label={formatMessage({
id: 'channel_switch_modal.unreadMentions',
defaultMessage: '{count, number} {count, plural, one {unread notification} other {unread notifications}}',
}, {
count: unreadMentions,
})}
>
<span className='badge'>
{unreadMentions}
</span>
@@ -159,25 +187,53 @@ const SwitchChannelSuggestion = React.forwardRef<HTMLLIElement, Props>((props, r
let icon;
if (channelIsArchived) {
icon = (
<span className='suggestion-list__icon suggestion-list__icon--large'>
<span
id={ids.channelType}
className='suggestion-list__icon suggestion-list__icon--large'
aria-label={formatMessage({
id: 'suggestion.archived_channel',
defaultMessage: 'Archived channel',
})}
>
<i className='icon icon-archive-outline'/>
</span>
);
} else if (props.hasDraft) {
} else if (hasDraft) {
icon = (
<span className='suggestion-list__icon suggestion-list__icon--large'>
<span
id={ids.channelType}
className='suggestion-list__icon suggestion-list__icon--large'
aria-label={formatMessage({
id: 'channel_switch_modal.has_draft',
defaultMessage: 'Has draft',
})}
>
<i className='icon icon-pencil-outline'/>
</span>
);
} else if (channel.type === Constants.OPEN_CHANNEL) {
icon = (
<span className='suggestion-list__icon suggestion-list__icon--large'>
<span
id={ids.channelType}
className='suggestion-list__icon suggestion-list__icon--large'
aria-label={formatMessage({
id: 'suggestion.public_channel',
defaultMessage: 'Public channel',
})}
>
<i className='icon icon-globe'/>
</span>
);
} else if (channel.type === Constants.PRIVATE_CHANNEL) {
icon = (
<span className='suggestion-list__icon suggestion-list__icon--large'>
<span
id={ids.channelType}
className='suggestion-list__icon suggestion-list__icon--large'
aria-label={formatMessage({
id: 'suggestion.private_channel',
defaultMessage: 'Private channel',
})}
>
<i className='icon icon-lock-outline'/>
</span>
);
@@ -189,7 +245,14 @@ const SwitchChannelSuggestion = React.forwardRef<HTMLLIElement, Props>((props, r
);
} else if (channel.type === Constants.GM_CHANNEL) {
icon = (
<span className='suggestion-list__icon suggestion-list__icon--large'>
<span
id={ids.channelType}
aria-label={formatMessage({
id: 'suggestion.group_channel',
defaultMessage: 'Group channel',
})}
className='suggestion-list__icon suggestion-list__icon--large'
>
<div className='status status--group'>{'G'}</div>
</span>
);
@@ -224,7 +287,7 @@ const SwitchChannelSuggestion = React.forwardRef<HTMLLIElement, Props>((props, r
let deactivated = '';
if (teammate.delete_at) {
deactivated = (' - ' + props.intl.formatMessage({id: 'channel_switch_modal.deactivated', defaultMessage: 'Deactivated'}));
deactivated = (' - ' + formatMessage({id: 'channel_switch_modal.deactivated', defaultMessage: 'Deactivated'}));
}
if (channel.display_name && !(teammate && teammate.is_bot)) {
@@ -232,7 +295,7 @@ const SwitchChannelSuggestion = React.forwardRef<HTMLLIElement, Props>((props, r
} else {
name = teammate.username;
if (teammate.id === currentUserId) {
name += (' ' + props.intl.formatMessage({id: 'suggestion.user.isCurrent', defaultMessage: '(you)'}));
name += (' ' + formatMessage({id: 'suggestion.user.isCurrent', defaultMessage: '(you)'}));
}
description = deactivated;
}
@@ -245,37 +308,60 @@ const SwitchChannelSuggestion = React.forwardRef<HTMLLIElement, Props>((props, r
let sharedIcon = null;
if (isRealChannel(channel) && channel.shared) {
sharedIcon = (
<SharedChannelIndicator
className='shared-channel-icon'
/>
<span id={ids.sharedIcon}>
<SharedChannelIndicator
className='shared-channel-icon'
/>
</span>
);
}
let teamName = null;
if (isRealChannel(channel) && channel.team_id && team) {
teamName = (<span className='ml-2 suggestion-list__team-name'>{team.display_name}</span>);
teamName = (
<span
id={ids.teamName}
className='ml-2 suggestion-list__team-name'
>
{team.display_name}
</span>
);
}
const showSlug = (isPartOfOnlyOneTeam || channel.type === Constants.DM_CHANNEL) && channel.type !== Constants.THREADS;
Reflect.deleteProperty(otherProps, 'dispatch');
return (
<SuggestionContainer
ref={ref}
id={id}
data-testid={channel.name}
aria-labelledby={`${name.toLowerCase().replaceAll(' ', '-')}-item-name`}
{...props}
item={item}
{...otherProps}
aria-labelledby={ids.name}
aria-describedby={joinIds(ids.unreadBadge, ids.description, ids.teamName, ids.channelType, ids.sharedIcon, ids.tag)}
>
{icon}
<div className='suggestion-list__ellipsis suggestion-list__flex'>
<span className='suggestion-list__main'>
<span
id={ids.name}
className={classNames({'suggestion-list__unread': item.unread && !channelIsArchived})}
id={`${name.toLowerCase().replaceAll(' ', '-')}-item-name`}
>{name}</span>
{showSlug && description && <span className='ml-2 suggestion-list__desc'>{description}</span>}
>
{name}
</span>
{showSlug && description && (
<span
id={ids.description}
className='ml-2 suggestion-list__desc'
>
{description}
</span>
)}
</span>
{customStatus}
{sharedIcon}
{tag}
{tag && <span id={ids.tag}>{tag}</span>}
{badge}
{!isPartOfOnlyOneTeam && teamName}
</div>
@@ -323,7 +409,7 @@ function mapStateToPropsForSwitchChannelSuggestion(state: GlobalState, ownProps:
};
}
const ConnectedSwitchChannelSuggestion = connect(mapStateToPropsForSwitchChannelSuggestion, null, null, {forwardRef: true})(injectIntl(SwitchChannelSuggestion, {forwardRef: true}));
export const ConnectedSwitchChannelSuggestion = connect(mapStateToPropsForSwitchChannelSuggestion, null, null, {forwardRef: true})(SwitchChannelSuggestion);
let prefix = '';
@@ -1045,7 +1045,7 @@ exports[`component/user_group_popover should match snapshot 1`] = `
className="status "
>
<svg
aria-label="Offline Icon"
aria-label="Offline"
className="offline--icon"
height="100%"
role="img"
@@ -1224,7 +1224,7 @@ exports[`component/user_group_popover should match snapshot 1`] = `
className="status "
>
<svg
aria-label="Offline Icon"
aria-label="Offline"
className="offline--icon"
height="100%"
role="img"
@@ -1403,7 +1403,7 @@ exports[`component/user_group_popover should match snapshot 1`] = `
className="status "
>
<svg
aria-label="Offline Icon"
aria-label="Offline"
className="offline--icon"
height="100%"
role="img"
@@ -1582,7 +1582,7 @@ exports[`component/user_group_popover should match snapshot 1`] = `
className="status "
>
<svg
aria-label="Offline Icon"
aria-label="Offline"
className="offline--icon"
height="100%"
role="img"
@@ -1761,7 +1761,7 @@ exports[`component/user_group_popover should match snapshot 1`] = `
className="status "
>
<svg
aria-label="Offline Icon"
aria-label="Offline"
className="offline--icon"
height="100%"
role="img"
@@ -432,7 +432,7 @@ exports[`component/user_group_popover/group_member_list should match snapshot 1`
className="status "
>
<svg
aria-label="Offline Icon"
aria-label="Offline"
className="offline--icon"
height="100%"
role="img"
@@ -611,7 +611,7 @@ exports[`component/user_group_popover/group_member_list should match snapshot 1`
className="status "
>
<svg
aria-label="Offline Icon"
aria-label="Offline"
className="offline--icon"
height="100%"
role="img"
@@ -790,7 +790,7 @@ exports[`component/user_group_popover/group_member_list should match snapshot 1`
className="status "
>
<svg
aria-label="Offline Icon"
aria-label="Offline"
className="offline--icon"
height="100%"
role="img"
@@ -969,7 +969,7 @@ exports[`component/user_group_popover/group_member_list should match snapshot 1`
className="status "
>
<svg
aria-label="Offline Icon"
aria-label="Offline"
className="offline--icon"
height="100%"
role="img"
@@ -1148,7 +1148,7 @@ exports[`component/user_group_popover/group_member_list should match snapshot 1`
className="status "
>
<svg
aria-label="Offline Icon"
aria-label="Offline"
className="offline--icon"
height="100%"
role="img"
@@ -15,7 +15,7 @@ export default function StatusAwayIcon(props: React.HTMLAttributes<HTMLSpanEleme
viewBox='0 0 20 20'
style={style}
role='img'
aria-label={formatMessage({id: 'mobile.set_status.away.icon', defaultMessage: 'Away Icon'})}
aria-label={formatMessage({id: 'mobile.set_status.away.icon', defaultMessage: 'Away'})}
>
<path
className='away--icon'
@@ -15,7 +15,7 @@ export default function StatusDndIcon(props: React.HTMLAttributes<HTMLSpanElemen
viewBox='0 0 20 20'
style={style}
role='img'
aria-label={formatMessage({id: 'mobile.set_status.dnd.icon', defaultMessage: 'Do Not Disturb Icon'})}
aria-label={formatMessage({id: 'mobile.set_status.dnd.icon', defaultMessage: 'Do Not Disturb'})}
>
<path
className='dnd--icon'
@@ -16,7 +16,7 @@ export default function StatusOfflineIcon(props: React.HTMLAttributes<HTMLSpanEl
viewBox='0 0 20 20'
style={style}
role='img'
aria-label={formatMessage({id: 'mobile.set_status.offline.icon', defaultMessage: 'Offline Icon'})}
aria-label={formatMessage({id: 'mobile.set_status.offline.icon', defaultMessage: 'Offline'})}
>
<path d='M10,0c5.519,0 10,4.481 10,10c0,5.519 -4.481,10 -10,10c-5.519,0 -10,-4.481 -10,-10c0,-5.519 4.481,-10 10,-10Zm0,2c4.415,0 8,3.585 8,8c0,4.415 -3.585,8 -8,8c-4.415,0 -8,-3.585 -8,-8c0,-4.415 3.585,-8 8,-8Z'/>
</svg>
@@ -15,7 +15,7 @@ export default function StatusOnlineIcon(props: React.HTMLAttributes<HTMLSpanEle
viewBox='0 0 20 20'
style={style}
role='img'
aria-label={formatMessage({id: 'mobile.set_status.online.icon', defaultMessage: 'Online Icon'})}
aria-label={formatMessage({id: 'mobile.set_status.online.icon', defaultMessage: 'Online'})}
>
<path
className='online--icon'
+11 -5
View File
@@ -3690,6 +3690,8 @@
"channel_settings.tab.info": "Info",
"channel_settings.unknown_error": "Something went wrong.",
"channel_switch_modal.deactivated": "Deactivated",
"channel_switch_modal.has_draft": "Has draft",
"channel_switch_modal.unreadMentions": "{count, number} {count, plural, one {unread notification} other {unread notifications}}",
"channel_toggle_button.private": "Private",
"channel_toggle_button.public": "Public",
"channelHeader.addToFavorites": "Add to Favorites",
@@ -4744,10 +4746,10 @@
"mfa.setup.step2_secret": "Alternatively, enter the secret key displayed below into the authenticator app manually.",
"mfa.setup.step3_code": "2. Enter the code generated by the authenticator app in the field below.",
"mfa.setupTitle": "Multi-factor Authentication Setup",
"mobile.set_status.away.icon": "Away Icon",
"mobile.set_status.dnd.icon": "Do Not Disturb Icon",
"mobile.set_status.offline.icon": "Offline Icon",
"mobile.set_status.online.icon": "Online Icon",
"mobile.set_status.away.icon": "Away",
"mobile.set_status.dnd.icon": "Do Not Disturb",
"mobile.set_status.offline.icon": "Offline",
"mobile.set_status.online.icon": "Online",
"modal.header_close": "Close",
"modal.manual_status.ask": "Do not ask me again",
"modal.manual_status.auto_responder.message_away": "Would you like to switch your status to \"Away\" and disable automatic replies?",
@@ -5370,7 +5372,7 @@
"shared_channel_indicator.tooltip_with_names": "Shared with: {remoteNames}",
"shared_channel_indicator.tooltip_with_names.few": "Shared with: {organizations}",
"shared_channel_indicator.tooltip_with_names.many": "Shared with: {organizations} and {count, number} {count, plural, one {other} other {others}}",
"shared_user_indicator.aria_label": "shared user indicator",
"shared_user_indicator.aria_label": "shared user",
"shared_user_indicator.tooltip": "From a trusted organization",
"shared_user_indicator.tooltip_with_names": "From: {remoteNames}",
"shortcuts.browser.channel_next": "Forward in history:\tAlt|Right",
@@ -5649,9 +5651,11 @@
"success_modal.subtitle": "Your final bill will be prorated. Your workspace now has {plan} limits.",
"suggestion.all": "All channel types",
"suggestion.archive": "Archived channels",
"suggestion.archived_channel": "Archived channel",
"suggestion.channels": "Channels",
"suggestion.commands": "Commands",
"suggestion.emoji": "Emoji",
"suggestion.group_channel": "Group channel",
"suggestion.group.members": "{member_count} {member_count, plural, one {member} other {members}}",
"suggestion.mention.all": "Notifies everyone in this channel",
"suggestion.mention.channel": "Notifies everyone in this channel",
@@ -5665,7 +5669,9 @@
"suggestion.mention.special": "Special Mentions",
"suggestion.mention.unread": "Unread",
"suggestion.private": "Private channels",
"suggestion.private_channel": "Private channel",
"suggestion.public": "Public channels",
"suggestion.public_channel": "Public channel",
"suggestion.search.direct": "Direct Messages",
"suggestion.search.group": "Group Mentions",
"suggestion.search.private": "Private Channels",
@@ -0,0 +1,40 @@
diff --git a/node_modules/dom-accessibility-api/dist/accessible-name-and-description.js b/node_modules/dom-accessibility-api/dist/accessible-name-and-description.js
index 585aa65..bb4ec8a 100644
--- a/node_modules/dom-accessibility-api/dist/accessible-name-and-description.js
+++ b/node_modules/dom-accessibility-api/dist/accessible-name-and-description.js
@@ -434,7 +434,14 @@ function computeTextAlternative(root) {
var skipToStep2E = context.recursion && isControl(current) && compute === "name";
if (!skipToStep2E) {
var ariaLabel = ((0, _util.isElement)(current) && current.getAttribute("aria-label") || "").trim();
- if (ariaLabel !== "" && compute === "name") {
+
+ // @hmhealey I commented out the check below for `compute === "name"` because the current version of this library
+ // doesn't allow for one element's description to use another element's aria-label when that other element is in
+ // the first element's aria-describedby. Chrome and Firefox both support that, and based on my reading of
+ // https://www.w3.org/TR/accname-1.2/#computation-steps, step "2.4 AriaLabel" states that that should it should
+ // be supported. This is likely also why the current version of this library fails the
+ // accname/description_from_content_of_describedby_element-manual test from https://github.com/web-platform-tests/wpt.
+ if (ariaLabel !== ""/* && compute === "name"*/) {
consultedNodes.add(current);
return ariaLabel;
}
diff --git a/node_modules/dom-accessibility-api/dist/accessible-name-and-description.mjs b/node_modules/dom-accessibility-api/dist/accessible-name-and-description.mjs
index 8f905f1..aa3f1c5 100644
--- a/node_modules/dom-accessibility-api/dist/accessible-name-and-description.mjs
+++ b/node_modules/dom-accessibility-api/dist/accessible-name-and-description.mjs
@@ -433,7 +433,14 @@ export function computeTextAlternative(root) {
var skipToStep2E = context.recursion && isControl(current) && compute === "name";
if (!skipToStep2E) {
var ariaLabel = (isElement(current) && current.getAttribute("aria-label") || "").trim();
- if (ariaLabel !== "" && compute === "name") {
+
+ // @hmhealey I commented out the check below for `compute === "name"` because the current version of this library
+ // doesn't allow for one element's description to use another element's aria-label when that other element is in
+ // the first element's aria-describedby. Chrome and Firefox both support that, and based on my reading of
+ // https://www.w3.org/TR/accname-1.2/#computation-steps, step "2.4 AriaLabel" states that that should it should
+ // be supported. This is likely also why the current version of this library fails the
+ // accname/description_from_content_of_describedby_element-manual test from https://github.com/web-platform-tests/wpt.
+ if (ariaLabel !== ""/* && compute === "name"*/) {
consultedNodes.add(current);
return ariaLabel;
}