From 174cfc9836c54d3bad20684d4f03adf7954d874a Mon Sep 17 00:00:00 2001 From: GCHQ-Developer-847 <111882109+GCHQ-Developer-847@users.noreply.github.com> Date: Wed, 19 Feb 2025 12:16:05 +0000 Subject: [PATCH 1/2] feat(react): add Cypress test for slotting interactive content after first load Add Cypress test to check that interactive elements are focussable when they are slotted in after first load (also includes updates to children of slotted elements). Update React storybook - move slotted interactive content behaviour to separate story (matching web components storybook) and add child slotted element for checking new fix. .#2773 --- .../component-tests/IcDialog/IcDialog.cy.tsx | 13 ++ .../IcDialog/IcDialogTestData.tsx | 32 +++++ packages/react/src/stories/ic-dialog.mdx | 6 + .../react/src/stories/ic-dialog.stories.js | 124 +++++++++--------- 4 files changed, 114 insertions(+), 61 deletions(-) diff --git a/packages/react/src/component-tests/IcDialog/IcDialog.cy.tsx b/packages/react/src/component-tests/IcDialog/IcDialog.cy.tsx index b7b0e7a527..64176e4ea8 100644 --- a/packages/react/src/component-tests/IcDialog/IcDialog.cy.tsx +++ b/packages/react/src/component-tests/IcDialog/IcDialog.cy.tsx @@ -7,6 +7,7 @@ import { NoBackgroundClickDialog, SimpleDialog, SlottedContentDialog, + SlottedUpdatedContentDialog, NoHeightConstraintDialog, AlertDialog, SizeDialog, @@ -81,6 +82,18 @@ describe("IcDialog end-to-end tests", () => { cy.get("ic-button#test-button").should(HAVE_FOCUS); }); + it("should focus interactive content added after first load - including children of slotted elements", () => { + mount(); + + cy.get(DIALOG).should("exist"); + cy.get("ic-button#display-btn-1-btn").click().wait(300); + cy.get("ic-button#display-btn-2-btn").click(); + cy.get("ic-button#display-dialog-btn").click().wait(300); + cy.get(DIALOG).should(HAVE_ATTR, "open"); + cy.get("ic-button#test-button-1").should(HAVE_FOCUS).realPress("Tab"); + cy.get("ic-button#test-button-2").should(HAVE_FOCUS); + }); + it("should not hide dialog on background click when background click is disabled", () => { mount(); diff --git a/packages/react/src/component-tests/IcDialog/IcDialogTestData.tsx b/packages/react/src/component-tests/IcDialog/IcDialogTestData.tsx index 13f41bfee9..f9c22ebb99 100644 --- a/packages/react/src/component-tests/IcDialog/IcDialogTestData.tsx +++ b/packages/react/src/component-tests/IcDialog/IcDialogTestData.tsx @@ -87,6 +87,38 @@ export const SlottedContentDialog = () => { ); }; +export const SlottedUpdatedContentDialog = () => { + const [showBtn1, setShowBtn1] = useState(false); + const [showBtn2, setShowBtn2] = useState(false); + const dialogEl = useRef(null); + const handleClick = () => { + dialogEl.current.open = true; + }; + return ( + <> + setShowBtn1(true)}> + Display button 1 + + setShowBtn2(true)}> + Display button 2 + + + Display dialog + + +
+ {showBtn1 && Test button 1} +
+ {showBtn2 && Test button 2} +
+ + ); +}; + export const DialogAccordion = () => { const dialogEl = useRef(null); const handleClick = () => { diff --git a/packages/react/src/stories/ic-dialog.mdx b/packages/react/src/stories/ic-dialog.mdx index 46b79c20ad..8da9798fd4 100644 --- a/packages/react/src/stories/ic-dialog.mdx +++ b/packages/react/src/stories/ic-dialog.mdx @@ -57,6 +57,12 @@ import * as IcDialogStories from './ic-dialog.stories'; +### Show / hide interactive elements + + + + + ### Hidden close button diff --git a/packages/react/src/stories/ic-dialog.stories.js b/packages/react/src/stories/ic-dialog.stories.js index fa73dfc8d9..7744a60099 100644 --- a/packages/react/src/stories/ic-dialog.stories.js +++ b/packages/react/src/stories/ic-dialog.stories.js @@ -35,66 +35,6 @@ const showLargeDialog = () => { dialog.open = true; }; -const DialogContent = () => { - const [show, setShow] = useState(false); - const handleShow = (show) => { - setShow(show); - }; - return ( - <> - - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod - tempor incididunt ut labore et dolore magna aliqua. - - handleShow(true)}>Show - handleShow(false)}>Hide - {show && ( - <> - Tab Here - - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do - eiusmod tempor incididunt ut labore et dolore magna aliqua. Lorem - ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod - tempor incididunt ut labore et dolore magna aliqua. Lorem ipsum - dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor - incididunt ut labore et dolore magna aliqua. Lorem ipsum dolor sit - amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt - ut labore et dolore magna aliqua. Lorem ipsum dolor sit amet, - consectetur adipiscing elit, sed do eiusmod tempor incididunt ut - labore et dolore magna aliqua. Lorem ipsum dolor sit amet, - consectetur adipiscing elit, sed do eiusmod tempor incididunt ut - labore et dolore magna aliqua. Lorem ipsum dolor sit amet, - consectetur adipiscing elit, sed do eiusmod tempor incididunt ut - labore et dolore magna aliqua. Lorem ipsum dolor sit amet, - consectetur adipiscing elit, sed do eiusmod tempor incididunt ut - labore et dolore magna aliqua. Lorem ipsum dolor sit amet, - consectetur adipiscing elit, sed do eiusmod tempor incididunt ut - labore et dolore magna aliqua. Lorem ipsum dolor sit amet, - consectetur adipiscing elit, sed do eiusmod tempor incididunt ut - labore et dolore magna aliqua. Lorem ipsum dolor sit amet, - consectetur adipiscing elit, sed do eiusmod tempor incididunt ut - labore et dolore magna aliqua. Lorem ipsum dolor sit amet, - consectetur adipiscing elit, sed do eiusmod tempor incididunt ut - labore et dolore magna aliqua. Lorem ipsum dolor sit amet, - consectetur adipiscing elit, sed do eiusmod tempor incididunt ut - labore et dolore magna aliqua. Lorem ipsum dolor sit amet, - consectetur adipiscing elit, sed do eiusmod tempor incididunt ut - labore et dolore magna aliqua. Lorem ipsum dolor sit amet, - consectetur adipiscing elit, sed do eiusmod tempor incididunt ut - labore et dolore magna aliqua. Lorem ipsum dolor sit amet, - consectetur adipiscing elit, sed do eiusmod tempor incididunt ut - labore et dolore magna aliqua. Lorem ipsum dolor sit amet, - consectetur adipiscing elit, sed do eiusmod tempor incididunt ut - labore et dolore magna aliqua. Lorem ipsum dolor sit amet, - consectetur adipiscing elit, sed do eiusmod tempor incididunt ut - labore et dolore magna aliqua. - - - )} - - ); -}; - const showDialog = () => { const dialog = document.querySelector("ic-dialog"); dialog.open = true; @@ -149,6 +89,49 @@ const dropdownItems = () => { }); }; +const showShowHideContentDialog = () => { + const dialog = document.querySelector("#show-hide-content-dialog"); + dialog.open = true; +}; + +const ShowHideContent = () => { + const [showEl1, setShowEl1] = useState(false); + const [showEl2, setShowEl2] = useState(false); + const handleShow = (show) => { + // Delay prevents false positive by ensuring the two slot updates happen at separate times + setTimeout(() => { + setShowEl1(show); + }, 2000); + setShowEl2(show); + }; + return ( + <> + + Demonstrates changes to slotted elements happening after first load. +
+ The button which is a child of an existing slotted element will update after a 2s delay. +
+
+ handleShow(true)}>Show + handleShow(false)}>Hide +
+ {showEl1 && ( + Child of slotted element + )} +
+ {showEl2 && ( + <> + Slotted element + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do + eiusmod tempor incididunt ut labore et dolore magna aliqua. + + + )} + + ); +}; + const showHiddenCloseButtonDialog = () => { const dialog = document.querySelector("#hidden-close-button-dialog"); dialog.open = true; @@ -203,7 +186,10 @@ export const Sizes = { size="medium" id="medium-dialog" > - + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod + tempor incididunt ut labore et dolore magna aliqua. + ( + <> + Launch show / hide content dialog + + + + + ), + + name: "Show / hide interactive elements", +} + export const HiddenCloseButton = { render: () => ( <> From 7c2c0f4cd5452eb516fd2e45e8a747840cf58777 Mon Sep 17 00:00:00 2001 From: GCHQ-Developer-847 <111882109+GCHQ-Developer-847@users.noreply.github.com> Date: Wed, 19 Feb 2025 13:22:18 +0000 Subject: [PATCH 2/2] fix(web-components): update dialog to make interactive slotted children content focussable Update ic-dialog to make interactive children of slotted elements focussable when added after first page load. Update web components storybook to help test that the fix works. .#2773 --- .../src/components/ic-dialog/ic-dialog.mdx | 2 +- .../components/ic-dialog/ic-dialog.stories.js | 40 +++++++++++++------ .../src/components/ic-dialog/ic-dialog.tsx | 26 ++++++++++-- 3 files changed, 51 insertions(+), 17 deletions(-) diff --git a/packages/web-components/src/components/ic-dialog/ic-dialog.mdx b/packages/web-components/src/components/ic-dialog/ic-dialog.mdx index 31c1b1f7f1..fa4606d65d 100644 --- a/packages/web-components/src/components/ic-dialog/ic-dialog.mdx +++ b/packages/web-components/src/components/ic-dialog/ic-dialog.mdx @@ -55,7 +55,7 @@ import * as IcDialogStories from "./ic-dialog.stories";
-### Show/hide interactive elements +### Show / hide interactive elements diff --git a/packages/web-components/src/components/ic-dialog/ic-dialog.stories.js b/packages/web-components/src/components/ic-dialog/ic-dialog.stories.js index 10517eb5ac..c1fb783016 100644 --- a/packages/web-components/src/components/ic-dialog/ic-dialog.stories.js +++ b/packages/web-components/src/components/ic-dialog/ic-dialog.stories.js @@ -911,40 +911,56 @@ export const DisableWidthConstraint = { export const ShowHideInteractiveElements = { render: () => html` - Launch small dialogLaunch show / hide content dialog + - - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod - tempor incididunt ut labore et dolore magna aliqua. + Demonstrates changes to slotted elements happening after first load. +
+ The button which is a child of an existing slotted element will update + after a 2s delay.
+
Show Hide
`, - name: "show/hide interactive elements", + name: "Show / hide interactive elements", }; export const HiddenCloseButton = { diff --git a/packages/web-components/src/components/ic-dialog/ic-dialog.tsx b/packages/web-components/src/components/ic-dialog/ic-dialog.tsx index b4472cee94..85df9fb631 100644 --- a/packages/web-components/src/components/ic-dialog/ic-dialog.tsx +++ b/packages/web-components/src/components/ic-dialog/ic-dialog.tsx @@ -16,6 +16,7 @@ import { isSlotUsed, checkResizeObserver, onComponentRequiredPropUndefined, + getSlotElements, } from "../../utils/helpers"; import { IcThemeMode } from "../../utils/types"; @@ -33,6 +34,7 @@ import { IcThemeMode } from "../../utils/types"; export class Dialog { private backdropEl: HTMLDivElement; private contentArea: HTMLSlotElement; + private contentAreaMutationObserver: MutationObserver = null; private DATA_GETS_FOCUS: string = "data-gets-focus"; private DATA_GETS_FOCUS_SELECTOR: string = "[data-gets-focus]"; private DIALOG_CONTROLS: string = "dialog-controls"; @@ -288,12 +290,26 @@ export class Dialog { }; private refreshInteractiveElementsOnSlotChange = () => { - this.contentArea = this.el.shadowRoot.querySelector("#dialog-content slot"); + const contentWrapper = this.el.shadowRoot.querySelector("#dialog-content"); + this.contentArea = contentWrapper.querySelector("slot"); + // Detect changes to slotted elements this.contentArea.addEventListener( "slotchange", this.getInteractiveElements ); + + this.contentAreaMutationObserver = new MutationObserver(() => { + this.getInteractiveElements(); + }); + + // Detect changes to children of slotted elements + getSlotElements(contentWrapper).forEach((el) => { + this.contentAreaMutationObserver.observe(el, { + childList: true, + subtree: true, + }); + }); }; private removeSlotChangeListener = () => { @@ -302,6 +318,8 @@ export class Dialog { "slotchange", this.getInteractiveElements ); + + this.contentAreaMutationObserver?.disconnect(); } }; @@ -353,9 +371,9 @@ export class Dialog { ); const slottedInteractiveElements = Array.from( this.el.querySelectorAll( - `a[href], button, input:not(.ic-input), textarea, select, details, [tabindex]:not([tabindex="-1"]), - ic-button, ic-checkbox, ic-select, ic-search-bar, ic-tab-group, ic-radio-group, - ic-back-to-top, ic-breadcrumb, ic-chip[dismissible="true"], ic-footer-link, ic-link, ic-navigation-button, + `a[href], button, input:not(.ic-input), textarea, select, details, [tabindex]:not([tabindex="-1"]), + ic-button, ic-checkbox, ic-select, ic-search-bar, ic-tab-group, ic-radio-group, + ic-back-to-top, ic-breadcrumb, ic-chip[dismissible="true"], ic-footer-link, ic-link, ic-navigation-button, ic-navigation-item, ic-switch, ic-text-field, ic-accordion-group, ic-accordion, ic-date-input, ic-date-picker` ) );