Skip to content

Commit 80ee5f7

Browse files
fix(segment-button): protect connectedCallback for when segment-content has not yet been created (#30133)
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 3f8346e commit 80ee5f7

File tree

2 files changed

+83
-7
lines changed

2 files changed

+83
-7
lines changed

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

Lines changed: 53 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,52 @@ export class SegmentButton implements ComponentInterface, ButtonInterface {
6565
this.updateState();
6666
}
6767

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

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}".`);
124+
let segmentContent;
125+
try {
126+
// Attempt to find the Segment Content by its contentId
127+
segmentContent = await this.waitForSegmentContent(segmentEl, this.contentId);
128+
} catch (error) {
129+
// If no associated Segment Content exists, log an error and return
130+
console.error('Segment Button: ', (error as Error).message);
85131
return;
86132
}
87133

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

Lines changed: 30 additions & 0 deletions
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>

0 commit comments

Comments
 (0)