use context provider for handling context menu close

Signed-off-by: Mark Dunphy <markd@spotify.com>
This commit is contained in:
Mark Dunphy
2025-04-08 09:19:51 -04:00
parent 5ed70bf42e
commit cc77358234
8 changed files with 159 additions and 26 deletions
+1 -8
View File
@@ -89,9 +89,7 @@ export const catalogReactTranslationRef: TranslationRef<
>;
// @alpha (undocumented)
export type ContextMenuItemComponent = (
props: ContextMenuItemProps,
) => React_2.JSX.Element;
export type ContextMenuItemComponent = (props: {}) => React_2.JSX.Element;
// @alpha (undocumented)
export const contextMenuItemComponentDataRef: ConfigurableExtensionDataRef<
@@ -100,11 +98,6 @@ export const contextMenuItemComponentDataRef: ConfigurableExtensionDataRef<
{}
>;
// @alpha (undocumented)
export type ContextMenuItemProps = {
onClose: () => void;
};
// @alpha (undocumented)
export function convertLegacyEntityCardExtension(
LegacyExtension: ComponentType<{}>,
+21
View File
@@ -243,6 +243,24 @@ export type EntityAutocompletePickerProps<
hidden?: boolean;
};
// @public (undocumented)
export type EntityContextMenuContextValue = {
onMenuClose: () => void;
};
// @public (undocumented)
export const EntityContextMenuProvider: (
props: EntityContextMenuProviderProps,
) => React_2.JSX.Element;
// @public (undocumented)
export interface EntityContextMenuProviderProps {
// (undocumented)
children: React_2.ReactNode;
// (undocumented)
onMenuClose: () => void;
}
// @public
export const EntityDisplayName: (props: EntityDisplayNameProps) => JSX.Element;
@@ -778,6 +796,9 @@ export function useEntity<TEntity extends Entity = Entity>(): {
entity: TEntity;
};
// @public (undocumented)
export function useEntityContextMenu(): EntityContextMenuContextValue;
// @public
export function useEntityList<
EntityFilters extends DefaultEntityFilters = DefaultEntityFilters,
@@ -23,6 +23,7 @@ import {
import MenuItem from '@material-ui/core/MenuItem';
import ListItemIcon from '@material-ui/core/ListItemIcon';
import ListItemText from '@material-ui/core/ListItemText';
import { useEntityContextMenu } from '../../hooks/useEntityContextMenu';
/** @alpha */
export type UseProps = () =>
@@ -44,14 +45,7 @@ export type EntityContextMenuItemParams = {
};
/** @alpha */
export type ContextMenuItemProps = {
onClose: () => void;
};
/** @alpha */
export type ContextMenuItemComponent = (
props: ContextMenuItemProps,
) => React.JSX.Element;
export type ContextMenuItemComponent = (props: {}) => React.JSX.Element;
/** @alpha */
export const contextMenuItemComponentDataRef =
@@ -66,7 +60,8 @@ export const EntityContextMenuItemBlueprint = createExtensionBlueprint({
output: [contextMenuItemComponentDataRef],
*factory(params: EntityContextMenuItemParams, { node }) {
const loader = async (): Promise<ContextMenuItemComponent> => {
return ({ onClose }) => {
return () => {
const { onMenuClose } = useEntityContextMenu();
const { title, ...menuItemProps } = params.useProps();
let handleClick = undefined;
@@ -74,9 +69,9 @@ export const EntityContextMenuItemBlueprint = createExtensionBlueprint({
handleClick = () => {
const result = menuItemProps.onClick();
if (result && 'finally' in result) {
result.finally(onClose);
result.finally(onMenuClose);
} else {
onClose();
onMenuClose();
}
};
}
@@ -26,7 +26,6 @@ export type { EntityCardType } from './extensionData';
export {
contextMenuItemComponentDataRef,
EntityContextMenuItemBlueprint,
type ContextMenuItemProps,
type ContextMenuItemComponent,
type EntityContextMenuItemParams,
type UseProps,
+9
View File
@@ -35,6 +35,15 @@ export type {
EntityListProviderProps,
PaginationMode,
} from './useEntityListProvider';
export type {
EntityContextMenuContextValue,
EntityContextMenuProviderProps,
} from './useEntityContextMenu';
export {
EntityContextMenuProvider,
useEntityContextMenu,
} from './useEntityContextMenu';
export { useEntityTypeFilter } from './useEntityTypeFilter';
export { useRelatedEntities } from './useRelatedEntities';
export { useStarredEntities } from './useStarredEntities';
@@ -0,0 +1,44 @@
/*
* Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import {
EntityContextMenuProvider,
useEntityContextMenu,
} from './useEntityContextMenu';
import { renderHook } from '@testing-library/react';
describe('useEntityContextMenu', () => {
it('should throw error when used outside of provider', () => {
expect(() => {
renderHook(() => useEntityContextMenu());
}).toThrow(
'useEntityContextMenu must be used within an EntityContextMenuProvider',
);
});
it('should return the context value', () => {
const mockOnMenuClose = jest.fn();
const { result } = renderHook(() => useEntityContextMenu(), {
wrapper: ({ children }) => (
<EntityContextMenuProvider onMenuClose={mockOnMenuClose}>
{children}
</EntityContextMenuProvider>
),
});
expect(result.current.onMenuClose).toBe(mockOnMenuClose);
});
});
@@ -0,0 +1,72 @@
/*
* Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import {
createVersionedContext,
createVersionedValueMap,
useVersionedContext,
} from '@backstage/version-bridge';
/** @public */
export type EntityContextMenuContextValue = {
onMenuClose: () => void;
};
const EntityContextMenuContext = createVersionedContext<{
1: EntityContextMenuContextValue;
}>('entity-context-menu-context');
/** @public */
export interface EntityContextMenuProviderProps {
children: React.ReactNode;
onMenuClose: () => void;
}
/** @public */
export const EntityContextMenuProvider = (
props: EntityContextMenuProviderProps,
) => {
const { children, onMenuClose } = props;
const value = { onMenuClose };
return (
<EntityContextMenuContext.Provider
value={createVersionedValueMap({ 1: value })}
>
{children}
</EntityContextMenuContext.Provider>
);
};
/** @public */
export function useEntityContextMenu() {
const versionedHolder = useVersionedContext<{
1: EntityContextMenuContextValue;
}>('entity-context-menu-context');
if (!versionedHolder) {
throw new Error(
'useEntityContextMenu must be used within an EntityContextMenuProvider',
);
}
const value = versionedHolder.atVersion(1);
if (!value) {
throw new Error('EntityContextMenu v1 is not available');
}
return value;
}
@@ -36,6 +36,7 @@ import useCopyToClipboard from 'react-use/esm/useCopyToClipboard';
import { catalogTranslationRef } from '../../alpha/translation';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
import type { ContextMenuItemComponent } from '@backstage/plugin-catalog-react/alpha';
import { EntityContextMenuProvider } from '@backstage/plugin-catalog-react';
/** @public */
export type EntityContextMenuClassKey = 'button';
@@ -187,12 +188,11 @@ export function EntityContextMenu(props: EntityContextMenuProps) {
</MenuItem>
</>
) : (
contextMenuItems.map((ContextMenuItem, idx) => (
<ContextMenuItem
key={`context-menu-item-${idx}`}
onClose={onClose}
/>
))
<EntityContextMenuProvider onMenuClose={onClose}>
{contextMenuItems.map((ContextMenuItem, idx) => (
<ContextMenuItem key={`context-menu-item-${idx}`} />
))}
</EntityContextMenuProvider>
)}
</MenuList>
</Popover>