mirror of
https://github.com/mattermost/mattermost.git
synced 2026-09-24 16:05:00 +08:00
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:
co-authored by
Mattermost Build
parent
3ab9a661c1
commit
a03cc134bb
+3
-3
@@ -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
|
||||
|
||||
+3
-3
@@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
|
||||
+50
-16
@@ -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={
|
||||
|
||||
+76
-4
@@ -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);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
+46
-32
@@ -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}
|
||||
|
||||
+5
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
+29
-14
@@ -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>
|
||||
|
||||
+1
@@ -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',
|
||||
|
||||
+24
-5
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
+46
-6
@@ -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 = '';
|
||||
|
||||
|
||||
+5
-5
@@ -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"
|
||||
|
||||
+5
-5
@@ -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'
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user