Fix accessibility issue with dropdown options (#30715)
* Added getOptionLabel to fix dropdown accessibility for options * Fix tests * Revert unnecessary cases * Other unnecessary case * Linting * PR Feedback * Cleanup * Cursor assisted - unit tests * Linting * Linting i18n * Fix test --------- Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
0d9c4810f4
Коммит
466e3c3502
@@ -16,7 +16,7 @@ import CheckboxWithSelectSettingItem from 'components/widgets/modals/components/
|
||||
import ModalHeader from 'components/widgets/modals/components/modal_header';
|
||||
import ModalSection from 'components/widgets/modals/components/modal_section';
|
||||
import RadioSettingItem from 'components/widgets/modals/components/radio_setting_item';
|
||||
import type {Option} from 'components/widgets/modals/components/react_select_item';
|
||||
import type {SelectOption} from 'components/widgets/modals/components/react_select_item';
|
||||
|
||||
import {focusElement} from 'utils/a11y_utils';
|
||||
import {NotificationLevels, DesktopSound, IgnoreChannelMentions} from 'utils/constants';
|
||||
@@ -162,11 +162,11 @@ export default function ChannelNotificationsModal(props: Props) {
|
||||
</>
|
||||
);
|
||||
|
||||
const handleChangeForMessageNotificationSoundSelect = (selectedOption: OnChangeValue<Option, boolean>) => {
|
||||
const handleChangeForMessageNotificationSoundSelect = (selectedOption: OnChangeValue<SelectOption, boolean>) => {
|
||||
stopTryNotificationRing();
|
||||
|
||||
if (selectedOption && 'value' in selectedOption) {
|
||||
handleChange({desktop_notification_sound: ((selectedOption as Option).value)});
|
||||
handleChange({desktop_notification_sound: ((selectedOption as SelectOption).value)});
|
||||
tryNotificationSound(selectedOption.value);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3,10 +3,12 @@
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import type {SelectOption} from 'components/widgets/modals/components/react_select_item';
|
||||
|
||||
import {renderWithContext, screen} from 'tests/react_testing_utils';
|
||||
import Constants, {NotificationLevels} from 'utils/constants';
|
||||
|
||||
import type {SelectOption, Props} from './index';
|
||||
import type {Props} from './index';
|
||||
import DesktopNotificationSettings, {
|
||||
shouldShowDesktopThreadsSection,
|
||||
shouldShowMobileThreadsSection,
|
||||
@@ -177,6 +179,88 @@ describe('DesktopNotificationSettings', () => {
|
||||
|
||||
expect(screen.queryByText('Trigger mobile notifications when I am:')).toBeNull();
|
||||
});
|
||||
|
||||
// New accessibility tests for the option labels fix
|
||||
describe('Accessibility - Option Labels', () => {
|
||||
test('should render notification option labels as proper text for screen readers', () => {
|
||||
renderWithContext(
|
||||
<DesktopNotificationSettings {...baseProps}/>,
|
||||
);
|
||||
|
||||
// Check that the radio button labels are properly rendered as text
|
||||
expect(screen.getByText('All new messages')).toBeInTheDocument();
|
||||
expect(screen.getByText('Mentions, direct messages, and group messages')).toBeInTheDocument();
|
||||
expect(screen.getByText('Nothing')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render mobile notification option labels correctly in select dropdown', () => {
|
||||
const props = {...baseProps, desktopAndMobileSettingsDifferent: true};
|
||||
renderWithContext(
|
||||
<DesktopNotificationSettings {...props}/>,
|
||||
);
|
||||
|
||||
// The select component should be present - use more specific selector since there are multiple comboboxes
|
||||
const selectElement = screen.getByLabelText(/Send mobile notifications for:/);
|
||||
expect(selectElement).toBeInTheDocument();
|
||||
|
||||
// The aria-labelledby should reference the correct label
|
||||
expect(selectElement).toHaveAttribute('aria-labelledby', 'sendMobileNotificationsLabel');
|
||||
});
|
||||
|
||||
test('should render mobile notification status options correctly', () => {
|
||||
const props = {...baseProps, desktopActivity: NotificationLevels.MENTION, desktopAndMobileSettingsDifferent: false};
|
||||
renderWithContext(
|
||||
<DesktopNotificationSettings {...props}/>,
|
||||
);
|
||||
|
||||
// The trigger mobile notifications select should be present
|
||||
const selectElement = screen.getByLabelText(/Trigger mobile notifications when I am:/);
|
||||
expect(selectElement).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should ensure option labels are MessageDescriptor or string, not React elements', () => {
|
||||
// This test ensures we don't regress to the [object,object] issue
|
||||
const result = getValueOfSendMobileNotificationForSelect(NotificationLevels.ALL);
|
||||
if (result && typeof result === 'object' && 'label' in result) {
|
||||
// The label should be a MessageDescriptor (object) or string, not a React element
|
||||
if (typeof result.label === 'object') {
|
||||
// If it's an object, it should be a MessageDescriptor with id and defaultMessage
|
||||
expect(result.label).toHaveProperty('id');
|
||||
expect(result.label).toHaveProperty('defaultMessage');
|
||||
|
||||
// Should NOT have React element properties
|
||||
expect(result.label).not.toHaveProperty('$$typeof');
|
||||
expect(result.label).not.toHaveProperty('type');
|
||||
expect(result.label).not.toHaveProperty('props');
|
||||
} else {
|
||||
// If it's not an object, it should be a string
|
||||
expect(typeof result.label).toBe('string');
|
||||
}
|
||||
expect(result.label).not.toBeNull();
|
||||
}
|
||||
});
|
||||
|
||||
test('should ensure mobile notification status option labels are properly formatted', () => {
|
||||
const result = getValueOfSendMobileNotificationWhenSelect(Constants.UserStatuses.ONLINE);
|
||||
if (result && typeof result === 'object' && 'label' in result) {
|
||||
// The label should be a MessageDescriptor (object) or string, not a React element
|
||||
if (typeof result.label === 'object') {
|
||||
// If it's an object, it should be a MessageDescriptor with id and defaultMessage
|
||||
expect(result.label).toHaveProperty('id');
|
||||
expect(result.label).toHaveProperty('defaultMessage');
|
||||
|
||||
// Should NOT have React element properties that would cause [object,object]
|
||||
expect(result.label).not.toHaveProperty('$$typeof');
|
||||
expect(result.label).not.toHaveProperty('type');
|
||||
expect(result.label).not.toHaveProperty('props');
|
||||
} else {
|
||||
// If it's not an object, it should be a string
|
||||
expect(typeof result.label).toBe('string');
|
||||
}
|
||||
expect(result.label).not.toBeNull();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('shouldShowDesktopThreadsSection', () => {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import React, {Fragment, useCallback, useEffect, useMemo, useRef, memo} from 'react';
|
||||
import type {ChangeEvent, ReactNode} from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import {FormattedMessage, useIntl, defineMessage} from 'react-intl';
|
||||
import ReactSelect from 'react-select';
|
||||
import type {OnChangeValue, Options} from 'react-select';
|
||||
|
||||
@@ -14,16 +14,13 @@ import SettingItemMin from 'components/setting_item_min';
|
||||
import type SettingItemMinComponent from 'components/setting_item_min';
|
||||
import NotificationPermissionSectionNotice from 'components/user_settings/notifications/desktop_and_mobile_notification_setting/notification_permission_section_notice';
|
||||
import NotificationPermissionTitleTag from 'components/user_settings/notifications/desktop_and_mobile_notification_setting/notification_permission_title_tag';
|
||||
import {getOptionLabel, type SelectOption} from 'components/widgets/modals/components/react_select_item';
|
||||
|
||||
import Constants, {NotificationLevels, UserSettingsNotificationSections} from 'utils/constants';
|
||||
import {formatAsComponent} from 'utils/i18n';
|
||||
|
||||
import type {Props as UserSettingsNotificationsProps} from '../user_settings_notifications';
|
||||
|
||||
export type SelectOption = {
|
||||
label: ReactNode;
|
||||
value: string;
|
||||
};
|
||||
|
||||
export type Props = {
|
||||
active: boolean;
|
||||
updateSection: (section: string) => void;
|
||||
@@ -63,6 +60,7 @@ function DesktopAndMobileNotificationSettings({
|
||||
}: Props) {
|
||||
const editButtonRef = useRef<SettingItemMinComponent>(null);
|
||||
const previousActiveRef = useRef(active);
|
||||
const intl = useIntl();
|
||||
|
||||
// Focus back on the edit button, after this section was closed after it was opened
|
||||
useEffect(() => {
|
||||
@@ -131,7 +129,7 @@ function DesktopAndMobileNotificationSettings({
|
||||
value={optionOfSendNotifications.value}
|
||||
onChange={handleChangeForSendDesktopNotificationsRadio}
|
||||
/>
|
||||
{optionOfSendNotifications.label}
|
||||
{formatAsComponent(optionOfSendNotifications.label)}
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
@@ -208,6 +206,8 @@ function DesktopAndMobileNotificationSettings({
|
||||
components={{IndicatorSeparator: NoIndicatorSeparatorComponent}}
|
||||
value={getValueOfSendMobileNotificationForSelect(pushActivity)}
|
||||
onChange={handleChangeForSendMobileNotificationsSelect}
|
||||
getOptionLabel={(option) => getOptionLabel(option, intl)}
|
||||
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
@@ -261,6 +261,7 @@ function DesktopAndMobileNotificationSettings({
|
||||
components={{IndicatorSeparator: NoIndicatorSeparatorComponent}}
|
||||
value={getValueOfSendMobileNotificationWhenSelect(pushStatus)}
|
||||
onChange={handleChangeForTriggerMobileNotificationsSelect}
|
||||
getOptionLabel={(option) => getOptionLabel(option, intl)}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
@@ -352,30 +353,24 @@ function NoIndicatorSeparatorComponent() {
|
||||
|
||||
const optionsOfSendNotifications = [
|
||||
{
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='user.settings.notifications.desktopAndMobile.allNewMessages'
|
||||
defaultMessage='All new messages'
|
||||
/>
|
||||
),
|
||||
label: defineMessage({
|
||||
id: 'user.settings.notifications.desktopAndMobile.allNewMessages',
|
||||
defaultMessage: 'All new messages',
|
||||
}),
|
||||
value: NotificationLevels.ALL,
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='user.settings.notifications.desktopAndMobile.onlyMentions'
|
||||
defaultMessage='Mentions, direct messages, and group messages'
|
||||
/>
|
||||
),
|
||||
label: defineMessage({
|
||||
id: 'user.settings.notifications.desktopAndMobile.onlyMentions',
|
||||
defaultMessage: 'Mentions, direct messages, and group messages',
|
||||
}),
|
||||
value: NotificationLevels.MENTION,
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='user.settings.notifications.desktopAndMobile.nothing'
|
||||
defaultMessage='Nothing'
|
||||
/>
|
||||
),
|
||||
label: defineMessage({
|
||||
id: 'user.settings.notifications.desktopAndMobile.nothing',
|
||||
defaultMessage: 'Nothing',
|
||||
}),
|
||||
value: NotificationLevels.NONE,
|
||||
},
|
||||
];
|
||||
@@ -462,30 +457,24 @@ export function shouldShowTriggerMobileNotificationsSection(sendPushNotification
|
||||
|
||||
const optionsOfSendMobileNotificationsWhenSelect: Options<SelectOption> = [
|
||||
{
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='user.settings.notifications.desktopAndMobile.online'
|
||||
defaultMessage='Online, away, or offline'
|
||||
/>
|
||||
),
|
||||
label: defineMessage({
|
||||
id: 'user.settings.notifications.desktopAndMobile.online',
|
||||
defaultMessage: 'Online, away, or offline',
|
||||
}),
|
||||
value: Constants.UserStatuses.ONLINE,
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='user.settings.notifications.desktopAndMobile.away'
|
||||
defaultMessage='Away or offline'
|
||||
/>
|
||||
),
|
||||
label: defineMessage({
|
||||
id: 'user.settings.notifications.desktopAndMobile.away',
|
||||
defaultMessage: 'Away or offline',
|
||||
}),
|
||||
value: Constants.UserStatuses.AWAY,
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='user.settings.notifications.desktopAndMobile.offline'
|
||||
defaultMessage='Offline'
|
||||
/>
|
||||
),
|
||||
label: defineMessage({
|
||||
id: 'user.settings.notifications.desktopAndMobile.offline',
|
||||
defaultMessage: 'Offline',
|
||||
}),
|
||||
value: Constants.UserStatuses.OFFLINE,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {ChangeEvent, ReactNode} from 'react';
|
||||
import type {ChangeEvent} from 'react';
|
||||
import React, {memo, useEffect, useRef, Fragment, useMemo, useCallback} from 'react';
|
||||
import {FormattedMessage, useIntl} from 'react-intl';
|
||||
import type {OnChangeValue} from 'react-select';
|
||||
@@ -12,6 +12,7 @@ import type {UserNotifyProps} from '@mattermost/types/users';
|
||||
import SettingItemMax from 'components/setting_item_max';
|
||||
import SettingItemMin from 'components/setting_item_min';
|
||||
import type SettingItemMinComponent from 'components/setting_item_min';
|
||||
import {getOptionLabel, type SelectOption} from 'components/widgets/modals/components/react_select_item';
|
||||
|
||||
import {UserSettingsNotificationSections} from 'utils/constants';
|
||||
import {
|
||||
@@ -28,11 +29,6 @@ import {
|
||||
|
||||
import type {Props as UserSettingsNotificationsProps} from '../user_settings_notifications';
|
||||
|
||||
export type SelectOption = {
|
||||
value: string;
|
||||
label: ReactNode;
|
||||
};
|
||||
|
||||
export type Props = {
|
||||
active: boolean;
|
||||
updateSection: (section: string) => void;
|
||||
@@ -151,6 +147,8 @@ function DesktopNotificationSoundsSettings({
|
||||
value={getValueOfNotificationSoundsSelect(desktopNotificationSound)}
|
||||
onChange={handleChangeForMessageNotificationSoundSelect}
|
||||
aria-labelledby='messageNotificationSoundLabel'
|
||||
getOptionLabel={(option) => getOptionLabel(option, intl)}
|
||||
|
||||
/>
|
||||
</div>
|
||||
</Fragment>
|
||||
@@ -190,6 +188,8 @@ function DesktopNotificationSoundsSettings({
|
||||
})}
|
||||
value={getValueOfIncomingCallSoundsSelect(callsNotificationSound)}
|
||||
onChange={handleChangeForIncomingCallSoundSelect}
|
||||
getOptionLabel={(option) => getOptionLabel(option, intl)}
|
||||
|
||||
/>
|
||||
</div>
|
||||
</Fragment>
|
||||
|
||||
@@ -3,13 +3,14 @@
|
||||
|
||||
import type {ReactNode} from 'react';
|
||||
import React from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import ReactSelect from 'react-select';
|
||||
import type {OnChangeValue} from 'react-select';
|
||||
|
||||
import type {BaseSettingItemProps} from './base_setting_item';
|
||||
import BaseSettingItem from './base_setting_item';
|
||||
import type {FieldsetCheckbox} from './checkbox_setting_item';
|
||||
import type {FieldsetReactSelect, Option} from './react_select_item';
|
||||
import {getOptionLabel, type FieldsetReactSelect, type SelectOption} from './react_select_item';
|
||||
|
||||
type Props = BaseSettingItemProps & {
|
||||
containerClassName?: string;
|
||||
@@ -19,8 +20,8 @@ type Props = BaseSettingItemProps & {
|
||||
checkboxFieldValue: boolean;
|
||||
handleCheckboxChange: (e: boolean) => void;
|
||||
selectFieldData: FieldsetReactSelect;
|
||||
selectFieldValue?: Option;
|
||||
handleSelectChange: (selected: OnChangeValue<Option, boolean>) => void;
|
||||
selectFieldValue?: SelectOption;
|
||||
handleSelectChange: (selected: OnChangeValue<SelectOption, boolean>) => void;
|
||||
isSelectDisabled?: boolean;
|
||||
selectPlaceholder?: string;
|
||||
}
|
||||
@@ -40,6 +41,8 @@ export default function CheckboxWithSelectSettingItem({
|
||||
isSelectDisabled,
|
||||
selectPlaceholder,
|
||||
}: Props) {
|
||||
const intl = useIntl();
|
||||
|
||||
const content = (
|
||||
<>
|
||||
<fieldset
|
||||
@@ -73,6 +76,7 @@ export default function CheckboxWithSelectSettingItem({
|
||||
onChange={(value) => handleSelectChange(value)}
|
||||
value={selectFieldValue}
|
||||
components={{IndicatorSeparator: NoIndicatorSeparatorComponent}}
|
||||
getOptionLabel={(option) => getOptionLabel(option, intl)}
|
||||
/>
|
||||
</fieldset>
|
||||
</>
|
||||
@@ -93,8 +97,3 @@ export default function CheckboxWithSelectSettingItem({
|
||||
function NoIndicatorSeparatorComponent() {
|
||||
return null;
|
||||
}
|
||||
|
||||
export type SelectOption = {
|
||||
value: string;
|
||||
label: ReactNode;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {defineMessage} from 'react-intl';
|
||||
|
||||
import {getOptionLabel, type SelectOption} from './react_select_item';
|
||||
|
||||
// Mock intl object for testing
|
||||
const mockIntl = {
|
||||
formatMessage: jest.fn((descriptor) => {
|
||||
if (typeof descriptor === 'object' && descriptor.defaultMessage) {
|
||||
return descriptor.defaultMessage;
|
||||
}
|
||||
return String(descriptor);
|
||||
}),
|
||||
};
|
||||
|
||||
describe('getOptionLabel', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('should return string label as-is', () => {
|
||||
const option: SelectOption = {
|
||||
value: 'test',
|
||||
label: 'Test Label',
|
||||
};
|
||||
|
||||
const result = getOptionLabel(option, mockIntl as any);
|
||||
expect(result).toBe('Test Label');
|
||||
|
||||
// formatMessage should NOT be called for string labels - they're returned directly
|
||||
expect(mockIntl.formatMessage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should handle MessageDescriptor label correctly', () => {
|
||||
const messageDescriptor = defineMessage({
|
||||
id: 'test1',
|
||||
defaultMessage: 'Help Text',
|
||||
});
|
||||
|
||||
const option: SelectOption = {
|
||||
value: 'test',
|
||||
label: messageDescriptor,
|
||||
};
|
||||
|
||||
const result = getOptionLabel(option, mockIntl as any);
|
||||
expect(result).toBe('Help Text');
|
||||
expect(mockIntl.formatMessage).toHaveBeenCalledWith(messageDescriptor);
|
||||
});
|
||||
|
||||
test('should return empty string for undefined label', () => {
|
||||
const option: SelectOption = {
|
||||
value: 'test',
|
||||
label: undefined as any,
|
||||
};
|
||||
|
||||
const result = getOptionLabel(option, mockIntl as any);
|
||||
expect(result).toBe('');
|
||||
|
||||
// formatMessage should NOT be called for undefined - formatAsString returns undefined, then we || ''
|
||||
expect(mockIntl.formatMessage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should handle accessibility scenarios - no more [object,object]', () => {
|
||||
// This test ensures we don't get "[object,object]" for screen readers
|
||||
const messageDescriptor = defineMessage({
|
||||
id: 'test1',
|
||||
defaultMessage: 'Help Text',
|
||||
});
|
||||
|
||||
const option: SelectOption = {
|
||||
value: 'all',
|
||||
label: messageDescriptor,
|
||||
};
|
||||
|
||||
const result = getOptionLabel(option, mockIntl as any);
|
||||
|
||||
// Should return proper text, not "[object,object]"
|
||||
expect(result).toBe('Help Text');
|
||||
expect(result).not.toBe('[object,object]');
|
||||
expect(result).not.toBe('[object Object]');
|
||||
expect(typeof result).toBe('string');
|
||||
expect(mockIntl.formatMessage).toHaveBeenCalledWith(messageDescriptor);
|
||||
});
|
||||
});
|
||||
@@ -1,17 +1,20 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {ReactNode} from 'react';
|
||||
import React from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import type {MessageDescriptor} from 'react-intl';
|
||||
import type {OnChangeValue} from 'react-select';
|
||||
import ReactSelect from 'react-select';
|
||||
|
||||
import {formatAsString} from 'utils/i18n';
|
||||
|
||||
import type {BaseSettingItemProps} from './base_setting_item';
|
||||
import BaseSettingItem from './base_setting_item';
|
||||
|
||||
export type Option = {
|
||||
export type SelectOption = {
|
||||
value: string;
|
||||
label: ReactNode;
|
||||
label: string | MessageDescriptor;
|
||||
};
|
||||
|
||||
export type FieldsetReactSelect = {
|
||||
@@ -21,15 +24,20 @@ export type FieldsetReactSelect = {
|
||||
dataTestId?: string;
|
||||
ariaLabelledby?: string;
|
||||
clearable?: boolean;
|
||||
options: Option[];
|
||||
options: SelectOption[];
|
||||
}
|
||||
|
||||
type Props = BaseSettingItemProps & {
|
||||
inputFieldData: FieldsetReactSelect;
|
||||
inputFieldValue: Option;
|
||||
handleChange: (selected: OnChangeValue<Option, boolean>) => void;
|
||||
inputFieldValue: SelectOption;
|
||||
handleChange: (selected: OnChangeValue<SelectOption, boolean>) => void;
|
||||
}
|
||||
|
||||
// Function to extract text from MessageDescriptor or return string as-is
|
||||
export const getOptionLabel = (option: SelectOption, intl: ReturnType<typeof useIntl>): string => {
|
||||
return formatAsString(intl.formatMessage, option.label) || '';
|
||||
};
|
||||
|
||||
function ReactSelectItemCreator({
|
||||
title,
|
||||
description,
|
||||
@@ -37,6 +45,7 @@ function ReactSelectItemCreator({
|
||||
inputFieldValue,
|
||||
handleChange,
|
||||
}: Props): JSX.Element {
|
||||
const intl = useIntl();
|
||||
const content = (
|
||||
<fieldset className='mm-modal-generic-section-item__fieldset-react-select'>
|
||||
<ReactSelect
|
||||
@@ -52,6 +61,8 @@ function ReactSelectItemCreator({
|
||||
onChange={handleChange}
|
||||
value={inputFieldValue}
|
||||
components={{IndicatorSeparator: NoIndicatorSeparatorComponent}}
|
||||
getOptionLabel={(option) => getOptionLabel(option, intl)}
|
||||
|
||||
/>
|
||||
</fieldset>
|
||||
);
|
||||
|
||||
156
webapp/channels/src/utils/notification_sounds.test.tsx
Обычный файл
156
webapp/channels/src/utils/notification_sounds.test.tsx
Обычный файл
@@ -0,0 +1,156 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {SelectOption} from 'components/widgets/modals/components/react_select_item';
|
||||
|
||||
import {
|
||||
optionsOfMessageNotificationSoundsSelect,
|
||||
optionsOfIncomingCallSoundsSelect,
|
||||
getValueOfNotificationSoundsSelect,
|
||||
getValueOfIncomingCallSoundsSelect,
|
||||
DesktopNotificationSounds,
|
||||
} from './notification_sounds';
|
||||
|
||||
describe('notification sounds accessibility', () => {
|
||||
describe('optionsOfMessageNotificationSoundsSelect', () => {
|
||||
test('should have proper structure for all sound options', () => {
|
||||
expect(optionsOfMessageNotificationSoundsSelect).toHaveLength(6);
|
||||
|
||||
optionsOfMessageNotificationSoundsSelect.forEach((option: SelectOption) => {
|
||||
expect(option).toHaveProperty('value');
|
||||
expect(option).toHaveProperty('label');
|
||||
expect(typeof option.value).toBe('string');
|
||||
|
||||
// Label should be MessageDescriptor or string, not React element
|
||||
if (typeof option.label === 'object') {
|
||||
expect(option.label).toHaveProperty('id');
|
||||
expect(option.label).toHaveProperty('defaultMessage');
|
||||
expect(typeof option.label.id).toBe('string');
|
||||
expect(typeof option.label.defaultMessage).toBe('string');
|
||||
} else {
|
||||
expect(typeof option.label).toBe('string');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('should have correct MessageDescriptor for each sound type', () => {
|
||||
const bingOption = optionsOfMessageNotificationSoundsSelect.find((o) => o.value === DesktopNotificationSounds.BING);
|
||||
expect(bingOption).toBeDefined();
|
||||
if (bingOption && typeof bingOption.label === 'object') {
|
||||
expect(bingOption.label.id).toBe('user.settings.notifications.desktopNotificationSound.soundBing');
|
||||
expect(bingOption.label.defaultMessage).toBe('Bing');
|
||||
}
|
||||
|
||||
const crackleOption = optionsOfMessageNotificationSoundsSelect.find((o) => o.value === DesktopNotificationSounds.CRACKLE);
|
||||
expect(crackleOption).toBeDefined();
|
||||
if (crackleOption && typeof crackleOption.label === 'object') {
|
||||
expect(crackleOption.label.id).toBe('user.settings.notifications.desktopNotificationSound.soundCrackle');
|
||||
expect(crackleOption.label.defaultMessage).toBe('Crackle');
|
||||
}
|
||||
});
|
||||
|
||||
test('should not contain React elements that would cause [object,object]', () => {
|
||||
optionsOfMessageNotificationSoundsSelect.forEach((option: SelectOption) => {
|
||||
// Ensure label is not a React element
|
||||
expect(option.label).not.toHaveProperty('$$typeof');
|
||||
expect(option.label).not.toHaveProperty('type');
|
||||
expect(option.label).not.toHaveProperty('props');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('optionsOfIncomingCallSoundsSelect', () => {
|
||||
test('should have proper structure for all call sound options', () => {
|
||||
expect(optionsOfIncomingCallSoundsSelect).toHaveLength(4);
|
||||
|
||||
optionsOfIncomingCallSoundsSelect.forEach((option: SelectOption) => {
|
||||
expect(option).toHaveProperty('value');
|
||||
expect(option).toHaveProperty('label');
|
||||
expect(typeof option.value).toBe('string');
|
||||
|
||||
// Label should be MessageDescriptor or string, not React element
|
||||
if (typeof option.label === 'object') {
|
||||
expect(option.label).toHaveProperty('id');
|
||||
expect(option.label).toHaveProperty('defaultMessage');
|
||||
expect(typeof option.label.id).toBe('string');
|
||||
expect(typeof option.label.defaultMessage).toBe('string');
|
||||
} else {
|
||||
expect(typeof option.label).toBe('string');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('should have correct MessageDescriptor for each call sound type', () => {
|
||||
const dynamicOption = optionsOfIncomingCallSoundsSelect.find((o) => o.value === 'Dynamic');
|
||||
expect(dynamicOption).toBeDefined();
|
||||
if (dynamicOption && typeof dynamicOption.label === 'object') {
|
||||
expect(dynamicOption.label.id).toBe('user.settings.notifications.desktopNotificationSound.soundDynamic');
|
||||
expect(dynamicOption.label.defaultMessage).toBe('Dynamic');
|
||||
}
|
||||
|
||||
const calmOption = optionsOfIncomingCallSoundsSelect.find((o) => o.value === 'Calm');
|
||||
expect(calmOption).toBeDefined();
|
||||
if (calmOption && typeof calmOption.label === 'object') {
|
||||
expect(calmOption.label.id).toBe('user.settings.notifications.desktopNotificationSound.soundCalm');
|
||||
expect(calmOption.label.defaultMessage).toBe('Calm');
|
||||
}
|
||||
});
|
||||
|
||||
test('should not contain React elements that would cause [object,object]', () => {
|
||||
optionsOfIncomingCallSoundsSelect.forEach((option: SelectOption) => {
|
||||
// Ensure label is not a React element
|
||||
expect(option.label).not.toHaveProperty('$$typeof');
|
||||
expect(option.label).not.toHaveProperty('type');
|
||||
expect(option.label).not.toHaveProperty('props');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getValueOfNotificationSoundsSelect', () => {
|
||||
test('should return valid option for known sound names', () => {
|
||||
const result = getValueOfNotificationSoundsSelect(DesktopNotificationSounds.BING);
|
||||
expect(result).toBeDefined();
|
||||
expect(result?.value).toBe(DesktopNotificationSounds.BING);
|
||||
|
||||
// Ensure the returned option has proper label structure
|
||||
if (result && typeof result.label === 'object') {
|
||||
expect(result.label).toHaveProperty('id');
|
||||
expect(result.label).toHaveProperty('defaultMessage');
|
||||
}
|
||||
});
|
||||
|
||||
test('should return undefined for unknown sound names', () => {
|
||||
const result = getValueOfNotificationSoundsSelect('UnknownSound');
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
test('should return undefined for undefined input', () => {
|
||||
const result = getValueOfNotificationSoundsSelect(undefined);
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getValueOfIncomingCallSoundsSelect', () => {
|
||||
test('should return valid option for known call sound names', () => {
|
||||
const result = getValueOfIncomingCallSoundsSelect('Dynamic');
|
||||
expect(result).toBeDefined();
|
||||
expect(result?.value).toBe('Dynamic');
|
||||
|
||||
// Ensure the returned option has proper label structure
|
||||
if (result && typeof result.label === 'object') {
|
||||
expect(result.label).toHaveProperty('id');
|
||||
expect(result.label).toHaveProperty('defaultMessage');
|
||||
}
|
||||
});
|
||||
|
||||
test('should return undefined for unknown call sound names', () => {
|
||||
const result = getValueOfIncomingCallSoundsSelect('UnknownCallSound');
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
test('should return undefined for undefined input', () => {
|
||||
const result = getValueOfIncomingCallSoundsSelect(undefined);
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,13 +1,13 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {ReactNode} from 'react';
|
||||
import React from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import {defineMessage} from 'react-intl';
|
||||
|
||||
import type {ChannelNotifyProps} from '@mattermost/types/channels';
|
||||
import type {UserNotifyProps} from '@mattermost/types/users';
|
||||
|
||||
import type {SelectOption} from 'components/widgets/modals/components/react_select_item';
|
||||
|
||||
import bing from 'sounds/bing.mp3';
|
||||
import calls_calm from 'sounds/calls_calm.mp3';
|
||||
import calls_cheerful from 'sounds/calls_cheerful.mp3';
|
||||
@@ -42,66 +42,54 @@ export const notificationSounds = new Map<string, string>([
|
||||
|
||||
export const notificationSoundKeys = Array.from(notificationSounds.keys());
|
||||
|
||||
export const optionsOfMessageNotificationSoundsSelect: Array<{value: string; label: ReactNode}> = notificationSoundKeys.map((soundName) => {
|
||||
export const optionsOfMessageNotificationSoundsSelect: SelectOption[] = notificationSoundKeys.map((soundName) => {
|
||||
if (soundName === DesktopNotificationSounds.BING) {
|
||||
return {
|
||||
value: soundName,
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='user.settings.notifications.desktopNotificationSound.soundBing'
|
||||
defaultMessage='Bing'
|
||||
/>
|
||||
),
|
||||
label: defineMessage({
|
||||
id: 'user.settings.notifications.desktopNotificationSound.soundBing',
|
||||
defaultMessage: 'Bing',
|
||||
}),
|
||||
};
|
||||
} else if (soundName === DesktopNotificationSounds.CRACKLE) {
|
||||
return {
|
||||
value: soundName,
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='user.settings.notifications.desktopNotificationSound.soundCrackle'
|
||||
defaultMessage='Crackle'
|
||||
/>
|
||||
),
|
||||
label: defineMessage({
|
||||
id: 'user.settings.notifications.desktopNotificationSound.soundCrackle',
|
||||
defaultMessage: 'Crackle',
|
||||
}),
|
||||
};
|
||||
} else if (soundName === DesktopNotificationSounds.DOWN) {
|
||||
return {
|
||||
value: soundName,
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='user.settings.notifications.desktopNotificationSound.soundDown'
|
||||
defaultMessage='Down'
|
||||
/>
|
||||
),
|
||||
label: defineMessage({
|
||||
id: 'user.settings.notifications.desktopNotificationSound.soundDown',
|
||||
defaultMessage: 'Down',
|
||||
}),
|
||||
};
|
||||
} else if (soundName === DesktopNotificationSounds.HELLO) {
|
||||
return {
|
||||
value: soundName,
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='user.settings.notifications.desktopNotificationSound.soundHello'
|
||||
defaultMessage='Hello'
|
||||
/>
|
||||
),
|
||||
label: defineMessage({
|
||||
id: 'user.settings.notifications.desktopNotificationSound.soundHello',
|
||||
defaultMessage: 'Hello',
|
||||
}),
|
||||
};
|
||||
} else if (soundName === DesktopNotificationSounds.RIPPLE) {
|
||||
return {
|
||||
value: soundName,
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='user.settings.notifications.desktopNotificationSound.soundRipple'
|
||||
defaultMessage='Ripple'
|
||||
/>
|
||||
),
|
||||
label: defineMessage({
|
||||
id: 'user.settings.notifications.desktopNotificationSound.soundRipple',
|
||||
defaultMessage: 'Ripple',
|
||||
}),
|
||||
};
|
||||
} else if (soundName === DesktopNotificationSounds.UPSTAIRS) {
|
||||
return {
|
||||
value: soundName,
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='user.settings.notifications.desktopNotificationSound.soundUpstairs'
|
||||
defaultMessage='Upstairs'
|
||||
/>
|
||||
),
|
||||
label: defineMessage({
|
||||
id: 'user.settings.notifications.desktopNotificationSound.soundUpstairs',
|
||||
defaultMessage: 'Upstairs',
|
||||
}),
|
||||
};
|
||||
}
|
||||
return {
|
||||
@@ -129,46 +117,38 @@ export const callsNotificationSounds = new Map([
|
||||
|
||||
export const callNotificationSoundKeys = Array.from(callsNotificationSounds.keys());
|
||||
|
||||
export const optionsOfIncomingCallSoundsSelect: Array<{value: string; label: ReactNode}> = callNotificationSoundKeys.map((soundName) => {
|
||||
export const optionsOfIncomingCallSoundsSelect: SelectOption[] = callNotificationSoundKeys.map((soundName) => {
|
||||
if (soundName === 'Dynamic') {
|
||||
return {
|
||||
value: soundName,
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='user.settings.notifications.desktopNotificationSound.soundDynamic'
|
||||
defaultMessage='Dynamic'
|
||||
/>
|
||||
),
|
||||
label: defineMessage({
|
||||
id: 'user.settings.notifications.desktopNotificationSound.soundDynamic',
|
||||
defaultMessage: 'Dynamic',
|
||||
}),
|
||||
};
|
||||
} else if (soundName === 'Calm') {
|
||||
return {
|
||||
value: soundName,
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='user.settings.notifications.desktopNotificationSound.soundCalm'
|
||||
defaultMessage='Calm'
|
||||
/>
|
||||
),
|
||||
label: defineMessage({
|
||||
id: 'user.settings.notifications.desktopNotificationSound.soundCalm',
|
||||
defaultMessage: 'Calm',
|
||||
}),
|
||||
};
|
||||
} else if (soundName === 'Urgent') {
|
||||
return {
|
||||
value: soundName,
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='user.settings.notifications.desktopNotificationSound.soundUrgent'
|
||||
defaultMessage='Urgent'
|
||||
/>
|
||||
),
|
||||
label: defineMessage({
|
||||
id: 'user.settings.notifications.desktopNotificationSound.soundUrgent',
|
||||
defaultMessage: 'Urgent',
|
||||
}),
|
||||
};
|
||||
} else if (soundName === 'Cheerful') {
|
||||
return {
|
||||
value: soundName,
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='user.settings.notifications.desktopNotificationSound.soundCheerful'
|
||||
defaultMessage='Cheerful'
|
||||
/>
|
||||
),
|
||||
label: defineMessage({
|
||||
id: 'user.settings.notifications.desktopNotificationSound.soundCheerful',
|
||||
defaultMessage: 'Cheerful',
|
||||
}),
|
||||
};
|
||||
}
|
||||
return {
|
||||
|
||||
Ссылка в новой задаче
Block a user