Skip to content

fix(toolbar): allow slot to size to content to account for images #30508

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Jun 26, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions core/src/components/toolbar/test/basic/toolbar.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,3 +180,67 @@ configs({ modes: ['ios', 'md', 'ionic-md'], palettes: ['light', 'dark'] }).forEa
});
});
});

configs({ modes: ['ios', 'md', 'ionic-md'], directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
test.describe(title('toolbar: basic'), () => {
test.describe(title('slot content'), () => {
test('should not have visual regressions with slotted svgs', async ({ page }) => {
await page.setContent(
`
<ion-header>
<ion-toolbar>
<img src="/src/components/toolbar/test/image.svg" slot="start"/>
<ion-title>Toolbar</ion-title>
<ion-img src="/src/components/toolbar/test/image.svg" slot="end"/>
</ion-toolbar>
</ion-header>
`,
config
);

const header = page.locator('ion-header');
await expect(header).toHaveScreenshot(screenshot(`toolbar-basic-slotted-svgs`));
});

test('should not have visual regressions with slotted images', async ({ page }) => {
await page.setContent(
`
<ion-header>
<ion-toolbar>
<img src="https://picsum.photos/id/237/50/50" slot="start" />
<ion-title>Toolbar</ion-title>
<ion-img src="https://picsum.photos/id/237/50/50" slot="end"></ion-img>
</ion-toolbar>
</ion-header>
`,
config
);

const header = page.locator('ion-header');
await expect(header).toHaveScreenshot(screenshot(`toolbar-basic-slotted-images`));
});

test('should not have visual regressions with nested slotted images', async ({ page }) => {
await page.setContent(
`
<ion-header>
<ion-toolbar>
<div slot="start">
<img src="https://picsum.photos/id/237/50/50" />
</div>
<ion-title>Toolbar</ion-title>
<div slot="end">
<ion-img src="https://picsum.photos/id/237/50/50"></ion-img>
</div>
</ion-toolbar>
</ion-header>
`,
config
);

const header = page.locator('ion-header');
await expect(header).toHaveScreenshot(screenshot(`toolbar-basic-nested-slotted-images`));
});
});
});
});
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions core/src/components/toolbar/test/image.svg
4 changes: 2 additions & 2 deletions core/src/components/toolbar/toolbar.ionic.scss
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,12 @@
// Ionic Toolbar Slot Placement
// --------------------------------------------------
// We're using the slots to force the main toolbar content to be
// cenetered in the toolbar. This is a bit of a hack but it works.
// centered in the toolbar. This is a bit of a hack but it works.
// The main content is placed in the center and then JavaScript evaluates
// the sizes of the different slots and sets what size the pairs should be
// based on the larger one. We then use `flex-basis` to set the expected
// size of the slots and disable `flex-shrink` so that the smaller slot cannot
// shrink and throw off the center, we also diable flex-grow so that slots do
// shrink and throw off the center, we also disable flex-grow so that slots do
// not grow more than they need. The slots are paired up so the mirroring slots
// always have the same size. That is, start and end are paired and primary
// and secondary are paired. All of this works together to force the main
Expand Down
21 changes: 20 additions & 1 deletion core/src/components/toolbar/toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,28 @@ export class Toolbar implements ComponentInterface {
const slots = ['start', 'end', 'primary', 'secondary'];
slots.forEach((slot) => {
if (this.el.classList.contains(`has-${slot}-content`)) {
const slotElement = this.el.shadowRoot?.querySelector(`slot[name="${slot}"]`) as HTMLElement | null;
const slotElement = this.el.shadowRoot?.querySelector(`slot[name="${slot}"]`) as HTMLSlotElement | null;
if (slotElement) {
// Check if the slot contains an img or ion-img
const assignedElements = slotElement.assignedElements({ flatten: true });
const hasImg = assignedElements.some((el) => {
if (el.tagName === 'IMG' || el.tagName === 'ION-IMG') {
return true;
}
// Check for nested images
return el.querySelector('img, ion-img');
});

// Temporarily allow slot to size to content by setting flex-basis
// to 'auto'. This ensures that slotted images can render at their
// intrinsic width for measurement.
if (hasImg) {
const { name } = slotPairs.find((pair) => pair.slots.includes(slot))!;
this.el.style.setProperty(`--${name}-size`, 'auto');
}

const width = slotElement.offsetWidth;

if (width > 0) {
slotWidths.set(slot, width);
} else {
Expand Down
Loading