diff --git a/CHANGELOG.txt b/CHANGELOG.txt index a73c120ee4..db71d15274 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -22,6 +22,7 @@ Changelog * Fix: Resolve issue local development of docs when running `make livehtml` (Sage Abdullah) * Fix: Resolve issue with unwanted padding in chooser modal listings (Sage Abdullah) * Fix: Ensure form builder emails that have date or datetime fields correctly localize dates based on the configured `LANGUAGE_CODE` (Mark Niehues) + * Fix: Ensure the Stimulus `UnsavedController` checks for nested removal/additions of inputs so that the unsaved warning shows in more valid cases when editing a page (Karthik Ayangar) * Docs: Add contributing development documentation on how to work with a fork of Wagtail (Nix Asteri, Dan Braghis) * Docs: Make sure the settings panel is listed in tabbed interface examples (Tibor Leupold) * Docs: Update content and page names to their US spelling instead of UK spelling (Victoria Poromon) diff --git a/client/src/controllers/UnsavedController.test.js b/client/src/controllers/UnsavedController.test.js index 4020c21f98..293d0e8bcc 100644 --- a/client/src/controllers/UnsavedController.test.js +++ b/client/src/controllers/UnsavedController.test.js @@ -112,6 +112,96 @@ describe('UnsavedController', () => { expect(events['w-unsaved:add']).toHaveLength(1); expect(events['w-unsaved:add'][0]).toHaveProperty('detail.type', 'edits'); }); + + it('should allow checking for when an input is removed', async () => { + expect(events['w-unsaved:add']).toHaveLength(0); + + await setup(); + + // setup should not fire any event + expect(events['w-unsaved:add']).toHaveLength(0); + + const input = document.getElementById('name'); + + input.remove(); + + await jest.runAllTimersAsync(); + + expect(events['w-unsaved:add']).toHaveLength(1); + expect(events['w-unsaved:add'][0]).toHaveProperty('detail.type', 'edits'); + }); + + it('should ignore when non-inputs are added', async () => { + expect(events['w-unsaved:add']).toHaveLength(0); // Ensure no initial events + + await setup(); + + expect(events['w-unsaved:add']).toHaveLength(0); // Verify no events after setup + + // Act (simulate the addition of a paragraph) + const paragraph = document.createElement('p'); + paragraph.id = 'paraName'; + paragraph.textContent = 'This is a new paragraph'; // Add some content for clarity + document.getElementsByTagName('form')[0].appendChild(paragraph); // paragraph is added + + await jest.runAllTimersAsync(); + + // Assert (verify no events were fired) + expect(events['w-unsaved:add']).toHaveLength(0); + }); + + it('should fire an event when a textarea is added', async () => { + expect(events['w-unsaved:add']).toHaveLength(0); // Ensure no initial events + + await setup(); + + expect(events['w-unsaved:add']).toHaveLength(0); // Verify no events after setup + + // Act (simulate adding a textarea with value) + const textarea = document.createElement('textarea'); + textarea.value = 'Some initial content'; + textarea.id = 'taName'; + document.getElementsByTagName('form')[0].appendChild(textarea); + + await jest.runAllTimersAsync(); // Allow any timers to trigger + + // Assert (verify event was fired) + expect(events['w-unsaved:add']).toHaveLength(1); + expect(events['w-unsaved:add'][0]).toHaveProperty('detail.type', 'edits'); + }); + + it('should fire an event when a nested input (select) is added', async () => { + // Arrange + expect(events['w-unsaved:add']).toHaveLength(0); // Ensure no initial events + + await setup(); + + expect(events['w-unsaved:add']).toHaveLength(0); // Verify no events after setup + + // Act + const div = document.createElement('div'); + const select = document.createElement('select'); + select.id = 'mySelect'; + + const option1 = document.createElement('option'); + option1.value = 'option1'; + option1.textContent = 'Option 1'; + select.appendChild(option1); + + const option2 = document.createElement('option'); + option2.value = 'option2'; + option2.textContent = 'Option 2'; + select.appendChild(option2); + + div.appendChild(select); + document.body.getElementsByTagName('form')[0].appendChild(div); + + await jest.runAllTimersAsync(); + + // Assert + expect(events['w-unsaved:add']).toHaveLength(1); + expect(events['w-unsaved:add'][0]).toHaveProperty('detail.type', 'edits'); + }); }); describe('showing a confirmation message when exiting the browser tab', () => { diff --git a/client/src/controllers/UnsavedController.ts b/client/src/controllers/UnsavedController.ts index 22d286bf73..d9fab07a70 100644 --- a/client/src/controllers/UnsavedController.ts +++ b/client/src/controllers/UnsavedController.ts @@ -198,6 +198,17 @@ export class UnsavedController extends Controller { if (current !== previous) this.notify(); } + getIsValidNode(node: Node | null) { + if (!node || node.nodeType !== node.ELEMENT_NODE) return false; + + const validElements = ['input', 'textarea', 'select']; + + return ( + validElements.includes((node as Element).localName) || + (node as Element).querySelector(validElements.join(',')) !== null + ); + } + /** * Notify the user of changes to the form. * Dispatch events to update the footer message via dispatching events. @@ -288,15 +299,11 @@ export class UnsavedController extends Controller { detail: { initialFormData }, }); - const isValidInputNode = (node) => - node.nodeType === node.ELEMENT_NODE && - ['INPUT', 'TEXTAREA', 'SELECT'].includes(node.tagName); - const observer = new MutationObserver((mutationList) => { const hasMutationWithValidInputNode = mutationList.some( (mutation) => - Array.from(mutation.addedNodes).some(isValidInputNode) || - Array.from(mutation.removedNodes).some(isValidInputNode), + Array.from(mutation.addedNodes).some(this.getIsValidNode) || + Array.from(mutation.removedNodes).some(this.getIsValidNode), ); if (hasMutationWithValidInputNode) this.check(); diff --git a/docs/releases/6.1.md b/docs/releases/6.1.md index 3ae5f7c7d2..81ebf6f827 100644 --- a/docs/releases/6.1.md +++ b/docs/releases/6.1.md @@ -35,6 +35,7 @@ depth: 1 * Resolve issue local development of docs when running `make livehtml` (Sage Abdullah) * Resolve issue with unwanted padding in chooser modal listings (Sage Abdullah) * Ensure form builder emails that have date or datetime fields correctly localize dates based on the configured `LANGUAGE_CODE` (Mark Niehues) + * Ensure the Stimulus `UnsavedController` checks for nested removal/additions of inputs so that the unsaved warning shows in more valid cases when editing a page (Karthik Ayangar) ### Documentation