chore(): sync feature-6.2 with main

chore(): sync feature-6.2 with main
This commit is contained in:
Liam DeBeasi
2022-07-21 09:43:36 -04:00
committed by GitHub
437 changed files with 1848 additions and 2821 deletions

View File

@@ -3,6 +3,37 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [6.1.15](https://github.com/ionic-team/ionic-framework/compare/v6.1.14...v6.1.15) (2022-07-20)
### Bug Fixes
* **datetime:** use scroll listener to detect month changes ([#25586](https://github.com/ionic-team/ionic-framework/issues/25586)) ([b7afcb0](https://github.com/ionic-team/ionic-framework/commit/b7afcb0f0c36d84f3b4d65844df28e6293bc1ea5)), closes [#25257](https://github.com/ionic-team/ionic-framework/issues/25257) [#25608](https://github.com/ionic-team/ionic-framework/issues/25608) [#24980](https://github.com/ionic-team/ionic-framework/issues/24980)
* **fab-button:** aria attributes are inherited ([#25635](https://github.com/ionic-team/ionic-framework/issues/25635)) ([64ae3d2](https://github.com/ionic-team/ionic-framework/commit/64ae3d2b9729c5c6be8644b1df6c8b3d40584d3b)), closes [#25633](https://github.com/ionic-team/ionic-framework/issues/25633)
* **modal:** allow for custom dialog implementations ([#25630](https://github.com/ionic-team/ionic-framework/issues/25630)) ([a6f3ae6](https://github.com/ionic-team/ionic-framework/commit/a6f3ae67ab91ab95408ad425156167edc3570978)), closes [#24080](https://github.com/ionic-team/ionic-framework/issues/24080)
* **react:** swipe to go back gesture works on ios ([#25563](https://github.com/ionic-team/ionic-framework/issues/25563)) ([7ec3683](https://github.com/ionic-team/ionic-framework/commit/7ec3683e94e5397022560ce8489532ff40d3f40c)), closes [#22342](https://github.com/ionic-team/ionic-framework/issues/22342)
### Performance Improvements
* **input:** passive event listener for touch start events ([#25610](https://github.com/ionic-team/ionic-framework/issues/25610)) ([2d1efdb](https://github.com/ionic-team/ionic-framework/commit/2d1efdbe6dd9436badab4684f2a484476489c166)), closes [#25599](https://github.com/ionic-team/ionic-framework/issues/25599)
## [6.1.14](https://github.com/ionic-team/ionic-framework/compare/v6.1.13...v6.1.14) (2022-07-13)
### Bug Fixes
* **datetime:** datetime works within stencil apps ([#25592](https://github.com/ionic-team/ionic-framework/issues/25592)) ([7b10fa6](https://github.com/ionic-team/ionic-framework/commit/7b10fa6476c2c2896c6810c57b3160f8c8896faa)), closes [#25591](https://github.com/ionic-team/ionic-framework/issues/25591)
* **react:** IonNav works with react ([#25565](https://github.com/ionic-team/ionic-framework/issues/25565)) ([420f9bb](https://github.com/ionic-team/ionic-framework/commit/420f9bbebd41f3eab6def795bcdd1933d5c5a47a)), closes [#24002](https://github.com/ionic-team/ionic-framework/issues/24002)
## [6.1.13](https://github.com/ionic-team/ionic-framework/compare/v6.1.12...v6.1.13) (2022-07-06)

View File

@@ -3,6 +3,22 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [6.1.15](https://github.com/ionic-team/ionic/compare/v6.1.14...v6.1.15) (2022-07-20)
**Note:** Version bump only for package @ionic/angular
## [6.1.14](https://github.com/ionic-team/ionic/compare/v6.1.13...v6.1.14) (2022-07-13)
**Note:** Version bump only for package @ionic/angular
## [6.1.13](https://github.com/ionic-team/ionic/compare/v6.1.12...v6.1.13) (2022-07-06)
**Note:** Version bump only for package @ionic/angular

View File

@@ -1,15 +1,15 @@
{
"name": "@ionic/angular",
"version": "6.1.13",
"version": "6.1.15",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@ionic/angular",
"version": "6.1.13",
"version": "6.1.15",
"license": "MIT",
"dependencies": {
"@ionic/core": "^6.1.13",
"@ionic/core": "^6.1.15",
"jsonc-parser": "^3.0.0",
"tslib": "^2.0.0"
},
@@ -1023,9 +1023,9 @@
"dev": true
},
"node_modules/@ionic/core": {
"version": "6.1.13",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-6.1.13.tgz",
"integrity": "sha512-CZ5P1El/bk6ZDKqey/67/ZgpUhVQTr+WyhGxFTnPCsIWg+VfOogQ7rHCkEqWfXJqdzNPsvtH5/Lck4qoD0nCkg==",
"version": "6.1.15",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-6.1.15.tgz",
"integrity": "sha512-NvBlHR7O4kfp9KYz6oLsghFzGZImA7hM4qS4tFRUI62R+Q5iCJEY4OmXE5bif5K+SQkMhQY+x1l2Nq20waLzLg==",
"dependencies": {
"@stencil/core": "^2.16.0",
"ionicons": "^6.0.2",
@@ -7939,9 +7939,9 @@
"dev": true
},
"@ionic/core": {
"version": "6.1.13",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-6.1.13.tgz",
"integrity": "sha512-CZ5P1El/bk6ZDKqey/67/ZgpUhVQTr+WyhGxFTnPCsIWg+VfOogQ7rHCkEqWfXJqdzNPsvtH5/Lck4qoD0nCkg==",
"version": "6.1.15",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-6.1.15.tgz",
"integrity": "sha512-NvBlHR7O4kfp9KYz6oLsghFzGZImA7hM4qS4tFRUI62R+Q5iCJEY4OmXE5bif5K+SQkMhQY+x1l2Nq20waLzLg==",
"requires": {
"@stencil/core": "^2.16.0",
"ionicons": "^6.0.2",

View File

@@ -1,6 +1,6 @@
{
"name": "@ionic/angular",
"version": "6.1.13",
"version": "6.1.15",
"description": "Angular specific wrappers for @ionic/core",
"keywords": [
"ionic",
@@ -44,7 +44,7 @@
"validate": "npm i && npm run lint && npm run test && npm run build"
},
"dependencies": {
"@ionic/core": "^6.1.13",
"@ionic/core": "^6.1.15",
"jsonc-parser": "^3.0.0",
"tslib": "^2.0.0"
},

View File

@@ -25,7 +25,7 @@
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
Cypress.Commands.add('ionSwipeToGoBack', (complete = false, selector = 'ion-router-outlet') => {
const increment = (complete) ? 60 : 25;
const increment = complete ? 60 : 25;
cy.get(selector)
.first()
.trigger('mousedown', 0, 275, { which: 1, force: true })
@@ -37,18 +37,16 @@ Cypress.Commands.add('ionSwipeToGoBack', (complete = false, selector = 'ion-rout
.wait(50)
.trigger('mousemove', increment * 4, 275, { which: 1, force: true })
.wait(50)
.trigger('mouseup', increment * 4, 275, { which: 1, force: true })
.trigger('mouseup', increment * 4, 275, { which: 1, force: true });
cy.wait(150);
})
});
Cypress.Commands.add('testStack', (selector, expected) => {
cy.document().then((doc) => {
const children = Array.from(
doc.querySelector(selector).children
).map(el => el.tagName.toLowerCase());
const children = Array.from(doc.querySelector(selector).children).map((el) => el.tagName.toLowerCase());
expect(children).to.deep.equal(expected);
})
})
});
});
Cypress.Commands.add('testLifeCycle', (selector, expected) => {
cy.get(`${selector} #ngOnInit`).invoke('text').should('equal', '1');
@@ -56,24 +54,26 @@ Cypress.Commands.add('testLifeCycle', (selector, expected) => {
cy.get(`${selector} #ionViewDidEnter`).invoke('text').should('equal', expected.ionViewDidEnter.toString());
cy.get(`${selector} #ionViewWillLeave`).invoke('text').should('equal', expected.ionViewWillLeave.toString());
cy.get(`${selector} #ionViewDidLeave`).invoke('text').should('equal', expected.ionViewDidLeave.toString());
})
});
Cypress.Commands.add('ionPageVisible', (selector) => {
cy.get(selector)
.should('have.class', 'ion-page')
.should('not.have.class', 'ion-page-hidden')
.should('not.have.class', 'ion-page-invisible')
.should('have.length', 1)
})
.should('have.length', 1);
});
Cypress.Commands.add('ionPageHidden', (selector) => {
cy.get(selector)
.should('have.class', 'ion-page')
.should('have.class', 'ion-page-hidden')
.should('have.length', 1)
})
cy.get(selector).should('have.class', 'ion-page').should('have.class', 'ion-page-hidden').should('have.length', 1);
});
Cypress.Commands.add('ionPageDoesNotExist', (selector) => {
cy.get(selector)
.should('not.exist')
cy.get(selector).should('not.exist');
});
Cypress.Commands.add('ionTabClick', (tabText) => {
// TODO: Figure out how to get rid of wait. It's a workaround for flakiness in CI.
cy.wait(250);
cy.contains('ion-tab-button', tabText).click({ force: true });
});

View File

@@ -63,6 +63,11 @@ declare namespace Cypress {
* ```
*/
ionPageDoesNotExist(selector: string): Chainable<any>
/**
* Clicks on a tab button with the given text.
*/
ionTabClick(tabText: string): Chainable<any>;
}
}

View File

@@ -230,13 +230,13 @@ describe('Tabs', () => {
tab.find('#goto-next').click();
testTabTitle('Tab 1 - Page 2 (2)');
cy.get('#tab-button-contact').click();
cy.ionTabClick('Tab Two');
testTabTitle('Tab 2 - Page 1');
cy.get('#tab-button-account').click();
cy.ionTabClick('Tab One');
testTabTitle('Tab 1 - Page 2 (2)');
cy.get('#tab-button-account').click();
cy.ionTabClick('Tab One');
testTabTitle('Tab 1 - Page 1');
testUrlContains(rootUrl);

View File

@@ -3,6 +3,35 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [6.1.15](https://github.com/ionic-team/ionic/compare/v6.1.14...v6.1.15) (2022-07-20)
### Bug Fixes
* **datetime:** use scroll listener to detect month changes ([#25586](https://github.com/ionic-team/ionic/issues/25586)) ([b7afcb0](https://github.com/ionic-team/ionic/commit/b7afcb0f0c36d84f3b4d65844df28e6293bc1ea5)), closes [#25257](https://github.com/ionic-team/ionic/issues/25257) [#25608](https://github.com/ionic-team/ionic/issues/25608) [#24980](https://github.com/ionic-team/ionic/issues/24980)
* **fab-button:** aria attributes are inherited ([#25635](https://github.com/ionic-team/ionic/issues/25635)) ([64ae3d2](https://github.com/ionic-team/ionic/commit/64ae3d2b9729c5c6be8644b1df6c8b3d40584d3b)), closes [#25633](https://github.com/ionic-team/ionic/issues/25633)
* **modal:** allow for custom dialog implementations ([#25630](https://github.com/ionic-team/ionic/issues/25630)) ([a6f3ae6](https://github.com/ionic-team/ionic/commit/a6f3ae67ab91ab95408ad425156167edc3570978)), closes [#24080](https://github.com/ionic-team/ionic/issues/24080)
### Performance Improvements
* **input:** passive event listener for touch start events ([#25610](https://github.com/ionic-team/ionic/issues/25610)) ([2d1efdb](https://github.com/ionic-team/ionic/commit/2d1efdbe6dd9436badab4684f2a484476489c166)), closes [#25599](https://github.com/ionic-team/ionic/issues/25599)
## [6.1.14](https://github.com/ionic-team/ionic/compare/v6.1.13...v6.1.14) (2022-07-13)
### Bug Fixes
* **datetime:** datetime works within stencil apps ([#25592](https://github.com/ionic-team/ionic/issues/25592)) ([7b10fa6](https://github.com/ionic-team/ionic/commit/7b10fa6476c2c2896c6810c57b3160f8c8896faa)), closes [#25591](https://github.com/ionic-team/ionic/issues/25591)
## [6.1.13](https://github.com/ionic-team/ionic/compare/v6.1.12...v6.1.13) (2022-07-06)

1456
core/package-lock.json generated
View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "@ionic/core",
"version": "6.1.13",
"version": "6.1.15",
"description": "Base components for Ionic",
"keywords": [
"ionic",
@@ -41,7 +41,7 @@
"@ionic/eslint-config": "^0.3.0",
"@ionic/prettier-config": "^2.0.0",
"@jest/core": "^26.6.3",
"@playwright/test": "^1.20.0",
"@playwright/test": "^1.23.3",
"@rollup/plugin-node-resolve": "^8.4.0",
"@rollup/plugin-virtual": "^2.0.3",
"@stencil/angular-output-target": "^0.4.0",

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 117 KiB

View File

@@ -20,7 +20,7 @@ test.describe('action sheet: basic', () => {
await buttonOption.click();
await ionActionSheetDidDismiss.next();
expect(ionActionSheetDidDismiss).toHaveReceivedEventDetail({ data: { type: '1' } });
expect(ionActionSheetDidDismiss).toHaveReceivedEventDetail({ data: { type: '1' }, role: undefined });
});
test('should return cancel button data', async ({ page }, testInfo) => {
test.skip(testInfo.project.metadata.rtl === true, 'This does not test LTR vs. RTL layout.');

View File

@@ -0,0 +1,12 @@
import { expect } from '@playwright/test';
import { test } from '@utils/test/playwright';
test.describe('checkbox: basic', () => {
test('should not have visual regressions', async ({ page }) => {
await page.goto(`/src/components/checkbox/test/basic`);
await page.setIonViewport();
expect(await page.screenshot()).toMatchSnapshot(`checkbox-basic-${page.getSnapshotSettings()}.png`);
});
});

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

View File

@@ -1,10 +0,0 @@
import { newE2EPage } from '@stencil/core/testing';
test('checkbox: basic', async () => {
const page = await newE2EPage({
url: '/src/components/checkbox/test/basic?ionic:_testing=true',
});
const compare = await page.compareScreenshot();
expect(compare).toMatchScreenshot();
});

View File

@@ -44,7 +44,7 @@
</ion-item>
<ion-item>
<ion-label>Secondary</ion-label>
<ion-label>Secondary, disabled</ion-label>
<ion-checkbox disabled checked color="secondary"></ion-checkbox>
</ion-item>
@@ -64,7 +64,7 @@
</ion-item>
<ion-item>
<ion-label>Dark</ion-label>
<ion-label>Dark, --size</ion-label>
<ion-checkbox checked color="dark" style="--size: 100px"></ion-checkbox>
</ion-item>
@@ -83,11 +83,6 @@
<ion-checkbox checked color="medium" slot="start"></ion-checkbox>
</ion-item>
<ion-item>
<ion-label>Dark</ion-label>
<ion-checkbox checked color="dark" slot="start" style="--size: 100px"></ion-checkbox>
</ion-item>
<ion-item>
<ion-label>Checkmark width</ion-label>
<ion-checkbox checked color="dark" slot="start" style="--checkmark-width: 7"></ion-checkbox>
@@ -109,33 +104,5 @@
</ion-item>
</ion-content>
</ion-app>
<script>
const inputs = document.querySelectorAll('ion-checkbox');
for (var i = 0; i < inputs.length; i++) {
const input = inputs[i];
input.addEventListener('ionBlur', function () {
console.log('Listen ionBlur: fired');
});
input.addEventListener('ionFocus', function () {
console.log('Listen ionFocus: fired');
});
input.addEventListener('ionChange', function (ev) {
console.log('Listen ionChange: fired', ev.detail);
});
input.addEventListener('click', function () {
console.log('Listen click: fired');
});
}
const clickItem = () => {
console.log('Item click: fired');
};
</script>
</body>
</html>

View File

@@ -0,0 +1,12 @@
import { expect } from '@playwright/test';
import { test } from '@utils/test/playwright';
test.describe('checkbox: indeterminate', () => {
test('should not have visual regressions', async ({ page }) => {
await page.goto(`/src/components/checkbox/test/indeterminate`);
await page.setIonViewport();
expect(await page.screenshot()).toMatchSnapshot(`checkbox-indeterminate-${page.getSnapshotSettings()}.png`);
});
});

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

View File

@@ -1,10 +0,0 @@
import { newE2EPage } from '@stencil/core/testing';
test('checkbox: indeterminate', async () => {
const page = await newE2EPage({
url: '/src/components/checkbox/test/indeterminate?ionic:_testing=true',
});
const compare = await page.compareScreenshot();
expect(compare).toMatchScreenshot();
});

View File

@@ -23,45 +23,6 @@
</ion-header>
<ion-content id="content">
<ion-list-header>
<ion-label> Native </ion-label>
</ion-list-header>
<div class="ion-padding-start">
<!-- Default to unchecked -->
<label for="unchecked">Unchecked</label>
<input name="unchecked" id="unchecked" type="checkbox" />
<br />
<!-- Default to checked -->
<label for="checked">Checked</label>
<input name="checked" id="checked" type="checkbox" checked />
<br />
<!-- Default to indeterminate -->
<label for="indeterminate">Indeterminate</label>
<input name="indeterminate" id="indeterminate" type="checkbox" class="indeterminate" />
<br />
<!-- Default to checked / indeterminate -->
<label for="both">Checked / Indeterminate</label>
<input name="both" id="both" type="checkbox" checked class="indeterminate" />
<br />
</div>
<ion-list-header>
<ion-label> Ionic </ion-label>
</ion-list-header>
<ion-item>
<ion-label>Unchecked</ion-label>
<ion-checkbox slot="end"></ion-checkbox>
</ion-item>
<ion-item>
<ion-label>Checked</ion-label>
<ion-checkbox slot="end" checked></ion-checkbox>
</ion-item>
<ion-item>
<ion-label>Indeterminate</ion-label>
<ion-checkbox slot="end" indeterminate></ion-checkbox>

View File

@@ -0,0 +1,12 @@
import { expect } from '@playwright/test';
import { test } from '@utils/test/playwright';
test.describe('checkbox: standalone', () => {
test('should not have visual regressions', async ({ page }) => {
await page.goto(`/src/components/checkbox/test/standalone`);
expect(await page.screenshot({ fullPage: true })).toMatchSnapshot(
`checkbox-standalone-${page.getSnapshotSettings()}.png`
);
});
});

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

View File

@@ -1,10 +0,0 @@
import { newE2EPage } from '@stencil/core/testing';
test('checkbox: standalone', async () => {
const page = await newE2EPage({
url: '/src/components/checkbox/test/standalone?ionic:_testing=true',
});
const compare = await page.compareScreenshot();
expect(compare).toMatchScreenshot();
});

View File

@@ -126,9 +126,8 @@ ion-picker-column-internal {
}
/**
* Safari 14 has an issue where Intersection
* Observer is incorrectly fired when
* unhiding the calendar content.
* Safari 14 has an issue where a scroll event
* is incorrectly fired when unhiding the calendar content.
* To workaround this, we set the opacity
* of the content to 0 and hide it offscreen.
*

View File

@@ -68,7 +68,6 @@ export class Datetime implements ComponentInterface {
private calendarBodyRef?: HTMLElement;
private popoverRef?: HTMLIonPopoverElement;
private clearFocusVisible?: () => void;
private overlayIsPresenting = false;
/**
* Whether to highlight the active day with a solid circle (as opposed
@@ -86,9 +85,8 @@ export class Datetime implements ComponentInterface {
private parsedYearValues?: number[];
private parsedDayValues?: number[];
private destroyCalendarIO?: () => void;
private destroyCalendarListener?: () => void;
private destroyKeyboardMO?: () => void;
private destroyOverlayListener?: () => void;
private minParts?: any;
private maxParts?: any;
@@ -806,20 +804,17 @@ export class Datetime implements ComponentInterface {
};
};
private initializeCalendarIOListeners = () => {
private initializeCalendarListener = () => {
const calendarBodyRef = this.getCalendarBodyEl();
if (!calendarBodyRef) {
return;
}
const mode = getIonMode(this);
/**
* For performance reasons, we only render 3
* months at a time: The current month, the previous
* month, and the next month. We have IntersectionObservers
* on the previous and next month elements to append/prepend
* new months.
* month, and the next month. We have a scroll listener
* on the calendar body to append/prepend new months.
*
* We can do this because Stencil is smart enough to not
* re-create the .calendar-month containers, but rather
@@ -835,43 +830,78 @@ export class Datetime implements ComponentInterface {
const startMonth = months[0] as HTMLElement;
const workingMonth = months[1] as HTMLElement;
const endMonth = months[2] as HTMLElement;
const mode = getIonMode(this);
const needsiOSRubberBandFix = mode === 'ios' && typeof navigator !== 'undefined' && navigator.maxTouchPoints > 1;
/**
* Before setting up the IntersectionObserver,
* Before setting up the scroll listener,
* scroll the middle month into view.
* scrollIntoView() will scroll entire page
* if element is not in viewport. Use scrollLeft instead.
*/
let endIO: IntersectionObserver | undefined;
let startIO: IntersectionObserver | undefined;
writeTask(() => {
calendarBodyRef.scrollLeft = startMonth.clientWidth * (isRTL(this.el) ? -1 : 1);
const ioCallback = (callbackType: 'start' | 'end', entries: IntersectionObserverEntry[]) => {
const refIO = callbackType === 'start' ? startIO : endIO;
const refMonth = callbackType === 'start' ? startMonth : endMonth;
const refMonthFn = callbackType === 'start' ? getPreviousMonth : getNextMonth;
const getChangedMonth = (parts: DatetimeParts): DatetimeParts | undefined => {
const box = calendarBodyRef.getBoundingClientRect();
const root = this.el!.shadowRoot!;
/**
* If the month is not fully in view, do not do anything
* Get the element that is in the center of the calendar body.
* This will be an element inside of the active month.
*/
const ev = entries[0];
if (!ev.isIntersecting) {
const elementAtCenter = root.elementFromPoint(box.x + box.width / 2, box.y + box.height / 2);
/**
* If there is no element then the
* component may be re-rendering on a slow device.
*/
if (!elementAtCenter) return;
const month = elementAtCenter.closest('.calendar-month');
if (!month) return;
/**
* The edge of the month must be lined up with
* the edge of the calendar body in order for
* the component to update. Otherwise, it
* may be the case that the user has paused their
* swipe or the browser has not finished snapping yet.
* Rather than check if the x values are equal,
* we give it a tolerance of 2px to account for
* sub pixel rendering.
*/
const monthBox = month.getBoundingClientRect();
if (Math.abs(monthBox.x - box.x) > 2) return;
/**
* From here, we can determine if the start
* month or the end month was scrolled into view.
* If no month was changed, then we can return from
* the scroll callback early.
*/
if (month === startMonth) {
return getPreviousMonth(parts);
} else if (month === endMonth) {
return getNextMonth(parts);
} else {
return;
}
};
const updateActiveMonth = () => {
if (needsiOSRubberBandFix) {
calendarBodyRef.style.removeProperty('pointer-events');
appliediOSRubberBandFix = false;
}
/**
* When presenting an inline overlay,
* subsequent presentations will cause
* the IO to fire again (since the overlay
* is now visible and therefore the calendar
* months are intersecting).
* If the month did not change
* then we can return early.
*/
if (this.overlayIsPresenting) {
this.overlayIsPresenting = false;
return;
}
const newDate = getChangedMonth(this.workingParts);
if (!newDate) return;
const { month, year, day } = refMonthFn(this.workingParts);
const { month, day, year } = newDate;
if (
isMonthDisabled(
@@ -885,25 +915,6 @@ export class Datetime implements ComponentInterface {
return;
}
/**
* On iOS, we need to set pointer-events: none
* when the user is almost done with the gesture
* so that they cannot quickly swipe while
* the scrollable container is snapping.
* Updating the container while snapping
* causes WebKit to snap incorrectly.
*/
if (mode === 'ios') {
const ratio = ev.intersectionRatio;
// `maxTouchPoints` will be 1 in device preview, but > 1 on device
const shouldDisable = Math.abs(ratio - 0.7) <= 0.1 && navigator.maxTouchPoints > 1;
if (shouldDisable) {
calendarBodyRef.style.setProperty('pointer-events', 'none');
return;
}
}
/**
* Prevent scrolling for other browsers
* to give the DOM time to update and the container
@@ -911,16 +922,6 @@ export class Datetime implements ComponentInterface {
*/
calendarBodyRef.style.setProperty('overflow', 'hidden');
/**
* Remove the IO temporarily
* otherwise you can sometimes get duplicate
* events when rubber banding.
*/
if (refIO === undefined) {
return;
}
refIO.disconnect();
/**
* Use a writeTask here to ensure
* that the state is updated and the
@@ -931,85 +932,57 @@ export class Datetime implements ComponentInterface {
* if we did not do this.
*/
writeTask(() => {
// Disconnect all active intersection observers
// to avoid a re-render causing a duplicate event.
if (this.destroyCalendarIO) {
this.destroyCalendarIO();
}
raf(() => {
this.setWorkingParts({
...this.workingParts,
month,
day: day!,
year,
});
calendarBodyRef.scrollLeft = workingMonth.clientWidth * (isRTL(this.el) ? -1 : 1);
calendarBodyRef.style.removeProperty('overflow');
calendarBodyRef.style.removeProperty('pointer-events');
endIO?.observe(endMonth);
startIO?.observe(startMonth);
this.setWorkingParts({
...this.workingParts,
month,
day: day!,
year,
});
/**
* Now that state has been updated
* and the correct month is in view,
* we can resume the IO.
*/
if (refIO === undefined) {
return;
}
refIO.observe(refMonth);
calendarBodyRef.scrollLeft = workingMonth.clientWidth * (isRTL(this.el) ? -1 : 1);
calendarBodyRef.style.removeProperty('overflow');
});
};
const threshold =
mode === 'ios' && typeof navigator !== 'undefined' && navigator.maxTouchPoints > 1 ? [0.7, 1] : 1;
// Intersection observers cannot accurately detect the
// intersection with a threshold of 1, when the observed
// element width is a sub-pixel value (i.e. 334.05px).
// Setting a root margin to 1px solves the issue.
const rootMargin = '1px';
/**
* When the container finishes scrolling we
* need to update the DOM with the selected month.
*/
let scrollTimeout: ReturnType<typeof setTimeout> | undefined;
/**
* Listen on the first month to
* prepend a new month and on the last
* month to append a new month.
* The 0.7 threshold is required on ios
* so that we can remove pointer-events
* when adding new months.
* Adding to a scroll snapping container
* while the container is snapping does not
* completely work as expected in WebKit.
* Adding pointer-events: none allows us to
* avoid these issues.
*
* This should be fine on Chromium, but
* when you set pointer-events: none
* it applies to active gestures which is not
* something WebKit does.
* We do not want to attempt to set pointer-events
* multiple times within a single swipe gesture as
* that adds unnecessary work to the main thread.
*/
let appliediOSRubberBandFix = false;
const scrollCallback = () => {
if (scrollTimeout) {
clearTimeout(scrollTimeout);
}
endIO = new IntersectionObserver((ev) => ioCallback('end', ev), {
threshold,
root: calendarBodyRef,
rootMargin,
});
endIO.observe(endMonth);
/**
* On iOS it is possible to quickly rubber band
* the scroll area before the scroll timeout has fired.
* This results in users reaching the end of the scrollable
* container before the DOM has updated.
* By setting `pointer-events: none` we can ensure that
* subsequent swipes do not happen while the container
* is snapping.
*/
if (!appliediOSRubberBandFix && needsiOSRubberBandFix) {
calendarBodyRef.style.setProperty('pointer-events', 'none');
appliediOSRubberBandFix = true;
}
startIO = new IntersectionObserver((ev) => ioCallback('start', ev), {
threshold,
root: calendarBodyRef,
rootMargin,
});
startIO.observe(startMonth);
// Wait ~3 frames
scrollTimeout = setTimeout(updateActiveMonth, 50);
};
this.destroyCalendarIO = () => {
endIO?.disconnect();
startIO?.disconnect();
calendarBodyRef.addEventListener('scroll', scrollCallback);
this.destroyCalendarListener = () => {
calendarBodyRef.removeEventListener('scroll', scrollCallback);
};
});
};
@@ -1031,10 +1004,10 @@ export class Datetime implements ComponentInterface {
* if the datetime has been hidden/presented by a modal or popover.
*/
private destroyInteractionListeners = () => {
const { destroyCalendarIO, destroyKeyboardMO } = this;
const { destroyCalendarListener, destroyKeyboardMO } = this;
if (destroyCalendarIO !== undefined) {
destroyCalendarIO();
if (destroyCalendarListener !== undefined) {
destroyCalendarListener();
}
if (destroyKeyboardMO !== undefined) {
@@ -1043,9 +1016,8 @@ export class Datetime implements ComponentInterface {
};
private initializeListeners() {
this.initializeCalendarIOListeners();
this.initializeCalendarListener();
this.initializeKeyboardListeners();
this.initializeOverlayListener();
}
componentDidLoad() {
@@ -1140,37 +1112,10 @@ export class Datetime implements ComponentInterface {
this.prevPresentation = presentation;
this.destroyInteractionListeners();
if (this.destroyOverlayListener !== undefined) {
this.destroyOverlayListener();
}
this.initializeListeners();
}
/**
* When doing subsequent presentations of an inline
* overlay, the IO callback will fire again causing
* the calendar to go back one month. We need to listen
* for the presentation of the overlay so we can properly
* cancel that IO callback.
*/
private initializeOverlayListener = () => {
const overlay = this.el.closest('ion-popover, ion-modal');
if (overlay === null) {
return;
}
const overlayListener = () => {
this.overlayIsPresenting = true;
};
overlay.addEventListener('willPresent', overlayListener);
this.destroyOverlayListener = () => {
overlay.removeEventListener('willPresent', overlayListener);
};
};
private processValue = (value?: string | string[] | null) => {
this.highlightActiveParts = !!value;
let valueToProcess = parseDate(value || getToday());
@@ -1471,11 +1416,12 @@ export class Datetime implements ComponentInterface {
items={items}
value={todayString}
onIonChange={(ev: CustomEvent) => {
// TODO(FW-1823) Remove this when iOS 14 support is dropped.
// Due to a Safari 14 issue we need to destroy
// the intersection observer before we update state
// the scroll listener before we update state
// and trigger a re-render.
if (this.destroyCalendarIO) {
this.destroyCalendarIO();
if (this.destroyCalendarListener) {
this.destroyCalendarListener();
}
const { value } = ev.detail;
@@ -1493,9 +1439,9 @@ export class Datetime implements ComponentInterface {
});
}
// We can re-attach the intersection observer after
// We can re-attach the scroll listener after
// the working parts have been updated.
this.initializeCalendarIOListeners();
this.initializeCalendarListener();
ev.stopPropagation();
}}
@@ -1569,11 +1515,12 @@ export class Datetime implements ComponentInterface {
items={days}
value={(workingParts.day || this.todayParts.day) ?? undefined}
onIonChange={(ev: CustomEvent) => {
// TODO(FW-1823) Remove this when iOS 14 support is dropped.
// Due to a Safari 14 issue we need to destroy
// the intersection observer before we update state
// the scroll listener before we update state
// and trigger a re-render.
if (this.destroyCalendarIO) {
this.destroyCalendarIO();
if (this.destroyCalendarListener) {
this.destroyCalendarListener();
}
this.setWorkingParts({
@@ -1588,9 +1535,9 @@ export class Datetime implements ComponentInterface {
});
}
// We can re-attach the intersection observer after
// We can re-attach the scroll listener after
// the working parts have been updated.
this.initializeCalendarIOListeners();
this.initializeCalendarListener();
ev.stopPropagation();
}}
@@ -1612,11 +1559,12 @@ export class Datetime implements ComponentInterface {
items={months}
value={workingParts.month}
onIonChange={(ev: CustomEvent) => {
// TODO(FW-1823) Remove this when iOS 14 support is dropped.
// Due to a Safari 14 issue we need to destroy
// the intersection observer before we update state
// the scroll listener before we update state
// and trigger a re-render.
if (this.destroyCalendarIO) {
this.destroyCalendarIO();
if (this.destroyCalendarListener) {
this.destroyCalendarListener();
}
this.setWorkingParts({
@@ -1631,9 +1579,9 @@ export class Datetime implements ComponentInterface {
});
}
// We can re-attach the intersection observer after
// We can re-attach the scroll listener after
// the working parts have been updated.
this.initializeCalendarIOListeners();
this.initializeCalendarListener();
ev.stopPropagation();
}}
@@ -1654,11 +1602,12 @@ export class Datetime implements ComponentInterface {
items={years}
value={workingParts.year}
onIonChange={(ev: CustomEvent) => {
// TODO(FW-1823) Remove this when iOS 14 support is dropped.
// Due to a Safari 14 issue we need to destroy
// the intersection observer before we update state
// the scroll listener before we update state
// and trigger a re-render.
if (this.destroyCalendarIO) {
this.destroyCalendarIO();
if (this.destroyCalendarListener) {
this.destroyCalendarListener();
}
this.setWorkingParts({
@@ -1673,9 +1622,9 @@ export class Datetime implements ComponentInterface {
});
}
// We can re-attach the intersection observer after
// We can re-attach the scroll listener after
// the working parts have been updated.
this.initializeCalendarIOListeners();
this.initializeCalendarListener();
ev.stopPropagation();
}}

View File

@@ -183,3 +183,69 @@ test.describe('datetime: footer', () => {
);
});
});
test.describe('datetime: swiping', () => {
// eslint-disable-next-line no-empty-pattern
test.beforeEach(({}, testInfo) => {
test.skip(testInfo.project.metadata.rtl === true, 'This does not test LTR vs RTL layouts.');
test.skip(testInfo.project.metadata.mode === 'ios', 'This does not have mode-specific logic.');
});
test('should move to prev month by swiping', async ({ page }) => {
await page.setContent(`
<ion-datetime value="2022-05-03"></ion-datetime>
`);
await page.waitForSelector('.datetime-ready');
const calendarBody = page.locator('ion-datetime .calendar-body');
const calendarHeader = page.locator('ion-datetime .calendar-month-year');
await expect(calendarHeader).toHaveText(/May 2022/);
await calendarBody.evaluate((el: HTMLElement) => (el.scrollLeft = 0));
await page.waitForChanges();
await expect(calendarHeader).toHaveText(/April 2022/);
});
test('should move to next month by swiping', async ({ page }) => {
await page.setContent(`
<ion-datetime value="2022-05-03"></ion-datetime>
`);
await page.waitForSelector('.datetime-ready');
const calendarBody = page.locator('ion-datetime .calendar-body');
const calendarHeader = page.locator('ion-datetime .calendar-month-year');
await expect(calendarHeader).toHaveText(/May 2022/);
await calendarBody.evaluate((el: HTMLElement) => (el.scrollLeft = el.scrollWidth));
await page.waitForChanges();
await expect(calendarHeader).toHaveText(/June 2022/);
});
test('should not re-render if swipe is in progress', async ({ page, browserName }) => {
test.skip(browserName === 'webkit', 'Wheel is not available in WebKit');
await page.setContent(`
<ion-datetime value="2022-05-03"></ion-datetime>
`);
await page.waitForSelector('.datetime-ready');
const calendarBody = page.locator('ion-datetime .calendar-body');
const calendarHeader = page.locator('ion-datetime .calendar-month-year');
await expect(calendarHeader).toHaveText(/May 2022/);
const box = await calendarBody.boundingBox();
if (box) {
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
await page.mouse.wheel(-50, 0);
await page.waitForChanges();
await expect(calendarHeader).toHaveText(/May 2022/);
}
});
});

View File

@@ -33,8 +33,8 @@ test.describe('datetime: minmax', () => {
const prevButton = page.locator('ion-datetime .calendar-next-prev ion-button:nth-child(1)');
const nextButton = page.locator('ion-datetime .calendar-next-prev ion-button:nth-child(2)');
await expect(nextButton).toBeEnabled();
await expect(prevButton).toBeDisabled();
await expect(nextButton).toHaveJSProperty('disabled', false);
await expect(prevButton).toHaveJSProperty('disabled', true);
await page.evaluate('initDatetimeChangeEvent()');
@@ -45,8 +45,8 @@ test.describe('datetime: minmax', () => {
await monthDidChangeSpy.next();
await expect(nextButton).toBeDisabled();
await expect(prevButton).toBeEnabled();
await expect(nextButton).toHaveJSProperty('disabled', true);
await expect(prevButton).toHaveJSProperty('disabled', false);
});
test('datetime: minmax months disabled', async ({ page }) => {

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 48 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 48 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

View File

@@ -1,45 +0,0 @@
import { expect } from '@playwright/test';
import { test } from '@utils/test/playwright';
test.describe('datetime: sub-pixel width', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/src/components/datetime/test/sub-pixel-width');
});
test('should update the month when next button is clicked', async ({ page }) => {
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
const datetimeMonthDidChange = await page.spyOnEvent('datetimeMonthDidChange');
const openModalBtn = page.locator('#open-modal');
await openModalBtn.click();
await ionModalDidPresent.next();
await page.waitForSelector('.datetime-ready');
const buttons = page.locator('ion-datetime .calendar-next-prev ion-button');
await buttons.nth(1).click();
await datetimeMonthDidChange.next();
const monthYear = page.locator('ion-datetime .calendar-month-year');
await expect(monthYear).toHaveText('March 2022');
});
test('should update the month when prev button is clicked', async ({ page }) => {
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
const datetimeMonthDidChange = await page.spyOnEvent('datetimeMonthDidChange');
const openModalBtn = page.locator('#open-modal');
await openModalBtn.click();
await ionModalDidPresent.next();
await page.waitForSelector('.datetime-ready');
const buttons = page.locator('ion-datetime .calendar-next-prev ion-button');
await buttons.nth(0).click();
await datetimeMonthDidChange.next();
const monthYear = page.locator('ion-datetime .calendar-month-year');
await expect(monthYear).toHaveText('January 2022');
});
});

View File

@@ -1,53 +0,0 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8" />
<title>Datetime - Sub Pixel Width</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0" />
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
<script src="../../../../../scripts/testing/scripts.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
<style>
ion-datetime {
width: 334.05px;
height: 500px;
}
#background {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<ion-app>
<ion-header translucent="true">
<ion-toolbar>
<ion-title>Datetime - Sub Pixel Width</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<h2>Modal</h2>
<ion-button id="open-modal">Present Modal</ion-button>
<ion-modal trigger="open-modal" id="modal">
<div id="background">
<ion-datetime id="picker" value="2022-02-01"></ion-datetime>
</div>
</ion-modal>
</ion-content>
</ion-app>
<script type="module">
import { InitMonthDidChangeEvent } from '../test/utils/month-did-change-event.js';
document.getElementById('open-modal').addEventListener('click', () => {
InitMonthDidChangeEvent();
});
</script>
</body>
</html>

View File

@@ -1,55 +0,0 @@
import { expect } from '@playwright/test';
import { test } from '@utils/test/playwright';
test.use({
viewport: {
width: 640,
height: 480,
},
deviceScaleFactor: 2,
});
/**
* This test emulates zoom behavior in the browser to make sure
* that key functions of the ion-datetime continue to function even
* if the page is zoomed in or out.
*/
test.describe('datetime: zoom in interactivity', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/src/components/datetime/test/zoom');
});
test('should update the month when next button is clicked', async ({ page }) => {
const openModalBtn = page.locator('#open-modal');
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
const datetimeMonthDidChange = await page.spyOnEvent('datetimeMonthDidChange');
await openModalBtn.click();
await ionModalDidPresent.next();
const buttons = page.locator('ion-datetime .calendar-next-prev ion-button');
await buttons.nth(1).click();
await datetimeMonthDidChange.next();
const monthYear = page.locator('ion-datetime .calendar-month-year');
await expect(monthYear).toHaveText('March 2022');
});
test('should update the month when prev button is clicked', async ({ page }) => {
const openModalBtn = page.locator('#open-modal');
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
const datetimeMonthDidChange = await page.spyOnEvent('datetimeMonthDidChange');
await openModalBtn.click();
await ionModalDidPresent.next();
const buttons = page.locator('ion-datetime .calendar-next-prev ion-button');
await buttons.nth(0).click();
await datetimeMonthDidChange.next();
const monthYear = page.locator('ion-datetime .calendar-month-year');
await expect(monthYear).toHaveText('January 2022');
});
});

View File

@@ -1,55 +0,0 @@
import { expect } from '@playwright/test';
import { test } from '@utils/test/playwright';
test.use({
viewport: {
width: 640,
height: 480,
},
deviceScaleFactor: 0.75,
});
/**
* This test emulates zoom behavior in the browser to make sure
* that key functions of the ion-datetime continue to function even
* if the page is zoomed in or out.
*/
test.describe('datetime: zoom out interactivity', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/src/components/datetime/test/zoom');
});
test('should update the month when next button is clicked', async ({ page }) => {
const openModalBtn = page.locator('#open-modal');
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
const datetimeMonthDidChange = await page.spyOnEvent('datetimeMonthDidChange');
await openModalBtn.click();
await ionModalDidPresent.next();
const buttons = page.locator('ion-datetime .calendar-next-prev ion-button');
await buttons.nth(1).click();
await datetimeMonthDidChange.next();
const monthYear = page.locator('ion-datetime .calendar-month-year');
await expect(monthYear).toHaveText('March 2022');
});
test('should update the month when prev button is clicked', async ({ page }) => {
const openModalBtn = page.locator('#open-modal');
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
const datetimeMonthDidChange = await page.spyOnEvent('datetimeMonthDidChange');
await openModalBtn.click();
await ionModalDidPresent.next();
const buttons = page.locator('ion-datetime .calendar-next-prev ion-button');
await buttons.nth(0).click();
await datetimeMonthDidChange.next();
const monthYear = page.locator('ion-datetime .calendar-month-year');
await expect(monthYear).toHaveText('January 2022');
});
});

View File

@@ -1,48 +0,0 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8" />
<title>Datetime - Zoom</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0" />
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
<script src="../../../../../scripts/testing/scripts.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
<style>
#background {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<ion-app>
<ion-header translucent="true">
<ion-toolbar>
<ion-title>Datetime - Zoom</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<h2>Modal</h2>
<ion-button id="open-modal">Present Modal</ion-button>
<ion-modal trigger="open-modal" id="modal">
<div id="background">
<ion-datetime id="picker" value="2022-02-01"></ion-datetime>
</div>
</ion-modal>
</ion-content>
</ion-app>
<script type="module">
import { InitMonthDidChangeEvent } from '../test/utils/month-did-change-event.js';
document.getElementById('open-modal').addEventListener('click', () => {
InitMonthDidChangeEvent();
});
</script>
</body>
</html>

View File

@@ -5,6 +5,8 @@ import { close } from 'ionicons/icons';
import { getIonMode } from '../../global/ionic-global';
import type { AnimationBuilder, Color, RouterDirection } from '../../interface';
import type { AnchorInterface, ButtonInterface } from '../../utils/element-interface';
import { inheritAriaAttributes } from '../../utils/helpers';
import type { Attributes } from '../../utils/helpers';
import { createColorClasses, hostContext, openURL } from '../../utils/theme';
/**
@@ -23,6 +25,7 @@ import { createColorClasses, hostContext, openURL } from '../../utils/theme';
})
export class FabButton implements ComponentInterface, AnchorInterface, ButtonInterface {
private fab: HTMLIonFabElement | null = null;
private inheritedAttributes: Attributes = {};
@Element() el!: HTMLElement;
@@ -142,8 +145,12 @@ export class FabButton implements ComponentInterface, AnchorInterface, ButtonInt
fab.toggle();
};
componentWillLoad() {
this.inheritedAttributes = inheritAriaAttributes(this.el);
}
render() {
const { el, disabled, color, href, activated, show, translucent, size } = this;
const { el, disabled, color, href, activated, show, translucent, size, inheritedAttributes } = this;
const inList = hostContext('ion-fab-list', el);
const mode = getIonMode(this);
const TagType = href === undefined ? 'button' : ('a' as any);
@@ -182,6 +189,7 @@ export class FabButton implements ComponentInterface, AnchorInterface, ButtonInt
onFocus={this.onFocus}
onBlur={this.onBlur}
onClick={(ev: Event) => openURL(href, ev, this.routerDirection, this.routerAnimation)}
{...inheritedAttributes}
>
<ion-icon icon={this.closeIcon} part="close-icon" class="close-icon" lazy={false}></ion-icon>
<span class="button-inner">

View File

@@ -0,0 +1,19 @@
import { newSpecPage } from '@stencil/core/testing';
import { FabButton } from '../fab-button';
describe('fab-button: aria attributes', () => {
it('should inherit aria attributes to inner <button>', async () => {
const page = await newSpecPage({
components: [FabButton],
html: `
<ion-fab-button aria-label="Hello World">My Button</ion-fab-button>
`,
});
const fabButton = page.body.querySelector('ion-fab-button');
const root = fabButton.shadowRoot;
const nativeButton = root.querySelector('button');
expect(nativeButton.getAttribute('aria-label')).toEqual('Hello World');
});
});

View File

@@ -496,8 +496,7 @@ export class Input implements ComponentInterface {
aria-label="reset"
type="button"
class="input-clear-icon"
onTouchStart={this.clearTextInput}
onMouseDown={this.clearTextInput}
onPointerDown={this.clearTextInput}
onKeyDown={this.clearTextOnEnter}
/>
)}

View File

@@ -0,0 +1,38 @@
import { expect } from '@playwright/test';
import { test } from '@utils/test/playwright';
test.describe('input: a11y', () => {
test('does not set a default aria-labelledby when there is not a neighboring ion-label', async ({
page,
}, testInfo) => {
test.skip(testInfo.project.metadata.rtl === true, 'Does not test LTR vs. RTL layout.');
await page.setContent('<ion-input></ion-input>');
const input = page.locator('ion-input > input');
const ariaLabelledBy = await input.getAttribute('aria-labelledby');
await expect(ariaLabelledBy).toBe(null);
});
test('set a default aria-labelledby when a neighboring ion-label exists', async ({ page }, testInfo) => {
test.skip(testInfo.project.metadata.rtl === true, 'Does not test LTR vs. RTL layout.');
await page.setContent(
`
<ion-item>
<ion-label>A11y Test</ion-label>
<ion-input></ion-input>
</ion-item>
`
);
const label = page.locator('ion-label');
const input = page.locator('ion-input > input');
const ariaLabelledBy = await input.getAttribute('aria-labelledby');
const labelId = await label.getAttribute('id');
await expect(ariaLabelledBy).toBe(labelId);
});
});

View File

@@ -1,31 +0,0 @@
import { newSpecPage } from '@stencil/core/testing';
import { Item } from '../../../item/item';
import { Label } from '../../../label/label';
import { Input } from '../../input';
describe('Input a11y', () => {
it('does not set a default aria-labelledby when there is not a neighboring ion-label', async () => {
const page = await newSpecPage({
components: [Input, Item, Label],
html: `<ion-input></ion-input>`,
});
const ariaLabelledBy = page.body.querySelector('ion-input > input').getAttribute('aria-labelledby');
expect(ariaLabelledBy).toBe(null);
});
it('set a default aria-labelledby when a neighboring ion-label exists', async () => {
const page = await newSpecPage({
components: [Input, Item, Label],
html: `<ion-item>
<ion-label>A11y Test</ion-label>
<ion-input></ion-input>
</ion-item>`,
});
const label = page.body.querySelector('ion-label');
const ariaLabelledBy = page.body.querySelector('ion-input > input').getAttribute('aria-labelledby');
expect(ariaLabelledBy).toBe(label.id);
});
});

View File

@@ -1,62 +0,0 @@
import { newE2EPage } from '@stencil/core/testing';
test('input: basic', async () => {
const page = await newE2EPage({
url: '/src/components/input/test/basic?ionic:_testing=true',
});
const compares = [];
compares.push(await page.compareScreenshot());
const fullInput = await page.find('#fullInput');
await fullInput.click();
const fullItem = await page.find('#fullItem');
expect(fullItem).toHaveClass('item-has-focus');
compares.push(await page.compareScreenshot('full input focused'));
const insetInput = await page.find('#insetInput');
await insetInput.click();
const insetItem = await page.find('#insetItem');
expect(insetItem).toHaveClass('item-has-focus');
compares.push(await page.compareScreenshot('inset input focused'));
const noneInput = await page.find('#noneInput');
await noneInput.click();
const noneItem = await page.find('#noneItem');
expect(noneItem).toHaveClass('item-has-focus');
compares.push(await page.compareScreenshot('no lines input focused'));
for (const compare of compares) {
expect(compare).toMatchScreenshot();
}
});
test('input: basic should not error on input', async () => {
const page = await newE2EPage({
url: '/src/components/input/test/basic?ionic:_testing=true',
});
const errors = [];
page.on('console', (msg) => {
if (msg.type() === 'error') {
errors.push(msg.text());
}
});
const inputs = await page.findAll('ion-input');
for (const input of inputs) {
await input.click();
await input.type('letters and 12345');
}
expect(errors.length).toEqual(0);
});

View File

@@ -0,0 +1,159 @@
import { expect } from '@playwright/test';
import { test } from '@utils/test/playwright';
test.describe('input: basic', () => {
test.describe('input with overflow', () => {
test('should not have visual regressions', async ({ page }) => {
await page.setContent(`
<ion-content>
<ion-list>
<ion-item>
<ion-input value="reallylonglonglonginputtoseetheedgesreallylonglonglonginputtoseetheedges"></ion-input>
</ion-item>
</ion-list>
</ion-content>
`);
const item = page.locator('ion-item');
// Validates the display of an input where text extends off the edge of the component.
expect(await item.screenshot()).toMatchSnapshot(`input-with-text-overflow-${page.getSnapshotSettings()}.png`);
});
});
test.describe('input with placeholder', () => {
test('should not have visual regressions', async ({ page }) => {
await page.setContent(`
<ion-content>
<ion-list>
<ion-item>
<ion-input placeholder="Placeholder"></ion-input>
</ion-item>
</ion-list>
</ion-content>
`);
const item = page.locator('ion-item');
// Validates the display of an input with a placeholder.
expect(await item.screenshot()).toMatchSnapshot(`input-with-placeholder-${page.getSnapshotSettings()}.png`);
});
});
test.describe('input disabled', () => {
test('should not have visual regressions', async ({ page }) => {
await page.setContent(`
<ion-content>
<ion-list>
<ion-item>
<ion-input value="Input disabled" disabled></ion-input>
</ion-item>
</ion-list>
</ion-content>
`);
const item = page.locator('ion-item');
// Validates the display of an input in a disabled state.
expect(await item.screenshot()).toMatchSnapshot(`input-disabled-${page.getSnapshotSettings()}.png`);
});
});
test.describe('input with lines="full"', () => {
test('should not have visual regressions', async ({ page }) => {
await page.setContent(`
<ion-content>
<ion-list>
<ion-item lines="full">
<ion-input placeholder="Full"></ion-input>
</ion-item>
</ion-list>
</ion-content>
`);
const item = page.locator('ion-item');
const input = page.locator('ion-input');
// Validates the display of an input with an ion-item using lines="full".
expect(await item.screenshot()).toMatchSnapshot(`input-with-lines-full-${page.getSnapshotSettings()}.png`);
await input.click();
// Verifies that the parent item receives .item-has-focus when the input is focused.
await expect(item).toHaveClass(/item-has-focus/);
// Validates the display of an input with an ion-item using lines="full" when focused.
expect(await item.screenshot()).toMatchSnapshot(
`input-with-lines-full-focused-${page.getSnapshotSettings()}.png`
);
});
});
test.describe('input with lines="inset"', () => {
test('should not have visual regressions', async ({ page }) => {
await page.setContent(`
<ion-content>
<ion-list>
<ion-item lines="inset">
<ion-input placeholder="Inset"></ion-input>
</ion-item>
</ion-list>
</ion-content>
`);
const item = page.locator('ion-item');
const input = page.locator('ion-input');
// Validates the display of an input with an ion-item using lines="inset".
expect(await item.screenshot()).toMatchSnapshot(`input-with-lines-inset-${page.getSnapshotSettings()}.png`);
await input.click();
// Verifies that the parent item receives .item-has-focus when the input is focused.
await expect(item).toHaveClass(/item-has-focus/);
// Validates the display of an input with an ion-item using lines="inset" when focused.
expect(await item.screenshot()).toMatchSnapshot(
`input-with-lines-inset-focused-${page.getSnapshotSettings()}.png`
);
});
});
test.describe('input with lines="none"', () => {
test('should not have visual regressions', async ({ page }) => {
await page.setContent(`
<ion-content>
<ion-list>
<ion-item lines="none">
<ion-input placeholder="None"></ion-input>
</ion-item>
</ion-list>
</ion-content>
`);
const item = page.locator('ion-item');
const input = page.locator('ion-input');
// Validates the display of an input with an ion-item using lines="none".
expect(await item.screenshot()).toMatchSnapshot(`input-with-lines-none-${page.getSnapshotSettings()}.png`);
await input.click();
// Verifies that the parent item receives .item-has-focus when the input is focused.
await expect(item).toHaveClass(/item-has-focus/);
// Validates the display of an input with an ion-item using lines="none" when focused.
expect(await item.screenshot()).toMatchSnapshot(
`input-with-lines-none-focused-${page.getSnapshotSettings()}.png`
);
});
});
test.describe('input with clear button', () => {
test('should not have visual regressions', async ({ page }) => {
await page.setContent(`
<ion-content>
<ion-list>
<ion-item>
<ion-label>Clear Input</ion-label>
<ion-input
clear-input
value="reallylonglonglonginputtoseetheedgesreallylonglonglonginputtoseetheedges"
></ion-input>
</ion-item>
</ion-list>
</ion-content>
`);
const item = page.locator('ion-item');
// Validates the display of an input with a clear button.
expect(await item.screenshot()).toMatchSnapshot(`input-with-clear-button-${page.getSnapshotSettings()}.png`);
});
});
});

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Some files were not shown because too many files have changed in this diff Show More