diff --git a/packages/example-app/src/ProjectView/actions/deleteAction.tsx b/packages/example-app/src/ProjectView/actions/deleteAction.tsx index 646776ab..0a54be45 100644 --- a/packages/example-app/src/ProjectView/actions/deleteAction.tsx +++ b/packages/example-app/src/ProjectView/actions/deleteAction.tsx @@ -1,5 +1,5 @@ import path from "path"; -import { selector, useRecoilCallback } from "recoil"; +import { selector } from "recoil"; import React from "react"; import { @@ -22,7 +22,6 @@ import { selectedNodesState } from "../ProjectView.state"; import { findRootPaths } from "../../path-utils"; import { deleteDirCallback, - deleteFileCallback, deleteFilesCallback, } from "../../Project/fs-operations"; import { IntlMessageFormat } from "intl-messageformat"; @@ -52,7 +51,7 @@ export const deleteActionState = selector({ isDisabled: !get(activePathExistsState), actionPerformed: getCallback(({ snapshot }) => async () => { const deleteDir = getCallback(deleteDirCallback); - const deleteFile = getCallback(deleteFileCallback); + const deleteFiles = getCallback(deleteFilesCallback); const selectedNodes = snapshot.getLoadable(selectedNodesState).getValue(); const windowManager = snapshot .getLoadable(windowManagerRefState) @@ -105,12 +104,31 @@ export const deleteActionState = selector({ }); if (confirmed) { directories.forEach((pathname) => deleteDir(pathname)); - filePaths.forEach((pathname) => deleteFile(pathname)); + deleteFiles(filePaths); } } else { - windowManager?.open(({ close }) => ( - - )); + windowManager + ?.open(({ close }) => ( + + )) + .then((confirmed) => { + if (confirmed) { + requestAnimationFrame(() => { + // It's important that the focus is restored to the previously focused item, which typically is the + // file in project view, that's being deleted. + // If the file is deleted before the focus is restored, the DOM element that was going to be focused + // will be removed by the time focus restoration logic runs. + // Even though `windowManager.open()` was changed to return a promise which is supposed to be resolved + // **after** the modal window is closed, + // in the CI environment the focus is not properly restored. + // In lack of a better solution, the deletion is done async with a setTimeout. + deleteFiles(filePaths); + }); + } + }); } }), }), @@ -120,11 +138,9 @@ function DeleteFilesConfirmationDialog({ close, filePaths, }: { - close: () => void; + close: (confirmed?: true) => void; filePaths: string[]; }) { - const deleteFiles = useRecoilCallback(deleteFilesCallback, []); - return ( notImplemented()}>} right={ <> - + + * + * + * } + * /> + * } + * /> + * + * )) + * .then((confirmed) => alert(`confirmed: ${confirmed ?? false}`)); + * }; + * ``` */ - open( + open( props: | React.ReactElement | ((args: { - close: () => void; + close: (result?: T) => void; }) => React.ReactElement) - ): void; + ): Promise; } const NotImplementedFn = () => { @@ -59,25 +94,39 @@ export const WindowManager: React.FC = ({ children }) => { const newKeyRef = useRef(0); const api = useMemo(() => { - const openModalWindow: WindowManagerAPI["open"] = (content) => { - newKeyRef.current++; - const close = () => { - setWindows((currentWindows) => - currentWindows.filter((aWindow) => aWindow !== window) - ); - }; - const window = ( - - {typeof content === "function" ? content({ close }) : content} - - ); - setWindows((currentWindows) => currentWindows.concat(window)); - }; return { - open: openModalWindow, + open: function (content: Parameters[0]) { + return new Promise((resolve) => { + newKeyRef.current++; + const close = (result?: T) => { + setWindows((currentWindows) => + currentWindows.filter((aWindow) => aWindow !== window) + ); + // Make sure (?) to resolve the promise after the dialog is closed, + // for the focus to be restored to the previous element before the + // potential further actions take place. + requestAnimationFrame(() => { + resolve(result); + }); + }; + const window = ( + { + close(); + }, + }} + key={newKeyRef.current} + > + {typeof content === "function" + ? // @ts-expect-error close signature is not correctly inferred + content({ close }) + : content} + + ); + setWindows((currentWindows) => currentWindows.concat(window)); + }); + }, }; }, []); diff --git a/yarn.lock b/yarn.lock index 734eac34..b4fefccd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2909,7 +2909,7 @@ __metadata: "@react-aria/link": ~3.2.5 "@react-aria/listbox": ~3.12.1 "@react-aria/menu": ~3.14.1 - "@react-aria/overlays": ~3.22.1 + "@react-aria/overlays": ~3.23.1 "@react-aria/progress": ~3.1.8 "@react-aria/select": ~3.14.7 "@react-aria/selection": ~3.18.1 @@ -5920,7 +5920,7 @@ __metadata: languageName: node linkType: hard -"@react-aria/overlays@npm:^3.22.1, @react-aria/overlays@npm:^3.23.1": +"@react-aria/overlays@npm:^3.22.1, @react-aria/overlays@npm:^3.23.1, @react-aria/overlays@npm:~3.23.1": version: 3.23.1 resolution: "@react-aria/overlays@npm:3.23.1" dependencies: @@ -5942,28 +5942,6 @@ __metadata: languageName: node linkType: hard -"@react-aria/overlays@npm:~3.22.1": - version: 3.22.1 - resolution: "@react-aria/overlays@npm:3.22.1" - dependencies: - "@react-aria/focus": ^3.17.1 - "@react-aria/i18n": ^3.11.1 - "@react-aria/interactions": ^3.21.3 - "@react-aria/ssr": ^3.9.4 - "@react-aria/utils": ^3.24.1 - "@react-aria/visually-hidden": ^3.8.12 - "@react-stately/overlays": ^3.6.7 - "@react-types/button": ^3.9.4 - "@react-types/overlays": ^3.8.7 - "@react-types/shared": ^3.23.1 - "@swc/helpers": ^0.5.0 - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 - checksum: 14a81edee5d2d2af05d21f209c712e4838753aaec7ad7bea4d362a2c354c70cb7f5b2e70cf549ce644f5849af1be7721550cfa577e973d4241605f55ab916115 - languageName: node - linkType: hard - "@react-aria/progress@npm:~3.1.8": version: 3.1.8 resolution: "@react-aria/progress@npm:3.1.8" @@ -6054,7 +6032,7 @@ __metadata: languageName: node linkType: hard -"@react-aria/ssr@npm:^3.9.4, @react-aria/ssr@npm:^3.9.5": +"@react-aria/ssr@npm:^3.9.5": version: 3.9.5 resolution: "@react-aria/ssr@npm:3.9.5" dependencies: @@ -6150,7 +6128,7 @@ __metadata: languageName: node linkType: hard -"@react-aria/visually-hidden@npm:^3.8.12, @react-aria/visually-hidden@npm:^3.8.14, @react-aria/visually-hidden@npm:~3.8.14": +"@react-aria/visually-hidden@npm:^3.8.14, @react-aria/visually-hidden@npm:~3.8.14": version: 3.8.14 resolution: "@react-aria/visually-hidden@npm:3.8.14" dependencies: @@ -6502,7 +6480,7 @@ __metadata: languageName: node linkType: hard -"@react-types/overlays@npm:^3.6.1, @react-types/overlays@npm:^3.8.7, @react-types/overlays@npm:^3.8.9": +"@react-types/overlays@npm:^3.6.1, @react-types/overlays@npm:^3.8.9": version: 3.8.9 resolution: "@react-types/overlays@npm:3.8.9" dependencies: