From a61c004fb0c10d9fb0eca0987edf798386251ec2 Mon Sep 17 00:00:00 2001 From: Sean Perkins Date: Tue, 12 Apr 2022 16:30:49 -0400 Subject: [PATCH 1/3] fix(datetime): resolve warnings when importing into Stencil app (#25106) --- core/src/components/datetime/datetime.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/components/datetime/datetime.tsx b/core/src/components/datetime/datetime.tsx index 396725828f..953d523833 100644 --- a/core/src/components/datetime/datetime.tsx +++ b/core/src/components/datetime/datetime.tsx @@ -1,12 +1,12 @@ import type { ComponentInterface, EventEmitter } from '@stencil/core'; import { Component, Element, Event, Host, Method, Prop, State, Watch, h, writeTask } from '@stencil/core'; -import { printIonWarning } from '@utils/logging'; import { caretDownSharp, caretUpSharp, chevronBack, chevronDown, chevronForward } from 'ionicons/icons'; import { getIonMode } from '../../global/ionic-global'; import type { Color, DatetimeChangeEventDetail, DatetimeParts, Mode, StyleEventDetail } from '../../interface'; import { startFocusVisible } from '../../utils/focus-visible'; import { getElementRoot, raf, renderHiddenInput } from '../../utils/helpers'; +import { printIonWarning } from '../../utils/logging'; import { isRTL } from '../../utils/rtl'; import { createColorClasses } from '../../utils/theme'; import type { PickerColumnItem } from '../picker-column-internal/picker-column-internal-interfaces'; From 2a313e91179e19660a758470ed2218bbcf03e0bb Mon Sep 17 00:00:00 2001 From: Sean Perkins Date: Tue, 12 Apr 2022 17:03:49 -0400 Subject: [PATCH 2/3] fix(ripple-effect): ripple displays on click or touch (#25102) Resolves #25094 --- core/playwright.config.ts | 15 ++++- .../ripple-effect/test/basic/index.html | 10 +-- .../test/basic/ripple-effect.e2e.ts | 63 +++++++++++++++++++ core/src/utils/tap-click.ts | 5 +- core/src/utils/test/playwright/fixtures.ts | 5 +- 5 files changed, 86 insertions(+), 12 deletions(-) create mode 100644 core/src/components/ripple-effect/test/basic/ripple-effect.e2e.ts diff --git a/core/playwright.config.ts b/core/playwright.config.ts index 677625fed4..52bae64692 100644 --- a/core/playwright.config.ts +++ b/core/playwright.config.ts @@ -46,14 +46,16 @@ const generateProjects = () => { ...project, metadata: { mode, - rtl: false + rtl: false, + _testing: true } }); projectsWithMetadata.push({ ...project, metadata: { mode, - rtl: true + rtl: true, + _testing: true } }); }); @@ -72,7 +74,14 @@ const config: PlaywrightTestConfig = { * Maximum time expect() should wait for the condition to be met. * For example in `await expect(locator).toHaveText();` */ - timeout: 5000 + timeout: 5000, + toMatchSnapshot: { + /** + * Increases the maximum allowed pixel difference to account + * for slight browser rendering inconsistencies. + */ + maxDiffPixelRatio: 0.05 + } }, /* Fail the build on CI if you accidentally left test.only in the source code. */ forbidOnly: !!process.env.CI, diff --git a/core/src/components/ripple-effect/test/basic/index.html b/core/src/components/ripple-effect/test/basic/index.html index be8b313dcf..d9016411e7 100644 --- a/core/src/components/ripple-effect/test/basic/index.html +++ b/core/src/components/ripple-effect/test/basic/index.html @@ -46,18 +46,18 @@

- Small + Small

- Large + Large

- Large + Large

- Large + Large

-
+
This is just a div + effect behind Nested button diff --git a/core/src/components/ripple-effect/test/basic/ripple-effect.e2e.ts b/core/src/components/ripple-effect/test/basic/ripple-effect.e2e.ts new file mode 100644 index 0000000000..ddc617e128 --- /dev/null +++ b/core/src/components/ripple-effect/test/basic/ripple-effect.e2e.ts @@ -0,0 +1,63 @@ +import { expect } from '@playwright/test'; +import type { IonicPage } from '@utils/test/playwright'; +import { test } from '@utils/test/playwright'; + +test.describe('ripple-effect: basic', () => { + test('should add .ion-activated when pressed', async ({ page }) => { + await verifyRippleEffect(page, '#small-btn'); + await verifyRippleEffect(page, '#large-btn'); + await verifyRippleEffect(page, '#large-btn-outline'); + await verifyRippleEffect(page, '#large-btn-clear'); + await verifyRippleEffect(page, '.block'); + }); + + test.describe('ripple effect with nested ion-button', () => { + test('should add .ion-activated when the block is pressed', async ({ page }) => { + await page.goto(`/src/components/ripple-effect/test/basic?ionic:_testing=false&ionic:mode=md`); + + const el = page.locator('#ripple-with-button'); + + await el.scrollIntoViewIfNeeded(); + + const boundingBox = await el.boundingBox(); + + if (boundingBox) { + await page.mouse.move(boundingBox.x + 5, boundingBox.y + 5); + await page.mouse.down(); + } + + // Waits for the ripple effect to be added + await page.waitForSelector('.ion-activated'); + + const elHandle = await el.elementHandle(); + const classes = await elHandle?.evaluate((el) => el.classList.value); + expect(classes).toMatch('ion-activated'); + }); + + test('should add .ion-activated when the button is pressed', async ({ page }) => { + await verifyRippleEffect(page, '#ripple-with-button ion-button'); + }); + }); +}); + +const verifyRippleEffect = async (page: IonicPage, selector: string) => { + await page.goto(`/src/components/ripple-effect/test/basic?ionic:_testing=false&ionic:mode=md`); + + const el = page.locator(selector); + + await el.scrollIntoViewIfNeeded(); + + const boundingBox = await el.boundingBox(); + + if (boundingBox) { + await page.mouse.move(boundingBox.x + boundingBox.width / 2, boundingBox.y + boundingBox.height / 2); + await page.mouse.down(); + } + + // Waits for the ripple effect to be added + await page.waitForSelector(`${selector}.ion-activated`); + + const elHandle = await el.elementHandle(); + const classes = await elHandle?.evaluate((el) => el.classList.value); + expect(classes).toMatch('ion-activated'); +}; diff --git a/core/src/utils/tap-click.ts b/core/src/utils/tap-click.ts index 4c486dad0e..b0c0577d37 100644 --- a/core/src/utils/tap-click.ts +++ b/core/src/utils/tap-click.ts @@ -15,7 +15,8 @@ export const startTapClick = (config: Config) => { const clearDefers = new WeakMap(); const isScrolling = () => { - return scrollingEl?.parentElement !== null; + // eslint-disable-next-line @typescript-eslint/prefer-optional-chain + return scrollingEl !== undefined && scrollingEl.parentElement !== null; }; // Touch Events @@ -169,7 +170,7 @@ const getActivatableTarget = (ev: any): any => { const path = ev.composedPath() as HTMLElement[]; for (let i = 0; i < path.length - 2; i++) { const el = path[i]; - if (el?.classList.contains('ion-activatable')) { + if (!(el instanceof ShadowRoot) && el.classList.contains('ion-activatable')) { return el; } } diff --git a/core/src/utils/test/playwright/fixtures.ts b/core/src/utils/test/playwright/fixtures.ts index 8af41e90b0..208aa554e4 100644 --- a/core/src/utils/test/playwright/fixtures.ts +++ b/core/src/utils/test/playwright/fixtures.ts @@ -43,7 +43,7 @@ export const test = base.extend({ * to be hydrated before proceeding with the test. */ page.goto = async (url: string) => { - const { mode, rtl } = testInfo.project.metadata; + const { mode, rtl, _testing } = testInfo.project.metadata; const splitUrl = url.split('?'); const paramsString = splitUrl[1]; @@ -55,8 +55,9 @@ export const test = base.extend({ const urlToParams = new URLSearchParams(paramsString); const formattedMode = urlToParams.get('ionic:mode') ?? mode; const formattedRtl = urlToParams.get('rtl') ?? rtl; + const ionicTesting = urlToParams.get('ionic:_testing') ?? _testing; - const formattedUrl = `${splitUrl[0]}?ionic:_testing=true&ionic:mode=${formattedMode}&rtl=${formattedRtl}`; + const formattedUrl = `${splitUrl[0]}?ionic:_testing=${ionicTesting}&ionic:mode=${formattedMode}&rtl=${formattedRtl}`; const results = await Promise.all([ page.waitForFunction(() => (window as any).testAppLoaded === true), From 0cd06a675474e1893b4c0801fab8ab79813537c8 Mon Sep 17 00:00:00 2001 From: Sean Perkins Date: Tue, 12 Apr 2022 17:33:27 -0400 Subject: [PATCH 3/3] fix(popover): only focus trap ion-item children (#24990) Resolves #24633 --- core/src/components/popover/test/basic/e2e.ts | 193 ++++++++++++++---- .../components/popover/test/basic/index.html | 166 ++++++++------- core/src/components/popover/utils.ts | 10 +- 3 files changed, 240 insertions(+), 129 deletions(-) diff --git a/core/src/components/popover/test/basic/e2e.ts b/core/src/components/popover/test/basic/e2e.ts index a14c350560..190fc6c14b 100644 --- a/core/src/components/popover/test/basic/e2e.ts +++ b/core/src/components/popover/test/basic/e2e.ts @@ -1,3 +1,4 @@ +import type { E2EPage } from '@stencil/core/testing'; import { newE2EPage } from '@stencil/core/testing'; import { testPopover } from '../test.utils'; @@ -8,51 +9,23 @@ const DIRECTORY = 'basic'; * Focusing happens async inside of popover so we need * to wait for the requestAnimationFrame to fire. */ -const expectActiveElementTextToEqual = async (page, textValue) => { - await page.waitFor((text) => document.activeElement.textContent === text, {}, textValue); +const expectActiveElementTextToEqual = async (page: E2EPage, textValue: string) => { + await page.evaluate((text) => document.activeElement!.textContent === text, textValue) }; -test('popover: focus trap', async () => { - const page = await newE2EPage({ url: '/src/components/popover/test/basic?ionic:_testing=true' }); +const getActiveElementSelectionStart = (page: E2EPage) => { + return page.evaluate(() => + document.activeElement instanceof HTMLTextAreaElement ? document.activeElement.selectionStart : null + ); +}; - await page.click('#basic-popover'); - await page.waitForSelector('#basic-popover'); - - const popover = await page.find('ion-popover'); - - expect(popover).not.toBe(null); - await popover.waitForVisible(); - - await page.keyboard.press('Tab'); - - await expectActiveElementTextToEqual(page, 'Item 0'); - - await page.keyboard.down('Shift'); - await page.keyboard.press('Tab'); - await page.keyboard.up('Shift'); - - await expectActiveElementTextToEqual(page, 'Item 3'); - - await page.keyboard.press('Tab'); - - await expectActiveElementTextToEqual(page, 'Item 0'); - - await page.keyboard.press('ArrowDown'); - - await expectActiveElementTextToEqual(page, 'Item 1'); - - await page.keyboard.press('ArrowDown'); - - await expectActiveElementTextToEqual(page, 'Item 2'); - - await page.keyboard.press('Home'); - - await expectActiveElementTextToEqual(page, 'Item 0'); - - await page.keyboard.press('End'); - - await expectActiveElementTextToEqual(page, 'Item 3'); -}); +const getActiveElementScrollTop = (page: E2EPage) => { + return page.evaluate(() => { + // Returns the closest ion-textarea or active element + const target = document.activeElement!.closest('ion-textarea') ?? document.activeElement; + return target!.scrollTop; + }); +}; test('popover: basic', async () => { await testPopover(DIRECTORY, '#basic-popover'); @@ -125,7 +98,141 @@ test('popover: htmlAttributes', async () => { expect(alert).not.toBe(null); await alert.waitForVisible(); - const attribute = await page.evaluate(() => document.querySelector('ion-popover').getAttribute('data-testid')); + const attribute = await page.evaluate(() => document.querySelector('ion-popover')!.getAttribute('data-testid')); expect(attribute).toEqual('basic-popover'); }); + +describe('popover: focus trap', () => { + + it('should focus the first ion-item on ArrowDown', async () => { + const page = await newE2EPage({ url: '/src/components/popover/test/basic?ionic:_testing=true' }); + + await page.click('#basic-popover'); + + const popover = await page.find('ion-popover'); + + expect(popover).not.toBe(null); + await popover.waitForVisible(); + + await page.keyboard.press('ArrowDown'); + + await expectActiveElementTextToEqual(page, 'Item 0'); + }); + + it('should work with ion-item children', async () => { + const page = await newE2EPage({ url: '/src/components/popover/test/basic?ionic:_testing=true' }); + + await page.click('#basic-popover'); + await page.waitForSelector('#basic-popover'); + + const popover = await page.find('ion-popover'); + + expect(popover).not.toBe(null); + await popover.waitForVisible(); + + await page.keyboard.press('Tab'); + + await expectActiveElementTextToEqual(page, 'Item 0'); + + await page.keyboard.down('Shift'); + await page.keyboard.press('Tab'); + await page.keyboard.up('Shift'); + + await expectActiveElementTextToEqual(page, 'Item 3'); + + await page.keyboard.press('Tab'); + + await expectActiveElementTextToEqual(page, 'Item 0'); + + await page.keyboard.press('ArrowDown'); + + await expectActiveElementTextToEqual(page, 'Item 1'); + + await page.keyboard.press('ArrowDown'); + + await expectActiveElementTextToEqual(page, 'Item 2'); + + await page.keyboard.press('Home'); + + await expectActiveElementTextToEqual(page, 'Item 0'); + + await page.keyboard.press('End'); + + await expectActiveElementTextToEqual(page, 'Item 3'); + }); + + it('should not override keyboard interactions for textarea elements', async () => { + const page = await newE2EPage({ url: '/src/components/popover/test/basic?ionic:_testing=true' }); + + await page.waitForSelector('#popover-with-textarea'); + await page.click('#popover-with-textarea'); + + const popover = await page.find('ion-popover'); + await popover.waitForVisible(); + + await page.waitForFunction('document.activeElement.tagName === "ION-POPOVER"'); + + await page.keyboard.press('Tab'); + // Checking within ion-textarea + + let activeElementTagName = await page.evaluate(() => document.activeElement!.tagName); + let scrollTop = null; + let selectionStart = null; + let previousSelectionStart = null; + + // This is the native textarea within ion-textarea + expect(activeElementTagName).toBe('TEXTAREA'); + + selectionStart = await getActiveElementSelectionStart(page); + expect(selectionStart).toBe(0); + + await page.keyboard.press('ArrowDown'); + + selectionStart = await getActiveElementSelectionStart(page); + expect(selectionStart).toBeGreaterThan(0); + previousSelectionStart = selectionStart; + + await page.keyboard.press('ArrowDown'); + + selectionStart = await getActiveElementSelectionStart(page); + expect(selectionStart).toBeGreaterThan(previousSelectionStart!); + + await page.keyboard.press('Tab'); + // Checking within HTML textarea + + // Reset tracking variables as the focus element has changed + scrollTop = null; + selectionStart = null; + previousSelectionStart = null; + + activeElementTagName = await page.evaluate(() => document.activeElement!.tagName); + expect(activeElementTagName).toBe('TEXTAREA'); + + selectionStart = await getActiveElementSelectionStart(page); + expect(selectionStart).toBe(0); + + await page.keyboard.press('ArrowDown'); + + selectionStart = await getActiveElementSelectionStart(page); + expect(selectionStart).toBeGreaterThan(0); + previousSelectionStart = selectionStart; + + await page.keyboard.press('ArrowDown'); + + selectionStart = await getActiveElementSelectionStart(page); + expect(selectionStart).toBeGreaterThan(previousSelectionStart!); + + await page.keyboard.press('Home'); + + scrollTop = await getActiveElementScrollTop(page); + expect(scrollTop).toBeGreaterThan(0); + + const previousScrollTop = scrollTop; + + await page.keyboard.press('End'); + + scrollTop = await getActiveElementScrollTop(page); + expect(scrollTop).toBeGreaterThanOrEqual(previousScrollTop); + }); +}); diff --git a/core/src/components/popover/test/basic/index.html b/core/src/components/popover/test/basic/index.html index e400fd9bfb..5ffefb9577 100644 --- a/core/src/components/popover/test/basic/index.html +++ b/core/src/components/popover/test/basic/index.html @@ -1,91 +1,67 @@ - - - Popover - Basic - - - - - - - - - - - - - - - - - - Popover - Basic - - - - Show Popover - Show Translucent Popover - Show Long List Popover - No Event Popover - Custom Class Popover - Popover With Header - Popover With Translucent Header - + + + Popover - Basic + + + + + + + + - - - - - - - - Popover - - - + + + + + + Show Popover + Show Translucent + Popover + Show Long List Popover + No Event Popover + Custom + Class Popover + Popover With + Header + Popover With Translucent Header + + + + + Show Popover + Show Translucent + Popover + Show Long List Popover + No Event Popover + Custom + Class Popover + Popover With + Header + Popover With Translucent Header + + Popover With Textarea +