@ionic/core 8.7.7-nightly.20251015 → 8.7.8-nightly.20251016
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/button.js +3 -7
- package/components/header.js +41 -3
- package/components/ion-input.js +6 -14
- package/components/ion-select.js +58 -10
- package/components/ion-textarea.js +5 -13
- package/components/{notch-controller.js → validity.js} +14 -1
- package/dist/cjs/ion-app_8.cjs.entry.js +41 -3
- package/dist/cjs/ion-button_2.cjs.entry.js +3 -7
- package/dist/cjs/ion-input.cjs.entry.js +7 -15
- package/dist/cjs/ion-select_3.cjs.entry.js +56 -10
- package/dist/cjs/ion-textarea.cjs.entry.js +6 -14
- package/dist/cjs/ionic.cjs.js +1 -1
- package/dist/cjs/loader.cjs.js +1 -1
- package/dist/cjs/{notch-controller-Bzqhjm4f.js → validity-C8QoAYT2.js} +14 -0
- package/dist/collection/components/button/button.js +3 -7
- package/dist/collection/components/header/header.js +5 -4
- package/dist/collection/components/header/header.utils.js +37 -0
- package/dist/collection/components/input/input.js +6 -14
- package/dist/collection/components/select/select.js +59 -11
- package/dist/collection/components/textarea/textarea.js +5 -13
- package/dist/collection/utils/forms/index.js +1 -0
- package/dist/collection/utils/forms/validity.js +15 -0
- package/dist/docs.json +1 -1
- package/dist/esm/ion-app_8.entry.js +41 -3
- package/dist/esm/ion-button_2.entry.js +3 -7
- package/dist/esm/ion-input.entry.js +6 -14
- package/dist/esm/ion-select_3.entry.js +55 -9
- package/dist/esm/ion-textarea.entry.js +5 -13
- package/dist/esm/ionic.js +1 -1
- package/dist/esm/loader.js +1 -1
- package/dist/esm/{notch-controller-BwelN_JM.js → validity-B8oWougr.js} +14 -1
- package/dist/ionic/ionic.esm.js +1 -1
- package/dist/ionic/p-43ed1ef5.entry.js +4 -0
- package/dist/ionic/p-4c85d268.entry.js +4 -0
- package/dist/ionic/p-4cc26913.entry.js +4 -0
- package/dist/ionic/p-8bdfc8f6.entry.js +4 -0
- package/dist/ionic/{p-DCv9sLH2.js → p-DieJyvMP.js} +1 -1
- package/dist/ionic/p-f65f9308.entry.js +4 -0
- package/dist/types/components/header/header.utils.d.ts +10 -0
- package/dist/types/components/input/input.d.ts +0 -4
- package/dist/types/components/select/select.d.ts +6 -0
- package/dist/types/components/textarea/textarea.d.ts +0 -4
- package/dist/types/utils/forms/index.d.ts +1 -0
- package/dist/types/utils/forms/validity.d.ts +10 -0
- package/hydrate/index.js +86 -41
- package/hydrate/index.mjs +86 -41
- package/package.json +2 -2
- package/dist/ionic/p-1c8a476d.entry.js +0 -4
- package/dist/ionic/p-7647da93.entry.js +0 -4
- package/dist/ionic/p-785026d7.entry.js +0 -4
- package/dist/ionic/p-78c74a3e.entry.js +0 -4
- package/dist/ionic/p-913a7c1e.entry.js +0 -4
|
@@ -152,4 +152,18 @@ const createNotchController = (el, getNotchSpacerEl, getLabelSlot) => {
|
|
|
152
152
|
};
|
|
153
153
|
};
|
|
154
154
|
|
|
155
|
+
/**
|
|
156
|
+
* Checks if the form element is in an invalid state based on
|
|
157
|
+
* Ionic validation classes.
|
|
158
|
+
*
|
|
159
|
+
* @param el The form element to check.
|
|
160
|
+
* @returns `true` if the element is invalid, `false` otherwise.
|
|
161
|
+
*/
|
|
162
|
+
const checkInvalidState = (el) => {
|
|
163
|
+
const hasIonTouched = el.classList.contains('ion-touched');
|
|
164
|
+
const hasIonInvalid = el.classList.contains('ion-invalid');
|
|
165
|
+
return hasIonTouched && hasIonInvalid;
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
exports.checkInvalidState = checkInvalidState;
|
|
155
169
|
exports.createNotchController = createNotchController;
|
|
@@ -216,11 +216,7 @@ export class Button {
|
|
|
216
216
|
target,
|
|
217
217
|
};
|
|
218
218
|
let fill = this.fill;
|
|
219
|
-
|
|
220
|
-
* We check both undefined and null to
|
|
221
|
-
* work around https://github.com/ionic-team/stencil/issues/3586.
|
|
222
|
-
*/
|
|
223
|
-
if (fill == null) {
|
|
219
|
+
if (fill === undefined) {
|
|
224
220
|
fill = this.inToolbar || this.inListHeader ? 'clear' : 'solid';
|
|
225
221
|
}
|
|
226
222
|
/**
|
|
@@ -233,7 +229,7 @@ export class Button {
|
|
|
233
229
|
{
|
|
234
230
|
type !== 'button' && this.renderHiddenButton();
|
|
235
231
|
}
|
|
236
|
-
return (h(Host, { key: '
|
|
232
|
+
return (h(Host, { key: 'ed82ea53705523f9afc5f1a9addff44cc6424f27', onClick: this.handleClick, "aria-disabled": disabled ? 'true' : null, class: createColorClasses(color, {
|
|
237
233
|
[mode]: true,
|
|
238
234
|
[buttonType]: true,
|
|
239
235
|
[`${buttonType}-${expand}`]: expand !== undefined,
|
|
@@ -248,7 +244,7 @@ export class Button {
|
|
|
248
244
|
'button-disabled': disabled,
|
|
249
245
|
'ion-activatable': true,
|
|
250
246
|
'ion-focusable': true,
|
|
251
|
-
}) }, h(TagType, Object.assign({ key: '
|
|
247
|
+
}) }, h(TagType, Object.assign({ key: 'fadec13053469dd0405bbbc61b70ced568aa4826' }, attrs, { class: "button-native", part: "native", disabled: disabled, onFocus: this.onFocus, onBlur: this.onBlur }, inheritedAttributes), h("span", { key: '6bf0e5144fb1148002e88038522402b789689d2c', class: "button-inner" }, h("slot", { key: '25da0ca155cfa9e2754842c34f4fd09f576ac2d2', name: "icon-only", onSlotchange: this.slotChanged }), h("slot", { key: '51414065bb11953ec9d818f8d9353589bc9072c5', name: "start" }), h("slot", { key: 'c9b5f8842aeabd20628df2f4600f1257ea913d8d' }), h("slot", { key: '478dd3671c7be1909fc84e672f0fa8dfe6082263', name: "end" })), mode === 'md' && h("ion-ripple-effect", { key: 'e1d55f85a55144d743f58a5914cd116cb065fa8c', type: this.rippleType }))));
|
|
252
248
|
}
|
|
253
249
|
static get is() { return "ion-button"; }
|
|
254
250
|
static get encapsulation() { return "shadow"; }
|
|
@@ -6,7 +6,7 @@ import { findIonContent, getScrollElement, printIonContentErrorMsg } from "../..
|
|
|
6
6
|
import { inheritAriaAttributes } from "../../utils/helpers";
|
|
7
7
|
import { hostContext } from "../../utils/theme";
|
|
8
8
|
import { getIonMode } from "../../global/ionic-global";
|
|
9
|
-
import { cloneElement, createHeaderIndex, handleContentScroll, handleHeaderFade, handleToolbarIntersection, setHeaderActive, setToolbarBackgroundOpacity, } from "./header.utils";
|
|
9
|
+
import { cloneElement, createHeaderIndex, handleContentScroll, handleHeaderFade, handleToolbarIntersection, setHeaderActive, setToolbarBackgroundOpacity, getRoleType, } from "./header.utils";
|
|
10
10
|
/**
|
|
11
11
|
* @virtualProp {"ios" | "md"} mode - The mode determines which platform styles to use.
|
|
12
12
|
*/
|
|
@@ -145,16 +145,17 @@ export class Header {
|
|
|
145
145
|
const { translucent, inheritedAttributes } = this;
|
|
146
146
|
const mode = getIonMode(this);
|
|
147
147
|
const collapse = this.collapse || 'none';
|
|
148
|
+
const isCondensed = collapse === 'condense';
|
|
148
149
|
// banner role must be at top level, so remove role if inside a menu
|
|
149
|
-
const roleType = hostContext('ion-menu', this.el)
|
|
150
|
-
return (h(Host, Object.assign({ key: '
|
|
150
|
+
const roleType = getRoleType(hostContext('ion-menu', this.el), isCondensed, mode);
|
|
151
|
+
return (h(Host, Object.assign({ key: '863c4568cd7b8c0ec55109f193bbbaed68a1346e', role: roleType, class: {
|
|
151
152
|
[mode]: true,
|
|
152
153
|
// Used internally for styling
|
|
153
154
|
[`header-${mode}`]: true,
|
|
154
155
|
[`header-translucent`]: this.translucent,
|
|
155
156
|
[`header-collapse-${collapse}`]: true,
|
|
156
157
|
[`header-translucent-${mode}`]: this.translucent,
|
|
157
|
-
} }, inheritedAttributes), mode === 'ios' && translucent && h("div", { key: '
|
|
158
|
+
} }, inheritedAttributes), mode === 'ios' && translucent && h("div", { key: '25c3bdce328b0b35607d154c8b8374679313d881', class: "header-background" }), h("slot", { key: 'b44fab0a9be7920b9650da26117c783e751e1702' })));
|
|
158
159
|
}
|
|
159
160
|
static get is() { return "ion-header"; }
|
|
160
161
|
static get originalStyleUrls() {
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
import { readTask, writeTask } from "@stencil/core";
|
|
5
5
|
import { clamp } from "../../utils/helpers";
|
|
6
6
|
const TRANSITION = 'all 0.2s ease-in-out';
|
|
7
|
+
const ROLE_NONE = 'none';
|
|
8
|
+
const ROLE_BANNER = 'banner';
|
|
7
9
|
export const cloneElement = (tagName) => {
|
|
8
10
|
const getCachedEl = document.querySelector(`${tagName}.ion-cloned-element`);
|
|
9
11
|
if (getCachedEl !== null) {
|
|
@@ -130,6 +132,7 @@ export const setHeaderActive = (headerIndex, active = true) => {
|
|
|
130
132
|
const toolbars = headerIndex.toolbars;
|
|
131
133
|
const ionTitles = toolbars.map((toolbar) => toolbar.ionTitleEl);
|
|
132
134
|
if (active) {
|
|
135
|
+
headerEl.setAttribute('role', ROLE_BANNER);
|
|
133
136
|
headerEl.classList.remove('header-collapse-condense-inactive');
|
|
134
137
|
ionTitles.forEach((ionTitle) => {
|
|
135
138
|
if (ionTitle) {
|
|
@@ -138,6 +141,16 @@ export const setHeaderActive = (headerIndex, active = true) => {
|
|
|
138
141
|
});
|
|
139
142
|
}
|
|
140
143
|
else {
|
|
144
|
+
/**
|
|
145
|
+
* There can only be one banner landmark per page.
|
|
146
|
+
* By default, all ion-headers have the banner role.
|
|
147
|
+
* This causes an accessibility issue when using a
|
|
148
|
+
* condensed header since there are two ion-headers
|
|
149
|
+
* on the page at once (active and inactive).
|
|
150
|
+
* To solve this, the role needs to be toggled
|
|
151
|
+
* based on which header is active.
|
|
152
|
+
*/
|
|
153
|
+
headerEl.setAttribute('role', ROLE_NONE);
|
|
141
154
|
headerEl.classList.add('header-collapse-condense-inactive');
|
|
142
155
|
/**
|
|
143
156
|
* The small title should only be accessed by screen readers
|
|
@@ -197,3 +210,27 @@ export const handleHeaderFade = (scrollEl, baseEl, condenseHeader) => {
|
|
|
197
210
|
});
|
|
198
211
|
});
|
|
199
212
|
};
|
|
213
|
+
/**
|
|
214
|
+
* Get the role type for the ion-header.
|
|
215
|
+
*
|
|
216
|
+
* @param isInsideMenu If ion-header is inside ion-menu.
|
|
217
|
+
* @param isCondensed If ion-header has collapse="condense".
|
|
218
|
+
* @param mode The current mode.
|
|
219
|
+
* @returns 'none' if inside ion-menu or if condensed in md
|
|
220
|
+
* mode, otherwise 'banner'.
|
|
221
|
+
*/
|
|
222
|
+
export const getRoleType = (isInsideMenu, isCondensed, mode) => {
|
|
223
|
+
// If the header is inside a menu, it should not have the banner role.
|
|
224
|
+
if (isInsideMenu) {
|
|
225
|
+
return ROLE_NONE;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Only apply role="none" to `md` mode condensed headers
|
|
229
|
+
* since the large header is never shown.
|
|
230
|
+
*/
|
|
231
|
+
if (isCondensed && mode === 'md') {
|
|
232
|
+
return ROLE_NONE;
|
|
233
|
+
}
|
|
234
|
+
// Default to banner role.
|
|
235
|
+
return ROLE_BANNER;
|
|
236
|
+
};
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* (C) Ionic http://ionicframework.com - MIT License
|
|
3
3
|
*/
|
|
4
4
|
import { Build, Host, forceUpdate, h, } from "@stencil/core";
|
|
5
|
-
import { createNotchController } from "../../utils/forms/index";
|
|
5
|
+
import { createNotchController, checkInvalidState } from "../../utils/forms/index";
|
|
6
6
|
import { inheritAriaAttributes, debounceEvent, inheritAttributes, componentOnReady } from "../../utils/helpers";
|
|
7
7
|
import { createSlotMutationController } from "../../utils/slot-mutation-controller";
|
|
8
8
|
import { createColorClasses, hostContext } from "../../utils/theme";
|
|
@@ -227,14 +227,6 @@ export class Input {
|
|
|
227
227
|
componentWillLoad() {
|
|
228
228
|
this.inheritedAttributes = Object.assign(Object.assign({}, inheritAriaAttributes(this.el)), inheritAttributes(this.el, ['tabindex', 'title', 'data-form-type', 'dir']));
|
|
229
229
|
}
|
|
230
|
-
/**
|
|
231
|
-
* Checks if the input is in an invalid state based on Ionic validation classes
|
|
232
|
-
*/
|
|
233
|
-
checkInvalidState() {
|
|
234
|
-
const hasIonTouched = this.el.classList.contains('ion-touched');
|
|
235
|
-
const hasIonInvalid = this.el.classList.contains('ion-invalid');
|
|
236
|
-
return hasIonTouched && hasIonInvalid;
|
|
237
|
-
}
|
|
238
230
|
connectedCallback() {
|
|
239
231
|
const { el } = this;
|
|
240
232
|
this.slotMutationController = createSlotMutationController(el, ['label', 'start', 'end'], () => forceUpdate(this));
|
|
@@ -242,7 +234,7 @@ export class Input {
|
|
|
242
234
|
// Watch for class changes to update validation state
|
|
243
235
|
if (Build.isBrowser && typeof MutationObserver !== 'undefined') {
|
|
244
236
|
this.validationObserver = new MutationObserver(() => {
|
|
245
|
-
const newIsInvalid =
|
|
237
|
+
const newIsInvalid = checkInvalidState(el);
|
|
246
238
|
if (this.isInvalid !== newIsInvalid) {
|
|
247
239
|
this.isInvalid = newIsInvalid;
|
|
248
240
|
// Force a re-render to update aria-describedby immediately
|
|
@@ -255,7 +247,7 @@ export class Input {
|
|
|
255
247
|
});
|
|
256
248
|
}
|
|
257
249
|
// Always set initial state
|
|
258
|
-
this.isInvalid =
|
|
250
|
+
this.isInvalid = checkInvalidState(el);
|
|
259
251
|
this.debounceChanged();
|
|
260
252
|
if (Build.isBrowser) {
|
|
261
253
|
document.dispatchEvent(new CustomEvent('ionInputDidLoad', {
|
|
@@ -519,7 +511,7 @@ export class Input {
|
|
|
519
511
|
* TODO(FW-5592): Remove hasStartEndSlots condition
|
|
520
512
|
*/
|
|
521
513
|
const labelShouldFloat = labelPlacement === 'stacked' || (labelPlacement === 'floating' && (hasValue || hasFocus || hasStartEndSlots));
|
|
522
|
-
return (h(Host, { key: '
|
|
514
|
+
return (h(Host, { key: '97b5308021064d9e7434ef2d3d96f27045c1b0c4', class: createColorClasses(this.color, {
|
|
523
515
|
[mode]: true,
|
|
524
516
|
'has-value': hasValue,
|
|
525
517
|
'has-focus': hasFocus,
|
|
@@ -530,14 +522,14 @@ export class Input {
|
|
|
530
522
|
'in-item': inItem,
|
|
531
523
|
'in-item-color': hostContext('ion-item.ion-color', this.el),
|
|
532
524
|
'input-disabled': disabled,
|
|
533
|
-
}) }, h("label", { key: '
|
|
525
|
+
}) }, h("label", { key: '353f68726ce180299bd9adc81e5ff7d26a48f54f', class: "input-wrapper", htmlFor: inputId, onClick: this.onLabelClick }, this.renderLabelContainer(), h("div", { key: '2034b4bad04fc157f3298a1805819216b6f439d0', class: "native-wrapper", onClick: this.onLabelClick }, h("slot", { key: '96bb5e30176b2bd76dfb75bfbf6c1c3d4403f4bb', name: "start" }), h("input", Object.assign({ key: '1a1d75b0e414a95c89d5a760757c33548d234aca', class: "native-input", ref: (input) => (this.nativeInput = input), id: inputId, disabled: disabled, autoCapitalize: this.autocapitalize, autoComplete: this.autocomplete, autoCorrect: this.autocorrect, autoFocus: this.autofocus, enterKeyHint: this.enterkeyhint, inputMode: this.inputmode, min: this.min, max: this.max, minLength: this.minlength, maxLength: this.maxlength, multiple: this.multiple, name: this.name, pattern: this.pattern, placeholder: this.placeholder || '', readOnly: readonly, required: this.required, spellcheck: this.spellcheck, step: this.step, type: this.type, value: value, onInput: this.onInput, onChange: this.onChange, onBlur: this.onBlur, onFocus: this.onFocus, onKeyDown: this.onKeydown, onCompositionstart: this.onCompositionStart, onCompositionend: this.onCompositionEnd, "aria-describedby": this.getHintTextID(), "aria-invalid": this.isInvalid ? 'true' : undefined }, this.inheritedAttributes)), this.clearInput && !readonly && !disabled && (h("button", { key: '95f3df17b7691d9a2e7dcd4a51f16a94aa3ca36f', "aria-label": "reset", type: "button", class: "input-clear-icon", onPointerDown: (ev) => {
|
|
534
526
|
/**
|
|
535
527
|
* This prevents mobile browsers from
|
|
536
528
|
* blurring the input when the clear
|
|
537
529
|
* button is activated.
|
|
538
530
|
*/
|
|
539
531
|
ev.preventDefault();
|
|
540
|
-
}, onClick: this.clearTextInput }, h("ion-icon", { key: '
|
|
532
|
+
}, onClick: this.clearTextInput }, h("ion-icon", { key: '16b0af75eed50c8115fb5597f73b5fbf71c2530e', "aria-hidden": "true", icon: clearIconData }))), h("slot", { key: 'c48da0f8ddb3764ac43efa705bb4a6bb2d9cc2fd', name: "end" })), shouldRenderHighlight && h("div", { key: 'f15238481fc20de56ca7ecb6e350b3c024cc755e', class: "input-highlight" })), this.renderBottomContent()));
|
|
541
533
|
}
|
|
542
534
|
static get is() { return "ion-input"; }
|
|
543
535
|
static get encapsulation() { return "scoped"; }
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/*!
|
|
2
2
|
* (C) Ionic http://ionicframework.com - MIT License
|
|
3
3
|
*/
|
|
4
|
-
import { Host, h, forceUpdate } from "@stencil/core";
|
|
5
|
-
import { compareOptions, createNotchController, isOptionSelected } from "../../utils/forms/index";
|
|
4
|
+
import { Build, Host, h, forceUpdate } from "@stencil/core";
|
|
5
|
+
import { compareOptions, createNotchController, isOptionSelected, checkInvalidState } from "../../utils/forms/index";
|
|
6
6
|
import { focusVisibleElement, renderHiddenInput, inheritAttributes } from "../../utils/helpers";
|
|
7
7
|
import { printIonWarning } from "../../utils/logging/index";
|
|
8
8
|
import { actionSheetController, alertController, popoverController, modalController } from "../../utils/overlays";
|
|
@@ -44,6 +44,10 @@ export class Select {
|
|
|
44
44
|
* is applied in both cases.
|
|
45
45
|
*/
|
|
46
46
|
this.hasFocus = false;
|
|
47
|
+
/**
|
|
48
|
+
* Track validation state for proper aria-live announcements.
|
|
49
|
+
*/
|
|
50
|
+
this.isInvalid = false;
|
|
47
51
|
/**
|
|
48
52
|
* The text to display on the cancel button.
|
|
49
53
|
*/
|
|
@@ -173,9 +177,46 @@ export class Select {
|
|
|
173
177
|
*/
|
|
174
178
|
forceUpdate(this);
|
|
175
179
|
});
|
|
180
|
+
// Watch for class changes to update validation state.
|
|
181
|
+
if (Build.isBrowser && typeof MutationObserver !== 'undefined') {
|
|
182
|
+
this.validationObserver = new MutationObserver(() => {
|
|
183
|
+
const newIsInvalid = checkInvalidState(this.el);
|
|
184
|
+
if (this.isInvalid !== newIsInvalid) {
|
|
185
|
+
this.isInvalid = newIsInvalid;
|
|
186
|
+
/**
|
|
187
|
+
* Screen readers tend to announce changes
|
|
188
|
+
* to `aria-describedby` when the attribute
|
|
189
|
+
* is changed during a blur event for a
|
|
190
|
+
* native form control.
|
|
191
|
+
* However, the announcement can be spotty
|
|
192
|
+
* when using a non-native form control
|
|
193
|
+
* and `forceUpdate()`.
|
|
194
|
+
* This is due to `forceUpdate()` internally
|
|
195
|
+
* rescheduling the DOM update to a lower
|
|
196
|
+
* priority queue regardless if it's called
|
|
197
|
+
* inside a Promise or not, thus causing
|
|
198
|
+
* the screen reader to potentially miss the
|
|
199
|
+
* change.
|
|
200
|
+
* By using a State variable inside a Promise,
|
|
201
|
+
* it guarantees a re-render immediately at
|
|
202
|
+
* a higher priority.
|
|
203
|
+
*/
|
|
204
|
+
Promise.resolve().then(() => {
|
|
205
|
+
this.hintTextID = this.getHintTextID();
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
this.validationObserver.observe(el, {
|
|
210
|
+
attributes: true,
|
|
211
|
+
attributeFilter: ['class'],
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
// Always set initial state
|
|
215
|
+
this.isInvalid = checkInvalidState(this.el);
|
|
176
216
|
}
|
|
177
217
|
componentWillLoad() {
|
|
178
218
|
this.inheritedAttributes = inheritAttributes(this.el, ['aria-label']);
|
|
219
|
+
this.hintTextID = this.getHintTextID();
|
|
179
220
|
}
|
|
180
221
|
componentDidLoad() {
|
|
181
222
|
/**
|
|
@@ -199,6 +240,11 @@ export class Select {
|
|
|
199
240
|
this.notchController.destroy();
|
|
200
241
|
this.notchController = undefined;
|
|
201
242
|
}
|
|
243
|
+
// Clean up validation observer to prevent memory leaks.
|
|
244
|
+
if (this.validationObserver) {
|
|
245
|
+
this.validationObserver.disconnect();
|
|
246
|
+
this.validationObserver = undefined;
|
|
247
|
+
}
|
|
202
248
|
}
|
|
203
249
|
/**
|
|
204
250
|
* Open the select overlay. The overlay is either an alert, action sheet, or popover,
|
|
@@ -715,11 +761,11 @@ export class Select {
|
|
|
715
761
|
}
|
|
716
762
|
renderListbox() {
|
|
717
763
|
const { disabled, inputId, isExpanded, required } = this;
|
|
718
|
-
return (h("button", { disabled: disabled, id: inputId, "aria-label": this.ariaLabel, "aria-haspopup": "dialog", "aria-expanded": `${isExpanded}`, "aria-describedby": this.
|
|
764
|
+
return (h("button", { disabled: disabled, id: inputId, "aria-label": this.ariaLabel, "aria-haspopup": "dialog", "aria-expanded": `${isExpanded}`, "aria-describedby": this.hintTextID, "aria-invalid": this.isInvalid ? 'true' : undefined, "aria-required": `${required}`, onFocus: this.onFocus, onBlur: this.onBlur, ref: (focusEl) => (this.focusEl = focusEl) }));
|
|
719
765
|
}
|
|
720
766
|
getHintTextID() {
|
|
721
|
-
const {
|
|
722
|
-
if (
|
|
767
|
+
const { helperText, errorText, helperTextId, errorTextId, isInvalid } = this;
|
|
768
|
+
if (isInvalid && errorText) {
|
|
723
769
|
return errorTextId;
|
|
724
770
|
}
|
|
725
771
|
if (helperText) {
|
|
@@ -731,10 +777,10 @@ export class Select {
|
|
|
731
777
|
* Renders the helper text or error text values
|
|
732
778
|
*/
|
|
733
779
|
renderHintText() {
|
|
734
|
-
const { helperText, errorText, helperTextId, errorTextId } = this;
|
|
780
|
+
const { helperText, errorText, helperTextId, errorTextId, isInvalid } = this;
|
|
735
781
|
return [
|
|
736
|
-
h("div", { id: helperTextId, class: "helper-text", part: "supporting-text helper-text" }, helperText),
|
|
737
|
-
h("div", { id: errorTextId, class: "error-text", part: "supporting-text error-text" }, errorText),
|
|
782
|
+
h("div", { id: helperTextId, class: "helper-text", part: "supporting-text helper-text", "aria-live": "polite" }, !isInvalid ? helperText : null),
|
|
783
|
+
h("div", { id: errorTextId, class: "error-text", part: "supporting-text error-text", role: "alert" }, isInvalid ? errorText : null),
|
|
738
784
|
];
|
|
739
785
|
}
|
|
740
786
|
/**
|
|
@@ -782,7 +828,7 @@ export class Select {
|
|
|
782
828
|
* TODO(FW-5592): Remove hasStartEndSlots condition
|
|
783
829
|
*/
|
|
784
830
|
const labelShouldFloat = labelPlacement === 'stacked' || (labelPlacement === 'floating' && (hasValue || isExpanded || hasStartEndSlots));
|
|
785
|
-
return (h(Host, { key: '
|
|
831
|
+
return (h(Host, { key: '35b5e18e6f79a802ff2d46d1242e80ff755cc0b9', onClick: this.onClick, class: createColorClasses(this.color, {
|
|
786
832
|
[mode]: true,
|
|
787
833
|
'in-item': inItem,
|
|
788
834
|
'in-item-color': hostContext('ion-item.ion-color', el),
|
|
@@ -800,7 +846,7 @@ export class Select {
|
|
|
800
846
|
[`select-justify-${justify}`]: justifyEnabled,
|
|
801
847
|
[`select-shape-${shape}`]: shape !== undefined,
|
|
802
848
|
[`select-label-placement-${labelPlacement}`]: true,
|
|
803
|
-
}) }, h("label", { key: '
|
|
849
|
+
}) }, h("label", { key: '6005b34a0c50bc4d7653a4276bc232ecd02e083c', class: "select-wrapper", id: "select-label", onClick: this.onLabelClick }, this.renderLabelContainer(), h("div", { key: 'c7e07aa81ae856c057f16275dd058f37c5670a47', class: "select-wrapper-inner" }, h("slot", { key: '7fc2deefe0424404caacdbbd9e08ed43ba55d28a', name: "start" }), h("div", { key: '157d74ee717b1bc30b5f1c233a09b0c8456aa68e', class: "native-wrapper", ref: (el) => (this.nativeWrapperEl = el), part: "container" }, this.renderSelectText(), this.renderListbox()), h("slot", { key: 'ea66db304528b82bf9317730b6dce3db2612f235', name: "end" }), !hasFloatingOrStackedLabel && this.renderSelectIcon()), hasFloatingOrStackedLabel && this.renderSelectIcon(), shouldRenderHighlight && h("div", { key: '786eb1530b7476f0615d4e7c0bf4e7e4dc66509c', class: "select-highlight" })), this.renderBottomContent()));
|
|
804
850
|
}
|
|
805
851
|
static get is() { return "ion-select"; }
|
|
806
852
|
static get encapsulation() { return "shadow"; }
|
|
@@ -1268,7 +1314,9 @@ export class Select {
|
|
|
1268
1314
|
static get states() {
|
|
1269
1315
|
return {
|
|
1270
1316
|
"isExpanded": {},
|
|
1271
|
-
"hasFocus": {}
|
|
1317
|
+
"hasFocus": {},
|
|
1318
|
+
"isInvalid": {},
|
|
1319
|
+
"hintTextID": {}
|
|
1272
1320
|
};
|
|
1273
1321
|
}
|
|
1274
1322
|
static get events() {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* (C) Ionic http://ionicframework.com - MIT License
|
|
3
3
|
*/
|
|
4
4
|
import { Build, Host, forceUpdate, h, writeTask, } from "@stencil/core";
|
|
5
|
-
import { createNotchController } from "../../utils/forms/index";
|
|
5
|
+
import { createNotchController, checkInvalidState } from "../../utils/forms/index";
|
|
6
6
|
import { inheritAriaAttributes, debounceEvent, inheritAttributes, componentOnReady } from "../../utils/helpers";
|
|
7
7
|
import { createSlotMutationController } from "../../utils/slot-mutation-controller";
|
|
8
8
|
import { createColorClasses, hostContext } from "../../utils/theme";
|
|
@@ -187,14 +187,6 @@ export class Textarea {
|
|
|
187
187
|
this.el.click();
|
|
188
188
|
}
|
|
189
189
|
}
|
|
190
|
-
/**
|
|
191
|
-
* Checks if the textarea is in an invalid state based on Ionic validation classes
|
|
192
|
-
*/
|
|
193
|
-
checkValidationState() {
|
|
194
|
-
const hasIonTouched = this.el.classList.contains('ion-touched');
|
|
195
|
-
const hasIonInvalid = this.el.classList.contains('ion-invalid');
|
|
196
|
-
return hasIonTouched && hasIonInvalid;
|
|
197
|
-
}
|
|
198
190
|
connectedCallback() {
|
|
199
191
|
const { el } = this;
|
|
200
192
|
this.slotMutationController = createSlotMutationController(el, ['label', 'start', 'end'], () => forceUpdate(this));
|
|
@@ -202,7 +194,7 @@ export class Textarea {
|
|
|
202
194
|
// Watch for class changes to update validation state
|
|
203
195
|
if (Build.isBrowser && typeof MutationObserver !== 'undefined') {
|
|
204
196
|
this.validationObserver = new MutationObserver(() => {
|
|
205
|
-
const newIsInvalid = this.
|
|
197
|
+
const newIsInvalid = checkInvalidState(this.el);
|
|
206
198
|
if (this.isInvalid !== newIsInvalid) {
|
|
207
199
|
this.isInvalid = newIsInvalid;
|
|
208
200
|
// Force a re-render to update aria-describedby immediately
|
|
@@ -215,7 +207,7 @@ export class Textarea {
|
|
|
215
207
|
});
|
|
216
208
|
}
|
|
217
209
|
// Always set initial state
|
|
218
|
-
this.isInvalid = this.
|
|
210
|
+
this.isInvalid = checkInvalidState(this.el);
|
|
219
211
|
this.debounceChanged();
|
|
220
212
|
if (Build.isBrowser) {
|
|
221
213
|
document.dispatchEvent(new CustomEvent('ionInputDidLoad', {
|
|
@@ -479,7 +471,7 @@ export class Textarea {
|
|
|
479
471
|
* TODO(FW-5592): Remove hasStartEndSlots condition
|
|
480
472
|
*/
|
|
481
473
|
const labelShouldFloat = labelPlacement === 'stacked' || (labelPlacement === 'floating' && (hasValue || hasFocus || hasStartEndSlots));
|
|
482
|
-
return (h(Host, { key: '
|
|
474
|
+
return (h(Host, { key: 'a70a62d7aae3831a50acd74f60b930925ada1326', class: createColorClasses(this.color, {
|
|
483
475
|
[mode]: true,
|
|
484
476
|
'has-value': hasValue,
|
|
485
477
|
'has-focus': hasFocus,
|
|
@@ -488,7 +480,7 @@ export class Textarea {
|
|
|
488
480
|
[`textarea-shape-${shape}`]: shape !== undefined,
|
|
489
481
|
[`textarea-label-placement-${labelPlacement}`]: true,
|
|
490
482
|
'textarea-disabled': disabled,
|
|
491
|
-
}) }, h("label", { key: '
|
|
483
|
+
}) }, h("label", { key: '8a2dd59a60f7469df84018eb0ede3a9ec3862703', class: "textarea-wrapper", htmlFor: inputId, onClick: this.onLabelClick }, this.renderLabelContainer(), h("div", { key: '1bfc368236e3da7a225a45118c27fbfc1fe5fa46', class: "textarea-wrapper-inner" }, h("div", { key: '215cbb2635ff52e31a8973376989b85e7245d40f', class: "start-slot-wrapper" }, h("slot", { key: '9f6b461cdee9d629deb695d2bea054ece2f32305', name: "start" })), h("div", { key: 'c1af35a2d5bc452bebe0b22a26d15ff52b4e9fc8', class: "native-wrapper", ref: (el) => (this.textareaWrapper = el) }, h("textarea", Object.assign({ key: '69a69b3cf0932baafbe37e6e846f1a571608d3f2', class: "native-textarea", ref: (el) => (this.nativeInput = el), id: inputId, disabled: disabled, autoCapitalize: this.autocapitalize, autoFocus: this.autofocus, enterKeyHint: this.enterkeyhint, inputMode: this.inputmode, minLength: this.minlength, maxLength: this.maxlength, name: this.name, placeholder: this.placeholder || '', readOnly: this.readonly, required: this.required, spellcheck: this.spellcheck, cols: this.cols, rows: this.rows, wrap: this.wrap, onInput: this.onInput, onChange: this.onChange, onBlur: this.onBlur, onFocus: this.onFocus, onKeyDown: this.onKeyDown, "aria-describedby": this.getHintTextID(), "aria-invalid": this.isInvalid ? 'true' : undefined }, this.inheritedAttributes), value)), h("div", { key: 'c053ea8b865d0e29763aed2e4939cc9c9e374c15', class: "end-slot-wrapper" }, h("slot", { key: '930aa641833b0df54b9ea10368fc2f46d5f491f6', name: "end" }))), shouldRenderHighlight && h("div", { key: '8d12597d15f5f429d80e8272ea99e64ed924e482', class: "textarea-highlight" })), this.renderBottomContent()));
|
|
492
484
|
}
|
|
493
485
|
static get is() { return "ion-textarea"; }
|
|
494
486
|
static get encapsulation() { return "scoped"; }
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* (C) Ionic http://ionicframework.com - MIT License
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Checks if the form element is in an invalid state based on
|
|
6
|
+
* Ionic validation classes.
|
|
7
|
+
*
|
|
8
|
+
* @param el The form element to check.
|
|
9
|
+
* @returns `true` if the element is invalid, `false` otherwise.
|
|
10
|
+
*/
|
|
11
|
+
export const checkInvalidState = (el) => {
|
|
12
|
+
const hasIonTouched = el.classList.contains('ion-touched');
|
|
13
|
+
const hasIonInvalid = el.classList.contains('ion-invalid');
|
|
14
|
+
return hasIonTouched && hasIonInvalid;
|
|
15
|
+
};
|
package/dist/docs.json
CHANGED
|
@@ -708,6 +708,8 @@ Footer.style = {
|
|
|
708
708
|
};
|
|
709
709
|
|
|
710
710
|
const TRANSITION = 'all 0.2s ease-in-out';
|
|
711
|
+
const ROLE_NONE = 'none';
|
|
712
|
+
const ROLE_BANNER = 'banner';
|
|
711
713
|
const cloneElement = (tagName) => {
|
|
712
714
|
const getCachedEl = document.querySelector(`${tagName}.ion-cloned-element`);
|
|
713
715
|
if (getCachedEl !== null) {
|
|
@@ -834,6 +836,7 @@ const setHeaderActive = (headerIndex, active = true) => {
|
|
|
834
836
|
const toolbars = headerIndex.toolbars;
|
|
835
837
|
const ionTitles = toolbars.map((toolbar) => toolbar.ionTitleEl);
|
|
836
838
|
if (active) {
|
|
839
|
+
headerEl.setAttribute('role', ROLE_BANNER);
|
|
837
840
|
headerEl.classList.remove('header-collapse-condense-inactive');
|
|
838
841
|
ionTitles.forEach((ionTitle) => {
|
|
839
842
|
if (ionTitle) {
|
|
@@ -842,6 +845,16 @@ const setHeaderActive = (headerIndex, active = true) => {
|
|
|
842
845
|
});
|
|
843
846
|
}
|
|
844
847
|
else {
|
|
848
|
+
/**
|
|
849
|
+
* There can only be one banner landmark per page.
|
|
850
|
+
* By default, all ion-headers have the banner role.
|
|
851
|
+
* This causes an accessibility issue when using a
|
|
852
|
+
* condensed header since there are two ion-headers
|
|
853
|
+
* on the page at once (active and inactive).
|
|
854
|
+
* To solve this, the role needs to be toggled
|
|
855
|
+
* based on which header is active.
|
|
856
|
+
*/
|
|
857
|
+
headerEl.setAttribute('role', ROLE_NONE);
|
|
845
858
|
headerEl.classList.add('header-collapse-condense-inactive');
|
|
846
859
|
/**
|
|
847
860
|
* The small title should only be accessed by screen readers
|
|
@@ -901,6 +914,30 @@ const handleHeaderFade = (scrollEl, baseEl, condenseHeader) => {
|
|
|
901
914
|
});
|
|
902
915
|
});
|
|
903
916
|
};
|
|
917
|
+
/**
|
|
918
|
+
* Get the role type for the ion-header.
|
|
919
|
+
*
|
|
920
|
+
* @param isInsideMenu If ion-header is inside ion-menu.
|
|
921
|
+
* @param isCondensed If ion-header has collapse="condense".
|
|
922
|
+
* @param mode The current mode.
|
|
923
|
+
* @returns 'none' if inside ion-menu or if condensed in md
|
|
924
|
+
* mode, otherwise 'banner'.
|
|
925
|
+
*/
|
|
926
|
+
const getRoleType = (isInsideMenu, isCondensed, mode) => {
|
|
927
|
+
// If the header is inside a menu, it should not have the banner role.
|
|
928
|
+
if (isInsideMenu) {
|
|
929
|
+
return ROLE_NONE;
|
|
930
|
+
}
|
|
931
|
+
/**
|
|
932
|
+
* Only apply role="none" to `md` mode condensed headers
|
|
933
|
+
* since the large header is never shown.
|
|
934
|
+
*/
|
|
935
|
+
if (isCondensed && mode === 'md') {
|
|
936
|
+
return ROLE_NONE;
|
|
937
|
+
}
|
|
938
|
+
// Default to banner role.
|
|
939
|
+
return ROLE_BANNER;
|
|
940
|
+
};
|
|
904
941
|
|
|
905
942
|
const headerIosCss = "ion-header{display:block;position:relative;-ms-flex-order:-1;order:-1;width:100%;z-index:10}ion-header ion-toolbar:first-of-type{padding-top:var(--ion-safe-area-top, 0)}.header-ios ion-toolbar:last-of-type{--border-width:0 0 0.55px}@supports ((-webkit-backdrop-filter: blur(0)) or (backdrop-filter: blur(0))){.header-background{left:0;right:0;top:0;bottom:0;position:absolute;-webkit-backdrop-filter:saturate(180%) blur(20px);backdrop-filter:saturate(180%) blur(20px)}.header-translucent-ios ion-toolbar{--opacity:.8}.header-collapse-condense-inactive .header-background{-webkit-backdrop-filter:blur(20px);backdrop-filter:blur(20px)}}.header-ios.ion-no-border ion-toolbar:last-of-type{--border-width:0}.header-collapse-fade ion-toolbar{--opacity-scale:inherit}.header-collapse-fade.header-transitioning ion-toolbar{--background:transparent;--border-style:none}.header-collapse-condense{z-index:9}.header-collapse-condense ion-toolbar{position:-webkit-sticky;position:sticky;top:0}.header-collapse-condense ion-toolbar:first-of-type{padding-top:0px;z-index:1}.header-collapse-condense ion-toolbar{z-index:0}.header-collapse-condense ion-toolbar:last-of-type{--border-width:0px}.header-collapse-condense ion-toolbar ion-searchbar{padding-top:0px;padding-bottom:13px}.header-collapse-main{--opacity-scale:1}.header-collapse-main ion-toolbar{--opacity-scale:inherit}.header-collapse-main ion-toolbar.in-toolbar ion-title,.header-collapse-main ion-toolbar.in-toolbar ion-buttons{-webkit-transition:all 0.2s ease-in-out;transition:all 0.2s ease-in-out}.header-collapse-condense ion-toolbar,.header-collapse-condense-inactive.header-transitioning:not(.header-collapse-condense) ion-toolbar{--background:var(--ion-background-color, #fff)}.header-collapse-condense-inactive.header-transitioning:not(.header-collapse-condense) ion-toolbar{--border-style:none;--opacity-scale:1}.header-collapse-condense-inactive:not(.header-collapse-condense) ion-toolbar.in-toolbar ion-title,.header-collapse-condense-inactive:not(.header-collapse-condense) ion-toolbar.in-toolbar ion-buttons.buttons-collapse{opacity:0;pointer-events:none}.header-collapse-condense-inactive.header-collapse-condense ion-toolbar.in-toolbar ion-title,.header-collapse-condense-inactive.header-collapse-condense ion-toolbar.in-toolbar ion-buttons.buttons-collapse{visibility:hidden}ion-header.header-ios:not(.header-collapse-main):has(~ion-content ion-header.header-ios[collapse=condense],~ion-content ion-header.header-ios.header-collapse-condense){opacity:0}";
|
|
906
943
|
|
|
@@ -1042,16 +1079,17 @@ const Header = class {
|
|
|
1042
1079
|
const { translucent, inheritedAttributes } = this;
|
|
1043
1080
|
const mode = getIonMode(this);
|
|
1044
1081
|
const collapse = this.collapse || 'none';
|
|
1082
|
+
const isCondensed = collapse === 'condense';
|
|
1045
1083
|
// banner role must be at top level, so remove role if inside a menu
|
|
1046
|
-
const roleType = hostContext('ion-menu', this.el)
|
|
1047
|
-
return (h(Host, Object.assign({ key: '
|
|
1084
|
+
const roleType = getRoleType(hostContext('ion-menu', this.el), isCondensed, mode);
|
|
1085
|
+
return (h(Host, Object.assign({ key: '863c4568cd7b8c0ec55109f193bbbaed68a1346e', role: roleType, class: {
|
|
1048
1086
|
[mode]: true,
|
|
1049
1087
|
// Used internally for styling
|
|
1050
1088
|
[`header-${mode}`]: true,
|
|
1051
1089
|
[`header-translucent`]: this.translucent,
|
|
1052
1090
|
[`header-collapse-${collapse}`]: true,
|
|
1053
1091
|
[`header-translucent-${mode}`]: this.translucent,
|
|
1054
|
-
} }, inheritedAttributes), mode === 'ios' && translucent && h("div", { key: '
|
|
1092
|
+
} }, inheritedAttributes), mode === 'ios' && translucent && h("div", { key: '25c3bdce328b0b35607d154c8b8374679313d881', class: "header-background" }), h("slot", { key: 'b44fab0a9be7920b9650da26117c783e751e1702' })));
|
|
1055
1093
|
}
|
|
1056
1094
|
get el() { return getElement(this); }
|
|
1057
1095
|
};
|
|
@@ -213,11 +213,7 @@ const Button = class {
|
|
|
213
213
|
target,
|
|
214
214
|
};
|
|
215
215
|
let fill = this.fill;
|
|
216
|
-
|
|
217
|
-
* We check both undefined and null to
|
|
218
|
-
* work around https://github.com/ionic-team/stencil/issues/3586.
|
|
219
|
-
*/
|
|
220
|
-
if (fill == null) {
|
|
216
|
+
if (fill === undefined) {
|
|
221
217
|
fill = this.inToolbar || this.inListHeader ? 'clear' : 'solid';
|
|
222
218
|
}
|
|
223
219
|
/**
|
|
@@ -230,7 +226,7 @@ const Button = class {
|
|
|
230
226
|
{
|
|
231
227
|
type !== 'button' && this.renderHiddenButton();
|
|
232
228
|
}
|
|
233
|
-
return (h(Host, { key: '
|
|
229
|
+
return (h(Host, { key: 'ed82ea53705523f9afc5f1a9addff44cc6424f27', onClick: this.handleClick, "aria-disabled": disabled ? 'true' : null, class: createColorClasses$1(color, {
|
|
234
230
|
[mode]: true,
|
|
235
231
|
[buttonType]: true,
|
|
236
232
|
[`${buttonType}-${expand}`]: expand !== undefined,
|
|
@@ -245,7 +241,7 @@ const Button = class {
|
|
|
245
241
|
'button-disabled': disabled,
|
|
246
242
|
'ion-activatable': true,
|
|
247
243
|
'ion-focusable': true,
|
|
248
|
-
}) }, h(TagType, Object.assign({ key: '
|
|
244
|
+
}) }, h(TagType, Object.assign({ key: 'fadec13053469dd0405bbbc61b70ced568aa4826' }, attrs, { class: "button-native", part: "native", disabled: disabled, onFocus: this.onFocus, onBlur: this.onBlur }, inheritedAttributes), h("span", { key: '6bf0e5144fb1148002e88038522402b789689d2c', class: "button-inner" }, h("slot", { key: '25da0ca155cfa9e2754842c34f4fd09f576ac2d2', name: "icon-only", onSlotchange: this.slotChanged }), h("slot", { key: '51414065bb11953ec9d818f8d9353589bc9072c5', name: "start" }), h("slot", { key: 'c9b5f8842aeabd20628df2f4600f1257ea913d8d' }), h("slot", { key: '478dd3671c7be1909fc84e672f0fa8dfe6082263', name: "end" })), mode === 'md' && h("ion-ripple-effect", { key: 'e1d55f85a55144d743f58a5914cd116cb065fa8c', type: this.rippleType }))));
|
|
249
245
|
}
|
|
250
246
|
get el() { return getElement(this); }
|
|
251
247
|
static get watchers() { return {
|