mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-18 19:21:34 +08:00
merge release-6.5.7
Release 6.5.7
This commit is contained in:
@ -3,6 +3,19 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [6.5.7](https://github.com/ionic-team/ionic/compare/v6.5.6...v6.5.7) (2023-03-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **content:** fullscreen values are recomputed on visible content ([#26847](https://github.com/ionic-team/ionic/issues/26847)) ([6dcd98b](https://github.com/ionic-team/ionic/commit/6dcd98b26ab8fd3bf0092416d613bf051fbdeacf)), closes [#26844](https://github.com/ionic-team/ionic/issues/26844)
|
||||
* **modal:** keyboard listener removed on dismiss ([#26856](https://github.com/ionic-team/ionic/issues/26856)) ([b4bcba3](https://github.com/ionic-team/ionic/commit/b4bcba353386b4d5d8d396e61ece421a15d42ff0))
|
||||
* **overlays:** focus trap refs cleared on dismiss ([#26855](https://github.com/ionic-team/ionic/issues/26855)) ([8d1d0fa](https://github.com/ionic-team/ionic/commit/8d1d0fa0c7a42a3c21a471131ba454774b26c314))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [6.5.6](https://github.com/ionic-team/ionic/compare/v6.5.5...v6.5.6) (2023-02-22)
|
||||
|
||||
|
||||
|
4
core/package-lock.json
generated
4
core/package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@ionic/core",
|
||||
"version": "6.5.6",
|
||||
"version": "6.5.7",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@ionic/core",
|
||||
"version": "6.5.6",
|
||||
"version": "6.5.7",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@stencil/core": "^2.18.0",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ionic/core",
|
||||
"version": "6.5.6",
|
||||
"version": "6.5.7",
|
||||
"description": "Base components for Ionic",
|
||||
"keywords": [
|
||||
"ionic",
|
||||
|
@ -149,6 +149,16 @@ export class Content implements ComponentInterface {
|
||||
}
|
||||
|
||||
this.resizeTimeout = setTimeout(() => {
|
||||
/**
|
||||
* Resize should only happen
|
||||
* if the content is visible.
|
||||
* When the content is hidden
|
||||
* then offsetParent will be null.
|
||||
*/
|
||||
if (this.el.offsetParent === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.resize();
|
||||
}, 100);
|
||||
}
|
||||
|
@ -108,45 +108,42 @@ test.describe('datetime: presentation', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: FW-3018
|
||||
test.skip('datetime: presentation: time', () => {
|
||||
let timePickerFixture: TimePickerFixture;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
timePickerFixture = new TimePickerFixture(page);
|
||||
await timePickerFixture.goto();
|
||||
test.describe('datetime: presentation: time', () => {
|
||||
test.beforeEach(async ({ skip }) => {
|
||||
skip.rtl();
|
||||
skip.mode('md');
|
||||
});
|
||||
|
||||
test('changing value from AM to AM should update the text', async () => {
|
||||
await timePickerFixture.setValue('04:20:00');
|
||||
await timePickerFixture.expectTime('4', '20', 'AM');
|
||||
test('changing value from AM to AM should update the text', async ({ page }) => {
|
||||
const timePickerFixture = new TimePickerFixture(page);
|
||||
await timePickerFixture.goto('04:20:00');
|
||||
|
||||
await timePickerFixture.setValue('11:03:00');
|
||||
await timePickerFixture.expectTime('11', '03', 'AM');
|
||||
await timePickerFixture.expectTime(11, 3, 'am');
|
||||
});
|
||||
|
||||
test('changing value from AM to PM should update the text', async () => {
|
||||
await timePickerFixture.setValue('05:30:00');
|
||||
await timePickerFixture.expectTime('5', '30', 'AM');
|
||||
test('changing value from AM to PM should update the text', async ({ page }) => {
|
||||
const timePickerFixture = new TimePickerFixture(page);
|
||||
await timePickerFixture.goto('05:30:00');
|
||||
|
||||
await timePickerFixture.setValue('16:40:00');
|
||||
await timePickerFixture.expectTime('4', '40', 'PM');
|
||||
await timePickerFixture.expectTime(16, 40, 'pm');
|
||||
});
|
||||
|
||||
test('changing the value from PM to AM should update the text', async () => {
|
||||
await timePickerFixture.setValue('16:40:00');
|
||||
await timePickerFixture.expectTime('4', '40', 'PM');
|
||||
test('changing the value from PM to AM should update the text', async ({ page }) => {
|
||||
const timePickerFixture = new TimePickerFixture(page);
|
||||
await timePickerFixture.goto('16:40:00');
|
||||
|
||||
await timePickerFixture.setValue('04:20:00');
|
||||
await timePickerFixture.expectTime('4', '20', 'AM');
|
||||
await timePickerFixture.expectTime(4, 20, 'am');
|
||||
});
|
||||
|
||||
test('changing the value from PM to PM should update the text', async () => {
|
||||
await timePickerFixture.setValue('16:40:00');
|
||||
await timePickerFixture.expectTime('4', '40', 'PM');
|
||||
test('changing the value from PM to PM should update the text', async ({ page }) => {
|
||||
const timePickerFixture = new TimePickerFixture(page);
|
||||
await timePickerFixture.goto('16:40:00');
|
||||
|
||||
await timePickerFixture.setValue('19:32:00');
|
||||
await timePickerFixture.expectTime('7', '32', 'PM');
|
||||
await timePickerFixture.expectTime(19, 32, 'pm');
|
||||
});
|
||||
});
|
||||
|
||||
@ -159,35 +156,27 @@ class TimePickerFixture {
|
||||
this.page = page;
|
||||
}
|
||||
|
||||
async goto() {
|
||||
async goto(value: string) {
|
||||
await this.page.setContent(`
|
||||
<ion-datetime presentation="time" value="2022-03-10T13:00:00"></ion-datetime>
|
||||
<ion-datetime presentation="time" value="${value}"></ion-datetime>
|
||||
`);
|
||||
await this.page.waitForSelector('.datetime-ready');
|
||||
this.timePicker = this.page.locator('ion-datetime');
|
||||
}
|
||||
|
||||
async setValue(value: string) {
|
||||
const ionChange = await this.page.spyOnEvent('ionChange');
|
||||
await this.timePicker.evaluate((el: HTMLIonDatetimeElement, newValue: string) => {
|
||||
el.value = newValue;
|
||||
}, value);
|
||||
|
||||
await ionChange.next();
|
||||
|
||||
// Changing the value can take longer than the default 100ms to repaint
|
||||
await this.page.waitForChanges(300);
|
||||
await this.page.waitForChanges();
|
||||
}
|
||||
|
||||
async expectTime(hour: string, minute: string, ampm: string) {
|
||||
expect(
|
||||
await this.timePicker.locator('ion-picker-column-internal:nth-child(1) .picker-item-active').textContent()
|
||||
).toBe(hour);
|
||||
expect(
|
||||
await this.timePicker.locator('ion-picker-column-internal:nth-child(2) .picker-item-active').textContent()
|
||||
).toBe(minute);
|
||||
expect(
|
||||
await this.timePicker.locator('ion-picker-column-internal:nth-child(3) .picker-item-active').textContent()
|
||||
).toBe(ampm);
|
||||
async expectTime(hour: number, minute: number, ampm: string) {
|
||||
const pickerColumns = this.timePicker.locator('ion-picker-column-internal');
|
||||
|
||||
await expect(pickerColumns.nth(0)).toHaveJSProperty('value', hour);
|
||||
await expect(pickerColumns.nth(1)).toHaveJSProperty('value', minute);
|
||||
await expect(pickerColumns.nth(2)).toHaveJSProperty('value', ampm);
|
||||
}
|
||||
}
|
||||
|
@ -333,7 +333,7 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
if (this.gesture) {
|
||||
this.gesture.enable(enable);
|
||||
} else if (enable) {
|
||||
await this.initSwipeToClose();
|
||||
this.initSwipeToClose();
|
||||
}
|
||||
}
|
||||
|
||||
@ -532,6 +532,38 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
backdropBreakpoint: this.backdropBreakpoint,
|
||||
});
|
||||
|
||||
/* tslint:disable-next-line */
|
||||
if (typeof window !== 'undefined') {
|
||||
/**
|
||||
* This needs to be setup before any
|
||||
* non-transition async work so it can be dereferenced
|
||||
* in the dismiss method. The dismiss method
|
||||
* only waits for the entering transition
|
||||
* to finish. It does not wait for all of the `present`
|
||||
* method to resolve.
|
||||
*/
|
||||
this.keyboardOpenCallback = () => {
|
||||
if (this.gesture) {
|
||||
/**
|
||||
* When the native keyboard is opened and the webview
|
||||
* is resized, the gesture implementation will become unresponsive
|
||||
* and enter a free-scroll mode.
|
||||
*
|
||||
* When the keyboard is opened, we disable the gesture for
|
||||
* a single frame and re-enable once the contents have repositioned
|
||||
* from the keyboard placement.
|
||||
*/
|
||||
this.gesture.enable(false);
|
||||
raf(() => {
|
||||
if (this.gesture) {
|
||||
this.gesture.enable(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
window.addEventListener(KEYBOARD_DID_OPEN, this.keyboardOpenCallback);
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO (FW-937) - In the next major release of Ionic, all card modals
|
||||
* will be swipeable by default. canDismiss will be used to determine if the
|
||||
@ -559,31 +591,7 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
if (this.isSheetModal) {
|
||||
this.initSheetGesture();
|
||||
} else if (hasCardModal) {
|
||||
await this.initSwipeToClose();
|
||||
}
|
||||
|
||||
/* tslint:disable-next-line */
|
||||
if (typeof window !== 'undefined') {
|
||||
this.keyboardOpenCallback = () => {
|
||||
if (this.gesture) {
|
||||
/**
|
||||
* When the native keyboard is opened and the webview
|
||||
* is resized, the gesture implementation will become unresponsive
|
||||
* and enter a free-scroll mode.
|
||||
*
|
||||
* When the keyboard is opened, we disable the gesture for
|
||||
* a single frame and re-enable once the contents have repositioned
|
||||
* from the keyboard placement.
|
||||
*/
|
||||
this.gesture.enable(false);
|
||||
raf(() => {
|
||||
if (this.gesture) {
|
||||
this.gesture.enable(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
window.addEventListener(KEYBOARD_DID_OPEN, this.keyboardOpenCallback);
|
||||
this.initSwipeToClose();
|
||||
}
|
||||
|
||||
this.currentTransition = undefined;
|
||||
@ -725,6 +733,7 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
/* tslint:disable-next-line */
|
||||
if (typeof window !== 'undefined' && this.keyboardOpenCallback) {
|
||||
window.removeEventListener(KEYBOARD_DID_OPEN, this.keyboardOpenCallback);
|
||||
this.keyboardOpenCallback = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import type { E2EPage } from '@utils/test/playwright';
|
||||
import { test } from '@utils/test/playwright';
|
||||
|
||||
import { openPopover, screenshotPopover } from '../test.utils';
|
||||
@ -38,145 +37,93 @@ test.describe('popover: focus trap', async () => {
|
||||
});
|
||||
|
||||
test('should focus the first ion-item on ArrowDown', async ({ page }) => {
|
||||
const item0 = page.locator('ion-popover ion-item:nth-of-type(1)');
|
||||
|
||||
await openPopover(page, 'basic-popover');
|
||||
|
||||
await page.keyboard.press('ArrowDown');
|
||||
await expectActiveElementTextToEqual(page, 'Item 0');
|
||||
await expect(item0).toBeFocused();
|
||||
});
|
||||
|
||||
test('should trap focus', async ({ page, browserName }) => {
|
||||
const tabKey = browserName === 'webkit' ? 'Alt+Tab' : 'Tab';
|
||||
const items = page.locator('ion-popover ion-item');
|
||||
|
||||
await openPopover(page, 'basic-popover');
|
||||
|
||||
await page.keyboard.press('Tab');
|
||||
await page.keyboard.press(tabKey);
|
||||
await expect(items.nth(0)).toBeFocused();
|
||||
|
||||
await expectActiveElementTextToEqual(page, 'Item 0');
|
||||
await page.keyboard.press(`Shift+${tabKey}`);
|
||||
await expect(items.nth(3)).toBeFocused();
|
||||
|
||||
await page.keyboard.down('Shift');
|
||||
if (browserName === 'webkit') {
|
||||
await page.keyboard.down('Alt');
|
||||
}
|
||||
await page.keyboard.press('Tab');
|
||||
await page.keyboard.up('Shift');
|
||||
if (browserName === 'webkit') {
|
||||
await page.keyboard.up('Alt');
|
||||
}
|
||||
|
||||
await expectActiveElementTextToEqual(page, 'Item 3');
|
||||
|
||||
await page.keyboard.press('Tab');
|
||||
|
||||
await expectActiveElementTextToEqual(page, 'Item 0');
|
||||
await page.keyboard.press(tabKey);
|
||||
await expect(items.nth(0)).toBeFocused();
|
||||
|
||||
await page.keyboard.press('ArrowDown');
|
||||
|
||||
await expectActiveElementTextToEqual(page, 'Item 1');
|
||||
await expect(items.nth(1)).toBeFocused();
|
||||
|
||||
await page.keyboard.press('ArrowDown');
|
||||
|
||||
await expectActiveElementTextToEqual(page, 'Item 2');
|
||||
await expect(items.nth(2)).toBeFocused();
|
||||
|
||||
await page.keyboard.press('Home');
|
||||
|
||||
await expectActiveElementTextToEqual(page, 'Item 0');
|
||||
await expect(items.nth(0)).toBeFocused();
|
||||
|
||||
await page.keyboard.press('End');
|
||||
|
||||
await expectActiveElementTextToEqual(page, 'Item 3');
|
||||
await expect(items.nth(3)).toBeFocused();
|
||||
});
|
||||
|
||||
test('should not override keyboard interactions for textarea elements', async ({ page, browserName }) => {
|
||||
await openPopover(page, 'popover-with-textarea');
|
||||
await page.waitForFunction(() => document.activeElement?.tagName === 'ION-POPOVER');
|
||||
const tabKey = browserName === 'webkit' ? 'Alt+Tab' : 'Tab';
|
||||
const popover = page.locator('ion-popover');
|
||||
const innerNativeTextarea = page.locator('ion-textarea textarea');
|
||||
const vanillaTextarea = page.locator('ion-textarea + textarea');
|
||||
|
||||
await page.keyboard.press('Tab');
|
||||
await openPopover(page, 'popover-with-textarea');
|
||||
|
||||
/**
|
||||
* Focusing happens async inside of popover so we need
|
||||
* to wait for the requestAnimationFrame to fire.
|
||||
*/
|
||||
await expect(popover).toBeFocused();
|
||||
|
||||
await page.keyboard.press(tabKey);
|
||||
|
||||
// for Firefox, ion-textarea is focused first
|
||||
// need to tab again to get to native input
|
||||
if (browserName === 'firefox') {
|
||||
await page.keyboard.press('Tab');
|
||||
await page.keyboard.press(tabKey);
|
||||
}
|
||||
|
||||
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 expect(innerNativeTextarea).toBeFocused();
|
||||
|
||||
await page.keyboard.press('ArrowDown');
|
||||
|
||||
selectionStart = await getActiveElementSelectionStart(page);
|
||||
expect(selectionStart).toBeGreaterThan(0);
|
||||
previousSelectionStart = selectionStart;
|
||||
await expect(innerNativeTextarea).toBeFocused();
|
||||
|
||||
await page.keyboard.press('ArrowDown');
|
||||
await page.keyboard.press('ArrowUp');
|
||||
|
||||
selectionStart = await getActiveElementSelectionStart(page);
|
||||
expect(selectionStart).toBeGreaterThan(previousSelectionStart!);
|
||||
await expect(innerNativeTextarea).toBeFocused();
|
||||
|
||||
await page.keyboard.press('Tab');
|
||||
await page.keyboard.press(tabKey);
|
||||
// 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 expect(vanillaTextarea).toBeFocused();
|
||||
|
||||
await page.keyboard.press('ArrowDown');
|
||||
|
||||
selectionStart = await getActiveElementSelectionStart(page);
|
||||
expect(selectionStart).toBeGreaterThan(0);
|
||||
previousSelectionStart = selectionStart;
|
||||
await expect(vanillaTextarea).toBeFocused();
|
||||
|
||||
await page.keyboard.press('ArrowDown');
|
||||
await page.keyboard.press('ArrowUp');
|
||||
|
||||
selectionStart = await getActiveElementSelectionStart(page);
|
||||
expect(selectionStart).toBeGreaterThan(previousSelectionStart!);
|
||||
await expect(vanillaTextarea).toBeFocused();
|
||||
|
||||
await page.keyboard.press('Home');
|
||||
|
||||
scrollTop = await getActiveElementScrollTop(page);
|
||||
expect(scrollTop).toBeGreaterThan(0);
|
||||
|
||||
const previousScrollTop = scrollTop;
|
||||
await expect(vanillaTextarea).toBeFocused();
|
||||
|
||||
await page.keyboard.press('End');
|
||||
|
||||
scrollTop = await getActiveElementScrollTop(page);
|
||||
expect(scrollTop).toBeGreaterThanOrEqual(previousScrollTop);
|
||||
await expect(vanillaTextarea).toBeFocused();
|
||||
});
|
||||
});
|
||||
|
||||
// TODO(FW-1424): convert these to Playwright assertions where possible
|
||||
|
||||
/**
|
||||
* Focusing happens async inside of popover so we need
|
||||
* to wait for the requestAnimationFrame to fire.
|
||||
*/
|
||||
const expectActiveElementTextToEqual = async (page: E2EPage, textValue: string) => {
|
||||
await page.evaluate((text) => document.activeElement!.textContent === text, textValue);
|
||||
};
|
||||
|
||||
const getActiveElementSelectionStart = (page: E2EPage) => {
|
||||
return page.evaluate(() =>
|
||||
document.activeElement instanceof HTMLTextAreaElement ? document.activeElement.selectionStart : null
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
||||
});
|
||||
};
|
||||
|
@ -8,7 +8,7 @@ export interface OverlayEventDetail<T = any> {
|
||||
}
|
||||
|
||||
export interface OverlayInterface {
|
||||
el: HTMLElement;
|
||||
el: HTMLIonOverlayElement;
|
||||
animated: boolean;
|
||||
keyboardClose: boolean;
|
||||
overlayIndex: number;
|
||||
|
@ -522,6 +522,14 @@ export const dismiss = async <OverlayDismissOptions>(
|
||||
*/
|
||||
overlay.el.classList.add('overlay-hidden');
|
||||
overlay.el.style.removeProperty('pointer-events');
|
||||
|
||||
/**
|
||||
* Clear any focus trapping references
|
||||
* when the overlay is dismissed.
|
||||
*/
|
||||
if (overlay.el.lastFocus !== undefined) {
|
||||
overlay.el.lastFocus = undefined;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
@ -104,7 +104,8 @@ test.describe('overlays: dismiss', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('overlays: focus', () => {
|
||||
// TODO FW-3536
|
||||
test.describe.skip('overlays: focus', () => {
|
||||
test.beforeEach(({ skip }) => {
|
||||
skip.rtl();
|
||||
});
|
||||
|
Reference in New Issue
Block a user