Skip to content

Commit f4ac445

Browse files
authored
fix(checkbox): show labels after page navigation (#31062)
Issue number: resolves #31052 --------- <!-- 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? After a page navigation, ion-checkbox's `onslotchange` event fires before the element's `textContent` has been updated. It is called again after `textContent` becomes readable on Safari, but is not called again after the `textContent` becomes readable on Chrome and Firefox. ## What is the new behavior? <!-- Please describe the behavior or changes that are being added by this PR. --> - Uses `MutationObserver` instead of `onslotchange` and fires specifically on character data changes. This ensures `hasLabelContent` is up to date. - `MutationObserver` does not fire on load, so `hasLabelContent` is initialized in `connectedCallback` ## 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. -->
1 parent 308aef5 commit f4ac445

File tree

1 file changed

+37
-31
lines changed

1 file changed

+37
-31
lines changed

core/src/components/checkbox/checkbox.tsx

Lines changed: 37 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -151,44 +151,54 @@ export class Checkbox implements ComponentInterface {
151151
connectedCallback() {
152152
const { el } = this;
153153

154-
// Watch for class changes to update validation state.
155154
if (Build.isBrowser && typeof MutationObserver !== 'undefined') {
156-
this.validationObserver = new MutationObserver(() => {
157-
const newIsInvalid = checkInvalidState(el);
158-
if (this.isInvalid !== newIsInvalid) {
159-
this.isInvalid = newIsInvalid;
160-
/**
161-
* Screen readers tend to announce changes
162-
* to `aria-describedby` when the attribute
163-
* is changed during a blur event for a
164-
* native form control.
165-
* However, the announcement can be spotty
166-
* when using a non-native form control
167-
* and `forceUpdate()`.
168-
* This is due to `forceUpdate()` internally
169-
* rescheduling the DOM update to a lower
170-
* priority queue regardless if it's called
171-
* inside a Promise or not, thus causing
172-
* the screen reader to potentially miss the
173-
* change.
174-
* By using a State variable inside a Promise,
175-
* it guarantees a re-render immediately at
176-
* a higher priority.
177-
*/
178-
Promise.resolve().then(() => {
179-
this.hintTextId = this.getHintTextId();
180-
});
155+
this.validationObserver = new MutationObserver((mutations) => {
156+
// Watch for label content changes
157+
if (mutations.some((mutation) => mutation.type === 'characterData' || mutation.type === 'childList')) {
158+
this.hasLabelContent = this.el.textContent !== '';
159+
}
160+
// Watch for class changes to update validation state.
161+
if (mutations.some((mutation) => mutation.type === 'attributes' && mutation.target === el)) {
162+
const newIsInvalid = checkInvalidState(el);
163+
if (this.isInvalid !== newIsInvalid) {
164+
this.isInvalid = newIsInvalid;
165+
/**
166+
* Screen readers tend to announce changes
167+
* to `aria-describedby` when the attribute
168+
* is changed during a blur event for a
169+
* native form control.
170+
* However, the announcement can be spotty
171+
* when using a non-native form control
172+
* and `forceUpdate()`.
173+
* This is due to `forceUpdate()` internally
174+
* rescheduling the DOM update to a lower
175+
* priority queue regardless if it's called
176+
* inside a Promise or not, thus causing
177+
* the screen reader to potentially miss the
178+
* change.
179+
* By using a State variable inside a Promise,
180+
* it guarantees a re-render immediately at
181+
* a higher priority.
182+
*/
183+
Promise.resolve().then(() => {
184+
this.hintTextId = this.getHintTextId();
185+
});
186+
}
181187
}
182188
});
183189

184190
this.validationObserver.observe(el, {
185191
attributes: true,
186192
attributeFilter: ['class'],
193+
characterData: true,
194+
childList: true,
195+
subtree: true,
187196
});
188197
}
189198

190199
// Always set initial state
191200
this.isInvalid = checkInvalidState(el);
201+
this.hasLabelContent = this.el.textContent !== '';
192202
}
193203

194204
componentWillLoad() {
@@ -267,10 +277,6 @@ export class Checkbox implements ComponentInterface {
267277
ev.stopPropagation();
268278
};
269279

270-
private onSlotChange = () => {
271-
this.hasLabelContent = this.el.textContent !== '';
272-
};
273-
274280
private getHintTextId(): string | undefined {
275281
const { helperText, errorText, helperTextId, errorTextId, isInvalid } = this;
276282

@@ -387,7 +393,7 @@ export class Checkbox implements ComponentInterface {
387393
id={this.inputLabelId}
388394
onClick={this.onDivLabelClick}
389395
>
390-
<slot onSlotchange={this.onSlotChange}></slot>
396+
<slot></slot>
391397
{this.renderHintText()}
392398
</div>
393399
<div class="native-wrapper">

0 commit comments

Comments
 (0)