MM-61595 Use dedicated popover component for post actions menu when empty (#31656)

* Link post actions menu button to existing menu

That menu isn't fully accessible because it uses the old Menu widget,
but the readout will at least be correct when tabbing through the post
controls.

* MM-61595 Use dedicated popover component for post actions menu when empty

* Update unit tests

* Split out ActionsMenuEmptyPopover to start of separate Popover component
Этот коммит содержится в:
Harrison Healey
2025-07-02 16:23:22 -04:00
коммит произвёл GitHub
родитель 9ad0f1e070
Коммит 6004d15f9e
12 изменённых файлов: 443 добавлений и 289 удалений

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

@@ -7,31 +7,14 @@ exports[`components/actions_menu/ActionsMenu has actions - marketplace disabled
onToggle={[Function]}
open={true}
>
<WithTooltip
title={
<Memo(MemoizedFormattedMessage)
defaultMessage="Message actions"
id="post_info.tooltip.actions"
/>
}
>
<button
aria-expanded="false"
aria-label="actions"
className="post-menu__item post-menu__item--active"
id="center_actions_button_post_id_1"
key="more-actions-button"
type="button"
>
<i
className="icon icon-apps"
/>
</button>
</WithTooltip>
<ActionsMenuButton
buttonId="center_actions_button_post_id_1"
isMenuOpen={true}
popupId="center_actions_dropdown_post_id_1"
/>
<Menu
ariaLabel="Post extra options"
id="center_actions_dropdown_post_id_1"
key="center_actions_dropdown_post_id_1"
listId="center_actions_dropdown_post_id_1"
openLeft={true}
openUp={false}
>
@@ -57,31 +40,14 @@ exports[`components/actions_menu/ActionsMenu has actions - marketplace enabled a
onToggle={[Function]}
open={true}
>
<WithTooltip
title={
<Memo(MemoizedFormattedMessage)
defaultMessage="Message actions"
id="post_info.tooltip.actions"
/>
}
>
<button
aria-expanded="false"
aria-label="actions"
className="post-menu__item post-menu__item--active"
id="center_actions_button_post_id_1"
key="more-actions-button"
type="button"
>
<i
className="icon icon-apps"
/>
</button>
</WithTooltip>
<ActionsMenuButton
buttonId="center_actions_button_post_id_1"
isMenuOpen={true}
popupId="center_actions_dropdown_post_id_1"
/>
<Menu
ariaLabel="Post extra options"
id="center_actions_dropdown_post_id_1"
key="center_actions_dropdown_post_id_1"
listId="center_actions_dropdown_post_id_1"
openLeft={true}
openUp={false}
>
@@ -120,86 +86,18 @@ exports[`components/actions_menu/ActionsMenu has actions - marketplace enabled a
exports[`components/actions_menu/ActionsMenu no actions - end user - menu should not be visible to end user 1`] = `""`;
exports[`components/actions_menu/ActionsMenu no actions - sysadmin - menu should show visit marketplace 1`] = `
<MenuWrapper
animationComponent={[Function]}
className=""
onToggle={[Function]}
open={true}
>
<WithTooltip
title={
<Memo(MemoizedFormattedMessage)
defaultMessage="Message actions"
id="post_info.tooltip.actions"
/>
}
>
<button
aria-expanded="false"
aria-label="actions"
className="post-menu__item post-menu__item--active"
id="center_actions_button_post_id_1"
key="more-actions-button"
type="button"
>
<i
className="icon icon-apps"
/>
</button>
</WithTooltip>
<Menu
ariaLabel="Post extra options"
id="center_actions_dropdown_post_id_1"
key="center_actions_dropdown_post_id_1"
openLeft={true}
openUp={false}
>
<Memo(SystemPermissionGate)
key="visit-marketplace-permissions"
permissions={
Array [
"manage_system",
]
}
>
<div
className="visit-marketplace-text"
>
<p>
<MemoizedFormattedMessage
defaultMessage="No Actions currently"
id="post_info.actions.noActions.first_line"
/>
</p>
<p>
<MemoizedFormattedMessage
defaultMessage="configured for this server"
id="post_info.actions.noActions.second_line"
/>
</p>
</div>
<div
className="visit-marketplace"
>
<button
className="btn btn-primary btn-sm visit-marketplace-button"
id="marketPlaceButton"
onClick={[Function]}
>
<ActionsMenuIcon
name="icon-view-grid-plus-outline"
/>
<span
className="visit-marketplace-button-text"
>
<MemoizedFormattedMessage
defaultMessage="Visit the Marketplace"
id="post_info.actions.visitMarketplace"
/>
</span>
</button>
</div>
</Memo(SystemPermissionGate)>
</Menu>
</MenuWrapper>
<Fragment>
<ActionsMenuButton
buttonId="center_actions_button_post_id_1"
isMenuOpen={true}
onClick={[Function]}
popupId="center_actions_dropdown_post_id_1"
/>
<ActionsMenuEmptyPopover
anchorElement={null}
isOpen={true}
onOpenMarketplace={[Function]}
onToggle={[MockFunction]}
/>
</Fragment>
`;

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

@@ -1,7 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/actions_menu/ActionsMenu returning empty ("") should match snapshot, return empty ("") on Center 1`] = `
<ContextConsumer>
<Component />
</ContextConsumer>
`;

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

@@ -17,3 +17,15 @@
text-align: left;
}
}
.ActionsMenuEmptyPopover {
& .MuiPaper-root {
min-width: 114px;
max-width: 496px;
max-height: 80vh;
padding: 4px 0;
background-color: var(--center-channel-bg);
box-shadow: var(--elevation-4), 0 0 0 1px rgba(var(--center-channel-color-rgb), 0.12) inset;
margin-block-start: 4px;
}
}

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

@@ -1,24 +1,20 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import classNames from 'classnames';
import React from 'react';
import type {IntlShape} from 'react-intl';
import {FormattedMessage, injectIntl} from 'react-intl';
import {injectIntl} from 'react-intl';
import type {AppBinding} from '@mattermost/types/apps';
import type {Post} from '@mattermost/types/posts';
import {AppCallResponseTypes} from 'mattermost-redux/constants/apps';
import Permissions from 'mattermost-redux/constants/permissions';
import type {ActionResult} from 'mattermost-redux/types/actions';
import SystemPermissionGate from 'components/permissions_gates/system_permission_gate';
import type {OpenedFromType} from 'components/plugin_marketplace/marketplace_modal';
import MarketplaceModal from 'components/plugin_marketplace/marketplace_modal';
import Menu from 'components/widgets/menu/menu';
import MenuWrapper from 'components/widgets/menu/menu_wrapper';
import WithTooltip from 'components/with_tooltip';
import Pluggable from 'plugins/pluggable';
import {createCallContext} from 'utils/apps';
@@ -29,6 +25,8 @@ import type {ModalData} from 'types/actions';
import type {HandleBindingClick, OpenAppsModal, PostEphemeralCallResponseForPost} from 'types/apps';
import type {PostDropdownMenuAction, PostDropdownMenuItemComponent} from 'types/store/plugins';
import ActionsMenuButton from './actions_menu_button';
import ActionsMenuEmptyPopover from './actions_menu_empty_popover';
import {ActionsMenuIcon} from './actions_menu_icon';
import './actions_menu.scss';
@@ -39,9 +37,9 @@ export const PLUGGABLE_COMPONENT = 'PostDropdownMenuItem';
export type Props = {
appBindings: AppBinding[] | null;
appsEnabled: boolean;
handleDropdownOpened?: (open: boolean) => void;
handleDropdownOpened: (open: boolean) => void;
intl: IntlShape;
isMenuOpen?: boolean;
isMenuOpen: boolean;
isSysAdmin: boolean;
location?: 'CENTER' | 'RHS_ROOT' | 'RHS_COMMENT' | 'SEARCH' | string;
pluginMenuItems?: PostDropdownMenuAction[];
@@ -95,7 +93,7 @@ export class ActionMenuClass extends React.PureComponent<Props, State> {
location: Locations.CENTER,
pluginMenuItems: [],
};
private buttonRef: React.RefObject<HTMLButtonElement>;
private buttonElement: HTMLButtonElement | null = null;
constructor(props: Props) {
super(props);
@@ -103,8 +101,6 @@ export class ActionMenuClass extends React.PureComponent<Props, State> {
this.state = {
openUp: false,
};
this.buttonRef = React.createRef<HTMLButtonElement>();
}
componentDidUpdate(prevProps: Props) {
@@ -121,6 +117,10 @@ export class ActionMenuClass extends React.PureComponent<Props, State> {
return state;
}
private buttonRef = (element: HTMLButtonElement | null) => {
this.buttonElement = element;
};
fetchBindings = () => {
if (this.props.appsEnabled && !this.state.appBindings) {
this.props.actions.fetchBindings(this.props.post.channel_id, this.props.teamId).then(({data}) => {
@@ -137,6 +137,8 @@ export class ActionMenuClass extends React.PureComponent<Props, State> {
dialogProps: {openedFrom},
};
this.props.actions.openModal(openMarketplaceData);
this.closeDropdown();
};
onClickAppBinding = async (binding: AppBinding) => {
@@ -189,45 +191,6 @@ export class ActionMenuClass extends React.PureComponent<Props, State> {
}
};
visitMarketplaceTip(): React.ReactElement {
return (
<SystemPermissionGate
permissions={[Permissions.MANAGE_SYSTEM]}
key='visit-marketplace-permissions'
>
<div className='visit-marketplace-text' >
<p>
<FormattedMessage
id='post_info.actions.noActions.first_line'
defaultMessage='No Actions currently'
/>
</p>
<p>
<FormattedMessage
id='post_info.actions.noActions.second_line'
defaultMessage='configured for this server'
/>
</p>
</div>
<div className='visit-marketplace' >
<button
id='marketPlaceButton'
className='btn btn-primary btn-sm visit-marketplace-button'
onClick={this.handleOpenMarketplace}
>
<ActionsMenuIcon name='icon-view-grid-plus-outline'/>
<span className='visit-marketplace-button-text'>
<FormattedMessage
id='post_info.actions.visitMarketplace'
defaultMessage='Visit the Marketplace'
/>
</span>
</button>
</div>
</SystemPermissionGate>
);
}
renderDivider = (suffix: string): React.ReactNode => {
return (
<li
@@ -238,14 +201,22 @@ export class ActionMenuClass extends React.PureComponent<Props, State> {
);
};
openDropdown = () => {
this.props.handleDropdownOpened(true);
};
closeDropdown = () => {
this.props.handleDropdownOpened(false);
};
handleDropdownOpened = (open: boolean) => {
this.props.handleDropdownOpened?.(open);
this.props.handleDropdownOpened(open);
if (!open) {
return;
}
const buttonRect = this.buttonRef.current?.getBoundingClientRect();
const buttonRect = this.buttonElement?.getBoundingClientRect();
let y;
if (typeof buttonRect?.y === 'undefined') {
y = typeof buttonRect?.top == 'undefined' ? 0 : buttonRect?.top;
@@ -342,7 +313,6 @@ export class ActionMenuClass extends React.PureComponent<Props, State> {
);
}
let menuItems;
const hasApps = Boolean(appBindings.length);
const hasPluggables = Boolean(this.props.pluginMenuItemComponents?.length);
const hasPluginItems = Boolean(pluginItems?.length);
@@ -352,65 +322,68 @@ export class ActionMenuClass extends React.PureComponent<Props, State> {
return null;
}
const buttonId = `${this.props.location}_actions_button_${this.props.post.id}`;
const popupId = `${this.props.location}_actions_dropdown_${this.props.post.id}`;
if (hasPluginMenuItems) {
const pluggable = (
<Pluggable
postId={this.props.post.id}
pluggableName={PLUGGABLE_COMPONENT}
key={this.props.post.id + 'pluggable'}
/>);
/>
);
menuItems = [
const menuItems = [
pluginItems,
appBindings,
pluggable,
marketPlace,
];
} else {
menuItems = [this.visitMarketplaceTip()];
if (!this.props.isSysAdmin) {
return null;
}
return (
<MenuWrapper
open={this.props.isMenuOpen}
onToggle={this.handleDropdownOpened}
>
<ActionsMenuButton
ref={this.buttonRef}
buttonId={buttonId}
popupId={popupId}
isMenuOpen={this.props.isMenuOpen}
/>
<Menu
listId={popupId}
openLeft={true}
openUp={this.state.openUp}
ariaLabel={formatMessage({id: 'post_info.menuAriaLabel', defaultMessage: 'Post extra options'})}
>
{menuItems}
</Menu>
</MenuWrapper>
);
} else if (this.props.isSysAdmin) {
return (
<>
<ActionsMenuButton
ref={this.buttonRef}
buttonId={buttonId}
onClick={this.openDropdown}
popupId={popupId}
isMenuOpen={this.props.isMenuOpen}
/>
<ActionsMenuEmptyPopover
anchorElement={this.buttonElement}
onOpenMarketplace={this.handleOpenMarketplace}
onToggle={this.props.handleDropdownOpened}
isOpen={this.props.isMenuOpen}
/>
</>
);
}
return (
<MenuWrapper
open={this.props.isMenuOpen}
onToggle={this.handleDropdownOpened}
>
<WithTooltip
title={
<FormattedMessage
id='post_info.tooltip.actions'
defaultMessage='Message actions'
/>
}
>
<button
key='more-actions-button'
ref={this.buttonRef}
id={`${this.props.location}_actions_button_${this.props.post.id}`}
aria-label={formatMessage({id: 'post_info.actions.tooltip.actions', defaultMessage: 'Actions'}).toLowerCase()}
className={classNames('post-menu__item', {
'post-menu__item--active': this.props.isMenuOpen,
})}
type='button'
aria-expanded='false'
>
<i className={'icon icon-apps'}/>
</button>
</WithTooltip>
<Menu
id={`${this.props.location}_actions_dropdown_${this.props.post.id}`}
openLeft={true}
openUp={this.state.openUp}
ariaLabel={formatMessage({id: 'post_info.menuAriaLabel', defaultMessage: 'Post extra options'})}
key={`${this.props.location}_actions_dropdown_${this.props.post.id}`}
>
{menuItems}
</Menu>
</MenuWrapper >
);
return null;
}
}

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

@@ -0,0 +1,55 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import classNames from 'classnames';
import React from 'react';
import {FormattedMessage, useIntl} from 'react-intl';
import WithTooltip from 'components/with_tooltip';
type ActionsMenuButtonProps = {
buttonId: string;
onClick?: React.MouseEventHandler<HTMLButtonElement>;
isMenuOpen: boolean;
popupId: string;
};
const ActionsMenuButton = React.forwardRef<HTMLButtonElement, ActionsMenuButtonProps>(({
buttonId,
onClick,
isMenuOpen,
popupId,
}, ref) => {
const {formatMessage} = useIntl();
return (
<WithTooltip
title={
<FormattedMessage
id='post_info.tooltip.actions'
defaultMessage='Message actions'
/>
}
>
<button
key='more-actions-button'
ref={ref}
id={buttonId}
aria-label={formatMessage({id: 'post_info.actions.tooltip.actions', defaultMessage: 'Actions'}).toLowerCase()}
className={classNames('post-menu__item', {
'post-menu__item--active': isMenuOpen,
})}
type='button'
aria-controls={popupId}
aria-expanded={isMenuOpen}
aria-haspopup={true}
onClick={onClick}
>
<i className={'icon icon-apps'}/>
</button>
</WithTooltip>
);
});
ActionsMenuButton.displayName = 'ActionsMenuButton';
export default ActionsMenuButton;

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

@@ -1,53 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react';
import ActionsMenu from 'components/actions_menu/actions_menu';
import type {Props} from 'components/actions_menu/actions_menu';
import {TestHelper} from 'utils/test_helper';
jest.mock('utils/utils', () => {
return {
isMobile: jest.fn(() => false),
localizeMessage: jest.fn().mockReturnValue(''),
};
});
jest.mock('utils/post_utils', () => {
const original = jest.requireActual('utils/post_utils');
return {
...original,
isSystemMessage: jest.fn(() => true),
};
});
describe('components/actions_menu/ActionsMenu returning empty ("")', () => {
test('should match snapshot, return empty ("") on Center', () => {
const baseProps: Omit<Props, 'intl'> = {
post: TestHelper.getPostMock({id: 'post_id_1'}),
teamId: 'team_id_1',
actions: {
openModal: jest.fn(),
openAppsModal: jest.fn(),
handleBindingClick: jest.fn(),
postEphemeralCallResponseForPost: jest.fn(),
fetchBindings: jest.fn(),
},
appBindings: [],
pluginMenuItems: [],
appsEnabled: false,
isSysAdmin: true,
canOpenMarketplace: false,
pluginMenuItemComponents: [],
};
const wrapper = shallow(
<ActionsMenu {...baseProps}/>,
);
expect(wrapper).toMatchSnapshot();
});
});

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

@@ -0,0 +1,72 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {FormattedMessage} from 'react-intl';
import {Permissions} from 'mattermost-redux/constants';
import SystemPermissionGate from 'components/permissions_gates/system_permission_gate';
import {ActionsMenuIcon} from './actions_menu_icon';
import Popover from './popover';
type Props = {
anchorElement: Element | null | undefined;
onOpenMarketplace: () => void;
onToggle: (open: boolean) => void;
isOpen: boolean;
}
export default function ActionsMenuEmptyPopover({
anchorElement,
onOpenMarketplace,
onToggle,
isOpen,
}: Props) {
return (
<Popover
anchorElement={anchorElement}
isOpen={isOpen}
onToggle={onToggle}
anchorOrigin={{vertical: 'bottom', horizontal: 'right'}}
transformOrigin={{vertical: 'top', horizontal: 'right'}}
>
<SystemPermissionGate
permissions={[Permissions.MANAGE_SYSTEM]}
key='visit-marketplace-permissions'
>
<div className='visit-marketplace-text'>
<p>
<FormattedMessage
id='post_info.actions.noActions.first_line'
defaultMessage='No Actions currently'
/>
</p>
<p>
<FormattedMessage
id='post_info.actions.noActions.second_line'
defaultMessage='configured for this server'
/>
</p>
</div>
<div className='visit-marketplace'>
<button
id='marketPlaceButton'
className='btn btn-primary btn-sm visit-marketplace-button'
onClick={onOpenMarketplace}
>
<ActionsMenuIcon name='icon-view-grid-plus-outline'/>
<span className='visit-marketplace-button-text'>
<FormattedMessage
id='post_info.actions.visitMarketplace'
defaultMessage='Visit the Marketplace'
/>
</span>
</button>
</div>
</SystemPermissionGate>
</Popover>
);
}

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

@@ -11,6 +11,9 @@ type Props = {
export function ActionsMenuIcon({name, dangerous}: Props) {
const colorClass = dangerous ? 'MenuItem__compass-icon-dangerous' : 'MenuItem__compass-icon';
return (
<span className={`${name} ${colorClass}`}/>
<span
className={`${name} ${colorClass}`}
aria-hidden={true}
/>
);
}

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

@@ -29,6 +29,8 @@ describe('components/actions_menu/ActionsMenu on mobile view', () => {
const baseProps: Omit<Props, 'intl'> = {
post: TestHelper.getPostMock({id: 'post_id_1'}),
teamId: 'team_id_1',
handleDropdownOpened: jest.fn(),
isMenuOpen: true,
actions: {
openModal: jest.fn(),
openAppsModal: jest.fn(),

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

@@ -0,0 +1,126 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useState} from 'react';
import {Permissions} from 'mattermost-redux/constants';
import ActionsMenu from 'components/actions_menu';
import ModalController from 'components/modal_controller';
import {act, renderWithContext, screen, userEvent, waitFor} from 'tests/react_testing_utils';
import {TestHelper} from 'utils/test_helper';
function ActionsMenuTestWrapper(props: Omit<React.ComponentProps<typeof ActionsMenu>, 'isMenuOpen' | 'handleDropdownOpened'>) {
const [isMenuOpen, setIsMenuOpen] = useState(false);
return (
<ActionsMenu
{...props}
handleDropdownOpened={setIsMenuOpen}
isMenuOpen={isMenuOpen}
/>
);
}
describe('ActionsMenu', () => {
test('should match snapshot, return empty ("") on Center', async () => {
const user1 = TestHelper.getUserMock({id: 'user1', roles: 'system_admin system_user'});
const post1 = TestHelper.getPostMock({id: 'post1', type: ''});
const baseProps = {
post: post1,
teamId: 'team_id_1',
};
renderWithContext(
<>
<ActionsMenuTestWrapper {...baseProps}/>
<ModalController/>
</>,
{
entities: {
general: {
config: {
PluginsEnabled: 'true',
EnableMarketplace: 'true',
},
},
posts: {
posts: {
[post1.id]: post1,
},
},
roles: {
roles: {
system_admin: TestHelper.getRoleMock({
permissions: [Permissions.MANAGE_SYSTEM, Permissions.SYSCONSOLE_WRITE_PLUGINS],
}),
},
},
users: {
currentUserId: user1.id,
profiles: {
[user1.id]: user1,
},
},
},
},
);
expect(screen.getByRole('button')).toHaveAccessibleName('actions');
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
// Open the menu
screen.getByRole('button').click();
// The dialog should open up
await waitFor(() => {
expect(screen.queryByRole('dialog', {name: 'actions'})).toBeVisible();
expect(screen.queryByRole('button', {name: 'Visit the Marketplace'})).toBeInTheDocument();
expect(screen.queryByRole('button', {name: 'Visit the Marketplace'})).toBeVisible();
});
// The focus starts on the dialog itself, so pressing tab should move it to the button
userEvent.tab();
expect(screen.queryByRole('button', {name: 'Visit the Marketplace'})).toHaveFocus();
// The focus should be trapped, so hitting tab again shouldn't change the focus
userEvent.tab();
expect(screen.queryByRole('button', {name: 'Visit the Marketplace'})).toHaveFocus();
// Pressing enter should open the marketplace modal and close the menu
userEvent.keyboard('{Enter}');
await waitFor(() => {
expect(screen.queryByRole('dialog', {name: 'actions'})).not.toBeInTheDocument();
expect(screen.queryByRole('dialog', {name: 'App Marketplace'})).toBeVisible();
});
// Pressing escape should close the marketplace modal
act(() => {
userEvent.keyboard('{Escape}');
});
await waitFor(() => {
expect(screen.queryByRole('dialog', {name: 'App Marketplace'})).not.toBeInTheDocument();
});
// Reopen the menu
screen.getByRole('button').click();
await waitFor(() => {
expect(screen.queryByRole('dialog')).toBeVisible();
});
expect(screen.queryByRole('dialog')).toHaveAccessibleName('actions');
// Pressing escape should close the dialog
userEvent.keyboard('{Escape}');
await waitFor(() => {
expect(screen.queryByRole('dialog', {name: 'actions'})).not.toBeInTheDocument();
});
});
});

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

@@ -0,0 +1,75 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type {PopoverOrigin} from '@mui/material/Popover';
import MuiPopover from '@mui/material/Popover';
import classNames from 'classnames';
import React, {useCallback} from 'react';
import {useSelector} from 'react-redux';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import CompassDesignProvider from 'components/compass_design_provider';
import {A11yClassNames} from 'utils/constants';
import './actions_menu.scss';
export type PopoverProps = {
anchorElement: Element | null | undefined;
children: React.ReactNode;
isOpen: boolean;
onToggle?: (isOpen: boolean) => void;
anchorOrigin?: PopoverOrigin;
transformOrigin?: PopoverOrigin;
}
const OPEN_ANIMATION_DURATION = 150;
const CLOSE_ANIMATION_DURATION = 100;
const defaultAnchorOrigin = {vertical: 'bottom', horizontal: 'left'} as PopoverOrigin;
const defaultTransformOrigin = {vertical: 'top', horizontal: 'left'} as PopoverOrigin;
export default function Popover({
anchorElement,
children,
isOpen,
onToggle,
anchorOrigin = defaultAnchorOrigin,
transformOrigin = defaultTransformOrigin,
}: PopoverProps) {
const theme = useSelector(getTheme);
const handleClose = useCallback(() => {
onToggle?.(false);
}, [onToggle]);
return (
<CompassDesignProvider theme={theme}>
<MuiPopover
anchorEl={anchorElement}
open={isOpen}
onClose={handleClose}
className={classNames(A11yClassNames.POPUP, 'ActionsMenuEmptyPopover')}
anchorOrigin={anchorOrigin}
transformOrigin={transformOrigin}
marginThreshold={0}
TransitionProps={{
mountOnEnter: true,
unmountOnExit: true,
timeout: {
enter: OPEN_ANIMATION_DURATION,
exit: CLOSE_ANIMATION_DURATION,
},
}}
role='dialog'
aria-modal={true}
aria-labelledby={anchorElement?.id}
>
{children}
</MuiPopover>
</CompassDesignProvider>
);
}

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

@@ -68,9 +68,7 @@ function isDependencyWarning(params: string[]) {
paramsHasComponent('Overlay') ||
paramsHasComponent('Position') ||
paramsHasComponent('Dropdown') ||
// React-Select
paramsHasComponent('Select')
paramsHasComponent('Tabs')
);
}