Skip to content

fix(ion-button): sync disabled state in ion-button renderHiddenButton#31225

Open
Zac-Smucker-Bryan wants to merge 5 commits into
ionic-team:mainfrom
Zac-Smucker-Bryan:zsb-ionic-framework-clean
Open

fix(ion-button): sync disabled state in ion-button renderHiddenButton#31225
Zac-Smucker-Bryan wants to merge 5 commits into
ionic-team:mainfrom
Zac-Smucker-Bryan:zsb-ionic-framework-clean

Conversation

@Zac-Smucker-Bryan

Copy link
Copy Markdown

Issue number: resolves #30968


What is the current behavior?

<ion-button> with type="submit" does not cause the associated <form> to be submitted even though it is not disabled. When the text of the input field is cleared and then input re-added, you can submit the input.

What is the new behavior?

<ion-button> with type="submit" should always submit the associated <form> when it is clicked and not disabled.

  • Add formButtonEl.disabled = this.disabled; to button.tsx in renderHiddenButton(). This addition syncs the disabled status of the button when that happens.
  • Add a unit test in button.e2e.ts to test the behavior.

Does this introduce a breaking change?

  • Yes
  • No

Other information

It seems to be related to a stencil update that affected when @watch('disabled') fires. As far as I know it is not related to Angular directly.

@Zac-Smucker-Bryan Zac-Smucker-Bryan requested a review from a team as a code owner June 17, 2026 14:52
@Zac-Smucker-Bryan Zac-Smucker-Bryan requested a review from gnbm June 17, 2026 14:52
@vercel

vercel Bot commented Jun 17, 2026

Copy link
Copy Markdown

@Zac-Smucker-Bryan is attempting to deploy a commit to the Ionic Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions github-actions Bot added the package: core @ionic/core package label Jun 17, 2026
@vercel

vercel Bot commented Jun 17, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
ionic-framework Ready Ready Preview, Comment Jun 18, 2026 8:07pm

Request Review

@thetaPC thetaPC left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small thing on the PR title: fix(ion-button): sync disabled state in ion-button renderHiddenButton

Two tweaks. The scope convention here uses the component name without the ion- prefix (for example fix(button), fix(modal), fix(checkbox)), so the scope should be button. And since the scope already says it's about the button, repeating ion-button in the description is redundant. Suggested:

fix(button): sync disabled state in renderHiddenButton

Small thing on the PR description:

The issue link in the description won't auto close the issue. GitHub's closing keywords only fire when the keyword is followed directly by # and the number, like resolves #30968. Here it's resolves #[30968](...), so the # is followed by a [ and GitHub doesn't parse it as an issue reference. The markdown link renders fine visually but the linkage and auto close on merge won't happen.

Please change it to:

Issue number: resolves #30968

GitHub will turn that into a link on its own, so the markdown link isn't needed.

Once you push your latest changes, make sure that the PR description is still valid.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to update the following comment to reflect the changes and why they were added.

/**
 * If the form already has a rendered form button
 * then do not append a new one again.
 */

Comment thread core/src/components/button/button.tsx Outdated
* then do not append a new one again.
*/
if (formButtonEl !== null && formEl.contains(formButtonEl)) {
formButtonEl.disabled = this.disabled;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should also keep type in sync on the hidden button. The early return path never updates it, so a runtime change from submit to reset (or vice versa) leaves the hidden button with its original type and it performs the wrong form action.

Suggested change
formButtonEl.disabled = this.disabled;
formButtonEl.disabled = this.disabled;
formButtonEl.type = this.type;

expect(submitEvent).toHaveReceivedEvent();
});

test('should submit the form when disabled state changes asynchronously', async ({ page }, testInfo) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test does not actually catch the regression. It still passes without the fix. I verified by reverting the formButtonEl.disabled = this.disabled; line, keeping the test, and rebuilding: it stays green.

The reason is timing. The bug only happens when the disabled change races with Stencil's initialization, which is the window where @Watch('disabled') gets missed. This test changes disabled with el.evaluate and setTimeout after the component has fully loaded, and by that point the watch is active again and syncs normally. So it reduces to "set disabled to false after load," which is the same as the manual workaround in the issue (clear the input, then retype) that already worked before your fix. Also worth noting that el.disabled = true is a no-op there since the button already starts disabled in the markup.

There is no post-load DOM API to suppress the watch, so core e2e cannot reproduce this one deterministically. The faithful reproduction needs a framework binding that sets disabled during init, which is exactly the Angular async validator scenario from the issue.

Please move the regression test to packages/angular/test/base/e2e/src/lazy/form.spec.ts. Set up a submit button that starts disabled and has its disabled state flip to false asynchronously during init (an async validator resolving, or a Promise.resolve().then(...) in ngOnInit), then assert the form submits on click. Confirm it fails without the fix before pushing, since that is the whole point of the test.

If you are feeling adventurous, add the same coverage to the standalone test app too.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@thetaPC Thanks for the detailed feedback - it helps me learn and I appreciate it. I have a version of the test, in form.spec.ts that I would like to test before making the changes in button.tsx (mine plus the one you suggested in #31225 (comment)), to be pushed later once I can confirm it fails before the push. But, I am struggling to get e2e running in the Angular testing folder in a way that I wasn't when the test was in core. Any guidance?

The link to Ionic's E2E testing guide is 404ing for me and I didn't quite know from the docs in https://github.com/ionic-team/ionic-framework/blob/main/docs/core/testing/README.md or https://github.com/ionic-team/ionic-framework/blob/main/docs/angular/testing.md about how to perform the E2E Angular tests specifically.

…enButton

This reverts commit 1d4c704.

Will retest changes without the fix and new test location
@Zac-Smucker-Bryan Zac-Smucker-Bryan force-pushed the zsb-ionic-framework-clean branch from c986c8e to 5fb20dd Compare June 18, 2026 20:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

package: core @ionic/core package

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants