Skip to content

Commit

Permalink
fix(segment-button): protect connectedCallback for when segment-conte…
Browse files Browse the repository at this point in the history
…nt 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. -->
  • Loading branch information
OS-pedrolourenco authored Feb 10, 2025
1 parent 295fa00 commit 14b6538
Show file tree
Hide file tree
Showing 3 changed files with 84 additions and 8 deletions.
51 changes: 43 additions & 8 deletions core/src/components/segment-button/segment-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { ComponentInterface } from '@stencil/core';
import { Component, Element, Host, Prop, Method, State, Watch, forceUpdate, h } from '@stencil/core';
import type { ButtonInterface } from '@utils/element-interface';
import type { Attributes } from '@utils/helpers';
import { addEventListener, removeEventListener, inheritAttributes } from '@utils/helpers';
import { addEventListener, removeEventListener, inheritAttributes, getNextSiblingOfType } from '@utils/helpers';
import { hostContext } from '@utils/theme';

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

connectedCallback() {
private waitForSegmentContent(ionSegment: HTMLIonSegmentElement | null, contentId: string): Promise<HTMLElement> {
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 = getNextSiblingOfType<HTMLIonSegmentViewElement>(ionSegment); // Skip the text nodes
const segmentContent = segmentView?.querySelector(
`ion-segment-content[id="${contentId}"]`
) as HTMLIonSegmentContentElement | null;
if (segmentContent && timeoutId) {
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();
Expand All @@ -76,12 +110,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;
}

Expand Down
30 changes: 30 additions & 0 deletions core/src/components/segment-view/test/basic/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,8 @@
<button class="expand" onClick="changeSegmentContent()">Change Segment Content</button>

<button class="expand" onClick="clearSegmentValue()">Clear Segment Value</button>

<button class="expand" onClick="addSegmentButtonAndContent()">Add New Segment Button & Content</button>
</ion-content>

<ion-footer>
Expand Down Expand Up @@ -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 = '<ion-label>New Button</ion-label>';

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);
}
</script>
</ion-app>
</body>
Expand Down
11 changes: 11 additions & 0 deletions core/src/utils/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -413,3 +413,14 @@ export const shallowEqualStringMap = (

return true;
};

export const getNextSiblingOfType = <T extends Element>(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;
};

0 comments on commit 14b6538

Please sign in to comment.