mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-18 03:00:58 +08:00
fix(button): submit form when pressing enter key (#27790)
Issue number: resolves #19368 --------- <!-- Please do not submit updates to dependencies unless it fixes an issue. --> <!-- Please try to limit your pull request to one type (bugfix, feature, etc). Submit multiple pull requests if needed. --> ## What is the current behavior? The form submits when clicking on the `ion-button`. However, users cannot submit the form when focused on a form element and pressing the `enter` button. This does not follow the behavior of the native button on a form. ## What is the new behavior? <!-- Please describe the behavior or changes that are being added by this PR. --> - Form now submits when a form element is focused and the `enter` button is pressed. - It also submits regardless of the amount of form elements present. - Form will not submit if the `ion-button` is disabled. ## Does this introduce a breaking change? - [ ] Yes - [x] No <!-- If this introduces a breaking change, please describe the impact and migration path for existing applications below. --> ## Other information <!-- Any other information that is important to this PR such as screenshots of how the component looks before and after the change. --> N/A
This commit is contained in:
@ -1,5 +1,5 @@
|
|||||||
import type { ComponentInterface, EventEmitter } from '@stencil/core';
|
import type { ComponentInterface, EventEmitter } from '@stencil/core';
|
||||||
import { Component, Element, Event, Host, Prop, h } from '@stencil/core';
|
import { Component, Element, Event, Host, Prop, Watch, h } from '@stencil/core';
|
||||||
import type { AnchorInterface, ButtonInterface } from '@utils/element-interface';
|
import type { AnchorInterface, ButtonInterface } from '@utils/element-interface';
|
||||||
import type { Attributes } from '@utils/helpers';
|
import type { Attributes } from '@utils/helpers';
|
||||||
import { inheritAriaAttributes, hasShadowDom } from '@utils/helpers';
|
import { inheritAriaAttributes, hasShadowDom } from '@utils/helpers';
|
||||||
@ -32,6 +32,8 @@ export class Button implements ComponentInterface, AnchorInterface, ButtonInterf
|
|||||||
private inItem = false;
|
private inItem = false;
|
||||||
private inListHeader = false;
|
private inListHeader = false;
|
||||||
private inToolbar = false;
|
private inToolbar = false;
|
||||||
|
private formButtonEl: HTMLButtonElement | null = null;
|
||||||
|
private formEl: HTMLFormElement | null = null;
|
||||||
private inheritedAttributes: Attributes = {};
|
private inheritedAttributes: Attributes = {};
|
||||||
|
|
||||||
@Element() el!: HTMLElement;
|
@Element() el!: HTMLElement;
|
||||||
@ -52,6 +54,13 @@ export class Button implements ComponentInterface, AnchorInterface, ButtonInterf
|
|||||||
* If `true`, the user cannot interact with the button.
|
* If `true`, the user cannot interact with the button.
|
||||||
*/
|
*/
|
||||||
@Prop({ reflect: true }) disabled = false;
|
@Prop({ reflect: true }) disabled = false;
|
||||||
|
@Watch('disabled')
|
||||||
|
disabledChanged() {
|
||||||
|
const { disabled } = this;
|
||||||
|
if (this.formButtonEl) {
|
||||||
|
this.formButtonEl.disabled = disabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set to `"block"` for a full-width button or to `"full"` for a full-width button
|
* Set to `"block"` for a full-width button or to `"full"` for a full-width button
|
||||||
@ -144,6 +153,22 @@ export class Button implements ComponentInterface, AnchorInterface, ButtonInterf
|
|||||||
*/
|
*/
|
||||||
@Event() ionBlur!: EventEmitter<void>;
|
@Event() ionBlur!: EventEmitter<void>;
|
||||||
|
|
||||||
|
connectedCallback(): void {
|
||||||
|
// Allow form to be submitted through `ion-button`
|
||||||
|
if (this.type !== 'button' && hasShadowDom(this.el)) {
|
||||||
|
this.formEl = this.findForm();
|
||||||
|
if (this.formEl) {
|
||||||
|
// Create a hidden native button inside of the form
|
||||||
|
this.formButtonEl = document.createElement('button');
|
||||||
|
this.formButtonEl.type = this.type;
|
||||||
|
this.formButtonEl.style.display = 'none';
|
||||||
|
// Only submit if the button is not disabled.
|
||||||
|
this.formButtonEl.disabled = this.disabled;
|
||||||
|
this.formEl.appendChild(this.formButtonEl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
componentWillLoad() {
|
componentWillLoad() {
|
||||||
this.inToolbar = !!this.el.closest('ion-buttons');
|
this.inToolbar = !!this.el.closest('ion-buttons');
|
||||||
this.inListHeader = !!this.el.closest('ion-list-header');
|
this.inListHeader = !!this.el.closest('ion-list-header');
|
||||||
@ -177,62 +202,71 @@ export class Button implements ComponentInterface, AnchorInterface, ButtonInterf
|
|||||||
return form;
|
return form;
|
||||||
}
|
}
|
||||||
if (typeof form === 'string') {
|
if (typeof form === 'string') {
|
||||||
const el = document.getElementById(form);
|
// Check if the string provided is a form id.
|
||||||
|
const el: HTMLElement | null = document.getElementById(form);
|
||||||
|
if (el) {
|
||||||
if (el instanceof HTMLFormElement) {
|
if (el instanceof HTMLFormElement) {
|
||||||
return el;
|
return el;
|
||||||
}
|
} else {
|
||||||
}
|
/**
|
||||||
|
* The developer specified a string for the form attribute, but the
|
||||||
|
* element with that id is not a form element.
|
||||||
|
*/
|
||||||
|
printIonWarning(
|
||||||
|
`Form with selector: "#${form}" could not be found. Verify that the id is attached to a <form> element.`,
|
||||||
|
this.el
|
||||||
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
/**
|
||||||
|
* The developer specified a string for the form attribute, but the
|
||||||
|
* element with that id could not be found in the DOM.
|
||||||
|
*/
|
||||||
|
printIonWarning(
|
||||||
|
`Form with selector: "#${form}" could not be found. Verify that the id is correct and the form is rendered in the DOM.`,
|
||||||
|
this.el
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (form !== undefined) {
|
||||||
|
/**
|
||||||
|
* The developer specified a HTMLElement for the form attribute,
|
||||||
|
* but the element is not a HTMLFormElement.
|
||||||
|
* This will also catch if the developer tries to pass in null
|
||||||
|
* as the form attribute.
|
||||||
|
*/
|
||||||
|
printIonWarning(
|
||||||
|
`The provided "form" element is invalid. Verify that the form is a HTMLFormElement and rendered in the DOM.`,
|
||||||
|
this.el
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* If the form element is not set, the button may be inside
|
||||||
|
* of a form element. Query the closest form element to the button.
|
||||||
|
*/
|
||||||
|
return this.el.closest('form');
|
||||||
|
}
|
||||||
|
|
||||||
|
private submitForm(ev: Event) {
|
||||||
|
// this button wants to specifically submit a form
|
||||||
|
// climb up the dom to see if we're in a <form>
|
||||||
|
// and if so, then use JS to submit it
|
||||||
|
if (this.formEl && this.formButtonEl) {
|
||||||
|
ev.preventDefault();
|
||||||
|
|
||||||
|
this.formButtonEl.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private handleClick = (ev: Event) => {
|
private handleClick = (ev: Event) => {
|
||||||
const { el } = this;
|
const { el } = this;
|
||||||
if (this.type === 'button') {
|
if (this.type === 'button') {
|
||||||
openURL(this.href, ev, this.routerDirection, this.routerAnimation);
|
openURL(this.href, ev, this.routerDirection, this.routerAnimation);
|
||||||
} else if (hasShadowDom(el)) {
|
} else if (hasShadowDom(el)) {
|
||||||
// this button wants to specifically submit a form
|
this.submitForm(ev);
|
||||||
// climb up the dom to see if we're in a <form>
|
|
||||||
// and if so, then use JS to submit it
|
|
||||||
let formEl = this.findForm();
|
|
||||||
const { form } = this;
|
|
||||||
|
|
||||||
if (!formEl && form !== undefined) {
|
|
||||||
/**
|
|
||||||
* The developer specified a form selector for
|
|
||||||
* the button to submit, but it was not found.
|
|
||||||
*/
|
|
||||||
if (typeof form === 'string') {
|
|
||||||
printIonWarning(
|
|
||||||
`Form with selector: "#${form}" could not be found. Verify that the id is correct and the form is rendered in the DOM.`,
|
|
||||||
el
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
printIonWarning(
|
|
||||||
`The provided "form" element is invalid. Verify that the form is a HTMLFormElement and rendered in the DOM.`,
|
|
||||||
el
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!formEl) {
|
|
||||||
/**
|
|
||||||
* If the form element is not set, the button may be inside
|
|
||||||
* of a form element. Query the closest form element to the button.
|
|
||||||
*/
|
|
||||||
formEl = el.closest('form');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (formEl) {
|
|
||||||
ev.preventDefault();
|
|
||||||
|
|
||||||
const fakeButton = document.createElement('button');
|
|
||||||
fakeButton.type = this.type;
|
|
||||||
fakeButton.style.display = 'none';
|
|
||||||
formEl.appendChild(fakeButton);
|
|
||||||
fakeButton.click();
|
|
||||||
fakeButton.remove();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -56,19 +56,93 @@ configs({ directions: ['ltr'], modes: ['ios'] }).forEach(({ title, config }) =>
|
|||||||
|
|
||||||
expect(submitEvent).toHaveReceivedEvent();
|
expect(submitEvent).toHaveReceivedEvent();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should submit the closest form by pressing the `enter` button on a form element', async ({
|
||||||
|
page,
|
||||||
|
}, testInfo) => {
|
||||||
|
testInfo.annotations.push({
|
||||||
|
type: 'issue',
|
||||||
|
description: 'https://github.com/ionic-team/ionic-framework/issues/19368',
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.setContent(
|
||||||
|
`
|
||||||
|
<form>
|
||||||
|
<input type="text" />
|
||||||
|
<ion-button type="submit">Submit</ion-button>
|
||||||
|
</form>
|
||||||
|
`,
|
||||||
|
config
|
||||||
|
);
|
||||||
|
|
||||||
|
const submitEvent = await page.spyOnEvent('submit');
|
||||||
|
|
||||||
|
await page.press('input', 'Enter');
|
||||||
|
|
||||||
|
expect(submitEvent).toHaveReceivedEvent();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should submit the closest form with multiple elements by pressing the `enter` button', async ({
|
||||||
|
page,
|
||||||
|
}, testInfo) => {
|
||||||
|
testInfo.annotations.push({
|
||||||
|
type: 'issue',
|
||||||
|
description: 'https://github.com/ionic-team/ionic-framework/issues/19368',
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.setContent(
|
||||||
|
`
|
||||||
|
<form>
|
||||||
|
<input type="text" />
|
||||||
|
<textarea></textarea>
|
||||||
|
<ion-button type="submit">Submit</ion-button>
|
||||||
|
</form>
|
||||||
|
`,
|
||||||
|
config
|
||||||
|
);
|
||||||
|
|
||||||
|
const submitEvent = await page.spyOnEvent('submit');
|
||||||
|
|
||||||
|
await page.press('input', 'Enter');
|
||||||
|
|
||||||
|
expect(submitEvent).toHaveReceivedEvent();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not submit the closest form when button is disabled', async ({ page }, testInfo) => {
|
||||||
|
testInfo.annotations.push({
|
||||||
|
type: 'issue',
|
||||||
|
description: 'https://github.com/ionic-team/ionic-framework/issues/19368',
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.setContent(
|
||||||
|
`
|
||||||
|
<form>
|
||||||
|
<input type="text" />
|
||||||
|
<ion-button type="submit" disabled>Submit</ion-button>
|
||||||
|
</form>
|
||||||
|
`,
|
||||||
|
config
|
||||||
|
);
|
||||||
|
|
||||||
|
const submitEvent = await page.spyOnEvent('submit');
|
||||||
|
|
||||||
|
await page.press('input', 'Enter');
|
||||||
|
|
||||||
|
expect(submitEvent).not.toHaveReceivedEvent();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe(title('should throw a warning if the form cannot be found'), () => {
|
test.describe(title('should throw a warning if the form cannot be found'), () => {
|
||||||
test('form is a string selector', async ({ page }) => {
|
test('form is a string selector', async ({ page }) => {
|
||||||
await page.setContent(`<ion-button type="submit" form="missingForm">Submit</ion-button>`, config);
|
|
||||||
|
|
||||||
const logs: string[] = [];
|
const logs: string[] = [];
|
||||||
|
|
||||||
page.on('console', (msg) => {
|
page.on('console', (msg) => {
|
||||||
|
if (msg.type() === 'warning') {
|
||||||
logs.push(msg.text());
|
logs.push(msg.text());
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.click('ion-button');
|
await page.setContent(`<ion-button type="submit" form="missingForm">Submit</ion-button>`, config);
|
||||||
|
|
||||||
expect(logs.length).toBe(1);
|
expect(logs.length).toBe(1);
|
||||||
expect(logs[0]).toContain(
|
expect(logs[0]).toContain(
|
||||||
@ -77,6 +151,14 @@ configs({ directions: ['ltr'], modes: ['ios'] }).forEach(({ title, config }) =>
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('form is an element reference', async ({ page }) => {
|
test('form is an element reference', async ({ page }) => {
|
||||||
|
const logs: string[] = [];
|
||||||
|
|
||||||
|
page.on('console', (msg) => {
|
||||||
|
if (msg.type() === 'warning') {
|
||||||
|
logs.push(msg.text());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
await page.setContent(
|
await page.setContent(
|
||||||
`
|
`
|
||||||
<ion-button type="submit">Submit</ion-button>
|
<ion-button type="submit">Submit</ion-button>
|
||||||
@ -90,14 +172,6 @@ configs({ directions: ['ltr'], modes: ['ios'] }).forEach(({ title, config }) =>
|
|||||||
config
|
config
|
||||||
);
|
);
|
||||||
|
|
||||||
const logs: string[] = [];
|
|
||||||
|
|
||||||
page.on('console', (msg) => {
|
|
||||||
logs.push(msg.text());
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.click('ion-button');
|
|
||||||
|
|
||||||
expect(logs.length).toBe(1);
|
expect(logs.length).toBe(1);
|
||||||
expect(logs[0]).toContain(
|
expect(logs[0]).toContain(
|
||||||
'[Ionic Warning]: The provided "form" element is invalid. Verify that the form is a HTMLFormElement and rendered in the DOM.'
|
'[Ionic Warning]: The provided "form" element is invalid. Verify that the form is a HTMLFormElement and rendered in the DOM.'
|
||||||
|
Reference in New Issue
Block a user