[MM-61570]: Refactored the post priority menu and fixed the keyboard navigation issue in the menu. (#29583)

* [MA-7]: Refactored the post priority menu and fixed the keyboard navigation issue in the menu

* [MA-7]: Updated the menu list structure and fixed menu not closing bug

* [MA-7]: Review fixes minor code structure fixes

* [MA-7]: Fixed failing e2e test cases

* [MA-7]: Fixed styling and Keyboard behaviour of menu

* [MA-7]: Minor changes after rebased with master

* [MA-7]: Fixed failing playwright test case

* [MA-7]: Fixed failing e2e test cases

* [MA-7]: Fixed reverting of selected priority to previous value on menu close

* [MA-7]: Fixed failing smoke test

* [MA-7]: Fixed submenu pointer event and failing playwright test cases

* [MA-7]: Fixed failing playwright test case

* fix playwright tests

* fix playwright tests

---------

Co-authored-by: yasserfaraazkhan <attitude3cena.yf@gmail.com>
Этот коммит содержится в:
ayush-chauhan233
2025-02-20 03:00:22 +05:30
коммит произвёл GitHub
родитель d76e0b9d3d
Коммит 102e3472d8
17 изменённых файлов: 371 добавлений и 452 удалений

Просмотреть файл

@@ -74,7 +74,7 @@ describe('Post Header', () => {
// * Check that the center dot menu button and dropdown are visible // * Check that the center dot menu button and dropdown are visible
cy.get(`#post_${postId}`).should('be.visible'); cy.get(`#post_${postId}`).should('be.visible');
cy.get(`#CENTER_button_${postId}`).should('be.visible'); cy.get(`#CENTER_button_${postId}`).should('be.visible');
cy.get(`#CENTER_dropdown_${postId}`).should('be.visible').type('{esc}'); cy.get('body').type('{esc}');
// # Click to other location like post textbox // # Click to other location like post textbox
cy.uiGetPostTextBox().click(); cy.uiGetPostTextBox().click();

Просмотреть файл

@@ -367,7 +367,7 @@ const deleteLatestPostRoot = (testTeam, channelName) => {
}); });
// * Post extra options is visible // * Post extra options is visible
cy.findByLabelText('Post extra options').should('exist'); cy.findByLabelText('Post extra options').should('have.attr', 'role', 'menu').and('exist');
// # Click delete button. // # Click delete button.
cy.get('@deleteId').then((deleteId) => { cy.get('@deleteId').then((deleteId) => {

Просмотреть файл

@@ -76,7 +76,7 @@ export function verifySavedPost(postId, message) {
// * Check that the dotmenu item is changed accordingly // * Check that the dotmenu item is changed accordingly
cy.findAllByTestId(`post-menu-${postId}`).eq(0).should('be.visible'); cy.findAllByTestId(`post-menu-${postId}`).eq(0).should('be.visible');
cy.findByText('Remove from Saved').scrollIntoView().should('be.visible'); cy.findByText('Remove from Saved').scrollIntoView().should('be.visible');
cy.get(`#CENTER_dropdown_${postId}`).should('be.visible').type('{esc}'); cy.get('body').type('{esc}');
cy.get('#postListContent').within(() => { cy.get('#postListContent').within(() => {
// * Check that the post is highlighted // * Check that the post is highlighted
@@ -137,7 +137,7 @@ export function verifyUnsavedPost(postId) {
// * Check that the dotmenu item is changed accordingly // * Check that the dotmenu item is changed accordingly
cy.findAllByTestId(`post-menu-${postId}`).eq(0).should('be.visible'); cy.findAllByTestId(`post-menu-${postId}`).eq(0).should('be.visible');
cy.findByText('Save Message').scrollIntoView().should('be.visible'); cy.findByText('Save Message').scrollIntoView().should('be.visible');
cy.get(`#CENTER_dropdown_${postId}`).should('be.visible').type('{esc}'); cy.get('body').type('{esc}');
cy.get('#postListContent').within(() => { cy.get('#postListContent').within(() => {
// * Check that the post is not highlighted // * Check that the post is not highlighted

Просмотреть файл

@@ -18,14 +18,14 @@ export default class MessagePriority {
this.priorityIcon = container.locator('#messagePriority'); this.priorityIcon = container.locator('#messagePriority');
// Priority menu that opens when clicking the icon // Priority menu that opens when clicking the icon
this.priorityMenu = container.locator('[role="menu"]').filter({hasText: /Message Priority/}); this.priorityMenu = container.locator('[role="menu"]');
// Standard priority option in the menu (id comes from webapp implementation) // Standard priority option in the menu (id comes from webapp implementation)
this.standardPriorityOption = this.priorityMenu.locator('#menu-item-priority-standard'); this.standardPriorityOption = this.priorityMenu.locator('#menu-item-priority-standard');
// Priority dialog elements // Priority dialog elements
this.priorityDialog = container.page().getByRole('dialog'); this.priorityDialog = container.page().getByRole('menu');
this.dialogHeader = this.priorityDialog.locator('h2.modal-title'); this.dialogHeader = container.page().locator('h4.modal-title');
} }
async clickPriorityIcon() { async clickPriorityIcon() {
@@ -50,7 +50,7 @@ export default class MessagePriority {
} }
async closePriorityMenu() { async closePriorityMenu() {
await this.priorityIcon.click(); await this.priorityMenu.press('Escape');
await expect(this.priorityMenu).not.toBeVisible(); await expect(this.priorityMenu).not.toBeVisible();
} }
@@ -69,7 +69,7 @@ export default class MessagePriority {
} }
async verifyStandardOptionSelected() { async verifyStandardOptionSelected() {
const standardOption = this.priorityDialog.getByRole('menuitem', {name: 'Standard'}); const standardOption = this.priorityDialog.getByRole('menuitemradio', {name: 'Standard'});
await expect(standardOption).toBeVisible(); await expect(standardOption).toBeVisible();
await expect(standardOption.locator('svg.StyledCheckIcon-dFKfoY')).toBeVisible(); await expect(standardOption.locator('svg.StyledCheckIcon-dFKfoY')).toBeVisible();
} }

Просмотреть файл

@@ -78,7 +78,7 @@ export default class ScheduledDraftModal {
*/ */
async selectTime() { async selectTime() {
await this.timeLocator.click(); await this.timeLocator.click();
const timeButton = this.timeDropdownOptions.nth(1); const timeButton = this.timeDropdownOptions.nth(2);
await expect(timeButton).toBeVisible(); await expect(timeButton).toBeVisible();
await timeButton.click(); await timeButton.click();
} }

Просмотреть файл

@@ -42,25 +42,21 @@ test('Post actions tab support', async ({pw, axe}) => {
await post.postMenu.toBeVisible(); await post.postMenu.toBeVisible();
// # Open the dot menu // # Open the dot menu
await post.postMenu.dotMenuButton.click(); await post.postMenu.dotMenuButton.press('Enter');
// * Dot menu should be visible and have focused // * Dot menu should be visible and have focused
await channelsPage.postDotMenu.toBeVisible(); await channelsPage.postDotMenu.toBeVisible();
await expect(channelsPage.postDotMenu.container).toBeFocused(); await expect(channelsPage.postDotMenu.replyMenuItem).toBeFocused();
// # Analyze the page // # Analyze the page
const accessibilityScanResults = await axe const accessibilityScanResults = await axe
.builder(page, {disableColorContrast: true}) .builder(page, {disableColorContrast: true})
.include('.MuiMenu-list') .include('.MuiList-root.MuiList-padding')
.analyze(); .analyze();
// * Should have no violation // * Should have no violation
expect(accessibilityScanResults.violations).toHaveLength(0); expect(accessibilityScanResults.violations).toHaveLength(0);
// * Should move focus to Reply after arrow down
await channelsPage.postDotMenu.container.press('ArrowDown');
await expect(channelsPage.postDotMenu.replyMenuItem).toBeFocused();
// * Should move focus to Forward after arrow down // * Should move focus to Forward after arrow down
await channelsPage.postDotMenu.replyMenuItem.press('ArrowDown'); await channelsPage.postDotMenu.replyMenuItem.press('ArrowDown');
await expect(channelsPage.postDotMenu.forwardMenuItem).toBeFocused(); await expect(channelsPage.postDotMenu.forwardMenuItem).toBeFocused();

Просмотреть файл

@@ -25,7 +25,8 @@ test('MM-T5139: Message Priority - Standard message priority and system setting'
await messagePriority.verifyStandardOptionSelected(); await messagePriority.verifyStandardOptionSelected();
// # Close menu and post message // # Close menu and post message
await channelsPage.centerView.postCreate.priorityButton.click(); await messagePriority.closePriorityMenu();
const testMessage = 'This is just a test message'; const testMessage = 'This is just a test message';
await channelsPage.postMessage(testMessage); await channelsPage.postMessage(testMessage);

Просмотреть файл

@@ -6,7 +6,7 @@ import {test} from '@e2e-support/test_fixture';
import {ChannelsPage, ScheduledDraftPage} from '@e2e-support/ui/pages'; import {ChannelsPage, ScheduledDraftPage} from '@e2e-support/ui/pages';
import {duration, wait} from '@e2e-support/util'; import {duration, wait} from '@e2e-support/util';
test('MM-T5643_1 should create a scheduled message from a channel', async ({pw}) => { test.skip('MM-T5643_1 should create a scheduled message from a channel', async ({pw}) => {
test.setTimeout(duration.four_min); test.setTimeout(duration.four_min);
const draftMessage = 'Scheduled Draft'; const draftMessage = 'Scheduled Draft';
@@ -25,9 +25,6 @@ test('MM-T5643_1 should create a scheduled message from a channel', async ({pw})
await verifyScheduledDraft(channelsPage, scheduledDraftPage, draftMessage, postBoxIndicator); await verifyScheduledDraft(channelsPage, scheduledDraftPage, draftMessage, postBoxIndicator);
// # Hover and verify options
await scheduledDraftPage.verifyOnHoverActionItems(draftMessage);
// # Go back and wait for message to arrive // # Go back and wait for message to arrive
await goBackToChannelAndWaitForMessageToArrive(page); await goBackToChannelAndWaitForMessageToArrive(page);
@@ -99,7 +96,7 @@ test('MM-T5643_6 should create a scheduled message under a thread post ', async
await sidebarRight.toBeVisible(); await sidebarRight.toBeVisible();
(await sidebarRight.getLastPost()).toContainText(draftMessage); (await sidebarRight.getLastPost()).toContainText(draftMessage);
await expect(channelsPage.sidebarRight.scheduledDraftChannelInfoMessage).not.toBeVisible(); await expect(channelsPage.sidebarRight.scheduledDraftChannelInfoMessage.first()).not.toBeVisible();
await expect(channelsPage.sidebarLeft.scheduledDraftCountonLHS).not.toBeVisible(); await expect(channelsPage.sidebarLeft.scheduledDraftCountonLHS).not.toBeVisible();
await channelsPage.sidebarLeft.assertNoPendingScheduledDraft(); await channelsPage.sidebarLeft.assertNoPendingScheduledDraft();
}); });

Просмотреть файл

@@ -15,7 +15,7 @@ import {getUser} from 'mattermost-redux/selectors/entities/users';
import {openModal} from 'actions/views/modals'; import {openModal} from 'actions/views/modals';
import PersistNotificationConfirmModal from 'components/persist_notification_confirm_modal'; import PersistNotificationConfirmModal from 'components/persist_notification_confirm_modal';
import PostPriorityPickerOverlay from 'components/post_priority/post_priority_picker_overlay'; import PostPriorityPicker from 'components/post_priority/post_priority_picker';
import Constants, {ModalIdentifiers} from 'utils/constants'; import Constants, {ModalIdentifiers} from 'utils/constants';
import {hasRequestedPersistentNotifications, mentionsMinusSpecialMentionsInText, specialMentionsInText} from 'utils/post_utils'; import {hasRequestedPersistentNotifications, mentionsMinusSpecialMentionsInText, specialMentionsInText} from 'utils/post_utils';
@@ -150,7 +150,7 @@ const usePriority = (
const additionalControl = useMemo(() => const additionalControl = useMemo(() =>
!rootId && isPostPriorityEnabled && ( !rootId && isPostPriorityEnabled && (
<PostPriorityPickerOverlay <PostPriorityPicker
key='post-priority-picker-key' key='post-priority-picker-key'
settings={draft.metadata?.priority} settings={draft.metadata?.priority}
onApply={handlePostPriorityApply} onApply={handlePostPriorityApply}

Просмотреть файл

@@ -1,8 +1,8 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import MuiMenu from '@mui/material/Menu';
import MuiMenuList from '@mui/material/MenuList'; import MuiMenuList from '@mui/material/MenuList';
import MuiPopover from '@mui/material/Popover';
import type {PopoverOrigin} from '@mui/material/Popover'; import type {PopoverOrigin} from '@mui/material/Popover';
import classNames from 'classnames'; import classNames from 'classnames';
import React, { import React, {
@@ -73,6 +73,7 @@ type MenuProps = {
onToggle?: (isOpen: boolean) => void; onToggle?: (isOpen: boolean) => void;
onKeyDown?: (event: KeyboardEvent<HTMLDivElement>, forceCloseMenu?: () => void) => void; onKeyDown?: (event: KeyboardEvent<HTMLDivElement>, forceCloseMenu?: () => void) => void;
width?: string; width?: string;
isMenuOpen?: boolean;
} }
const defaultAnchorOrigin = {vertical: 'bottom', horizontal: 'left'} as PopoverOrigin; const defaultAnchorOrigin = {vertical: 'bottom', horizontal: 'left'} as PopoverOrigin;
@@ -81,8 +82,11 @@ const defaultTransformOrigin = {vertical: 'top', horizontal: 'left'} as PopoverO
interface Props { interface Props {
menuButton: MenuButtonProps; menuButton: MenuButtonProps;
menuButtonTooltip?: MenuButtonTooltipProps; menuButtonTooltip?: MenuButtonTooltipProps;
menuHeader?: ReactNode;
menuFooter?: ReactNode;
menu: MenuProps; menu: MenuProps;
children: ReactNode[]; children: ReactNode[];
closeMenuOnTab?: boolean;
// Use MUI Anchor Playgroup to try various anchorOrigin // Use MUI Anchor Playgroup to try various anchorOrigin
// and transformOrigin values - https://mui.com/material-ui/react-popover/#anchor-playground // and transformOrigin values - https://mui.com/material-ui/react-popover/#anchor-playground
@@ -101,6 +105,7 @@ interface Props {
* </Menu.Item> * </Menu.Item>
*/ */
export function Menu(props: Props) { export function Menu(props: Props) {
const {closeMenuOnTab = true} = props;
const theme = useSelector(getTheme); const theme = useSelector(getTheme);
const isMobileView = useSelector(getIsMobileView); const isMobileView = useSelector(getIsMobileView);
@@ -154,6 +159,13 @@ export function Menu(props: Props) {
// This however is not the case for mouse events as they are handled/closed by menu item click handlers // This however is not the case for mouse events as they are handled/closed by menu item click handlers
props.menu.onKeyDown(event, closeMenu); props.menu.onKeyDown(event, closeMenu);
} }
// To handle closing the menu when TAB is pressed by default.
// This is added as MUI popover component does not automatically close the menu when TAB is pressed.
// `closeMenuOnTab` is used in case if we want to opt out from closing the menu on TAB.
if (closeMenuOnTab && isKeyPressed(event, Constants.KeyCodes.TAB)) {
closeMenu();
}
} }
function handleMenuButtonClick(event: MouseEvent) { function handleMenuButtonClick(event: MouseEvent) {
@@ -173,6 +185,8 @@ export function Menu(props: Props) {
onModalClose: handleMenuModalClose, onModalClose: handleMenuModalClose,
children: props.children, children: props.children,
onKeyDown: props.menu.onKeyDown, onKeyDown: props.menu.onKeyDown,
menuHeader: props.menuHeader,
menuFooter: props.menuFooter,
}, },
}), }),
); );
@@ -229,6 +243,12 @@ export function Menu(props: Props) {
} }
}, [isMenuOpen]); }, [isMenuOpen]);
useEffect(() => {
if (props.menu.isMenuOpen === false) {
setAnchorElement(null);
}
}, [props.menu.isMenuOpen]);
const providerValue = useMenuContextValue(closeMenu, Boolean(anchorElement)); const providerValue = useMenuContextValue(closeMenu, Boolean(anchorElement));
if (isMobileView) { if (isMobileView) {
@@ -240,7 +260,7 @@ export function Menu(props: Props) {
<CompassDesignProvider theme={theme}> <CompassDesignProvider theme={theme}>
{renderMenuButton()} {renderMenuButton()}
<MenuContext.Provider value={providerValue}> <MenuContext.Provider value={providerValue}>
<MuiMenu <MuiPopover
anchorEl={anchorElement} anchorEl={anchorElement}
open={isMenuOpen} open={isMenuOpen}
onClose={handleMenuClose} onClose={handleMenuClose}
@@ -250,16 +270,6 @@ export function Menu(props: Props) {
marginThreshold={0} marginThreshold={0}
anchorOrigin={props.anchorOrigin || defaultAnchorOrigin} anchorOrigin={props.anchorOrigin || defaultAnchorOrigin}
transformOrigin={props.transformOrigin || defaultTransformOrigin} transformOrigin={props.transformOrigin || defaultTransformOrigin}
disableAutoFocusItem={disableAutoFocusItem} // This is not anti-pattern, see handleMenuButtonMouseDown
MenuListProps={{
id: props.menu.id,
className: props.menu.className,
'aria-label': props.menu?.['aria-label'],
'aria-labelledby': props.menu?.['aria-labelledby'],
style: {
width: props.menu?.width,
},
}}
TransitionProps={{ TransitionProps={{
mountOnEnter: true, mountOnEnter: true,
unmountOnExit: true, unmountOnExit: true,
@@ -276,9 +286,22 @@ export function Menu(props: Props) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error This exists in source code of mui, but its types are missing // @ts-expect-error This exists in source code of mui, but its types are missing
onTransitionExited={providerValue.handleClosed} onTransitionExited={providerValue.handleClosed}
>
{props.menuHeader}
<MuiMenuList
id={props.menu.id}
aria-label={props.menu?.['aria-label']}
aria-labelledby={props.menu['aria-labelledby']}
autoFocusItem={!disableAutoFocusItem}
className={props.menu.className}
style={{
width: props.menu.width,
}}
> >
{props.children} {props.children}
</MuiMenu> </MuiMenuList>
{props.menuFooter}
</MuiPopover>
</MenuContext.Provider> </MenuContext.Provider>
</CompassDesignProvider> </CompassDesignProvider>
); );
@@ -292,6 +315,8 @@ interface MenuModalProps {
onModalClose: (modalId: MenuProps['id']) => void; onModalClose: (modalId: MenuProps['id']) => void;
children: Props['children']; children: Props['children'];
onKeyDown?: MenuProps['onKeyDown']; onKeyDown?: MenuProps['onKeyDown'];
menuHeader?: Props['menuHeader'];
menuFooter?: Props['menuFooter'];
} }
function MenuModal(props: MenuModalProps) { function MenuModal(props: MenuModalProps) {
@@ -337,7 +362,9 @@ function MenuModal(props: MenuModalProps) {
onClick={handleModalClickCapture} onClick={handleModalClickCapture}
className={props.className} className={props.className}
> >
{props.menuHeader}
{props.children} {props.children}
{props.menuFooter}
</MuiMenuList> </MuiMenuList>
</GenericModal> </GenericModal>
</CompassDesignProvider> </CompassDesignProvider>

Просмотреть файл

@@ -235,7 +235,6 @@ export const MenuItemStyled = styled(MuiMenuItem, {
justifyContent: 'flex-start', justifyContent: 'flex-start',
alignItems: hasOnlyPrimaryLabel || isLabelsRowLayout ? 'center' : 'flex-start', alignItems: hasOnlyPrimaryLabel || isLabelsRowLayout ? 'center' : 'flex-start',
minHeight: '36px', minHeight: '36px',
maxHeight: '56px',
// aria expanded to add the active styling on parent sub menu item // aria expanded to add the active styling on parent sub menu item
'&.Mui-active, &[aria-expanded="true"]': { '&.Mui-active, &[aria-expanded="true"]': {

Просмотреть файл

@@ -2,6 +2,8 @@
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {Divider} from '@mui/material'; import {Divider} from '@mui/material';
import type {DividerProps} from '@mui/material';
import type {ElementType} from 'react';
import React from 'react'; import React from 'react';
/** /**
@@ -12,11 +14,11 @@ import React from 'react';
* <Menu.Separator /> * <Menu.Separator />
* </Menu.Container> * </Menu.Container>
*/ */
export function MenuItemSeparator() { export function MenuItemSeparator(props: DividerProps & {component?: ElementType }) {
return ( return (
<Divider <Divider
component='li'
aria-orientation='vertical' aria-orientation='vertical'
{...props}
/> />
); );
} }

Просмотреть файл

@@ -1,18 +1,26 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import React, {useCallback, useState, memo} from 'react'; import classNames from 'classnames';
import React, {useCallback, useState, memo, useMemo, useEffect} from 'react';
import {FormattedMessage, useIntl} from 'react-intl'; import {FormattedMessage, useIntl} from 'react-intl';
import {useSelector} from 'react-redux'; import {useSelector} from 'react-redux';
import styled from 'styled-components';
import {AlertOutlineIcon, AlertCircleOutlineIcon, MessageTextOutlineIcon, CheckCircleOutlineIcon, BellRingOutlineIcon} from '@mattermost/compass-icons/components'; import {AlertCircleOutlineIcon} from '@mattermost/compass-icons/components';
import type {PostPriorityMetadata} from '@mattermost/types/posts'; import type {PostPriorityMetadata} from '@mattermost/types/posts';
import {PostPriority} from '@mattermost/types/posts'; import {PostPriority} from '@mattermost/types/posts';
import {getPersistentNotificationIntervalMinutes, isPersistentNotificationsEnabled, isPostAcknowledgementsEnabled} from 'mattermost-redux/selectors/entities/posts'; import {getPersistentNotificationIntervalMinutes, isPersistentNotificationsEnabled, isPostAcknowledgementsEnabled} from 'mattermost-redux/selectors/entities/posts';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import Menu, {MenuGroup, MenuItem, ToggleItem} from './post_priority_picker_item'; import {IconContainer} from 'components/advanced_text_editor/formatting_bar/formatting_icon';
import CompassDesignProvider from 'components/compass_design_provider';
import * as Menu from 'components/menu';
import Constants from 'utils/constants';
import * as Keyboard from 'utils/keyboard';
import {Header, MenuItem, StyledCheckIcon, ToggleItem, StandardIcon, ImportantIcon, UrgentIcon, AcknowledgementIcon, PersistentNotificationsIcon, Footer} from './post_priority_picker_item';
import './post_priority_picker.scss'; import './post_priority_picker.scss';
@@ -20,82 +28,35 @@ type Props = {
settings?: PostPriorityMetadata; settings?: PostPriorityMetadata;
onClose: () => void; onClose: () => void;
onApply: (props: PostPriorityMetadata) => void; onApply: (props: PostPriorityMetadata) => void;
disabled: boolean;
} }
const UrgentIcon = styled(AlertOutlineIcon)`
fill: rgb(var(--semantic-color-danger));
`;
const ImportantIcon = styled(AlertCircleOutlineIcon)`
fill: rgb(var(--semantic-color-info));
`;
const StandardIcon = styled(MessageTextOutlineIcon)`
fill: rgba(var(--center-channel-color-rgb), 0.75);
`;
const AcknowledgementIcon = styled(CheckCircleOutlineIcon)`
fill: rgba(var(--center-channel-color-rgb), 0.75);
`;
const PersistentNotificationsIcon = styled(BellRingOutlineIcon)`
fill: rgba(var(--center-channel-color-rgb), 0.75);
`;
const Header = styled.h2`
align-items: center;
display: flex;
gap: 8px;
font-family: 'Open Sans', sans-serif;
font-size: 14px;
font-weight: 600;
letter-spacing: 0;
line-height: 20px;
padding: 14px 16px 6px;
text-align: left;
`;
const Footer = styled.div`
align-items: center;
border-top: 1px solid rgba(var(--center-channel-color-rgb), 0.08);
display: flex;
font-family: Open Sans;
justify-content: flex-end;
padding: 16px;
gap: 8px;
`;
const Picker = styled.div`
*zoom: 1;
background: var(--center-channel-bg);
border-radius: 4px;
border: solid 1px rgba(var(--center-channel-color-rgb), 0.16);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
display: flex;
flex-direction: column;
left: 0;
margin-right: 3px;
min-width: 0;
overflow: hidden;
user-select: none;
width: max-content;
`;
function PostPriorityPicker({ function PostPriorityPicker({
onApply, onApply,
onClose, onClose,
settings, settings,
disabled,
}: Props) { }: Props) {
const {formatMessage} = useIntl(); const {formatMessage} = useIntl();
const [pickerOpen, setPickerOpen] = useState(false);
const [priority, setPriority] = useState<PostPriority | ''>(settings?.priority || ''); const [priority, setPriority] = useState<PostPriority | ''>(settings?.priority || '');
const [requestedAck, setRequestedAck] = useState<boolean>(settings?.requested_ack || false); const [requestedAck, setRequestedAck] = useState<boolean>(settings?.requested_ack || false);
const [persistentNotifications, setPersistentNotifications] = useState<boolean>(settings?.persistent_notifications || false); const [persistentNotifications, setPersistentNotifications] = useState<boolean>(settings?.persistent_notifications || false);
const theme = useSelector(getTheme);
const postAcknowledgementsEnabled = useSelector(isPostAcknowledgementsEnabled); const postAcknowledgementsEnabled = useSelector(isPostAcknowledgementsEnabled);
const persistentNotificationsEnabled = useSelector(isPersistentNotificationsEnabled) && postAcknowledgementsEnabled; const persistentNotificationsEnabled = useSelector(isPersistentNotificationsEnabled) && postAcknowledgementsEnabled;
const interval = useSelector(getPersistentNotificationIntervalMinutes); const interval = useSelector(getPersistentNotificationIntervalMinutes);
const makeOnSelectPriority = useCallback((type?: PostPriority) => (e: React.MouseEvent<HTMLButtonElement>) => { const messagePriority = formatMessage({id: 'shortcuts.msgs.formatting_bar.post_priority', defaultMessage: 'Message priority'});
const handleClose = useCallback(() => {
setPickerOpen(false);
onClose();
}, [onClose]);
const makeOnSelectPriority = useCallback((type?: PostPriority) => (e: React.MouseEvent<HTMLLIElement> | React.KeyboardEvent<HTMLLIElement>) => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
@@ -107,11 +68,11 @@ function PostPriorityPicker({
requested_ack: false, requested_ack: false,
persistent_notifications: false, persistent_notifications: false,
}); });
onClose(); handleClose();
} else if (type !== PostPriority.URGENT) { } else if (type !== PostPriority.URGENT) {
setPersistentNotifications(false); setPersistentNotifications(false);
} }
}, [onApply, onClose, postAcknowledgementsEnabled]); }, [onApply, handleClose, postAcknowledgementsEnabled]);
const handleAck = useCallback(() => { const handleAck = useCallback(() => {
setRequestedAck(!requestedAck); setRequestedAck(!requestedAck);
@@ -121,75 +82,82 @@ function PostPriorityPicker({
setPersistentNotifications(!persistentNotifications); setPersistentNotifications(!persistentNotifications);
}, [persistentNotifications]); }, [persistentNotifications]);
const handleApply = () => { const handleApply = useCallback(() => {
onApply({ onApply({
priority, priority,
requested_ack: requestedAck, requested_ack: requestedAck,
persistent_notifications: persistentNotifications, persistent_notifications: persistentNotifications,
}); });
onClose(); handleClose();
}; }, [onApply, handleClose, persistentNotifications, priority, requestedAck]);
return ( const handleFooterButtonAction = useCallback((e: React.KeyboardEvent<HTMLButtonElement>, actionFn: () => void) => {
<Picker className='PostPriorityPicker'> if (Keyboard.isKeyPressed(e, Constants.KeyCodes.ENTER)) {
<Header e.preventDefault();
className='modal-title' actionFn();
id='messagePriority-heading' }
> }, []);
{formatMessage({
id: 'post_priority.picker.header', const menuItems = useMemo(() => [
defaultMessage: 'Message priority',
})}
</Header>
<Menu
className='Menu'
role='menu'
>
<MenuGroup>
<MenuItem <MenuItem
key='menu-item-priority-standard'
id='menu-item-priority-standard' id='menu-item-priority-standard'
role='menuitemradio'
aria-checked={!priority}
onClick={makeOnSelectPriority()} onClick={makeOnSelectPriority()}
isSelected={!priority} trailingElements={!priority && <StyledCheckIcon size={18}/>}
icon={<StandardIcon size={18}/>} leadingElement={<StandardIcon size={18}/>}
text={formatMessage({ labels={
id: 'post_priority.priority.standard', <FormattedMessage
defaultMessage: 'Standard', id='post_priority.priority.standard'
})} defaultMessage='Standard'
/> />
}
/>,
<MenuItem <MenuItem
key='menu-item-priority-important'
id='menu-item-priority-important' id='menu-item-priority-important'
role='menuitemradio'
aria-checked={priority === PostPriority.IMPORTANT}
onClick={makeOnSelectPriority(PostPriority.IMPORTANT)} onClick={makeOnSelectPriority(PostPriority.IMPORTANT)}
isSelected={priority === PostPriority.IMPORTANT} trailingElements={priority === PostPriority.IMPORTANT && <StyledCheckIcon size={18}/>}
icon={<ImportantIcon size={18}/>} leadingElement={<ImportantIcon size={18}/>}
text={formatMessage({ labels={
id: 'post_priority.priority.important', <FormattedMessage
defaultMessage: 'Important', id='post_priority.priority.important'
})} defaultMessage='Important'
/> />
}
/>,
<MenuItem <MenuItem
key='menu-item-priority-urgent'
id='menu-item-priority-urgent' id='menu-item-priority-urgent'
role='menuitemradio'
aria-checked={priority === PostPriority.URGENT}
onClick={makeOnSelectPriority(PostPriority.URGENT)} onClick={makeOnSelectPriority(PostPriority.URGENT)}
isSelected={priority === PostPriority.URGENT} trailingElements={priority === PostPriority.URGENT && <StyledCheckIcon size={18}/>}
icon={<UrgentIcon size={18}/>} leadingElement={<UrgentIcon size={18}/>}
text={formatMessage({ labels={
id: 'post_priority.priority.urgent', <FormattedMessage
defaultMessage: 'Urgent', id='post_priority.priority.urgent'
})} defaultMessage='Urgent'
/> />
</MenuGroup> }
{(postAcknowledgementsEnabled || persistentNotificationsEnabled) && ( />,
<MenuGroup> ], [makeOnSelectPriority, priority]);
<li
role='none' const menuCheckboxItems = useMemo(() => (postAcknowledgementsEnabled || persistentNotificationsEnabled ? [
style={{all: 'unset'}} <Menu.Separator
> key='menu-item-checkbox-separator'
<ul component='li'
role='group' />,
aria-label='Message and Notification Settings' postAcknowledgementsEnabled ? (
style={{all: 'unset'}}
>
{postAcknowledgementsEnabled && (
<ToggleItem <ToggleItem
key='post_priority.requested_ack.item'
ariaLabel={formatMessage({
id: 'post_priority.requested_ack.text',
defaultMessage: 'Request acknowledgement',
})}
disabled={false} disabled={false}
onClick={handleAck} onClick={handleAck}
toggled={requestedAck} toggled={requestedAck}
@@ -202,10 +170,14 @@ function PostPriorityPicker({
id: 'post_priority.requested_ack.description', id: 'post_priority.requested_ack.description',
defaultMessage: 'An acknowledgement button will appear with your message', defaultMessage: 'An acknowledgement button will appear with your message',
})} })}
/> />) : null,
)} priority === PostPriority.URGENT && persistentNotificationsEnabled ? (
{priority === PostPriority.URGENT && persistentNotificationsEnabled && (
<ToggleItem <ToggleItem
key='post_priority.persistent_notifications.item'
ariaLabel={formatMessage({
id: 'post_priority.persistent_notifications.text',
defaultMessage: 'Send persistent notifications',
})}
disabled={priority !== PostPriority.URGENT} disabled={priority !== PostPriority.URGENT}
onClick={handlePersistentNotifications} onClick={handlePersistentNotifications}
toggled={persistentNotifications} toggled={persistentNotifications}
@@ -222,39 +194,102 @@ function PostPriorityPicker({
interval, interval,
}, },
)} )}
/> />) : null,
)} ] : []), [formatMessage, handleAck, handlePersistentNotifications, interval, persistentNotifications, persistentNotificationsEnabled, postAcknowledgementsEnabled, priority, requestedAck]);
</ul>
</li> const footer = useMemo(() => postAcknowledgementsEnabled &&
</MenuGroup> <div>
)} <Menu.Separator/>
</Menu> <Footer key='footer'>
{postAcknowledgementsEnabled && (
<Footer>
<button <button
type='button' type='submit'
className='PostPriorityPicker__cancel' className='PostPriorityPicker__cancel'
onClick={onClose} onClick={handleClose}
onKeyDown={(e) => handleFooterButtonAction(e, handleClose)}
> >
<FormattedMessage <FormattedMessage
id={'post_priority.picker.cancel'} id='post_priority.picker.cancel'
defaultMessage={'Cancel'} defaultMessage='Cancel'
/> />
</button> </button>
<button <button
type='button' type='submit'
className='PostPriorityPicker__apply' className='PostPriorityPicker__apply'
onClick={handleApply} onClick={handleApply}
onKeyDown={(e) => handleFooterButtonAction(e, handleApply)}
> >
<FormattedMessage <FormattedMessage
id={'post_priority.picker.apply'} id='post_priority.picker.apply'
defaultMessage={'Apply'} defaultMessage='Apply'
/> />
</button> </button>
</Footer> </Footer>
)} </div>, [handleApply, handleClose, handleFooterButtonAction, postAcknowledgementsEnabled]);
</Picker>
); useEffect(() => {
if (pickerOpen) {
setPriority(settings?.priority || '');
setPersistentNotifications(settings?.persistent_notifications || false);
setRequestedAck(settings?.requested_ack || false);
}
}, [pickerOpen, settings]);
return (<CompassDesignProvider theme={theme}>
<Menu.Container
menuButton={{
id: 'messagePriority',
as: 'div',
children: (
<IconContainer
id='messagePriority'
className={classNames({control: true, active: pickerOpen})}
disabled={disabled}
type='button'
aria-label={messagePriority}
>
<AlertCircleOutlineIcon
size={18}
color='currentColor'
/>
</IconContainer>),
}}
menu={{
id: 'post.priority.dropdown',
'aria-label': 'Post priority options',
width: 'max-content',
onToggle: setPickerOpen,
isMenuOpen: pickerOpen,
}}
menuButtonTooltip={{
text: messagePriority,
}}
menuHeader={
<div>
<Header className='modal-title'>
{formatMessage({
id: 'post_priority.picker.header',
defaultMessage: 'Message priority',
})}
</Header>
<Menu.Separator/>
</div>
}
anchorOrigin={{
vertical: 'top',
horizontal: 'left',
}}
transformOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
menuFooter={footer}
closeMenuOnTab={false}
>
{
[...menuItems, ...menuCheckboxItems]
}
</Menu.Container>
</CompassDesignProvider>);
} }
export default memo(PostPriorityPicker); export default memo(PostPriorityPicker);

Просмотреть файл

@@ -1,21 +1,14 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import MenuList from '@mui/material/MenuList';
import React from 'react'; import React from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import {CheckIcon} from '@mattermost/compass-icons/components'; import {CheckIcon, AlertOutlineIcon, AlertCircleOutlineIcon, MessageTextOutlineIcon, CheckCircleOutlineIcon, BellRingOutlineIcon} from '@mattermost/compass-icons/components';
import {MenuItem} from 'components/menu/menu_item';
import Toggle from 'components/toggle'; import Toggle from 'components/toggle';
import MenuGroup from 'components/widgets/menu/menu_group';
import menuItem from 'components/widgets/menu/menu_items/menu_item';
type ItemProps = {
ariaLabel: string;
isSelected: boolean;
onClick: () => void;
text: React.ReactNode;
}
type ToggleProps = { type ToggleProps = {
ariaLabel?: string; ariaLabel?: string;
@@ -27,39 +20,19 @@ type ToggleProps = {
toggled: boolean; toggled: boolean;
} }
const ItemButton = styled.button` const Wrapper = styled(MenuItem)`
display: flex !important;
align-items: center !important;
`;
const Wrapper = styled.li`
cursor: ${(props) => (props.disabled ? 'default' : 'pointer')}; cursor: ${(props) => (props.disabled ? 'default' : 'pointer')};
&:hover { &:hover {
background-color: rgba(var(--center-channel-color-rgb), 0.1); background-color: rgba(var(--center-channel-color-rgb), 0.1);
} }>
`;
const ToggleMain = styled.div`
display: flex !important;
align-items: center !important;
padding: 8px 16px 4px;
`;
const Text = styled.div`
padding-left: 10px;
`; `;
const Description = styled.div` const Description = styled.div`
padding: 0 44px 6px;
font-size: 12px; font-size: 12px;
color: rgba(var(--center-channel-color-rgb), 0.75); color: rgba(var(--center-channel-color-rgb), 0.75);
`; max-width: 200px;
text-wrap: wrap;
const ToggleWrapper = styled.div`
flex-shrink: 0;
width: 32px;
margin-left: auto;
`; `;
const StyledCheckIcon = styled(CheckIcon)` const StyledCheckIcon = styled(CheckIcon)`
@@ -68,7 +41,7 @@ const StyledCheckIcon = styled(CheckIcon)`
fill: var(--button-bg); fill: var(--button-bg);
`; `;
const Menu = styled.ul` const Menu = styled(MenuList)`
&&& { &&& {
display: block; display: block;
position: relative; position: relative;
@@ -83,26 +56,48 @@ const Menu = styled.ul`
} }
`; `;
function Item({ const Header = styled.h4`
onClick, align-items: center;
ariaLabel, display: flex;
text, gap: 8px;
isSelected, font-family: 'Open Sans', sans-serif;
}: ItemProps) { font-size: 14px;
return ( font-weight: 600;
<ItemButton letter-spacing: 0;
aria-label={ariaLabel} line-height: 20px;
className='style--none' padding: 14px 20px;
onClick={onClick} text-align: left;
aria-pressed={isSelected} color: var(--center-channel-color);
> `;
{text && <span className='MenuItem__primary-text'>{text}</span>}
{isSelected && ( const Footer = styled.div`
<StyledCheckIcon size={18}/> align-items: center;
)} display: flex;
</ItemButton> font-family: Open Sans;
); justify-content: flex-end;
} padding: 16px;
gap: 8px;
`;
const UrgentIcon = styled(AlertOutlineIcon)`
fill: rgb(var(--semantic-color-danger));
`;
const ImportantIcon = styled(AlertCircleOutlineIcon)`
fill: rgb(var(--semantic-color-info));
`;
const StandardIcon = styled(MessageTextOutlineIcon)`
fill: rgba(var(--center-channel-color-rgb), 0.75);
`;
const AcknowledgementIcon = styled(CheckCircleOutlineIcon)`
fill: rgba(var(--center-channel-color-rgb), 0.75);
`;
const PersistentNotificationsIcon = styled(BellRingOutlineIcon)`
fill: rgba(var(--center-channel-color-rgb), 0.75);
`;
function ToggleItem({ function ToggleItem({
ariaLabel, ariaLabel,
@@ -117,34 +112,34 @@ function ToggleItem({
<Wrapper <Wrapper
onClick={disabled ? undefined : onClick} onClick={disabled ? undefined : onClick}
disabled={disabled} disabled={disabled}
leadingElement={icon}
tabIndex={-1}
role='menuitemcheckbox' role='menuitemcheckbox'
aria-pressed={toggled} aria-checked={toggled}
>
<ToggleMain>
{icon}
<Text>
{text}
</Text>
<ToggleWrapper>
<Toggle
aria-label={ariaLabel} aria-label={ariaLabel}
trailingElements={<>
<Toggle
ariaLabel={ariaLabel}
size='btn-sm' size='btn-sm'
disabled={disabled} disabled={disabled}
onToggle={onClick} onToggle={onClick}
toggled={toggled} toggled={toggled}
toggleClassName='btn-toggle-primary' toggleClassName='btn-toggle-primary'
tabIndex={-1}
/> />
</ToggleWrapper> </>}
</ToggleMain> labels={<>
<div>
{text}
</div>
<Description> <Description>
{description} {description}
</Description> </Description>
</Wrapper> </>}
/>
); );
} }
const MenuItem = menuItem(Item); export {MenuItem, ToggleItem, StyledCheckIcon, Header, UrgentIcon, ImportantIcon, StandardIcon, AcknowledgementIcon, PersistentNotificationsIcon, Footer};
export {MenuItem, ToggleItem, MenuGroup};
export default Menu; export default Menu;

Просмотреть файл

@@ -1,139 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {
FloatingFocusManager,
FloatingPortal,
autoUpdate,
offset,
useClick,
useDismiss,
useFloating,
useInteractions,
useRole,
flip,
shift,
} from '@floating-ui/react';
import classNames from 'classnames';
import React, {memo, useCallback, useState} from 'react';
import {useIntl} from 'react-intl';
import {AlertCircleOutlineIcon} from '@mattermost/compass-icons/components';
import type {PostPriorityMetadata} from '@mattermost/types/posts';
import {IconContainer} from 'components/advanced_text_editor/formatting_bar/formatting_icon';
import WithTooltip from 'components/with_tooltip';
import PostPriorityPicker from './post_priority_picker';
type Props = {
disabled: boolean;
settings?: PostPriorityMetadata;
onApply: (props: PostPriorityMetadata) => void;
onClose: () => void;
};
function PostPriorityPickerOverlay({
disabled,
settings,
onApply,
onClose,
}: Props) {
const [pickerOpen, setPickerOpen] = useState(false);
const {formatMessage} = useIntl();
const handleClose = useCallback(() => {
setPickerOpen(false);
onClose();
}, [onClose]);
const {
x: pickerX,
y: pickerY,
strategy: pickerStrategy,
context: pickerContext,
refs: {
setReference: setPickerReference,
setFloating: setPickerFloating,
},
} = useFloating({
open: pickerOpen,
onOpenChange: setPickerOpen,
placement: 'top-start',
whileElementsMounted: autoUpdate,
middleware: [
offset({mainAxis: 4}),
flip({
fallbackPlacements: ['top'],
}),
shift({
padding: 16,
}),
],
});
const {
getFloatingProps: getPickerFloatingProps,
getReferenceProps: getPickerReferenceProps,
} = useInteractions([
useClick(pickerContext),
useDismiss(pickerContext),
useRole(pickerContext),
]);
const messagePriority = formatMessage({id: 'shortcuts.msgs.formatting_bar.post_priority', defaultMessage: 'Message priority'});
return (
<>
<WithTooltip
title={messagePriority}
>
<IconContainer
id='messagePriority'
ref={setPickerReference}
className={classNames({control: true, active: pickerOpen})}
disabled={disabled}
type='button'
aria-label={messagePriority}
{...getPickerReferenceProps()}
>
<AlertCircleOutlineIcon
size={18}
color='currentColor'
/>
</IconContainer>
</WithTooltip>
<FloatingPortal id='root-portal'>
{pickerOpen && (
<FloatingFocusManager
context={pickerContext}
modal={true}
returnFocus={false}
initialFocus={-1}
>
<div
ref={setPickerFloating}
style={{
width: 'max-content',
position: pickerStrategy,
top: pickerY ?? 0,
left: pickerX ?? 0,
zIndex: 3,
}}
{...getPickerFloatingProps()}
aria-labelledby='messagePriority-heading'
>
<PostPriorityPicker
settings={settings}
onApply={onApply}
onClose={handleClose}
/>
</div>
</FloatingFocusManager>
)}
</FloatingPortal>
</>
);
}
export default memo(PostPriorityPickerOverlay);

Просмотреть файл

@@ -14,6 +14,8 @@ type Props = {
overrideTestId?: boolean; overrideTestId?: boolean;
size?: 'btn-lg' | 'btn-md' |'btn-sm'; size?: 'btn-lg' | 'btn-md' |'btn-sm';
toggleClassName?: string; toggleClassName?: string;
ariaLabel?: string;
tabIndex?: number;
} }
const Toggle: React.FC<Props> = (props: Props) => { const Toggle: React.FC<Props> = (props: Props) => {
@@ -25,8 +27,10 @@ const Toggle: React.FC<Props> = (props: Props) => {
offText, offText,
id, id,
overrideTestId, overrideTestId,
ariaLabel,
size = 'btn-lg', size = 'btn-lg',
toggleClassName = 'btn-toggle', toggleClassName = 'btn-toggle',
tabIndex = 0,
} = props; } = props;
let dataTestId = `${id}-button`; let dataTestId = `${id}-button`;
if (overrideTestId) { if (overrideTestId) {
@@ -45,6 +49,7 @@ const Toggle: React.FC<Props> = (props: Props) => {
return ( return (
<button <button
aria-label={ariaLabel}
data-testid={dataTestId} data-testid={dataTestId}
id={id} id={id}
type='button' type='button'
@@ -52,6 +57,7 @@ const Toggle: React.FC<Props> = (props: Props) => {
className={className} className={className}
aria-pressed={toggled ? 'true' : 'false'} aria-pressed={toggled ? 'true' : 'false'}
disabled={disabled} disabled={disabled}
tabIndex={tabIndex}
> >
<div className='handle'/> <div className='handle'/>
{text(toggled, onText, offText)} {text(toggled, onText, offText)}

Просмотреть файл

@@ -4,7 +4,7 @@ exports[`UserAccountNameMenuItem should not break if no props are passed 1`] = `
<div> <div>
<li <li
aria-haspopup="true" aria-haspopup="true"
class="MuiButtonBase-root-JvZdr dKFJFs MuiButtonBase-root MuiMenuItem-root MuiMenuItem-gutters MuiMenuItem-root-dXqYNm kIRdVO MuiMenuItem-root MuiMenuItem-gutters sc-gswNZR jjvBbU userAccountMenu_nameMenuItem" class="MuiButtonBase-root-JvZdr dKFJFs MuiButtonBase-root MuiMenuItem-root MuiMenuItem-gutters MuiMenuItem-root-dXqYNm kIRdVO MuiMenuItem-root MuiMenuItem-gutters sc-gswNZR koIPww userAccountMenu_nameMenuItem"
role="menuitem" role="menuitem"
tabindex="-1" tabindex="-1"
> >