Skip to content

chore(git): sync with main - #31422

Draft
ShaneK wants to merge 134 commits into
nextfrom
sync-next-with-main
Draft

chore(git): sync with main#31422
ShaneK wants to merge 134 commits into
nextfrom
sync-next-with-main

Conversation

@ShaneK

@ShaneK ShaneK commented Sep 2, 2026

Copy link
Copy Markdown
Member

Sync next with main.

This is the first sync since main crossed the v9 boundary (main is 9.0.2, next was 8.8.19), so it is much larger than a routine sync: 123 commits and 256 conflicts.

Resolution policy

main wins for code, next wins for styling. Where the two could not both be kept, main's render tree and component logic were taken and next's theme-aware styling was layered on top.

Notable resolutions

  • ion-select / ion-input / ion-textarea diverged on render tree, not style. Took main's v9 structure (.select-start / .select-control / .select-end, StartContainerController, boolean autocorrect, floating labels that no longer float for slotted content) since BREAKING.md documents part="inner" as removed. Kept next's theme-aware derived values (getFill(), getShape(), getSize(), getLabelPlacement(), the theme class map, the theme-aware clear icon). The ionic-theme outline hook (<div class="input-outline"> etc.) is gone from these three and needs re-adding against the new DOM as part of the ionic-modular work.
  • picker-legacy / picker-legacy-column: accepted main's removal. Also dropped ion-picker-legacy from OVERLAY_FOCUS_TRAP_SELECTOR and from the Vue overlay container list.
  • packages/react proxies.ts -> components.ts: main deleted proxies.ts with the react-output-target 1.5.0 upgrade. Ported next's IonDivider / IonGallery / IonGalleryItem into the generated components.ts format, and updated the build-core archive path.
  • Angular standalone: main moved packages/angular/standalone/ to packages/angular/src/standalone/ with per-component files. Added ion-divider.ts, ion-gallery.ts, ion-gallery-item.ts and registered them.
  • Angular lazy proxies.ts: 83 conflicts. 79 resolved as next's inputs line plus main's added outputs / standalone: false. Four by hand: dropped IonPickerLegacy, ion-select-option inputs are the union, ion-tab keeps main's required tab input plus next's mode / theme.
  • datetime: next moved the color rules into datetime.native.scss while main rewrote them in the shared file for the removal of the default color prop. Kept next's file split and ported main's ion-color(primary, base) default plus :host(.ion-color) override into the native theme.
  • alert.common.scss: rename detection appended main's copy of the label-placement and justify rules on top of next's identical copy. Removed the duplicate.
  • select.tsx: the merge left two identical copies of wrapSelectTextNodes. Removed one.
  • BREAKING.md: rebuilt as the union of both v9 sections. All 15 component subsections alphabetized, next's Textarea Shadow DOM note folded into main's Textarea section, TOC regenerated.
  • Snapshots: 193 conflicting baselines took next's, matching the precedent set in 17ab188. Eight orphaned textarea fill Firefox baselines stayed deleted since next's fill spec is LTR-only and has no non-custom shaped cases.
  • core/package-lock.json: regenerated from main's lockfile against the merged package.json. @playwright/test stays at 1.62.1 to match the CI image.

Verification

  • tsc --noEmit on core/src is clean (spec-file errors are pre-existing tsconfig noise, present on untouched files too).
  • stencil test --spec: 86 suites, 819 tests, all passing.
  • No conflict markers anywhere; lockfile version/resolved/integrity fields are consistent.
  • Scanned every merged source file for duplicated blocks and duplicate class members, which is how the two duplication bugs above were found.

Before merging

  • Snapshots must be regenerated. All 193 baselines are next's and none of them match the merged code. Run npm run test.e2e.docker.update-snapshots
  • Regenerate the derived files from a real build rather than trusting the hand merges: core/api.txt, core/src/components.d.ts, and the framework proxy files
  • Merge with a regular merge commit, not a squash

ShaneK and others added 30 commits December 10, 2025 14:04
Syncing Major 9.0 with main
This PR just serves to update major-9.0 to reflect the current state of
main
Sync major-9.0 with main after v8.8.3 release.
…k per outlet (#31055)

Issue number: resolves internal

---------

## What is the current behavior?
The swipe-to-go-back gesture on ion-router-outlet is controlled
differently across frameworks:

- React and Vue check the `swipeBackEnabled` config option on every
swipe attempt in their framework wrappers (`StackManager.canStart()` and
`IonRouterOutlet.canStart()`)
- Angular reads the config once at mount and controls the gesture
through its directive's `swipeGesture` setter
- There is no public, per-outlet property to enable or disable the
gesture. The only mechanism is the global `swipeBackEnabled` config
option

## What is the new behavior?
`ion-router-outlet` now exposes a public `swipeGesture` property in core
that controls the swipe-to-go-back gesture per outlet instance. It
defaults to true in "ios" mode and false in "md" mode, matching existing
behavior.

- The `swipeBackEnabled` config is read once when the outlet mounts (as
the default for `swipeGesture`) rather than checked on every swipe
attempt
- The config check has been removed from React's StackManager and Vue's
IonRouterOutlet -- core now owns this state
- Angular's directive forwards the value to the core component's
`swipeGesture` property for consistency
- Apps can disable the gesture on a specific outlet: `<IonRouterOutlet
swipeGesture={false} />`
- Apps that set `swipeBackEnabled` once at startup require no changes

## Does this introduce a breaking change?

- [X] Yes
- [ ] No

## Other information

I tried to align the updates to the BREAKING.md documents with the RR6
PR's changes to it to prevent issues from merging as much as possible
## What is the current behavior?
Ionic Framework uses React 17 and Stencil react-output-target 0.5.3.

## What is the new behavior?
Updated to React 18 and Stencil react-output-target 1.5.0.

- Types updated for compatibility with type changes in React 18.
- `@testing-library/react-hooks` was moved to `@testing-library/react`
so this dependency has been removed.
- `reflect: true` added to `value` prop on ion-radio. Props without
reflection enabled are not rendered in HTML with updated
react-output-target. Issue:
stenciljs/output-targets#476

BREAKING CHANGE:

Drops support for React 17. Applications will need to update to React 18
to be officially supported by Ionic Framework.
…31078)

Issue number: internal

Co-authored-by: Brandy Smith <brandyscarney@users.noreply.github.com>
Issue number: resolves #24177

---------

<!-- 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?
Currently, Ionic Framework React Router only supports React Router 5.
This has many issues and unsupported/broken features.

## What is the new behavior?
With this change, Ionic Framework will support React Router 6 while
still supporting transitions in the same way a native app does.

Most of what caused this change to take a long time is that React Router
5 and React Router 6 have fundamental differences in how they handle
components once they're no longer part of the view. In this change, we
move away from relying on React Router directly so much and have our own
implementation for deciding how views get dealt with during navigation
and when they're cleaned up, allowing for us to still transition between
them like we need to while still using React Router as much as we
possibly can.

This change will also lay the foundation for the migration to React
Router 7, which will ideally be easier since most of the hard work has
been dealt with here.

## Does this introduce a breaking change?

- [X] Yes
- [ ] No

<!--
  If this introduces a breaking change:
1. Describe the impact and migration path for existing applications
below.
  2. Update the BREAKING.md file with the breaking change.
3. Add "BREAKING CHANGE: [...]" to the commit description when merging.
See
https://github.com/ionic-team/ionic-framework/blob/main/docs/CONTRIBUTING.md#footer
for more information.
-->


## Other information

<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->

Current dev build (last updated 2026-04-27):

> **⚠️ WARNING:** If you're going to use this dev build on an existing
react project, you'll need to migrate to React Router 6. Migrating a
large project at this point might be a bad idea since this will not
release until v9, which will require further migrations and have other
breaking changes! I have a preview of migration documentation for this
[here](https://ionic-docs-git-v9-react-router-ionic1.vercel.app/docs/updating/9-0#react-router).

```
8.8.4-dev.11777318673.18d001f6
```

The dev build linked above will be the last one for this branch alone.
Everything going forward will be the major-9.0 branch, which may include
large breaking changes.

---------

Co-authored-by: Maria Hutt <thetaPC@users.noreply.github.com>
Co-authored-by: Sean Perkins <13732623+sean-perkins@users.noreply.github.com>
Issue number: internal

---------

<!-- 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?
`@stencil/react-output-target` is pinned to exactly `1.5.0` in
`core/package.json` and `packages/react/package.json`. This was a
defensive pin after `1.5.1` shipped a regression that dropped the
`Partial<C>` wrapper from `StencilProps`, which broke everything in our
react apps by [requiring all props to be
defined](https://github.com/ionic-team/ionic-framework/actions/runs/25007199736/job/73233599812?pr=30831).
Exact-pinning meant we couldn't pick up future patches in the `1.5.x`
line.

## What is the new behavior?
Both `package.json` ranges move to `^1.5.2`. Upstream fully reverted [PR
#788](stenciljs/output-targets#788) in
[`1.5.2`](https://github.com/stenciljs/output-targets/blob/main/packages/react/CHANGELOG.md),
so the generator emits `Components.${tag}` again and the runtime
restores `Partial<C>`. Regenerating
`packages/react/src/components/components.ts` against the `1.5.2`
generator and running prettier produces a file byte-identical to the one
committed under `1.5.0`, so the published shape is unchanged.

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!--
  If this introduces a breaking change:
1. Describe the impact and migration path for existing applications
below.
  2. Update the BREAKING.md file with the breaking change.
3. Add "BREAKING CHANGE: [...]" to the commit description when merging.
See
https://github.com/ionic-team/ionic-framework/blob/main/docs/CONTRIBUTING.md#footer
for more information.
-->


## Other information

<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->

The original break, the upstream conversation, and the rationale for
staying on the `1.5.x` line (rather than taking the new "errors on truly
required props" behavior) are all in the [PR #788
thread](stenciljs/output-targets#788).
# Conflicts:
#	packages/react-router/package-lock.json
#	packages/react/package-lock.json
#	packages/react/package.json
Sync major-9.0 with main.
…ation (#31105)

Issue number: internal

---------

<!-- 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?

Currently, reviewing react router based PRs isn't possible using the
vercel preview link

## What is the new behavior?

This PR adds support for the preview link to support react router 6 and
tries to fix some weird edge cases where navigating to a route would
sometimes not show the tests in that route

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!--
  If this introduces a breaking change:
1. Describe the impact and migration path for existing applications
below.
  2. Update the BREAKING.md file with the breaking change.
3. Add "BREAKING CHANGE: [...]" to the commit description when merging.
See
https://github.com/ionic-team/ionic-framework/blob/main/docs/CONTRIBUTING.md#footer
for more information.
-->


## Other information

The new React Router preview app shares the React package gate
(`REACT_PKG_OK`) because `@ionic/react-router` is built inside
`build_react_pkgs()`. Test app builds run in parallel under that gate,
then `wait` reports per-app failures.

Preview URLs (once Vercel builds this branch):

- React Router test app:
https://ionic-framework-git-chore-vercel-add-rr-ionic1.vercel.app/react-router/
- Dir-index nav fix demo:
https://ionic-framework-git-chore-vercel-add-rr-ionic1.vercel.app/src/components/progress-bar/test
(no trailing slash; clicking `basic/`, `buffer/`, `determinate/` should
now land on the test page instead of the root landing)
Sync major-9.0 with main.
Ionic's vue-output-target uses Stencil's vue-output-target version
0.10.7 and 0.10.8.
- Update to `@stencil/vue-output-target@0.13.1`.
- Change Vue model event names from kebab-case to camelCase. Stencil
vue-output-target no longer converts them automatically.
- Generated proxies file has been updated by latest vue-output-target.
# Conflicts:
#	packages/react-router/package-lock.json
#	packages/react/package-lock.json
#	packages/react/package.json
#	packages/vue/package-lock.json
#	packages/vue/package.json
Sync major-9.0 with main.
Issue number: internal

---------

## What is the current behavior?

`@ionic/vue-router` and `@ionic/vue` build against vue-router 4

## What is the new behavior?

Bumps `vue-router` to `^5.0.6` and `vue` to `^3.5.0` (vue-router 5
raises its peer to `^3.5.0`). We also added Playwright tests for Vue
router that are in full parity with the previous Jest and removed the
Jest tests and replaced them with Playwright.

This PR also makes the current Vue test app, which is also used for the
Vue Router automated tests, get rebuilt with the current PR version for
testing in the Vercel preview links.

## Does this introduce a breaking change?

- [X] Yes
- [ ] No

Consumer apps that pin `vue-router` themselves need to upgrade to
`^5.0.0`, and apps that explicitly pin `vue` need to bump to `^3.5.0`

## Other information

CI changes: CI no longer runs `npm run test.spec` (the script and Jest
devDeps are gone), and now runs playwright tests

Preview (Vue + Vue Router test app, demos both packages from this PR):

https://ionic-framework-git-feat-vue-router-upgrade-ionic1.vercel.app/vue/
…31134)

Issue number: internal

---------

## What is the current behavior?

After the user navigates back in browser history and then pushes a new
route, `ion-back-button` on a tab child page silently fails.

## What is the new behavior?

When the computed history delta would be non-negative, fall back to
`router.replace` on `prevInfo.pathname` (or to `defaultHref` if
`prevInfo` has no pathname). `incomingRouteParams` was already set to a
pop/back above, so the view stack still animates the transition as a
back navigation, and the params are consumed by the resulting
`afterEach` hook instead of leaking into the next navigation.

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

## Other information

---------

Co-authored-by: Maria Hutt <thetaPC@users.noreply.github.com>
…ne (#31137)

Issue number: resolves #24074

---------

## What is the current behavior?

In vue-router, a `router.push` performed with `routerDirection="none"`
doesn't set `pushedByRoute` on the resulting route info. The next page's
`ion-back-button` then can't find a previous entry and falls back to
`defaultHref` instead of going back through history.

The same bug was previously fixed for react-router in v6, this is the
vue version of the fix (Angular doesn't need this fix because the router
works very differently)

## What is the new behavior?

`createIonRouter` now sets `pushedByRoute` from the leaving location
when the navigation is `routerAction === "push"`, `routerDirection ===
"none"`, and stays within the same tab context. Cross-tab pushes with
direction `none` still go through the existing tab-aware branch, which
has different `pushedByRoute` semantics

After the fix, `ion-back-button` returns to the prior page through
history and only falls back to `defaultHref` when there's genuinely no
history to pop.

## Does this introduce a breaking change?

- [ ] Yes
- [X] No

## Other information

New Playwright spec `direction-none-back.spec.ts` covers both the
`forward` and `none` paths and asserts the back button lands on Page A,
not the `defaultHref` fallback

[Test
Page](https://ionic-framework-git-fw-7145-ionic1.vercel.app/vue/direction-none-back/a)
<!-- 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?
<!-- Please describe the current behavior that you are modifying. -->
Stencil nightly build uses the Stencil react-output-target version
specified in Ionic's `package.json`s.

## What is the new behavior?
<!-- Please describe the behavior or changes that are being added by
this PR. -->
Install latest Stencil react-output-target in nightly build.
- Run `npm run lint` in `packages/react` to format generated
`compontents.ts`. Otherwise it will be reported by `git diff`.

## Does this introduce a breaking change?

- [ ] Yes
- [X] No

<!--
  If this introduces a breaking change:
1. Describe the impact and migration path for existing applications
below.
  2. Update the BREAKING.md file with the breaking change.
3. Add "BREAKING CHANGE: [...]" to the commit description when merging.
See
https://github.com/ionic-team/ionic-framework/blob/main/docs/CONTRIBUTING.md#footer
for more information.
-->

---------

Co-authored-by: Shane <shane@shanessite.net>
…#31145)

Issue number: resolves #29413

---------

## What is the current behavior?

In `viewStacks.ts`, `unmountLeavingViews` and `mountIntermediaryViews`
walk the outlet's view stack using `startIndex - delta` (or `startIndex
+ delta`) as the loop end, with no bound on `viewStack.length`. `delta`
comes from the popstate event's history delta. Apps that mount
`<ion-tabs>` at the root with no outer `<ion-router-outlet>` only have
one outlet registered, so `usingLinearNavigation` is true and these
helpers actually run. Each tab switch adds a browser history entry but
reuses existing view items, so `|delta|` can easily exceed the stack
depth above the entering view. The loop then reads `viewStack[i]` as
`undefined` and throws `TypeError: viewItem is undefined` from
`viewItem.mount = false`. The navigation aborts mid-transition, which is
what surfaces the secondary `enteringEl is undefined` warning and leaves
that route stuck

## What is the new behavior?

Both helpers bail when the entering view item isn't in the stack
(`startIndex === -1`) and clamp the loop end to
`Math.min(viewStack.length, ...)`, so a delta that overruns the stack
stops at the last real view item instead of indexing past the end. A new
Vitest spec at
`packages/vue/test/base/tests/unit/tabs-single-outlet.spec.ts` mounts
`<ion-tabs>` as the app root with flat routes, builds up history across
tabs and sub-pages, then calls `router.go(-6)`. Without the fix the spec
catches the unhandled `TypeError` from `viewStacks.ts`

## Does this introduce a breaking change?

- [ ] Yes
- [X] No

## Other information

Current dev build:
```
8.8.7-dev.11778707797.1723d277
```
Issue number: resolves #29705

---------

## What is the current behavior?

In a Vue tabs app, clicking the active tab button after landing directly
on a tab child (browser refresh on `/tabs/tab1/child`, or browser back
from an external `window.location.href`) is a silent no-op.
`IonTabBar.checkActiveTab` only records the active tab's `currentHref`
when there's a `prevActiveTab`, so the initial deep load leaves
`currentHref` matching `originalHref` and the `originalHref !==
currentHref` guard in `IonTabButton.onClick` skips `resetTab`. Even past
that guard, `resetTab` issues `router.go(routeInfo.position -
currentHistoryPosition)`, which resolves to `router.go(0)` when the
tab's only history entry is the current page.

## What is the new behavior?

`IonTabBar.checkActiveTab` now records `currentHref` on initial setup so
the tab state reflects the deep-loaded URL. `resetTab` takes the tab's
`originalHref` and, when the first history entry for the tab is the
current entry, falls back to `handleNavigate(originalHref, "pop",
"back", undefined, tab)`. The `pop` action routes through
`router.replace`, so the deep-loaded child entry is replaced rather than
stacked on top and no stale child entry stays in browser history.

## Does this introduce a breaking change?

- [ ] Yes
- [X] No

## Other information

Manual test:
[/vue/tabs/tab1/childone](https://ionic-framework-git-fw-6452-ionic1.vercel.app/vue/tabs/tab1/childone).
Load that URL directly, click the Tab 1 button, you should land on
`/vue/tabs/tab1`
#31142)

Issue number: resolves #24995

---------

## What is the current behavior?

In `createIonRouter`, the replace-action branch resolves
`routeInfo.routerDirection` and `routeInfo.routerAnimation` by
preferring the leaving route's values over the ones the caller passed
in. The leaving route wins whenever it has any value set, so
`useIonRouter.navigate(url, 'forward', 'replace')` called after a back
navigation ends up with `routerDirection: 'back'`, and the same call
from the initial route ends up with `'none'`. The caller's explicit
direction is silently discarded.

## What is the new behavior?

The precedence is flipped: the caller's `routerDirection` and
`routerAnimation` are kept, and the leaving route's values are only used
as a fallback when nothing was provided. `forward + replace` now
animates forward regardless of how the user got to the leaving page, and
the default `useIonRouter.replace(url)` keeps the `'root'` direction it
was constructed with instead of inheriting whatever the leaving route
had recorded.

## Does this introduce a breaking change?

- [ ] Yes
- [X] No

## Other information
…ch (#31139)

Issue number: resolves #25141

---------

## What is the current behavior?

Navigate from `/tabs/tab1` into `/tabs/tab1/childone`, click Tab 2, then
click Tab 1 again. Tab 1's child page is re-activated. Press the browser
back button and the URL becomes `/tabs/tab2`, but `ion-router-outlet`
still shows `tab1`. URL and displayed page are out of sync.

The pop branch in `handleHistoryChange` doesn't notice that the
browser's linear predecessor and the in-tab back target are different,
so the new routeInfo grabs its pathname from `location.path` while the
outlet renders the incorrect view

## What is the new behavior?

When the pop destination differs from the in-tab back target and either
side is in a tab context, the router calls `handleNavigate` to replace
the URL with the in-tab predecessor's pathname. The outlet keeps
rendering the in-tab page and the URL now matches what you see

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

## Other information

Companion to the React Router 6 fix in
[#30831](#30831),
which fixed the problem in Ionic React Router
…ent instance (#31150)

Issue number: resolves #25013

---------

## What is the current behavior?

A `beforeRouteEnter(to, from, next)` guard that calls `next((vm) =>
vm.member = 5)` never runs the callback. Vue Router's `<router-view>`
invokes whatever ends up in `matchedRoute.enterCallbacks[name]` from a
post-flush watcher once the view's instance is set, but
`IonRouterOutlet` does its own rendering and skips that step.
`matchedRoute.instances.default` gets populated in `registerIonPage`, so
internal guards still fire, but the queued enter callbacks sit there
unused

## What is the new behavior?

`createViewStacks`'s `registerIonPage` now reads
`matchedRoute.enterCallbacks.default`, resets it, and runs each callback
with the resolved instance. The reset happens before invocation so a
callback that triggers a synchronous navigation can push fresh entries
onto the next matched record without getting wiped. There's a new Vitest
case in `routing.spec.ts` that navigates into a page whose
`beforeRouteEnter` does `next((vm) => { vm.member = 5 })` and asserts
both the callback ran with the instance and the mutation reached the DOM

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

## Other information
# Conflicts:
#	core/src/components/alert/alert.tsx
#	core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-action-sheet-diff-ios-ltr-Mobile-Safari-linux.png
#	core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-action-sheet-diff-md-ltr-Mobile-Safari-linux.png
#	core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-menu-diff-ios-ltr-Mobile-Safari-linux.png
#	core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-menu-diff-md-ltr-Mobile-Safari-linux.png
#	core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-picker-diff-ios-ltr-Mobile-Safari-linux.png
#	core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-picker-diff-md-ltr-Mobile-Safari-linux.png
#	core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-toast-diff-ios-ltr-Mobile-Safari-linux.png
#	core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-toast-diff-md-ltr-Mobile-Safari-linux.png
#	core/src/components/picker-legacy/test/basic/picker.e2e.ts-snapshots/picker-basic-ios-ltr-Mobile-Safari-linux.png
#	core/src/components/picker-legacy/test/basic/picker.e2e.ts-snapshots/picker-basic-ios-rtl-Mobile-Safari-linux.png
#	core/src/components/picker-legacy/test/basic/picker.e2e.ts-snapshots/picker-basic-md-ltr-Mobile-Safari-linux.png
#	core/src/components/picker-legacy/test/basic/picker.e2e.ts-snapshots/picker-basic-md-rtl-Mobile-Safari-linux.png
#	core/src/components/picker-legacy/test/basic/picker.e2e.ts-snapshots/picker-value-selected-ios-ltr-Mobile-Safari-linux.png
#	core/src/components/picker-legacy/test/basic/picker.e2e.ts-snapshots/picker-value-selected-ios-rtl-Mobile-Safari-linux.png
#	core/src/components/picker-legacy/test/basic/picker.e2e.ts-snapshots/picker-value-selected-md-ltr-Mobile-Safari-linux.png
#	core/src/components/picker-legacy/test/basic/picker.e2e.ts-snapshots/picker-value-selected-md-rtl-Mobile-Safari-linux.png
#	packages/angular/test/base/src/app/standalone/home-page/home-page.component.html
#	packages/react-router/package-lock.json
#	packages/react/package-lock.json
#	packages/react/package.json
#	packages/react/test/base/src/App.tsx
#	packages/vue-router/package-lock.json
#	packages/vue/package-lock.json
#	packages/vue/package.json
chuhuangvio-itch and others added 5 commits September 2, 2026 19:02
## What is the current behavior?

`sanitizeDOMString` (`core/src/utils/sanitization/index.ts`) blocks
untrusted HTML containing `onload=` before it reaches `innerHTML`,
because `onload` can fire synchronously while the string is being parsed
into the working document fragment — before the later
attribute-allowlist pass runs. The check is a plain lowercase substring
match (`untrustedString.includes('onload=')`), so it misses `onLoad=`,
`ONLOAD=`, or `onload =` (whitespace before `=`) even though HTML parses
all of those as the same event handler.

## What is the new behavior?

-
- Replaced the substring check with a case-insensitive regex that also
tolerates whitespace around `=` (`/onload\s*=/i`), matching how HTML
actually parses attribute names.
- Added a test covering the case and whitespace variants.

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

## Other information

`sanitizeDOMString` is used by `ion-toast`, `ion-loading`, the
`ion-alert` message, `ion-refresher-content`, and
`ion-infinite-scroll-content` to sanitize developer-supplied HTML
strings that may embed end-user input (e.g. another user's display name
rendered in a toast/alert). This closes a gap where a payload like `<svg
onLoad=...>` could bypass the intended guard and reach `innerHTML`
unfiltered.

I didn't find an existing `SECURITY.md` or private vulnerability
reporting channel enabled on this repo, so opening this directly as a PR
with the fix rather than filing a separate public issue describing the
bypass.

---------

Co-authored-by: ShaneK <shane@shanessite.net>
@ShaneK
ShaneK requested a review from a team as a code owner September 2, 2026 20:21
@ShaneK
ShaneK requested a review from thetaPC September 2, 2026 20:21
@vercel

vercel Bot commented Sep 2, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated
ionic-framework Building Building Preview Sep 3, 2026 3:03pm UTC

Request Review

@github-actions github-actions Bot added package: core @ionic/core package package: angular @ionic/angular package package: vue @ionic/vue package package: react @ionic/react package labels Sep 2, 2026
@ShaneK
ShaneK marked this pull request as draft September 2, 2026 20:25
The merge moved main's slotted margin rules into the shared
textarea.common.scss, which only imports themes/mixins and so cannot
reach $form-control-label-margin from themes/native/native.globals.
That broke the sass build. The rules already exist, namespaced, in
textarea.native.scss where next relocated them.

Also regenerates api.txt and the Angular proxies from a real core build:
ion-searchbar's cancelButtonIcon signature and the standalone directive
inputs were stale after the merge, and ion-divider/ion-gallery/
ion-gallery-item needed standalone: false in the lazy proxies.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

package: angular @ionic/angular package package: core @ionic/core package package: react @ionic/react package package: vue @ionic/vue package

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants