chore: sync with main

This commit is contained in:
Liam DeBeasi
2023-08-29 09:58:37 -04:00
57 changed files with 827 additions and 234 deletions

View File

@ -153,6 +153,15 @@ export class Button implements ComponentInterface, AnchorInterface, ButtonInterf
*/
@Event() ionBlur!: EventEmitter<void>;
/**
* This is responsible for rendering a hidden native
* button element inside the associated form. This allows
* users to submit a form by pressing "Enter" when a text
* field inside of the form is focused. The native button
* rendered inside of `ion-button` is in the Shadow DOM
* and therefore does not participate in form submission
* which is why the following code is necessary.
*/
private renderHiddenButton() {
const formEl = (this.formEl = this.findForm());
if (formEl) {
@ -323,6 +332,13 @@ export class Button implements ComponentInterface, AnchorInterface, ButtonInterf
fill = this.inToolbar || this.inListHeader ? 'clear' : 'solid';
}
/**
* We call renderHiddenButton in the render function to account
* for any properties being set async. For example, changing the
* "type" prop from "button" to "submit" after the component has
* loaded would warrant the hidden button being added to the
* associated form.
*/
{
type !== 'button' && this.renderHiddenButton();
}

View File

@ -154,6 +154,18 @@ describe('parseMinParts()', () => {
minute: 30,
});
});
it('should return undefined when given invalid info', () => {
const today = {
day: 14,
month: 3,
year: 2022,
minute: 4,
hour: 2,
};
expect(parseMinParts(undefined, today)).toEqual(undefined);
expect(parseMinParts(null, today)).toEqual(undefined);
expect(parseMinParts('foo', today)).toEqual(undefined);
});
});
describe('parseMaxParts()', () => {
@ -205,4 +217,16 @@ describe('parseMaxParts()', () => {
minute: 59,
});
});
it('should return undefined when given invalid info', () => {
const today = {
day: 14,
month: 3,
year: 2022,
minute: 4,
hour: 2,
};
expect(parseMaxParts(undefined, today)).toEqual(undefined);
expect(parseMaxParts(null, today)).toEqual(undefined);
expect(parseMaxParts('foo', today)).toEqual(undefined);
});
});

View File

@ -154,9 +154,16 @@ export const parseAmPm = (hour: number) => {
* month, day, hour, and minute information.
*/
export const parseMaxParts = (max: string, todayParts: DatetimeParts): DatetimeParts | undefined => {
const parsedMax = parseDate(max);
if (!parsedMax) return;
const { month, day, year, hour, minute } = parsedMax;
const result = parseDate(max);
/**
* If min was not a valid date then return undefined.
*/
if (result === undefined) {
return;
}
const { month, day, year, hour, minute } = result;
/**
* When passing in `max` or `min`, developers
@ -192,9 +199,16 @@ export const parseMaxParts = (max: string, todayParts: DatetimeParts): DatetimeP
* month, day, hour, and minute information.
*/
export const parseMinParts = (min: string, todayParts: DatetimeParts): DatetimeParts | undefined => {
const parsedMin = parseDate(min);
if (!parsedMin) return;
const { month, day, year, hour, minute } = parsedMin;
const result = parseDate(min);
/**
* If min was not a valid date then return undefined.
*/
if (result === undefined) {
return;
}
const { month, day, year, hour, minute } = result;
/**
* When passing in `max` or `min`, developers

View File

@ -532,7 +532,7 @@ export class Input implements ComponentInterface {
* Clear the input if the control has not been previously cleared during focus.
* Do not clear if the user hitting enter to submit a form.
*/
if (!this.didInputClearOnEdit && this.hasValue() && ev.key !== 'Enter') {
if (!this.didInputClearOnEdit && this.hasValue() && ev.key !== 'Enter' && ev.key !== 'Tab') {
this.value = '';
this.emitInputChange(ev);
}

View File

@ -0,0 +1,57 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8" />
<title>Input - Basic</title>
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
<script src="../../../../../scripts/testing/scripts.js"></script>
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
<style>
.grid {
display: grid;
grid-template-columns: repeat(5, minmax(250px, 1fr));
grid-row-gap: 20px;
grid-column-gap: 20px;
}
h2 {
font-size: 12px;
font-weight: normal;
color: #6f7378;
margin-top: 10px;
}
@media screen and (max-width: 800px) {
.grid {
grid-template-columns: 1fr;
padding: 0;
}
}
</style>
</head>
<body>
<ion-app>
<ion-header>
<ion-toolbar>
<ion-title>Input - Basic</ion-title>
</ion-toolbar>
</ion-header>
<ion-content id="content" class="ion-padding">
<div class="grid">
<div class="grid-item">
<h2>Default</h2>
<ion-input value="hi@ionic.io" label="Email"></ion-input>
</div>
</div>
</ion-content>
</ion-app>
</body>
</html>

View File

@ -0,0 +1,43 @@
import { expect } from '@playwright/test';
import { test, configs } from '@utils/test/playwright';
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
test.describe(title('input: clearOnEdit'), () => {
test('should clear when typed into', async ({ page }) => {
await page.setContent(`<ion-input value="abc" clear-on-edit="true" aria-label="input"></ion-input>`, config);
const ionInput = await page.spyOnEvent('ionInput');
const input = page.locator('ion-input');
await input.locator('input').type('h');
await ionInput.next();
await expect(input).toHaveJSProperty('value', 'h');
});
test('should not clear when enter is pressed', async ({ page }) => {
await page.setContent(`<ion-input value="abc" clear-on-edit="true" aria-label="input"></ion-input>`, config);
const input = page.locator('ion-input');
await input.locator('input').focus();
await page.keyboard.press('Enter');
await page.waitForChanges();
await expect(input).toHaveJSProperty('value', 'abc');
});
test('should not clear when tab is pressed', async ({ page }) => {
await page.setContent(`<ion-input value="abc" clear-on-edit="true" aria-label="input"></ion-input>`, config);
const input = page.locator('ion-input');
await input.locator('input').focus();
await page.keyboard.press('Tab');
await page.waitForChanges();
await expect(input).toHaveJSProperty('value', 'abc');
});
});
});

View File

@ -0,0 +1,64 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8" />
<title>Menu - Async</title>
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"
/>
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
<script src="../../../../../scripts/testing/scripts.js"></script>
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
<script type="module">
import { menuController } from '../../../../dist/ionic/index.esm.js';
window.menuController = menuController;
</script>
</head>
<body>
<ion-app>
<div id="menu-container">
<ion-menu content-id="main">
<ion-content class="ion-padding"> Menu Content </ion-content>
</ion-menu>
</div>
<div class="ion-page" id="main">
<ion-header>
<ion-toolbar>
<ion-title>Menu - Async</ion-title>
<ion-buttons slot="start" id="buttons-container"></ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
Main Content
<button onclick="trigger()" id="trigger">Add Menu To DOM</button>
</ion-content>
</div>
</ion-app>
<script>
const buttons = document.querySelector('ion-buttons');
const menuButton = document.createElement('ion-menu-button');
const menu = document.querySelector('ion-menu');
const menuContainer = document.querySelector('#menu-container');
let firstLoad = true;
// When the menu loads, immediately remove it from the DOM
document.body.addEventListener('ionMenuChange', () => {
if (firstLoad) {
menuContainer.removeChild(menu);
buttons.appendChild(menuButton);
firstLoad = false;
}
});
const trigger = () => {
menuContainer.append(menu);
};
</script>
</body>
</html>

View File

@ -0,0 +1,25 @@
import { expect } from '@playwright/test';
import { configs, test } from '@utils/test/playwright';
/**
* This behavior does not vary across modes/directions
*/
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
test.describe(title('menu button: async'), () => {
test('menu button should be visible if menu is moved', async ({ page }) => {
await page.goto(`/src/components/menu-button/test/async`, config);
const menu = page.locator('ion-menu');
const menuButton = page.locator('ion-menu-button');
const triggerButton = page.locator('#trigger');
await expect(menu).not.toBeAttached();
await expect(menuButton).toBeHidden();
await triggerButton.click();
await expect(menu).toBeAttached();
await expect(menuButton).toBeVisible();
});
});
});

View File

@ -38,6 +38,7 @@ export class Menu implements ComponentInterface, MenuI {
private lastOnEnd = 0;
private gesture?: Gesture;
private blocker = GESTURE_CONTROLLER.createBlocker({ disableScroll: true });
private didLoad = false;
isAnimating = false;
width!: number;
@ -216,6 +217,7 @@ export class Menu implements ComponentInterface, MenuI {
// register this menu with the app's menu controller
menuController._register(this);
this.menuChanged();
this.gesture = (await import('../../utils/gesture')).createGesture({
el: document,
@ -237,10 +239,22 @@ export class Menu implements ComponentInterface, MenuI {
}
async componentDidLoad() {
this.ionMenuChange.emit({ disabled: this.disabled, open: this._isOpen });
this.didLoad = true;
this.menuChanged();
this.updateState();
}
private menuChanged() {
/**
* Inform dependent components such as ion-menu-button
* that the menu is ready. Note that we only want to do this
* once the menu has been rendered which is why we check for didLoad.
*/
if (this.didLoad) {
this.ionMenuChange.emit({ disabled: this.disabled, open: this._isOpen });
}
}
async disconnectedCallback() {
/**
* The menu should be closed when it is

View File

@ -27,6 +27,9 @@ export interface PickerButton {
export interface PickerColumn {
name: string;
align?: string;
/**
* Changing this value allows the initial value of a picker column to be set.
*/
selectedIndex?: number;
prevSelected?: number;
prefix?: string;

View File

@ -0,0 +1,37 @@
import { expect } from '@playwright/test';
import { test, configs } from '@utils/test/playwright';
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
test.describe(title('textarea: clearOnEdit'), () => {
test('should clear when typed into', async ({ page }) => {
await page.setContent(
`<ion-textarea value="abc" clear-on-edit="true" aria-label="textarea"></ion-textarea>`,
config
);
const ionInput = await page.spyOnEvent('ionInput');
const textarea = page.locator('ion-textarea');
await textarea.locator('textarea').type('h');
await ionInput.next();
await expect(textarea).toHaveJSProperty('value', 'h');
});
test('should not clear when tab is pressed', async ({ page }) => {
await page.setContent(
`<ion-textarea value="abc" clear-on-edit="true" aria-label="textarea"></ion-textarea>`,
config
);
const textarea = page.locator('ion-textarea');
await textarea.locator('textarea').focus();
await page.keyboard.press('Tab');
await page.waitForChanges();
await expect(textarea).toHaveJSProperty('value', 'abc');
});
});
});

View File

@ -434,7 +434,7 @@ export class Textarea implements ComponentInterface {
/**
* Check if we need to clear the text input if clearOnEdit is enabled
*/
private checkClearOnEdit(ev: Event) {
private checkClearOnEdit(ev: KeyboardEvent) {
if (!this.clearOnEdit) {
return;
}
@ -442,7 +442,7 @@ export class Textarea implements ComponentInterface {
* Clear the textarea if the control has not been previously cleared
* during focus.
*/
if (!this.didTextareaClearOnEdit && this.hasValue()) {
if (!this.didTextareaClearOnEdit && this.hasValue() && ev.key !== 'Tab') {
this.value = '';
this.emitInputChange(ev);
}
@ -501,7 +501,7 @@ export class Textarea implements ComponentInterface {
this.ionBlur.emit(ev);
};
private onKeyDown = (ev: Event) => {
private onKeyDown = (ev: KeyboardEvent) => {
this.checkClearOnEdit(ev);
};