Skip to content

Commit 14b6538

Browse files
fix(segment-button): protect connectedCallback for when segment-content has not yet been created (cherry-pick) (#30138)
Issue number: internal --------- <!-- Please do not submit updates to dependencies unless it fixes an issue. --> <!-- Please try to limit your pull request to one type (bugfix, feature, etc). Submit multiple pull requests if needed. --> ## What is the current behavior? <!-- Please describe the current behavior that you are modifying. --> 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? <!-- Please describe the behavior or changes that are being added by this PR. --> - `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 <!-- If this introduces a breaking change: 1. Describe the impact and migration path for existing applications below. 2. Update the BREAKING.md file with the breaking change. 3. Add "BREAKING CHANGE: [...]" to the commit description when merging. See https://github.com/ionic-team/ionic-framework/blob/main/docs/CONTRIBUTING.md#footer for more information. --> ## Other information <!-- Any other information that is important to this PR such as screenshots of how the component looks before and after the change. -->
1 parent 295fa00 commit 14b6538

File tree

3 files changed

+84
-8
lines changed

3 files changed

+84
-8
lines changed

core/src/components/segment-button/segment-button.tsx

+43-8
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { ComponentInterface } from '@stencil/core';
22
import { Component, Element, Host, Prop, Method, State, Watch, forceUpdate, h } from '@stencil/core';
33
import type { ButtonInterface } from '@utils/element-interface';
44
import type { Attributes } from '@utils/helpers';
5-
import { addEventListener, removeEventListener, inheritAttributes } from '@utils/helpers';
5+
import { addEventListener, removeEventListener, inheritAttributes, getNextSiblingOfType } from '@utils/helpers';
66
import { hostContext } from '@utils/theme';
77

88
import { getIonMode } from '../../global/ionic-global';
@@ -65,7 +65,41 @@ export class SegmentButton implements ComponentInterface, ButtonInterface {
6565
this.updateState();
6666
}
6767

68-
connectedCallback() {
68+
private waitForSegmentContent(ionSegment: HTMLIonSegmentElement | null, contentId: string): Promise<HTMLElement> {
69+
return new Promise((resolve, reject) => {
70+
let timeoutId: NodeJS.Timeout | undefined = undefined;
71+
let animationFrameId: number;
72+
73+
const check = () => {
74+
if (!ionSegment) {
75+
reject(new Error(`Segment not found when looking for Segment Content`));
76+
return;
77+
}
78+
79+
const segmentView = getNextSiblingOfType<HTMLIonSegmentViewElement>(ionSegment); // Skip the text nodes
80+
const segmentContent = segmentView?.querySelector(
81+
`ion-segment-content[id="${contentId}"]`
82+
) as HTMLIonSegmentContentElement | null;
83+
if (segmentContent && timeoutId) {
84+
clearTimeout(timeoutId); // Clear the timeout if the segmentContent is found
85+
cancelAnimationFrame(animationFrameId);
86+
resolve(segmentContent);
87+
} else {
88+
animationFrameId = requestAnimationFrame(check); // Keep checking on the next animation frame
89+
}
90+
};
91+
92+
check();
93+
94+
// Set a timeout to reject the promise
95+
timeoutId = setTimeout(() => {
96+
cancelAnimationFrame(animationFrameId);
97+
reject(new Error(`Unable to find Segment Content with id="${contentId} within 1000 ms`));
98+
}, 1000);
99+
});
100+
}
101+
102+
async connectedCallback() {
69103
const segmentEl = (this.segmentEl = this.el.closest('ion-segment'));
70104
if (segmentEl) {
71105
this.updateState();
@@ -76,12 +110,13 @@ export class SegmentButton implements ComponentInterface, ButtonInterface {
76110
// Return if there is no contentId defined
77111
if (!this.contentId) return;
78112

79-
// Attempt to find the Segment Content by its contentId
80-
const segmentContent = document.getElementById(this.contentId) as HTMLIonSegmentContentElement | null;
81-
82-
// If no associated Segment Content exists, log an error and return
83-
if (!segmentContent) {
84-
console.error(`Segment Button: Unable to find Segment Content with id="${this.contentId}".`);
113+
let segmentContent;
114+
try {
115+
// Attempt to find the Segment Content by its contentId
116+
segmentContent = await this.waitForSegmentContent(segmentEl, this.contentId);
117+
} catch (error) {
118+
// If no associated Segment Content exists, log an error and return
119+
console.error('Segment Button: ', (error as Error).message);
85120
return;
86121
}
87122

core/src/components/segment-view/test/basic/index.html

+30
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,8 @@
123123
<button class="expand" onClick="changeSegmentContent()">Change Segment Content</button>
124124

125125
<button class="expand" onClick="clearSegmentValue()">Clear Segment Value</button>
126+
127+
<button class="expand" onClick="addSegmentButtonAndContent()">Add New Segment Button & Content</button>
126128
</ion-content>
127129

128130
<ion-footer>
@@ -158,6 +160,34 @@
158160
segment.value = undefined;
159161
});
160162
}
163+
164+
async function addSegmentButtonAndContent() {
165+
const segment = document.querySelector('ion-segment');
166+
const segmentView = document.querySelector('ion-segment-view');
167+
168+
const newButton = document.createElement('ion-segment-button');
169+
const newId = `new-${Date.now()}`;
170+
newButton.setAttribute('content-id', newId);
171+
newButton.setAttribute('value', newId);
172+
newButton.innerHTML = '<ion-label>New Button</ion-label>';
173+
174+
segment.appendChild(newButton);
175+
176+
setTimeout(() => {
177+
// Timeout to test waitForSegmentContent() in segment-button
178+
const newContent = document.createElement('ion-segment-content');
179+
newContent.setAttribute('id', newId);
180+
newContent.innerHTML = 'New Content';
181+
182+
segmentView.appendChild(newContent);
183+
184+
// Necessary timeout to ensure the value is set after the content is added.
185+
// Otherwise, the transition is unsuccessful and the content is not shown.
186+
setTimeout(() => {
187+
segment.setAttribute('value', newId);
188+
}, 200);
189+
}, 200);
190+
}
161191
</script>
162192
</ion-app>
163193
</body>

core/src/utils/helpers.ts

+11
Original file line numberDiff line numberDiff line change
@@ -413,3 +413,14 @@ export const shallowEqualStringMap = (
413413

414414
return true;
415415
};
416+
417+
export const getNextSiblingOfType = <T extends Element>(element: Element): T | null => {
418+
let sibling = element.nextSibling;
419+
while (sibling) {
420+
if (sibling.nodeType === Node.ELEMENT_NODE && (sibling as T) !== null) {
421+
return sibling as T;
422+
}
423+
sibling = sibling.nextSibling;
424+
}
425+
return null;
426+
};

0 commit comments

Comments
 (0)