chore(git): update feature-8.5 from main (#30238)

This commit is contained in:
Brandy Smith
2025-03-10 12:49:16 -04:00
committed by GitHub
66 changed files with 161 additions and 56 deletions

View File

@ -13,6 +13,6 @@ jobs:
- name: 'Auto-assign issue' - name: 'Auto-assign issue'
uses: pozil/auto-assign-issue@39c06395cbac76e79afc4ad4e5c5c6db6ecfdd2e # v2.2.0 uses: pozil/auto-assign-issue@39c06395cbac76e79afc4ad4e5c5c6db6ecfdd2e # v2.2.0
with: with:
assignees: brandyscarney, thetaPC, joselrio, rugoncalves, BenOsodrac, JoaoFerreira-FrontEnd, OS-giulianasilva, tanner-reits assignees: brandyscarney, thetaPC, ShaneK
numOfAssignee: 1 numOfAssignee: 1
allowSelfAssign: false allowSelfAssign: false

66
core/package-lock.json generated
View File

@ -15,10 +15,10 @@
}, },
"devDependencies": { "devDependencies": {
"@axe-core/playwright": "^4.10.0", "@axe-core/playwright": "^4.10.0",
"@capacitor/core": "^6.0.0", "@capacitor/core": "^7.0.0",
"@capacitor/haptics": "^6.0.0", "@capacitor/haptics": "^7.0.0",
"@capacitor/keyboard": "^6.0.0", "@capacitor/keyboard": "^7.0.0",
"@capacitor/status-bar": "^6.0.0", "@capacitor/status-bar": "^7.0.0",
"@clack/prompts": "^0.10.0", "@clack/prompts": "^0.10.0",
"@ionic/eslint-config": "^0.3.0", "@ionic/eslint-config": "^0.3.0",
"@ionic/prettier-config": "^2.0.0", "@ionic/prettier-config": "^2.0.0",
@ -663,39 +663,43 @@
"dev": true "dev": true
}, },
"node_modules/@capacitor/core": { "node_modules/@capacitor/core": {
"version": "6.2.0", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/@capacitor/core/-/core-6.2.0.tgz", "resolved": "https://registry.npmjs.org/@capacitor/core/-/core-7.0.1.tgz",
"integrity": "sha512-B9IlJtDpUqhhYb+T8+cp2Db/3RETX36STgjeU2kQZBs/SLAcFiMama227o+msRjLeo3DO+7HJjWVA1+XlyyPEg==", "integrity": "sha512-1Ob9bvA/p8g8aNwK6VesxEekGXowLVf6APjkW4LRnr05H+7z/bke+Q5pn9zqh/GgTbIxAQ/rwZrAZvvxkdm1UA==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"tslib": "^2.1.0" "tslib": "^2.1.0"
} }
}, },
"node_modules/@capacitor/haptics": { "node_modules/@capacitor/haptics": {
"version": "6.0.2", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/@capacitor/haptics/-/haptics-6.0.2.tgz", "resolved": "https://registry.npmjs.org/@capacitor/haptics/-/haptics-7.0.0.tgz",
"integrity": "sha512-xcFdIH4iIIeW2+1lzmlYMVicqB9ytaiuZ9NE3a9laKFPvMGC7hdj6i6tHFezwPJ/96xkHOwXT2b0F8Mh9xtTWg==", "integrity": "sha512-8uI8rWyAbq8EzkjS+sHZSncyzujHzVbuLKgj8J5H0yUL6+r26F16gVA2iuQuIBvzbSMy7Y0/pUuWlwZr/H8AKg==",
"dev": true, "dev": true,
"license": "MIT",
"peerDependencies": { "peerDependencies": {
"@capacitor/core": "^6.0.0" "@capacitor/core": ">=7.0.0"
} }
}, },
"node_modules/@capacitor/keyboard": { "node_modules/@capacitor/keyboard": {
"version": "6.0.3", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/@capacitor/keyboard/-/keyboard-6.0.3.tgz", "resolved": "https://registry.npmjs.org/@capacitor/keyboard/-/keyboard-7.0.0.tgz",
"integrity": "sha512-V/mURxBI68HvClYjrGBlOriWkwYN7r+cWid/igJz/3scNc/V81DgQ9fpoLr4W0I5NY7YxOesjIJLuLO+LT18mQ==", "integrity": "sha512-Tqwy8wG+sx4UqiFCX4Q+bFw6uKgG7BiHKAPpeefoIgoEB8H8Jf3xZNZoVPnJIMuPsCdSvuyHXZbJXH9IEEirGA==",
"dev": true, "dev": true,
"license": "MIT",
"peerDependencies": { "peerDependencies": {
"@capacitor/core": "^6.0.0" "@capacitor/core": ">=7.0.0"
} }
}, },
"node_modules/@capacitor/status-bar": { "node_modules/@capacitor/status-bar": {
"version": "6.0.2", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/@capacitor/status-bar/-/status-bar-6.0.2.tgz", "resolved": "https://registry.npmjs.org/@capacitor/status-bar/-/status-bar-7.0.0.tgz",
"integrity": "sha512-AmRIX6QvFemItlY7/69ARkIAqitRQqJ2qwgZmD1KqgFb78pH+XFXm1guvS/a8CuOOm/IqZ4ddDbl20yxtBqzGA==", "integrity": "sha512-wsvPkWkoSOXMIgVHu4c6P1sOuDSZ1ClUo5OpLRwj7u8DYzlV4jlmNzztQn2Lvsiqx1z4kfukSaqe40k1Lo4c9g==",
"dev": true, "dev": true,
"license": "MIT",
"peerDependencies": { "peerDependencies": {
"@capacitor/core": "^6.0.0" "@capacitor/core": ">=7.0.0"
} }
}, },
"node_modules/@clack/core": { "node_modules/@clack/core": {
@ -10977,32 +10981,32 @@
"dev": true "dev": true
}, },
"@capacitor/core": { "@capacitor/core": {
"version": "6.2.0", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/@capacitor/core/-/core-6.2.0.tgz", "resolved": "https://registry.npmjs.org/@capacitor/core/-/core-7.0.1.tgz",
"integrity": "sha512-B9IlJtDpUqhhYb+T8+cp2Db/3RETX36STgjeU2kQZBs/SLAcFiMama227o+msRjLeo3DO+7HJjWVA1+XlyyPEg==", "integrity": "sha512-1Ob9bvA/p8g8aNwK6VesxEekGXowLVf6APjkW4LRnr05H+7z/bke+Q5pn9zqh/GgTbIxAQ/rwZrAZvvxkdm1UA==",
"dev": true, "dev": true,
"requires": { "requires": {
"tslib": "^2.1.0" "tslib": "^2.1.0"
} }
}, },
"@capacitor/haptics": { "@capacitor/haptics": {
"version": "6.0.2", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/@capacitor/haptics/-/haptics-6.0.2.tgz", "resolved": "https://registry.npmjs.org/@capacitor/haptics/-/haptics-7.0.0.tgz",
"integrity": "sha512-xcFdIH4iIIeW2+1lzmlYMVicqB9ytaiuZ9NE3a9laKFPvMGC7hdj6i6tHFezwPJ/96xkHOwXT2b0F8Mh9xtTWg==", "integrity": "sha512-8uI8rWyAbq8EzkjS+sHZSncyzujHzVbuLKgj8J5H0yUL6+r26F16gVA2iuQuIBvzbSMy7Y0/pUuWlwZr/H8AKg==",
"dev": true, "dev": true,
"requires": {} "requires": {}
}, },
"@capacitor/keyboard": { "@capacitor/keyboard": {
"version": "6.0.3", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/@capacitor/keyboard/-/keyboard-6.0.3.tgz", "resolved": "https://registry.npmjs.org/@capacitor/keyboard/-/keyboard-7.0.0.tgz",
"integrity": "sha512-V/mURxBI68HvClYjrGBlOriWkwYN7r+cWid/igJz/3scNc/V81DgQ9fpoLr4W0I5NY7YxOesjIJLuLO+LT18mQ==", "integrity": "sha512-Tqwy8wG+sx4UqiFCX4Q+bFw6uKgG7BiHKAPpeefoIgoEB8H8Jf3xZNZoVPnJIMuPsCdSvuyHXZbJXH9IEEirGA==",
"dev": true, "dev": true,
"requires": {} "requires": {}
}, },
"@capacitor/status-bar": { "@capacitor/status-bar": {
"version": "6.0.2", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/@capacitor/status-bar/-/status-bar-6.0.2.tgz", "resolved": "https://registry.npmjs.org/@capacitor/status-bar/-/status-bar-7.0.0.tgz",
"integrity": "sha512-AmRIX6QvFemItlY7/69ARkIAqitRQqJ2qwgZmD1KqgFb78pH+XFXm1guvS/a8CuOOm/IqZ4ddDbl20yxtBqzGA==", "integrity": "sha512-wsvPkWkoSOXMIgVHu4c6P1sOuDSZ1ClUo5OpLRwj7u8DYzlV4jlmNzztQn2Lvsiqx1z4kfukSaqe40k1Lo4c9g==",
"dev": true, "dev": true,
"requires": {} "requires": {}
}, },

View File

@ -37,10 +37,10 @@
}, },
"devDependencies": { "devDependencies": {
"@axe-core/playwright": "^4.10.0", "@axe-core/playwright": "^4.10.0",
"@capacitor/core": "^6.0.0", "@capacitor/core": "^7.0.0",
"@capacitor/haptics": "^6.0.0", "@capacitor/haptics": "^7.0.0",
"@capacitor/keyboard": "^6.0.0", "@capacitor/keyboard": "^7.0.0",
"@capacitor/status-bar": "^6.0.0", "@capacitor/status-bar": "^7.0.0",
"@clack/prompts": "^0.10.0", "@clack/prompts": "^0.10.0",
"@ionic/eslint-config": "^0.3.0", "@ionic/eslint-config": "^0.3.0",
"@ionic/prettier-config": "^2.0.0", "@ionic/prettier-config": "^2.0.0",

View File

@ -237,6 +237,18 @@ export class Alert implements ComponentInterface, OverlayInterface {
return; return;
} }
/**
* Ensure when alert container is being focused, and the user presses the tab + shift keys, the focus will be set to the last alert button.
*/
if (ev.target.classList.contains('alert-wrapper')) {
if (ev.key === 'Tab' && ev.shiftKey) {
ev.preventDefault();
const lastChildBtn = this.wrapperEl?.querySelector('.alert-button:last-child') as HTMLButtonElement;
lastChildBtn.focus();
return;
}
}
// The only inputs we want to navigate between using arrow keys are the radios // The only inputs we want to navigate between using arrow keys are the radios
// ignore the keydown event if it is not on a radio button // ignore the keydown event if it is not on a radio button
if ( if (
@ -400,7 +412,19 @@ export class Alert implements ComponentInterface, OverlayInterface {
await this.delegateController.attachViewToDom(); await this.delegateController.attachViewToDom();
await present(this, 'alertEnter', iosEnterAnimation, mdEnterAnimation); await present(this, 'alertEnter', iosEnterAnimation, mdEnterAnimation).then(() => {
/**
* Check if alert has only one button and no inputs.
* If so, then focus on the button. Otherwise, focus the alert wrapper.
* This will map to the default native alert behavior.
*/
if (this.buttons.length === 1 && this.inputs.length === 0) {
const queryBtn = this.wrapperEl?.querySelector('.alert-button') as HTMLButtonElement;
queryBtn.focus();
} else {
this.wrapperEl?.focus();
}
});
unlock(); unlock();
} }
@ -725,8 +749,8 @@ export class Alert implements ComponentInterface, OverlayInterface {
const { overlayIndex, header, subHeader, message, htmlAttributes } = this; const { overlayIndex, header, subHeader, message, htmlAttributes } = this;
const mode = getIonMode(this); const mode = getIonMode(this);
const hdrId = `alert-${overlayIndex}-hdr`; const hdrId = `alert-${overlayIndex}-hdr`;
const subHdrId = `alert-${overlayIndex}-sub-hdr`;
const msgId = `alert-${overlayIndex}-msg`; const msgId = `alert-${overlayIndex}-msg`;
const subHdrId = `alert-${overlayIndex}-sub-hdr`;
const role = this.inputs.length > 0 || this.buttons.length > 0 ? 'alertdialog' : 'alert'; const role = this.inputs.length > 0 || this.buttons.length > 0 ? 'alertdialog' : 'alert';
/** /**
@ -739,12 +763,7 @@ export class Alert implements ComponentInterface, OverlayInterface {
return ( return (
<Host <Host
role={role}
aria-modal="true"
aria-labelledby={ariaLabelledBy}
aria-describedby={message !== undefined ? msgId : null}
tabindex="-1" tabindex="-1"
{...(htmlAttributes as any)}
style={{ style={{
zIndex: `${20000 + overlayIndex}`, zIndex: `${20000 + overlayIndex}`,
}} }}
@ -761,7 +780,16 @@ export class Alert implements ComponentInterface, OverlayInterface {
<div tabindex="0" aria-hidden="true"></div> <div tabindex="0" aria-hidden="true"></div>
<div class="alert-wrapper ion-overlay-wrapper" ref={(el) => (this.wrapperEl = el)}> <div
class="alert-wrapper ion-overlay-wrapper"
role={role}
aria-modal="true"
aria-labelledby={ariaLabelledBy}
aria-describedby={message !== undefined ? msgId : null}
tabindex="0"
ref={(el) => (this.wrapperEl = el)}
{...(htmlAttributes as any)}
>
<div class="alert-head"> <div class="alert-head">
{header && ( {header && (
<h2 id={hdrId} class="alert-title"> <h2 id={hdrId} class="alert-title">

View File

@ -16,6 +16,7 @@ const testAria = async (
await didPresent.next(); await didPresent.next();
const alert = page.locator('ion-alert'); const alert = page.locator('ion-alert');
const alertwrapper = alert.locator('.alert-wrapper');
const header = alert.locator('.alert-title'); const header = alert.locator('.alert-title');
const subHeader = alert.locator('.alert-sub-title'); const subHeader = alert.locator('.alert-sub-title');
@ -42,8 +43,8 @@ const testAria = async (
* expect().toHaveAttribute() can't check for a null value, so grab and check * expect().toHaveAttribute() can't check for a null value, so grab and check
* the values manually instead. * the values manually instead.
*/ */
const ariaLabelledBy = await alert.getAttribute('aria-labelledby'); const ariaLabelledBy = await alertwrapper.getAttribute('aria-labelledby');
const ariaDescribedBy = await alert.getAttribute('aria-describedby'); const ariaDescribedBy = await alertwrapper.getAttribute('aria-describedby');
expect(ariaLabelledBy).toBe(expectedAriaLabelledBy); expect(ariaLabelledBy).toBe(expectedAriaLabelledBy);
expect(ariaDescribedBy).toBe(expectedAriaDescribedBy); expect(ariaDescribedBy).toBe(expectedAriaDescribedBy);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -1,7 +1,7 @@
import { h } from '@stencil/core';
import { newSpecPage } from '@stencil/core/testing'; import { newSpecPage } from '@stencil/core/testing';
import { Alert } from '../alert'; import { Alert } from '../alert';
import { h } from '@stencil/core';
describe('alert: id', () => { describe('alert: id', () => {
it('alert should be assigned an incrementing id', async () => { it('alert should be assigned an incrementing id', async () => {
@ -49,7 +49,7 @@ describe('alert: id', () => {
template: () => <ion-alert htmlAttributes={{ id }} overlayIndex={-1}></ion-alert>, template: () => <ion-alert htmlAttributes={{ id }} overlayIndex={-1}></ion-alert>,
}); });
const alert = page.body.querySelector('ion-alert')!; const alertwrapper = page.body.querySelector('.alert-wrapper')!;
expect(alert.id).toBe(id); expect(alertwrapper.id).toBe(id);
}); });
}); });

View File

@ -19,6 +19,7 @@ configs({ directions: ['ltr'] }).forEach(({ config, screenshot, title }) => {
await page.keyboard.press(tabKey); await page.keyboard.press(tabKey);
await expect(alertBtns.nth(0)).toBeFocused(); await expect(alertBtns.nth(0)).toBeFocused();
await page.keyboard.press(`Shift+${tabKey}`); // this will focus the alert-wrapper
await page.keyboard.press(`Shift+${tabKey}`); await page.keyboard.press(`Shift+${tabKey}`);
await expect(alertBtns.nth(2)).toBeFocused(); await expect(alertBtns.nth(2)).toBeFocused();
@ -30,7 +31,7 @@ configs({ directions: ['ltr'] }).forEach(({ config, screenshot, title }) => {
const alertFixture = new AlertFixture(page, screenshot); const alertFixture = new AlertFixture(page, screenshot);
const alert = await alertFixture.open('#basic'); const alert = await alertFixture.open('#basic');
await expect(alert).toHaveAttribute('data-testid', 'basic-alert'); await expect(alert.locator('.alert-wrapper')).toHaveAttribute('data-testid', 'basic-alert');
}); });
test('should dismiss when async handler resolves', async ({ page }) => { test('should dismiss when async handler resolves', async ({ page }) => {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -2,7 +2,7 @@ import type { ComponentInterface, EventEmitter } from '@stencil/core';
import { Component, Element, Event, Host, Prop, State, Watch, h } from '@stencil/core'; import { Component, Element, Event, Host, Prop, State, Watch, h } from '@stencil/core';
import { findClosestIonContent, disableContentScrollY, resetContentScrollY } from '@utils/content'; import { findClosestIonContent, disableContentScrollY, resetContentScrollY } from '@utils/content';
import type { Attributes } from '@utils/helpers'; import type { Attributes } from '@utils/helpers';
import { inheritAriaAttributes, clamp, debounceEvent, renderHiddenInput } from '@utils/helpers'; import { inheritAriaAttributes, clamp, debounceEvent, renderHiddenInput, isSafeNumber } from '@utils/helpers';
import { printIonWarning } from '@utils/logging'; import { printIonWarning } from '@utils/logging';
import { isRTL } from '@utils/rtl'; import { isRTL } from '@utils/rtl';
import { createColorClasses, hostContext } from '@utils/theme'; import { createColorClasses, hostContext } from '@utils/theme';
@ -109,7 +109,11 @@ export class Range implements ComponentInterface {
*/ */
@Prop() min = 0; @Prop() min = 0;
@Watch('min') @Watch('min')
protected minChanged() { protected minChanged(newValue: number) {
if (!isSafeNumber(newValue)) {
this.min = 0;
}
if (!this.noUpdate) { if (!this.noUpdate) {
this.updateRatio(); this.updateRatio();
} }
@ -120,7 +124,11 @@ export class Range implements ComponentInterface {
*/ */
@Prop() max = 100; @Prop() max = 100;
@Watch('max') @Watch('max')
protected maxChanged() { protected maxChanged(newValue: number) {
if (!isSafeNumber(newValue)) {
this.max = 100;
}
if (!this.noUpdate) { if (!this.noUpdate) {
this.updateRatio(); this.updateRatio();
} }
@ -151,6 +159,12 @@ export class Range implements ComponentInterface {
* Specifies the value granularity. * Specifies the value granularity.
*/ */
@Prop() step = 1; @Prop() step = 1;
@Watch('step')
protected stepChanged(newValue: number) {
if (!isSafeNumber(newValue)) {
this.step = 1;
}
}
/** /**
* If `true`, tick marks are displayed based on the step value. * If `true`, tick marks are displayed based on the step value.
@ -300,6 +314,11 @@ export class Range implements ComponentInterface {
} }
this.inheritedAttributes = inheritAriaAttributes(this.el); this.inheritedAttributes = inheritAriaAttributes(this.el);
// If min, max, or step are not safe, set them to 0, 100, and 1, respectively.
// Each watch does this, but not before the initial load.
this.min = isSafeNumber(this.min) ? this.min : 0;
this.max = isSafeNumber(this.max) ? this.max : 100;
this.step = isSafeNumber(this.step) ? this.step : 1;
} }
componentDidLoad() { componentDidLoad() {

View File

@ -28,6 +28,25 @@ describe('Range', () => {
}); });
}); });
it('should handle undefined min and max values by falling back to defaults', async () => {
const page = await newSpecPage({
components: [Range],
html: `<ion-range id="my-custom-range">
<div slot="label">Range</div>
</ion-range>`,
});
const range = page.body.querySelector('ion-range')!;
// Here we have to cast this to any, but in its react wrapper it accepts undefined as a valid value
range.min = undefined as any;
range.max = undefined as any;
range.step = undefined as any;
await page.waitForChanges();
expect(range.min).toBe(0);
expect(range.max).toBe(100);
expect(range.step).toBe(1);
});
it('should return the clamped value for a range dual knob component', () => { it('should return the clamped value for a range dual knob component', () => {
sharedRange.min = 0; sharedRange.min = 0;
sharedRange.max = 100; sharedRange.max = 100;

View File

@ -145,6 +145,7 @@ export class Toggle implements ComponentInterface {
const isNowChecked = !checked; const isNowChecked = !checked;
this.checked = isNowChecked; this.checked = isNowChecked;
this.setFocus();
this.ionChange.emit({ this.ionChange.emit({
checked: isNowChecked, checked: isNowChecked,
value, value,

View File

@ -11,6 +11,12 @@ describe('floating point utils', () => {
const n = getDecimalPlaces(5); const n = getDecimalPlaces(5);
expect(n).toBe(0); expect(n).toBe(0);
}); });
it('should handle nullish values', () => {
expect(getDecimalPlaces(undefined as any)).toBe(0);
expect(getDecimalPlaces(null as any)).toBe(0);
expect(getDecimalPlaces(NaN as any)).toBe(0);
});
}); });
describe('roundToMaxDecimalPlaces', () => { describe('roundToMaxDecimalPlaces', () => {
@ -18,5 +24,11 @@ describe('floating point utils', () => {
const n = roundToMaxDecimalPlaces(5.12345, 1.12, 2.123); const n = roundToMaxDecimalPlaces(5.12345, 1.12, 2.123);
expect(n).toBe(5.123); expect(n).toBe(5.123);
}); });
it('should handle nullish values', () => {
expect(roundToMaxDecimalPlaces(undefined as any)).toBe(0);
expect(roundToMaxDecimalPlaces(null as any)).toBe(0);
expect(roundToMaxDecimalPlaces(NaN as any)).toBe(0);
});
}); });
}); });

View File

@ -1,4 +1,7 @@
import { isSafeNumber } from '@utils/helpers';
export function getDecimalPlaces(n: number) { export function getDecimalPlaces(n: number) {
if (!isSafeNumber(n)) return 0;
if (n % 1 === 0) return 0; if (n % 1 === 0) return 0;
return n.toString().split('.')[1].length; return n.toString().split('.')[1].length;
} }
@ -36,6 +39,7 @@ export function getDecimalPlaces(n: number) {
* be used as a reference for the desired specificity. * be used as a reference for the desired specificity.
*/ */
export function roundToMaxDecimalPlaces(n: number, ...references: number[]) { export function roundToMaxDecimalPlaces(n: number, ...references: number[]) {
if (!isSafeNumber(n)) return 0;
const maxPlaces = Math.max(...references.map((r) => getDecimalPlaces(r))); const maxPlaces = Math.max(...references.map((r) => getDecimalPlaces(r)));
return Number(n.toFixed(maxPlaces)); return Number(n.toFixed(maxPlaces));
} }

View File

@ -424,3 +424,10 @@ export const getNextSiblingOfType = <T extends Element>(element: Element): T | n
} }
return null; return null;
}; };
/**
* Checks input for usable number. Not NaN and not Infinite.
*/
export const isSafeNumber = (input: unknown): input is number => {
return typeof input === 'number' && !isNaN(input) && isFinite(input);
};

View File

@ -1,9 +1,17 @@
import type { CapacitorGlobal } from '@capacitor/core'; import type { CapacitorGlobal } from '@capacitor/core';
import { win } from '@utils/browser'; import { win } from '@utils/browser';
type CustomCapacitorGlobal = CapacitorGlobal & {
// Capacitor from @capacitor/core no longer exports Plugins, but we're pulling
// Capacitor from window.Capacitor, which does
Plugins: {
[key: string]: any;
};
};
export const getCapacitor = () => { export const getCapacitor = () => {
if (win !== undefined) { if (win !== undefined) {
return (win as any).Capacitor as CapacitorGlobal; return (win as any).Capacitor as CustomCapacitorGlobal;
} }
return undefined; return undefined;
}; };

View File

@ -99,7 +99,8 @@ const isCordova = (win: any): boolean => !!(win['cordova'] || win['phonegap'] ||
const isCapacitorNative = (win: any): boolean => { const isCapacitorNative = (win: any): boolean => {
const capacitor = win['Capacitor']; const capacitor = win['Capacitor'];
return !!capacitor?.isNative; // TODO(ROU-11693): Remove when we no longer support Capacitor 2, which does not have isNativePlatform
return !!(capacitor?.isNative || (capacitor?.isNativePlatform && !!capacitor.isNativePlatform()));
}; };
const isElectron = (win: Window): boolean => testUserAgent(win, /electron/i); const isElectron = (win: Window): boolean => testUserAgent(win, /electron/i);

View File

@ -29,7 +29,7 @@ export const PlatformConfiguration = {
}, },
Capacitor: { Capacitor: {
Capacitor: { Capacitor: {
isNative: true, isNativePlatform: () => true,
}, },
}, },
PWA: { PWA: {