Add DialogApi.open() and deprecate show/showModal

The existing show() and showModal() methods render dialog chrome (a
Material UI Dialog) as part of the implementation. This causes focus
trap conflicts when the caller's content uses components from a
different design library (e.g. Backstage UI).

The new open() method renders the caller's content as-is, without any
dialog chrome. The caller provides the full dialog component including
overlay, backdrop, and surface, making the API design-library-agnostic.

The deprecated show/showModal are re-implemented on top of open() with
a MUI Dialog wrapper for backward compatibility, and emit console
warnings when used.

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
Made-with: Cursor
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
Made-with: Cursor
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
Made-with: Cursor
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
Made-with: Cursor
This commit is contained in:
Patrik Oldsberg
2026-04-11 23:52:35 +02:00
parent 0ecb8225cb
commit e4804abb44
10 changed files with 438 additions and 263 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog': patch
---
Migrated the unregister entity context menu item from the deprecated `DialogApi.showModal` to the new `DialogApi.open` method.
@@ -0,0 +1,5 @@
---
'@backstage/plugin-app': patch
---
Updated the default `DialogApi` implementation to support the new `open` method. The dialog display layer no longer renders any dialog chrome — callers provide their own dialog component. The deprecated `show` and `showModal` methods now use `open` internally with a Material UI dialog wrapper for backward compatibility.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/frontend-plugin-api': patch
---
Added `open` method to `DialogApi` that renders dialogs without any built-in dialog chrome, giving the caller full control over the dialog presentation. This avoids focus trap conflicts that occur when mixing components from different design libraries. The existing `show` and `showModal` methods are now deprecated in favor of `open`.
@@ -907,6 +907,12 @@ export function createTranslationResource<
// @public
export interface DialogApi {
open<TResult = void>(
elementOrComponent:
| JSX.Element
| ((props: { dialog: DialogApiDialog<TResult> }) => JSX.Element),
): DialogApiDialog<TResult>;
// @deprecated
show<TResult = void>(
elementOrComponent:
| JSX.Element
@@ -914,6 +920,7 @@ export interface DialogApi {
dialog: DialogApiDialog<TResult | undefined>;
}) => JSX.Element),
): DialogApiDialog<TResult | undefined>;
// @deprecated
showModal<TResult = void>(
elementOrComponent:
| JSX.Element
@@ -21,24 +21,31 @@ import { createApiRef } from '../system';
*
* @remarks
*
* Dialogs can be opened using either {@link DialogApi.show} or {@link DialogApi.showModal}.
* Dialogs are opened using {@link DialogApi.open}.
*
* @public
*/
export interface DialogApiDialog<TResult = void> {
/**
* Closes the dialog with that provided result.
* Closes the dialog with the provided result.
*
* @remarks
*
* If the dialog is a modal dialog a result must always be provided. If it's a regular dialog then passing a result is optional.
* Whether a result is required depends on the `TResult` type parameter
* chosen when the dialog was opened. If the type includes `undefined`,
* calling `close()` without a result is allowed.
*/
close(
...args: undefined extends TResult ? [result?: TResult] : [result: TResult]
): void;
/**
* Replaces the content of the dialog with the provided element or component, causing it to be rerenedered.
* Replaces the rendered dialog with the provided element or component.
*
* @remarks
*
* Just like the element or component passed to {@link DialogApi.open}, the
* caller is responsible for providing the full dialog including all chrome.
*/
update(
elementOrComponent:
@@ -48,10 +55,6 @@ export interface DialogApiDialog<TResult = void> {
/**
* Wait until the dialog is closed and return the result.
*
* @remarks
*
* If the dialog is a modal dialog a result will always be returned. If it's a regular dialog then the result may be `undefined`.
*/
result(): Promise<TResult>;
}
@@ -63,51 +66,67 @@ export interface DialogApiDialog<TResult = void> {
*/
export interface DialogApi {
/**
* Opens a modal dialog and returns a handle to it.
* Opens a dialog and returns a handle to it.
*
* @remarks
*
* This dialog can be closed by calling the `close` method on the returned handle, optionally providing a result.
* The dialog can also be closed by the user by clicking the backdrop or pressing the escape key.
*
* If the dialog is closed without a result, the result will be `undefined`.
* The provided element or component is rendered as-is in the app's React tree.
* It is the caller's responsibility to provide all dialog chrome, such as an
* overlay, backdrop, and dialog surface. This makes the method agnostic to the
* design library used for the dialog.
*
* @example
*
* ### Example with inline dialog content
* ### Example with inline dialog element
* ```tsx
* const dialog = dialogApi.show<boolean>(
* <DialogContent>
* <DialogTitle>Are you sure?</DialogTitle>
* <DialogActions>
* <Button onClick={() => dialog.close(true)}>Yes</Button>
* <Button onClick={() => dialog.close(false)}>No</Button>
* </DialogActions>
* </DialogContent>
* const dialog = dialogApi.open<boolean>(
* <Dialog isOpen onOpenChange={isOpen => !isOpen && dialog.close()}>
* <DialogHeader>Are you sure?</DialogHeader>
* <DialogBody>This action cannot be undone.</DialogBody>
* <DialogFooter>
* <Button onPress={() => dialog.close(false)}>Cancel</Button>
* <Button onPress={() => dialog.close(true)}>Confirm</Button>
* </DialogFooter>
* </Dialog>
* );
* const result = await dialog.result();
* ```
*
* @example
*
* ### Example with separate dialog component
* ### Example with a dialog component
* ```tsx
* function CustomDialog({ dialog }: { dialog: DialogApiDialog<boolean | undefined> }) {
* function ConfirmDialog({ dialog }: { dialog: DialogApiDialog<boolean> }) {
* return (
* <DialogContent>
* <DialogTitle>Are you sure?</DialogTitle>
* <DialogActions>
* <Button onClick={() => dialog.close(true)}>Yes</Button>
* <Button onClick={() => dialog.close(false)}>No</Button>
* </DialogActions>
* </DialogContent>
* )
* <Dialog isOpen onOpenChange={isOpen => !isOpen && dialog.close()}>
* <DialogHeader>Are you sure?</DialogHeader>
* <DialogBody>This action cannot be undone.</DialogBody>
* <DialogFooter>
* <Button onPress={() => dialog.close(false)}>Cancel</Button>
* <Button onPress={() => dialog.close(true)}>Confirm</Button>
* </DialogFooter>
* </Dialog>
* );
* }
* const result = await dialogApi.show(CustomDialog).result();
* const result = await dialogApi.open(ConfirmDialog).result();
* ```
*
* @param elementOrComponent - The element or component to render. If a component is provided, it will be provided with a `dialog` prop that contains the dialog handle.
*/
open<TResult = void>(
elementOrComponent:
| JSX.Element
| ((props: { dialog: DialogApiDialog<TResult> }) => JSX.Element),
): DialogApiDialog<TResult>;
/**
* Opens a dialog with built-in dialog chrome and returns a handle to it.
*
* @deprecated Use {@link DialogApi.open} instead. The `open` method does not
* render any dialog chrome, giving the caller full control over the dialog
* presentation. This avoids focus trap conflicts across design libraries.
*
* @param elementOrComponent - The element or component to render in the dialog. If a component is provided, it will be provided with a `dialog` prop that contains the dialog handle.
* @public
*/
show<TResult = void>(
elementOrComponent:
@@ -118,48 +137,13 @@ export interface DialogApi {
): DialogApiDialog<TResult | undefined>;
/**
* Opens a modal dialog and returns a handle to it.
* Opens a modal dialog with built-in dialog chrome and returns a handle to it.
*
* @remarks
*
* This dialog can not be closed in any other way than calling the `close` method on the returned handle and providing a result.
*
* @example
*
* ### Example with inline dialog content
* ```tsx
* const dialog = dialogApi.showModal<boolean>(
* <DialogContent>
* <DialogTitle>Are you sure?</DialogTitle>
* <DialogActions>
* <Button onClick={() => dialog.close(true)}>Yes</Button>
* <Button onClick={() => dialog.close(false)}>No</Button>
* </DialogActions>
* </DialogContent>
* );
* const result = await dialog.result();
* ```
*
* @example
*
* ### Example with separate dialog component
* ```tsx
* function CustomDialog({ dialog }: { dialog: DialogApiDialog<boolean> }) {
* return (
* <DialogContent>
* <DialogTitle>Are you sure?</DialogTitle>
* <DialogActions>
* <Button onClick={() => dialog.close(true)}>Yes</Button>
* <Button onClick={() => dialog.close(false)}>No</Button>
* </DialogActions>
* </DialogContent>
* )
* }
* const result = await dialogApi.showModal(CustomDialog).result();
* ```
* @deprecated Use {@link DialogApi.open} instead. The `open` method does not
* render any dialog chrome, giving the caller full control over the dialog
* presentation. This avoids focus trap conflicts across design libraries.
*
* @param elementOrComponent - The element or component to render in the dialog. If a component is provided, it will be provided with a `dialog` prop that contains the dialog handle.
* @public
*/
showModal<TResult = void>(
elementOrComponent:
-70
View File
@@ -1,70 +0,0 @@
/*
* 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 { DialogApi, DialogApiDialog } from '@backstage/frontend-plugin-api';
export type OnShowDialog = (options: {
component: (props: { dialog: DialogApiDialog<any> }) => React.JSX.Element;
modal: boolean;
}) => DialogApiDialog<unknown>;
/**
* Default implementation for the {@link DialogApi}.
* @internal
*/
export class DefaultDialogApi implements DialogApi {
#onShow?: OnShowDialog;
show<TResult = void>(
elementOrComponent:
| JSX.Element
| ((props: {
dialog: DialogApiDialog<TResult | undefined>;
}) => JSX.Element),
): DialogApiDialog<TResult | undefined> {
if (!this.#onShow) {
throw new Error('Dialog API has not been connected');
}
return this.#onShow({
component:
typeof elementOrComponent === 'function'
? elementOrComponent
: () => elementOrComponent,
modal: false,
}) as DialogApiDialog<TResult | undefined>;
}
showModal<TResult = void>(
elementOrComponent:
| JSX.Element
| ((props: { dialog: DialogApiDialog<TResult> }) => JSX.Element),
): DialogApiDialog<TResult> {
if (!this.#onShow) {
throw new Error('Dialog API has not been connected');
}
return this.#onShow({
component:
typeof elementOrComponent === 'function'
? elementOrComponent
: () => elementOrComponent,
modal: true,
}) as DialogApiDialog<TResult>;
}
connect(onShow: OnShowDialog): void {
this.#onShow = onShow;
}
}
+141
View File
@@ -0,0 +1,141 @@
/*
* 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 { DialogApi, DialogApiDialog } from '@backstage/frontend-plugin-api';
import Dialog from '@material-ui/core/Dialog';
export type OnOpenDialog = (options: {
component: (props: { dialog: DialogApiDialog<any> }) => JSX.Element;
}) => DialogApiDialog<unknown>;
/**
* Default implementation for the {@link DialogApi}.
* @internal
*/
export class DefaultDialogApi implements DialogApi {
#onOpen?: OnOpenDialog;
open<TResult = void>(
elementOrComponent:
| JSX.Element
| ((props: { dialog: DialogApiDialog<TResult> }) => JSX.Element),
): DialogApiDialog<TResult> {
if (!this.#onOpen) {
throw new Error('Dialog API has not been connected');
}
return this.#onOpen({
component:
typeof elementOrComponent === 'function'
? elementOrComponent
: () => elementOrComponent,
}) as DialogApiDialog<TResult>;
}
/** @deprecated Use {@link DefaultDialogApi.open} instead */
show<TResult = void>(
elementOrComponent:
| JSX.Element
| ((props: {
dialog: DialogApiDialog<TResult | undefined>;
}) => JSX.Element),
): DialogApiDialog<TResult | undefined> {
// eslint-disable-next-line no-console
console.warn(
'DialogApi.show() is deprecated and will be removed in a future release. Use DialogApi.open() instead.',
);
const innerDialog = this.open<TResult | undefined>(({ dialog }) => (
<DeprecatedMuiDialogWrapper
dialog={dialog}
content={elementOrComponent}
modal={false}
/>
));
return wrapDialogHandle(innerDialog, false);
}
/** @deprecated Use {@link DefaultDialogApi.open} instead */
showModal<TResult = void>(
elementOrComponent:
| JSX.Element
| ((props: { dialog: DialogApiDialog<TResult> }) => JSX.Element),
): DialogApiDialog<TResult> {
// eslint-disable-next-line no-console
console.warn(
'DialogApi.showModal() is deprecated and will be removed in a future release. Use DialogApi.open() instead.',
);
const innerDialog = this.open<TResult>(({ dialog }) => (
<DeprecatedMuiDialogWrapper
dialog={dialog}
content={elementOrComponent}
modal
/>
));
return wrapDialogHandle(innerDialog, true);
}
connect(onOpen: OnOpenDialog): void {
this.#onOpen = onOpen;
}
}
function DeprecatedMuiDialogWrapper({
dialog,
content,
modal,
}: {
dialog: DialogApiDialog<any>;
content:
| JSX.Element
| ((props: { dialog: DialogApiDialog<any> }) => JSX.Element);
modal: boolean;
}) {
if (typeof content === 'function') {
const Content = content;
return (
<Dialog open onClose={modal ? undefined : () => dialog.close()}>
<Content dialog={dialog} />
</Dialog>
);
}
return (
<Dialog open onClose={modal ? undefined : () => dialog.close()}>
{content}
</Dialog>
);
}
function wrapDialogHandle<TResult>(
innerDialog: DialogApiDialog<TResult>,
modal: boolean,
): DialogApiDialog<TResult> {
return {
close(...args: any[]) {
(innerDialog.close as any)(...args);
},
result() {
return innerDialog.result();
},
update(newContent: any) {
innerDialog.update(({ dialog }: { dialog: DialogApiDialog<TResult> }) => (
<DeprecatedMuiDialogWrapper
dialog={dialog}
content={newContent}
modal={modal}
/>
));
},
};
}
+205 -97
View File
@@ -16,6 +16,7 @@
import { renderTestApp } from '@backstage/frontend-test-utils';
import { act, useEffect } from 'react';
import { screen } from '@testing-library/react';
import {
AppRootElementBlueprint,
DialogApi,
@@ -49,11 +50,11 @@ async function withDialogApi<T>(
}
describe('DialogDisplay', () => {
function AutoDialog({
function AutoCloseDialog({
dialog,
result,
}: {
dialog: DialogApiDialog<string | undefined>;
dialog: DialogApiDialog<string>;
result?: string;
}) {
useEffect(() => {
@@ -64,112 +65,219 @@ describe('DialogDisplay', () => {
return <div />;
}
it('should render a simple dialog', async () => {
const result = await withDialogApi(async dialogApi => {
const dialog = await act(() => dialogApi.show<string>(<div>Test</div>));
dialog.close('test');
return dialog.result();
});
expect(result).toBe('test');
});
it('should allow dialog to be updated', async () => {
const result = await withDialogApi(async dialogApi => {
const dialog = await act(() => dialogApi.show(AutoDialog));
setTimeout(async () => {
await act(async () => {
dialog.update(props => <AutoDialog {...props} result="test2" />);
});
}, 100);
return dialog.result();
describe('open', () => {
it('should render a dialog and return a result', async () => {
const result = await withDialogApi(async dialogApi => {
const dialog = await act(() => dialogApi.open<string>(<div>Test</div>));
dialog.close('test');
return dialog.result();
});
expect(result).toBe('test');
});
expect(result).toBe('test2');
});
it('should allow dialog to be closed by pressing escape', async () => {
const result = await withDialogApi(async dialogApi => {
const dialog = await act(() => dialogApi.show(AutoDialog));
setTimeout(async () => {
await userEvent.keyboard('{Escape}');
}, 100);
return dialog.result();
it('should render dialog content directly without wrapper chrome', async () => {
await withDialogApi(async dialogApi => {
await act(() =>
dialogApi.open(<div data-testid="bare-content">Hello</div>),
);
expect(await screen.findByTestId('bare-content')).toBeInTheDocument();
return undefined;
});
});
expect(result).toBe(undefined);
});
it('should allow a stack of dialogs', async () => {
const result = await withDialogApi(async dialogApi => {
const dialog1 = await act(() => dialogApi.show(AutoDialog));
const dialog2 = await act(() => dialogApi.show(AutoDialog));
const dialog3 = await act(() => dialogApi.show(AutoDialog));
setTimeout(async () => {
await act(async () => {
dialog3.close('test3');
dialog1.close('test1');
dialog2.close('test2');
});
}, 100);
return Promise.all([
dialog1.result(),
dialog2.result(),
dialog3.result(),
]);
});
expect(result).toEqual(['test1', 'test2', 'test3']);
});
it('should only cancel one dialog at a time', async () => {
const result = await withDialogApi(async dialogApi => {
const dialog1 = await act(() => dialogApi.show(AutoDialog));
const dialog2 = await act(() => dialogApi.show(AutoDialog));
const dialog3 = await act(() => dialogApi.show(AutoDialog));
setTimeout(async () => {
await userEvent.keyboard('{Escape}');
await act(async () => {
dialog1.close('test1');
dialog2.close('test2');
dialog3.close('test3');
});
}, 100);
return Promise.all([
dialog1.result(),
dialog2.result(),
dialog3.result(),
]);
});
expect(result).toEqual(['test1', 'test2', undefined]);
});
it('should not allow modal dialog to be closed by pressing escape', async () => {
const result = await withDialogApi(async dialogApi => {
const dialog = await act(() => dialogApi.showModal(AutoDialog));
setTimeout(async () => {
await userEvent.keyboard('{Escape}');
it('should allow dialog to be updated', async () => {
const result = await withDialogApi(async dialogApi => {
const dialog = await act(() => dialogApi.open<string>(AutoCloseDialog));
setTimeout(async () => {
await act(async () => {
dialog.close('test');
dialog.update(props => (
<AutoCloseDialog {...props} result="test2" />
));
});
}, 100);
}, 100);
return dialog.result();
return dialog.result();
});
expect(result).toBe('test2');
});
expect(result).toBe('test');
it('should allow a stack of dialogs, rendering only the most recent', async () => {
const result = await withDialogApi(async dialogApi => {
const dialog1 = await act(() =>
dialogApi.open<string>(AutoCloseDialog),
);
const dialog2 = await act(() =>
dialogApi.open<string>(AutoCloseDialog),
);
const dialog3 = await act(() =>
dialogApi.open<string>(AutoCloseDialog),
);
setTimeout(async () => {
await act(async () => {
dialog3.close('test3');
dialog1.close('test1');
dialog2.close('test2');
});
}, 100);
return Promise.all([
dialog1.result(),
dialog2.result(),
dialog3.result(),
]);
});
expect(result).toEqual(['test1', 'test2', 'test3']);
});
it('should accept a component that receives the dialog handle', async () => {
function TestDialog({ dialog }: { dialog: DialogApiDialog<string> }) {
return (
<button onClick={() => dialog.close('from-component')}>Close</button>
);
}
const result = await withDialogApi(async dialogApi => {
const dialog = await act(() => dialogApi.open<string>(TestDialog));
setTimeout(async () => {
const button = await screen.findByRole('button', { name: 'Close' });
await userEvent.click(button);
}, 100);
return dialog.result();
});
expect(result).toBe('from-component');
});
});
describe('deprecated show', () => {
function AutoDialog({
dialog,
result,
}: {
dialog: DialogApiDialog<string | undefined>;
result?: string;
}) {
useEffect(() => {
if (result) {
dialog.close(result);
}
}, [dialog, result]);
return <div />;
}
it('should render a simple dialog', async () => {
const result = await withDialogApi(async dialogApi => {
const dialog = await act(() => dialogApi.show<string>(<div>Test</div>));
dialog.close('test');
return dialog.result();
});
expect(result).toBe('test');
});
it('should allow dialog to be updated', async () => {
const result = await withDialogApi(async dialogApi => {
const dialog = await act(() => dialogApi.show(AutoDialog));
setTimeout(async () => {
await act(async () => {
dialog.update(props => <AutoDialog {...props} result="test2" />);
});
}, 100);
return dialog.result();
});
expect(result).toBe('test2');
});
it('should allow dialog to be closed by pressing escape', async () => {
const result = await withDialogApi(async dialogApi => {
const dialog = await act(() => dialogApi.show(AutoDialog));
setTimeout(async () => {
await userEvent.keyboard('{Escape}');
}, 100);
return dialog.result();
});
expect(result).toBe(undefined);
});
it('should allow a stack of dialogs', async () => {
const result = await withDialogApi(async dialogApi => {
const dialog1 = await act(() => dialogApi.show(AutoDialog));
const dialog2 = await act(() => dialogApi.show(AutoDialog));
const dialog3 = await act(() => dialogApi.show(AutoDialog));
setTimeout(async () => {
await act(async () => {
dialog3.close('test3');
dialog1.close('test1');
dialog2.close('test2');
});
}, 100);
return Promise.all([
dialog1.result(),
dialog2.result(),
dialog3.result(),
]);
});
expect(result).toEqual(['test1', 'test2', 'test3']);
});
it('should only cancel one dialog at a time', async () => {
const result = await withDialogApi(async dialogApi => {
const dialog1 = await act(() => dialogApi.show(AutoDialog));
const dialog2 = await act(() => dialogApi.show(AutoDialog));
const dialog3 = await act(() => dialogApi.show(AutoDialog));
setTimeout(async () => {
await userEvent.keyboard('{Escape}');
await act(async () => {
dialog1.close('test1');
dialog2.close('test2');
dialog3.close('test3');
});
}, 100);
return Promise.all([
dialog1.result(),
dialog2.result(),
dialog3.result(),
]);
});
expect(result).toEqual(['test1', 'test2', undefined]);
});
it('should not allow modal dialog to be closed by pressing escape', async () => {
const result = await withDialogApi(async dialogApi => {
const dialog = await act(() => dialogApi.showModal(AutoDialog));
setTimeout(async () => {
await userEvent.keyboard('{Escape}');
setTimeout(async () => {
await act(async () => {
dialog.close('test');
});
}, 100);
}, 100);
return dialog.result();
});
expect(result).toBe('test');
});
});
});
+12 -21
View File
@@ -22,8 +22,7 @@ import {
dialogApiRef,
} from '@backstage/frontend-plugin-api';
import { createDeferred } from '@backstage/types';
import { OnShowDialog } from '../apis/DefaultDialogApi';
import Dialog from '@material-ui/core/Dialog';
import { OnOpenDialog } from '../apis/DefaultDialogApi';
let dialogId = 0;
function getDialogId() {
@@ -33,20 +32,25 @@ function getDialogId() {
type DialogState = DialogApiDialog<unknown> & {
id: string;
modal: boolean;
};
/**
* The other half of the default implementation of the {@link DialogApi}.
*
* This component is responsible for rendering the dialogs in the React tree and managing a stack of dialogs.
* It expects the implementation of the {@link DialogApi} to be the `DefaultDialogApi`. If one is replaced the other must be too.
* This component is responsible for rendering the dialogs in the React tree
* and managing a stack of dialogs. It renders only the most recently opened
* dialog, without any dialog chrome the caller is expected to provide their
* own dialog component (overlay, backdrop, surface, etc.).
*
* It expects the implementation of the {@link DialogApi} to be the
* `DefaultDialogApi`. If one is replaced the other must be too.
*
* @internal
*/
function DialogDisplay({
dialogApi,
}: {
dialogApi: DialogApi & { connect(onShow: OnShowDialog): void };
dialogApi: DialogApi & { connect(onOpen: OnOpenDialog): void };
}) {
const [dialogs, setDialogs] = useState<
{ dialog: DialogState; element: React.JSX.Element }[]
@@ -58,7 +62,6 @@ function DialogDisplay({
const deferred = createDeferred<unknown>();
const dialog: DialogState = {
id,
modal: options.modal,
close(result) {
deferred.resolve(result);
setDialogs(ds => ds.filter(d => d.dialog.id !== id));
@@ -85,19 +88,7 @@ function DialogDisplay({
}, [dialogApi]);
if (dialogs.length > 0) {
const lastDialog = dialogs[dialogs.length - 1];
return (
<Dialog
open
onClose={() => {
if (!lastDialog.dialog.modal) {
lastDialog.dialog.close();
}
}}
>
{lastDialog.element}
</Dialog>
);
return dialogs[dialogs.length - 1].element;
}
return null;
@@ -121,7 +112,7 @@ export const dialogDisplayAppRootElement =
function isInternalDialogApi(
dialogApi?: DialogApi,
): dialogApi is DialogApi & { connect(onShow: OnShowDialog): void } {
): dialogApi is DialogApi & { connect(onOpen: OnOpenDialog): void } {
if (!dialogApi) {
return false;
}
@@ -26,7 +26,6 @@ import { alertApiRef, useApi, useRouteRef } from '@backstage/core-plugin-api';
import {
dialogApiRef,
useTranslationRef,
type DialogApiDialog,
} from '@backstage/frontend-plugin-api';
import { catalogTranslationRef } from './translation';
import { useNavigate, useSearchParams } from 'react-router-dom';
@@ -110,7 +109,7 @@ export const unregisterEntityContextMenuItem =
title: t('entityContextMenu.unregisterMenuTitle'),
disabled: !unregisterPermission.allowed,
onClick: async () => {
dialogApi.showModal(({ dialog }: { dialog: DialogApiDialog }) => (
dialogApi.open(({ dialog }) => (
<UnregisterEntityDialog
open
entity={entity}