chore: merge 8.7.4

This commit is contained in:
ShaneK
2025-09-17 11:59:34 -07:00
131 changed files with 2042 additions and 670 deletions

View File

@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [8.7.4](https://github.com/ionic-team/ionic-framework/compare/v8.7.3...v8.7.4) (2025-09-17)
### Bug Fixes
* **input:** improve error text accessibility ([#30635](https://github.com/ionic-team/ionic-framework/issues/30635)) ([c339bc3](https://github.com/ionic-team/ionic-framework/commit/c339bc36827b62ef871325869a9a5db9b17ac785))
* **overlays,picker:** remove invalid aria-hidden attribute ([#30563](https://github.com/ionic-team/ionic-framework/issues/30563)) ([49f96d7](https://github.com/ionic-team/ionic-framework/commit/49f96d7f1e9050a95e3e33a821c0467ecc0bed64)), closes [#30040](https://github.com/ionic-team/ionic-framework/issues/30040)
## [8.7.3](https://github.com/ionic-team/ionic-framework/compare/v8.7.2...v8.7.3) (2025-08-20)
**Note:** Version bump only for package @ionic/angular

View File

@ -1,15 +1,15 @@
{
"name": "@ionic/angular",
"version": "8.7.3",
"version": "8.7.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@ionic/angular",
"version": "8.7.3",
"version": "8.7.4",
"license": "MIT",
"dependencies": {
"@ionic/core": "^8.7.3",
"@ionic/core": "^8.7.4",
"ionicons": "^8.0.13",
"jsonc-parser": "^3.0.0",
"tslib": "^2.3.0"
@ -1398,9 +1398,9 @@
"dev": true
},
"node_modules/@ionic/core": {
"version": "8.7.3",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.3.tgz",
"integrity": "sha512-KdyMxpMDQj+uqpztpK6yvN/T96hqcDiGXQ4T+aAZ+LW3wV3+0it6/rbh9C1B/wCl4Isnm4IRltPabgEfNJ50nw==",
"version": "8.7.4",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.4.tgz",
"integrity": "sha512-ZCJYKLWdxq+x4OmEDvodqR+y/FSDJYkkFHozWe1+b/p0l9lNN13lLuSZVs0AEOgPtO89Atl67rTbpGE2ad/SCw==",
"license": "MIT",
"dependencies": {
"@stencil/core": "4.36.2",

View File

@ -1,6 +1,6 @@
{
"name": "@ionic/angular",
"version": "8.7.3",
"version": "8.7.4",
"description": "Angular specific wrappers for @ionic/core",
"keywords": [
"ionic",
@ -37,6 +37,7 @@
"eslint": "eslint . --ext .ts",
"prerelease": "npm run validate && np prerelease --yolo --any-branch --tag next",
"sync": "./scripts/sync.sh",
"local.sync.and.pack": "./scripts/sync-and-pack.sh",
"test": "echo 'angular no tests yet'",
"tsc": "tsc -p .",
"validate": "npm i && npm run lint && npm run test && npm run build"
@ -47,7 +48,7 @@
}
},
"dependencies": {
"@ionic/core": "^8.7.3",
"@ionic/core": "^8.7.4",
"ionicons": "^8.0.13",
"jsonc-parser": "^3.0.0",
"tslib": "^2.3.0"

View File

@ -0,0 +1,30 @@
set -e
# Delete old packages
rm -f *.tgz
# Pack @ionic/core
echo "\n📦 Packing @ionic/core..."
npm pack ../../core
# Update package.json with global path for the @ionic/core package
echo "\n⚙ Updating package.json with global path for @ionic/core..."
CORE_PACKAGE=$(ls ionic-core-*.tgz | head -1)
sed -i "" "s|\"@ionic/core\": \".*\"|\"@ionic/core\": \"file:$(pwd)/$CORE_PACKAGE\"|" package.json
# Remove package-lock.json
rm -f package-lock.json
# Install Dependencies
echo "\n🔧 Installing dependencies..."
npm install
# Build the project
echo "\n🔨 Building the project..."
npm run build
# Pack @ionic/angular
echo "\n📦 Packing @ionic/angular..."
npm pack ./dist
echo "\n✅ Packed ionic-angular package!\n $(pwd)/$(ls ionic-angular-*.tgz | head -1)\n"

View File

@ -28,6 +28,7 @@ import { AlertComponent } from '../alert/alert.component';
import { AccordionComponent } from '../accordion/accordion.component';
import { AccordionModalComponent } from '../accordion/accordion-modal/accordion-modal.component';
import { TabsBasicComponent } from '../tabs-basic/tabs-basic.component';
import { TemplateFormComponent } from '../template-form/template-form.component';
@NgModule({
declarations: [
@ -53,7 +54,8 @@ import { TabsBasicComponent } from '../tabs-basic/tabs-basic.component';
AlertComponent,
AccordionComponent,
AccordionModalComponent,
TabsBasicComponent
TabsBasicComponent,
TemplateFormComponent
],
imports: [
CommonModule,

View File

@ -19,6 +19,7 @@ import { NavigationPage3Component } from '../navigation-page3/navigation-page3.c
import { AlertComponent } from '../alert/alert.component';
import { AccordionComponent } from '../accordion/accordion.component';
import { TabsBasicComponent } from '../tabs-basic/tabs-basic.component';
import { TemplateFormComponent } from '../template-form/template-form.component';
export const routes: Routes = [
{
@ -33,6 +34,7 @@ export const routes: Routes = [
{ path: 'textarea', loadChildren: () => import('../textarea/textarea.module').then(m => m.TextareaModule) },
{ path: 'searchbar', loadChildren: () => import('../searchbar/searchbar.module').then(m => m.SearchbarModule) },
{ path: 'form', component: FormComponent },
{ path: 'template-form', component: TemplateFormComponent },
{ path: 'modals', component: ModalComponent },
{ path: 'modal-inline', loadChildren: () => import('../modal-inline').then(m => m.ModalInlineModule) },
{ path: 'view-child', component: ViewChildComponent },

View File

@ -25,6 +25,11 @@
Form Test
</ion-label>
</ion-item>
<ion-item routerLink="/lazy/template-form">
<ion-label>
Template-Driven Form Test
</ion-label>
</ion-item>
<ion-item routerLink="/lazy/modals">
<ion-label>
Modals Test

View File

@ -0,0 +1,116 @@
<ion-header>
<ion-toolbar>
<ion-title>Template-Driven Form Validation Test</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<form #templateForm="ngForm" (ngSubmit)="onSubmit(templateForm)">
<ion-list>
<!-- Test ion-input with required validation -->
<ion-item>
<ion-input
label="Required Input"
[(ngModel)]="inputValue"
name="inputField"
required
#inputField="ngModel"
id="template-input-test"
errorText="This field is required"
helperText="Enter some text">
</ion-input>
</ion-item>
<!-- Display validation state for debugging -->
<ion-item>
<ion-label>
<p>Input Touched: <span id="input-touched">{{inputField.touched}}</span></p>
<p>Input Invalid: <span id="input-invalid">{{inputField.invalid}}</span></p>
<p>Input Errors: <span id="input-errors">{{inputField.errors | json}}</span></p>
</ion-label>
</ion-item>
<!-- Test ion-textarea with required validation -->
<ion-item>
<ion-textarea
label="Required Textarea"
[(ngModel)]="textareaValue"
name="textareaField"
required
#textareaField="ngModel"
id="template-textarea-test"
errorText="This field is required"
helperText="Enter some text"
rows="4">
</ion-textarea>
</ion-item>
<!-- Display validation state for debugging -->
<ion-item>
<ion-label>
<p>Textarea Touched: <span id="textarea-touched">{{textareaField.touched}}</span></p>
<p>Textarea Invalid: <span id="textarea-invalid">{{textareaField.invalid}}</span></p>
<p>Textarea Errors: <span id="textarea-errors">{{textareaField.errors | json}}</span></p>
</ion-label>
</ion-item>
<!-- Additional test with minlength validation -->
<ion-item>
<ion-input
label="Min Length Input (3 chars)"
[(ngModel)]="minLengthValue"
name="minLengthField"
required
minlength="3"
#minLengthField="ngModel"
id="template-minlength-test"
errorText="Minimum 3 characters required"
helperText="Enter at least 3 characters">
</ion-input>
</ion-item>
<!-- Display validation state for minlength field -->
<ion-item>
<ion-label>
<p>MinLength Touched: <span id="minlength-touched">{{minLengthField.touched}}</span></p>
<p>MinLength Invalid: <span id="minlength-invalid">{{minLengthField.invalid}}</span></p>
<p>MinLength Errors: <span id="minlength-errors">{{minLengthField.errors | json}}</span></p>
</ion-label>
</ion-item>
</ion-list>
<div class="ion-padding">
<p>Form Valid: <span id="form-valid">{{templateForm.valid}}</span></p>
<p>Form Submitted: <span id="form-submitted">{{submitted}}</span></p>
<ion-button type="submit" id="submit-button" [disabled]="!templateForm.valid">
Submit Form
</ion-button>
<ion-button type="button" id="reset-button" (click)="resetForm(templateForm)">
Reset Form
</ion-button>
<ion-button type="button" id="touch-all-button" (click)="templateForm.form.markAllAsTouched()">
Mark All as Touched
</ion-button>
</div>
<div class="ion-padding">
<h3>Form Values:</h3>
<pre id="form-values">{{templateForm.value | json}}</pre>
</div>
</form>
<div class="ion-padding">
<h3>Instructions to reproduce issue:</h3>
<ol>
<li>Click in the "Required Input" field</li>
<li>Click outside without entering text</li>
<li>The field should show as touched and invalid</li>
<li>The error text should appear below the input</li>
<li>For screen readers, the validation state should be announced</li>
</ol>
<p><strong>Note:</strong> With template-driven forms, Angular applies validation classes to the wrapper element, not directly to ion-input/ion-textarea.</p>
</div>
</ion-content>

View File

@ -0,0 +1,26 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-template-form',
templateUrl: './template-form.component.html',
standalone: false
})
export class TemplateFormComponent {
inputValue = '';
textareaValue = '';
minLengthValue = '';
// Track if form has been submitted
submitted = false;
onSubmit(form: any) {
this.submitted = true;
console.log('Form submitted:', form.value);
console.log('Form valid:', form.valid);
}
resetForm(form: any) {
form.reset();
this.submitted = false;
}
}

View File

@ -40,6 +40,14 @@ export const routes: Routes = [
]
},
{ path: 'tabs-basic', loadComponent: () => import('../tabs-basic/tabs-basic.component').then(c => c.TabsBasicComponent) },
{
path: 'validation',
children: [
{ path: 'input-validation', loadComponent: () => import('../validation/input-validation/input-validation.component').then(c => c.InputValidationComponent) },
{ path: 'textarea-validation', loadComponent: () => import('../validation/textarea-validation/textarea-validation.component').then(c => c.TextareaValidationComponent) },
{ path: '**', redirectTo: 'input-validation' }
]
},
{
path: 'value-accessors',
children: [

View File

@ -107,6 +107,22 @@
</ion-item>
</ion-list>
<ion-list>
<ion-list-header>
<ion-label>Validation Tests</ion-label>
</ion-list-header>
<ion-item routerLink="/standalone/validation/input-validation">
<ion-label>
Input Validation Test
</ion-label>
</ion-item>
<ion-item routerLink="/standalone/validation/textarea-validation">
<ion-label>
Textarea Validation Test
</ion-label>
</ion-item>
</ion-list>
<ion-list>
<ion-list-header>
<ion-label>Value Accessors</ion-label>

View File

@ -0,0 +1,131 @@
<ion-header>
<ion-toolbar>
<ion-title>Input - Validation Test</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<div class="validation-info">
<h2>Screen Reader Testing Instructions:</h2>
<ol>
<li>Enable your screen reader (VoiceOver, NVDA, JAWS, etc.)</li>
<li>Tab through the form fields</li>
<li>When you tab away from an empty required field, the error should be announced immediately</li>
<li>The error text should be announced BEFORE the next field is announced</li>
<li>Test in Chrome, Safari, and Firefox to verify consistent behavior</li>
</ol>
</div>
<form [formGroup]="form">
<div class="grid">
<div>
<h2>Required Email Field</h2>
<ion-input
#emailInput
id="email-input"
type="email"
label="Email"
labelPlacement="floating"
fill="outline"
placeholder="Enter your email"
[helperText]="fieldMetadata.email.helperText"
[errorText]="fieldMetadata.email.errorText"
formControlName="email"
required
></ion-input>
</div>
<div>
<h2>Required Name Field</h2>
<ion-input
#nameInput
id="name-input"
type="text"
label="Full Name"
labelPlacement="floating"
fill="outline"
placeholder="Enter your full name"
[helperText]="fieldMetadata.name.helperText"
[errorText]="fieldMetadata.name.errorText"
formControlName="name"
required
></ion-input>
</div>
<div>
<h2>Phone Number (Pattern Validation)</h2>
<ion-input
#phoneInput
id="phone-input"
type="tel"
label="Phone"
labelPlacement="floating"
fill="outline"
placeholder="(555) 555-5555"
pattern="^\(\d{3}\) \d{3}-\d{4}$"
[helperText]="fieldMetadata.phone.helperText"
[errorText]="fieldMetadata.phone.errorText"
formControlName="phone"
required
></ion-input>
</div>
<div>
<h2>Password (Min Length)</h2>
<ion-input
#passwordInput
id="password-input"
type="password"
label="Password"
labelPlacement="floating"
fill="outline"
placeholder="Enter password"
minlength="8"
[helperText]="fieldMetadata.password.helperText"
[errorText]="fieldMetadata.password.errorText"
formControlName="password"
required
></ion-input>
</div>
<div>
<h2>Age (Number Range)</h2>
<ion-input
#ageInput
id="age-input"
type="number"
label="Age"
labelPlacement="floating"
fill="outline"
placeholder="Enter your age"
min="18"
max="120"
[helperText]="fieldMetadata.age.helperText"
[errorText]="fieldMetadata.age.errorText"
formControlName="age"
required
></ion-input>
</div>
<div>
<h2>Optional Field (No Validation)</h2>
<ion-input
#optionalInput
id="optional-input"
type="text"
label="Optional Info"
labelPlacement="floating"
fill="outline"
placeholder="This field is optional"
[helperText]="fieldMetadata.optional.helperText"
formControlName="optional"
></ion-input>
</div>
</div>
</form>
<div class="ion-padding">
<ion-button id="submit-btn" expand="block" [disabled]="form.invalid" (click)="onSubmit()">Submit Form</ion-button>
<ion-button id="reset-btn" expand="block" fill="outline" (click)="form.reset()">Reset Form</ion-button>
</div>
</ion-content>

View File

@ -0,0 +1,36 @@
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
grid-row-gap: 20px;
grid-column-gap: 20px;
}
h2 {
font-size: 12px;
font-weight: normal;
color: var(--ion-color-step-600);
margin-top: 10px;
margin-bottom: 5px;
}
.validation-info {
margin: 20px;
padding: 10px;
background: var(--ion-color-light);
border-radius: 4px;
}
.validation-info h2 {
font-size: 14px;
font-weight: 600;
margin-bottom: 10px;
}
.validation-info ol {
margin: 0;
padding-left: 20px;
}
.validation-info li {
margin-bottom: 5px;
}

View File

@ -0,0 +1,85 @@
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import {
FormBuilder,
ReactiveFormsModule,
Validators
} from '@angular/forms';
import {
IonButton,
IonContent,
IonHeader,
IonInput,
IonTitle,
IonToolbar
} from '@ionic/angular/standalone';
@Component({
selector: 'app-input-validation',
templateUrl: './input-validation.component.html',
styleUrls: ['./input-validation.component.scss'],
standalone: true,
imports: [
CommonModule,
ReactiveFormsModule,
IonInput,
IonButton,
IonHeader,
IonToolbar,
IonTitle,
IonContent
]
})
export class InputValidationComponent {
// Field metadata for labels and error messages
fieldMetadata = {
email: {
label: 'Email',
helperText: "We'll never share your email",
errorText: 'Please enter a valid email address'
},
name: {
label: 'Full Name',
helperText: 'First and last name',
errorText: 'Name is required'
},
phone: {
label: 'Phone',
helperText: 'Format: (555) 555-5555',
errorText: 'Please enter a valid phone number'
},
password: {
label: 'Password',
helperText: 'At least 8 characters',
errorText: 'Password must be at least 8 characters'
},
age: {
label: 'Age',
helperText: 'Must be 18 or older',
errorText: 'Please enter a valid age (18-120)'
},
optional: {
label: 'Optional Info',
helperText: 'You can skip this field',
errorText: ''
}
};
form = this.fb.group({
email: ['', [Validators.required, Validators.email]],
name: ['', Validators.required],
phone: ['', [Validators.required, Validators.pattern(/^\(\d{3}\) \d{3}-\d{4}$/)]],
password: ['', [Validators.required, Validators.minLength(8)]],
age: ['', [Validators.required, Validators.min(18), Validators.max(120)]],
optional: ['']
});
constructor(private fb: FormBuilder) {}
// Submit form
onSubmit(): void {
if (this.form.valid) {
alert('Form submitted successfully!');
}
}
}

View File

@ -0,0 +1,133 @@
<ion-header>
<ion-toolbar>
<ion-title>Textarea - Validation Test</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<div class="validation-info">
<h2>Screen Reader Testing Instructions:</h2>
<ol>
<li>Enable your screen reader (VoiceOver, NVDA, JAWS, etc.)</li>
<li>Tab through the form fields</li>
<li>When you tab away from an empty required field, the error should be announced immediately</li>
<li>The error text should be announced BEFORE the next field is announced</li>
<li>Test in Chrome, Safari, and Firefox to verify consistent behavior</li>
</ol>
</div>
<form [formGroup]="form">
<div class="grid">
<div>
<h2>Required Description (Min Length)</h2>
<ion-textarea
#descriptionTextarea
id="description-textarea"
[label]="fieldMetadata.description.label"
labelPlacement="floating"
fill="outline"
placeholder="Enter a description"
[rows]="fieldMetadata.description.rows"
minlength="20"
[helperText]="fieldMetadata.description.helperText"
[errorText]="fieldMetadata.description.errorText"
formControlName="description"
required
></ion-textarea>
</div>
<div>
<h2>Required Comments</h2>
<ion-textarea
#commentsTextarea
id="comments-textarea"
[label]="fieldMetadata.comments.label"
labelPlacement="floating"
fill="outline"
placeholder="Enter your comments"
[rows]="fieldMetadata.comments.rows"
[helperText]="fieldMetadata.comments.helperText"
[errorText]="fieldMetadata.comments.errorText"
formControlName="comments"
required
></ion-textarea>
</div>
<div>
<h2>Bio (Max Length)</h2>
<ion-textarea
#bioTextarea
id="bio-textarea"
[label]="fieldMetadata.bio.label"
labelPlacement="floating"
fill="outline"
placeholder="Tell us about yourself"
[rows]="fieldMetadata.bio.rows"
maxlength="200"
[counter]="fieldMetadata.bio.counter"
[helperText]="fieldMetadata.bio.helperText"
[errorText]="fieldMetadata.bio.errorText"
formControlName="bio"
required
></ion-textarea>
</div>
<div>
<h2>Address (Pattern Validation)</h2>
<ion-textarea
#addressTextarea
id="address-textarea"
[label]="fieldMetadata.address.label"
labelPlacement="floating"
fill="outline"
placeholder="Enter your full address"
[rows]="fieldMetadata.address.rows"
[helperText]="fieldMetadata.address.helperText"
[errorText]="fieldMetadata.address.errorText"
formControlName="address"
required
></ion-textarea>
</div>
<div>
<h2>Review (Min/Max Length)</h2>
<ion-textarea
#reviewTextarea
id="review-textarea"
[label]="fieldMetadata.review.label"
labelPlacement="floating"
fill="outline"
placeholder="Write your review"
[rows]="fieldMetadata.review.rows"
minlength="50"
maxlength="500"
[counter]="fieldMetadata.review.counter"
[helperText]="fieldMetadata.review.helperText"
[errorText]="fieldMetadata.review.errorText"
formControlName="review"
required
></ion-textarea>
</div>
<div>
<h2>Optional Notes</h2>
<ion-textarea
#notesTextarea
id="notes-textarea"
[label]="fieldMetadata.notes.label"
labelPlacement="floating"
fill="outline"
placeholder="Any additional notes (optional)"
[rows]="fieldMetadata.notes.rows"
[helperText]="fieldMetadata.notes.helperText"
formControlName="notes"
></ion-textarea>
</div>
</div>
</form>
<div class="ion-padding">
<ion-button id="submit-btn" expand="block" [disabled]="form.invalid" (click)="onSubmit()">Submit Form</ion-button>
<ion-button id="reset-btn" expand="block" fill="outline" (click)="form.reset()">Reset Form</ion-button>
</div>
</ion-content>

View File

@ -0,0 +1,36 @@
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
grid-row-gap: 20px;
grid-column-gap: 20px;
}
h2 {
font-size: 12px;
font-weight: normal;
color: var(--ion-color-step-600);
margin-top: 10px;
margin-bottom: 5px;
}
.validation-info {
margin: 20px;
padding: 10px;
background: var(--ion-color-light);
border-radius: 4px;
}
.validation-info h2 {
font-size: 14px;
font-weight: 600;
margin-bottom: 10px;
}
.validation-info ol {
margin: 0;
padding-left: 20px;
}
.validation-info li {
margin-bottom: 5px;
}

View File

@ -0,0 +1,105 @@
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import {
AbstractControl,
FormBuilder,
ReactiveFormsModule,
ValidationErrors,
Validators
} from '@angular/forms';
import {
IonButton,
IonContent,
IonHeader,
IonTextarea,
IonTitle,
IonToolbar
} from '@ionic/angular/standalone';
// Custom validator for address (must be at least 10 chars and contain a digit)
function addressValidator(control: AbstractControl): ValidationErrors | null {
const value = control.value;
if (!value || value.length < 10) {
return { invalidAddress: true };
}
// Check if it contains at least one number (for street/zip)
return /\d/.test(value) ? null : { invalidAddress: true };
}
@Component({
selector: 'app-textarea-validation',
templateUrl: './textarea-validation.component.html',
styleUrls: ['./textarea-validation.component.scss'],
standalone: true,
imports: [
CommonModule,
ReactiveFormsModule,
IonTextarea,
IonButton,
IonHeader,
IonToolbar,
IonTitle,
IonContent
]
})
export class TextareaValidationComponent {
// Field metadata for labels and error messages
fieldMetadata = {
description: {
label: 'Description',
helperText: 'At least 20 characters',
errorText: 'Description must be at least 20 characters',
rows: 4
},
comments: {
label: 'Comments',
helperText: 'Please provide your feedback',
errorText: 'Comments are required',
rows: 4
},
bio: {
label: 'Bio',
helperText: 'Maximum 200 characters',
errorText: 'Bio is required',
rows: 4,
counter: true
},
address: {
label: 'Address',
helperText: 'Include street, city, state, and zip',
errorText: 'Please enter a complete address',
rows: 3
},
review: {
label: 'Product Review',
helperText: 'Between 50-500 characters',
errorText: 'Review must be between 50-500 characters',
rows: 5,
counter: true
},
notes: {
label: 'Additional Notes',
helperText: 'This field is optional',
errorText: '',
rows: 3
}
};
form = this.fb.group({
description: ['', [Validators.required, Validators.minLength(20)]],
comments: ['', Validators.required],
bio: ['', [Validators.required, Validators.maxLength(200)]],
address: ['', [Validators.required, addressValidator]],
review: ['', [Validators.required, Validators.minLength(50), Validators.maxLength(500)]],
notes: ['']
});
constructor(private fb: FormBuilder) {}
// Submit form
onSubmit(): void {
if (this.form.valid) {
alert('Form submitted successfully!');
}
}
}