diff --git a/packages/ui/src/elements/LeaveWithoutSaving/usePreventLeave.tsx b/packages/ui/src/elements/LeaveWithoutSaving/usePreventLeave.tsx index f8c029741df..5b1caf5c63b 100644 --- a/packages/ui/src/elements/LeaveWithoutSaving/usePreventLeave.tsx +++ b/packages/ui/src/elements/LeaveWithoutSaving/usePreventLeave.tsx @@ -112,10 +112,35 @@ export const usePreventLeave = ({ return element as HTMLAnchorElement } + function findClosestButton(element: HTMLElement | null): HTMLButtonElement | null { + while (element && element.tagName.toLowerCase() !== 'button') { + element = element.parentElement + } + return element as HTMLButtonElement + } + + function isDrawerCloseButton(button: HTMLButtonElement): boolean { + // Check for drawer close button classes or IDs + const className = button.className || '' + const id = button.id || '' + + // Only intercept document drawer close buttons, not all modal close buttons + return ( + className.includes('drawer__close') || + className.includes('drawer__header__close') || + className.includes('doc-drawer__header-close') || + id.startsWith('close-drawer__') + ) + } + function handleClick(event: MouseEvent) { try { const target = event.target as HTMLElement const anchor = findClosestAnchor(target) + const button = findClosestButton(target) + + let shouldPrevent = false + if (anchor) { const currentUrl = window.location.href const newUrl = anchor.href @@ -125,17 +150,22 @@ export const usePreventLeave = ({ const isPageLeaving = !(newUrl === currentUrl || isAnchor || isDownloadLink || isNewTab) - if (isPageLeaving && prevent && (!onPrevent ? !window.confirm(message) : true)) { - // Keep a reference of the href + if (isPageLeaving) { + shouldPrevent = true cancelledURL.current = newUrl + } + } else if (button && isDrawerCloseButton(button)) { + // Handle drawer close buttons + shouldPrevent = true + } - // Cancel the route change - event.preventDefault() - event.stopPropagation() + if (shouldPrevent && prevent && (!onPrevent ? !window.confirm(message) : true)) { + // Cancel the action + event.preventDefault() + event.stopPropagation() - if (typeof onPrevent === 'function') { - onPrevent() - } + if (typeof onPrevent === 'function') { + onPrevent() } } } catch (err) { diff --git a/test/_community/payload-types.ts b/test/_community/payload-types.ts index 599c9dec1d7..c0c16ad8011 100644 --- a/test/_community/payload-types.ts +++ b/test/_community/payload-types.ts @@ -130,7 +130,7 @@ export interface Post { root: { type: string; children: { - type: string; + type: any; version: number; [k: string]: unknown; }[]; diff --git a/test/admin/e2e/document-view/e2e.spec.ts b/test/admin/e2e/document-view/e2e.spec.ts index c2c2747f3c1..692799a4725 100644 --- a/test/admin/e2e/document-view/e2e.spec.ts +++ b/test/admin/e2e/document-view/e2e.spec.ts @@ -509,6 +509,132 @@ describe('Document View', () => { const afterHeader = page.locator('[id^=doc-drawer_posts_1_] .doc-drawer__after-header') await expect(afterHeader).toBeVisible() }) + + test('should prompt leave without saving modal when closing document drawer with unsaved changes', async () => { + // Navigate to a post document + await navigateToDoc(page, postsUrl) + + // Open the relationship drawer + await page + .locator('.field-type.relationship .relationship--single-value__drawer-toggler') + .click() + + const drawer = page.locator('[id^=doc-drawer_posts_1_]') + const drawerEditView = drawer.locator('.drawer__content .collection-edit') + await expect(drawerEditView).toBeVisible() + + // Make changes to the form in the drawer + const drawerTitleField = drawerEditView.locator('#field-title') + const testTitle = 'Modified Title That Should Trigger Modal' + await drawerTitleField.fill(testTitle) + await expect(drawerTitleField).toHaveValue(testTitle) + + // Try to close the drawer by clicking the X button + const drawerCloseButton = drawer.locator('.doc-drawer__header-close') + await expect(drawerCloseButton).toBeVisible() + await drawerCloseButton.click() + + // Should show the leave without saving modal + const leaveModal = page.locator('#leave-without-saving') + await expect(leaveModal).toBeVisible() + + // Verify modal content + await expect(leaveModal.locator('.confirmation-modal__title')).toHaveText('Leave without saving?') + + // Test cancel functionality - click "Stay on this page" + const cancelButton = leaveModal.locator('.confirmation-modal__controls .btn').first() + await cancelButton.click() + + // Modal should close and drawer should still be open + await expect(leaveModal).toBeHidden() + await expect(drawer).toBeVisible() + await expect(drawerTitleField).toHaveValue(testTitle) + + // Try closing again and this time confirm leaving + await drawerCloseButton.click() + await expect(leaveModal).toBeVisible() + + // Click "Leave anyway" button + const leaveButton = leaveModal.locator('.confirmation-modal__controls .btn--style-primary') + await leaveButton.click() + + // Modal and drawer should both be closed + await expect(leaveModal).toBeHidden() + await expect(drawer).toBeHidden() + }) + + test('should not prompt leave without saving modal when closing document drawer without changes', async () => { + // Navigate to a post document + await navigateToDoc(page, postsUrl) + + // Open the relationship drawer + await page + .locator('.field-type.relationship .relationship--single-value__drawer-toggler') + .click() + + const drawer = page.locator('[id^=doc-drawer_posts_1_]') + const drawerEditView = drawer.locator('.drawer__content .collection-edit') + await expect(drawerEditView).toBeVisible() + + // Don't make any changes, just close the drawer + const drawerCloseButton = drawer.locator('.doc-drawer__header-close') + await expect(drawerCloseButton).toBeVisible() + await drawerCloseButton.click() + + // Should not show the leave without saving modal + const leaveModal = page.locator('#leave-without-saving') + await expect(leaveModal).toBeHidden() + + // Drawer should be closed immediately + await expect(drawer).toBeHidden() + }) + + test('should prompt leave without saving modal when closing document drawer using bottom close button', async () => { + // Navigate to a post document + await navigateToDoc(page, postsUrl) + + // Open the relationship drawer + await page + .locator('.field-type.relationship .relationship--single-value__drawer-toggler') + .click() + + const drawer = page.locator('[id^=doc-drawer_posts_1_]') + const drawerEditView = drawer.locator('.drawer__content .collection-edit') + await expect(drawerEditView).toBeVisible() + + // Make changes to the form in the drawer + const drawerTitleField = drawerEditView.locator('#field-title') + const testTitle = 'Another Modified Title' + await drawerTitleField.fill(testTitle) + await expect(drawerTitleField).toHaveValue(testTitle) + + // Try to close the drawer using a generic drawer close button (if it exists) + // Look for buttons with drawer__close class + const drawerCloseButtons = drawer.locator('button.drawer__close, button[class*="drawer__close"]') + const closeButtonCount = await drawerCloseButtons.count() + + if (closeButtonCount > 0) { + await drawerCloseButtons.first().click() + + // Should show the leave without saving modal + const leaveModal = page.locator('#leave-without-saving') + await expect(leaveModal).toBeVisible() + + // Cancel to clean up + const cancelButton = leaveModal.locator('.confirmation-modal__controls .btn').first() + await cancelButton.click() + await expect(leaveModal).toBeHidden() + } + + // Clean up by saving or discarding changes + const drawerCloseButton = drawer.locator('.doc-drawer__header-close') + await drawerCloseButton.click() + const leaveModal = page.locator('#leave-without-saving') + if (await leaveModal.isVisible()) { + const leaveButton = leaveModal.locator('.confirmation-modal__controls .btn--style-primary') + await leaveButton.click() + } + }) }) describe('descriptions', () => {