From fe0b32ff367d3908acdcfd00243bb4bfb3ca436c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Louren=C3=A7o?= Date: Thu, 16 Jan 2025 16:04:56 +0000 Subject: [PATCH] fix(segment-button): protect connectedCallback for when segment-content has not yet been created (#30133) Issue number: internal --------- ## What is the current behavior? When the `connectedCallback` method is called for a segment-button and its corresponding segment-content has not been created in that instant, a console error is thrown and the method returns. ## What is the new behavior? - `connectedCallback` will now wait, at most 1 second, for the corresponding segment-content to be created. - The new behaviour can be tested in segment-view/basic. ## Does this introduce a breaking change? - [ ] Yes - [x] No ## Other information --- .../segment-button/segment-button.tsx | 60 ++++++++++++++++--- .../segment-view/test/basic/index.html | 30 ++++++++++ 2 files changed, 83 insertions(+), 7 deletions(-) diff --git a/core/src/components/segment-button/segment-button.tsx b/core/src/components/segment-button/segment-button.tsx index e670643066..d4a5d74266 100644 --- a/core/src/components/segment-button/segment-button.tsx +++ b/core/src/components/segment-button/segment-button.tsx @@ -67,7 +67,52 @@ export class SegmentButton implements ComponentInterface, ButtonInterface { this.updateState(); } - connectedCallback() { + private getNextSiblingOfType(element: Element): T | null { + let sibling = element.nextSibling; + while (sibling) { + if (sibling.nodeType === Node.ELEMENT_NODE && (sibling as T) !== null) { + return sibling as T; + } + sibling = sibling.nextSibling; + } + return null; + } + + private waitForSegmentContent(ionSegment: HTMLIonSegmentElement | null, contentId: string): Promise { + return new Promise((resolve, reject) => { + let timeoutId: NodeJS.Timeout | undefined = undefined; + let animationFrameId: number; + + const check = () => { + if (!ionSegment) { + reject(new Error(`Segment not found when looking for Segment Content`)); + return; + } + + const segmentView = this.getNextSiblingOfType(ionSegment); // Skip the text nodes + const segmentContent = segmentView?.querySelector( + `ion-segment-content[id="${contentId}"]` + ) as HTMLIonSegmentContentElement | null; + if (segmentContent) { + clearTimeout(timeoutId); // Clear the timeout if the segmentContent is found + cancelAnimationFrame(animationFrameId); + resolve(segmentContent); + } else { + animationFrameId = requestAnimationFrame(check); // Keep checking on the next animation frame + } + }; + + check(); + + // Set a timeout to reject the promise + timeoutId = setTimeout(() => { + cancelAnimationFrame(animationFrameId); + reject(new Error(`Unable to find Segment Content with id="${contentId} within 1000 ms`)); + }, 1000); + }); + } + + async connectedCallback() { const segmentEl = (this.segmentEl = this.el.closest('ion-segment')); if (segmentEl) { this.updateState(); @@ -78,12 +123,13 @@ export class SegmentButton implements ComponentInterface, ButtonInterface { // Return if there is no contentId defined if (!this.contentId) return; - // Attempt to find the Segment Content by its contentId - const segmentContent = document.getElementById(this.contentId) as HTMLIonSegmentContentElement | null; - - // If no associated Segment Content exists, log an error and return - if (!segmentContent) { - console.error(`Segment Button: Unable to find Segment Content with id="${this.contentId}".`); + let segmentContent; + try { + // Attempt to find the Segment Content by its contentId + segmentContent = await this.waitForSegmentContent(segmentEl, this.contentId); + } catch (error) { + // If no associated Segment Content exists, log an error and return + console.error('Segment Button: ', (error as Error).message); return; } diff --git a/core/src/components/segment-view/test/basic/index.html b/core/src/components/segment-view/test/basic/index.html index 69d36d4a6c..78dec1d9ff 100644 --- a/core/src/components/segment-view/test/basic/index.html +++ b/core/src/components/segment-view/test/basic/index.html @@ -123,6 +123,8 @@ + + @@ -158,6 +160,34 @@ segment.value = undefined; }); } + + async function addSegmentButtonAndContent() { + const segment = document.querySelector('ion-segment'); + const segmentView = document.querySelector('ion-segment-view'); + + const newButton = document.createElement('ion-segment-button'); + const newId = `new-${Date.now()}`; + newButton.setAttribute('content-id', newId); + newButton.setAttribute('value', newId); + newButton.innerHTML = 'New Button'; + + segment.appendChild(newButton); + + setTimeout(() => { + // Timeout to test waitForSegmentContent() in segment-button + const newContent = document.createElement('ion-segment-content'); + newContent.setAttribute('id', newId); + newContent.innerHTML = 'New Content'; + + segmentView.appendChild(newContent); + + // Necessary timeout to ensure the value is set after the content is added. + // Otherwise, the transition is unsuccessful and the content is not shown. + setTimeout(() => { + segment.setAttribute('value', newId); + }, 200); + }, 200); + }