Merge pull request #33847 from backstage/rugvip/dialog-api-open
DialogApi: Add open() method, deprecate show/showModal
This commit is contained in:
@@ -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.
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
));
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user