Skip to content

Commit ec4eb83

Browse files
committed
refactor(checkbox): use promise
1 parent 45b78c1 commit ec4eb83

File tree

2 files changed

+54
-4
lines changed

2 files changed

+54
-4
lines changed

core/src/components/checkbox/checkbox.tsx

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { ComponentInterface, EventEmitter } from '@stencil/core';
2-
import { Component, Element, Event, Host, Method, Prop, State, h, forceUpdate } from '@stencil/core';
2+
import { Component, Element, Event, Host, Method, Prop, State, h, forceUpdate, Build } from '@stencil/core';
3+
import { checkInvalidState } from '@utils/forms';
34
import type { Attributes } from '@utils/helpers';
45
import { inheritAriaAttributes, renderHiddenInput } from '@utils/helpers';
56
import { createColorClasses, hostContext } from '@utils/theme';
@@ -35,6 +36,7 @@ export class Checkbox implements ComponentInterface {
3536
private helperTextId = `${this.inputId}-helper-text`;
3637
private errorTextId = `${this.inputId}-error-text`;
3738
private inheritedAttributes: Attributes = {};
39+
private validationObserver?: MutationObserver;
3840

3941
@Element() el!: HTMLIonCheckboxElement;
4042

@@ -125,6 +127,8 @@ export class Checkbox implements ComponentInterface {
125127
*/
126128
@State() isInvalid = false;
127129

130+
@State() private hintTextID?: string;
131+
128132
/**
129133
* Emitted when the checked property has changed as a result of a user action such as a click.
130134
*
@@ -143,7 +147,45 @@ export class Checkbox implements ComponentInterface {
143147
@Event() ionBlur!: EventEmitter<void>;
144148

145149
connectedCallback() {
146-
// Always set initial state.
150+
const { el } = this;
151+
152+
// Watch for class changes to update validation state.
153+
if (Build.isBrowser && typeof MutationObserver !== 'undefined') {
154+
this.validationObserver = new MutationObserver(() => {
155+
const newIsInvalid = checkInvalidState(el);
156+
if (this.isInvalid !== newIsInvalid) {
157+
this.isInvalid = newIsInvalid;
158+
/**
159+
* Screen readers tend to announce changes
160+
* to `aria-describedby` when the attribute
161+
* is changed during a blur event for a
162+
* native form control.
163+
* However, the announcement can be spotty
164+
* when using a non-native form control
165+
* and `forceUpdate()`.
166+
* This is due to `forceUpdate()` internally
167+
* rescheduling the DOM update to a lower
168+
* priority queue regardless if it's called
169+
* inside a Promise or not, thus causing
170+
* the screen reader to potentially miss the
171+
* change.
172+
* By using a State variable inside a Promise,
173+
* it guarantees a re-render immediately at
174+
* a higher priority.
175+
*/
176+
Promise.resolve().then(() => {
177+
this.hintTextID = this.getHintTextID();
178+
});
179+
}
180+
});
181+
182+
this.validationObserver.observe(el, {
183+
attributes: true,
184+
attributeFilter: ['class'],
185+
});
186+
}
187+
188+
// Always set initial state
147189
this.isInvalid = this.checkInvalidState();
148190
}
149191

@@ -153,6 +195,14 @@ export class Checkbox implements ComponentInterface {
153195
};
154196
}
155197

198+
disconnectedCallback() {
199+
// Clean up validation observer to prevent memory leaks.
200+
if (this.validationObserver) {
201+
this.validationObserver.disconnect();
202+
this.validationObserver = undefined;
203+
}
204+
}
205+
156206
/** @internal */
157207
@Method()
158208
async setFocus() {
@@ -301,7 +351,7 @@ export class Checkbox implements ComponentInterface {
301351
<Host
302352
role="checkbox"
303353
aria-checked={indeterminate ? 'mixed' : `${checked}`}
304-
aria-describedby={this.getHintTextID()}
354+
aria-describedby={this.hintTextID}
305355
aria-invalid={this.isInvalid ? 'true' : undefined}
306356
aria-labelledby={hasLabelContent ? this.inputLabelId : null}
307357
aria-label={inheritedAttributes['aria-label'] || null}

core/src/utils/forms/validity.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
type FormElement = HTMLIonInputElement | HTMLIonTextareaElement | HTMLIonSelectElement;
1+
type FormElement = HTMLIonInputElement | HTMLIonTextareaElement | HTMLIonSelectElement | HTMLIonCheckboxElement;
22

33
/**
44
* Checks if the form element is in an invalid state based on

0 commit comments

Comments
 (0)