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

Sets the `flex-basis` variable to `auto` if the slot contains an img to allow it to calculate the width using the image's intrinsic width.

---------

Co-authored-by: Brandy Smith <6577830+brandyscarney@users.noreply.github.com>
This commit is contained in:
Brandy Smith
2025-06-26 10:10:55 -04:00
committed by GitHub
parent 33dcf98bd2
commit f42e39e0d5
31 changed files with 87 additions and 3 deletions

View File

@@ -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`));
});
});
});
});

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="50px" height="50px"><path fill="#c5dbff" d="M0 0h512v512H0z"/><path d="M256 304c61.6 0 112-50.4 112-112S317.6 80 256 80s-112 50.4-112 112 50.4 112 112 112zm0 40c-74.2 0-224 37.8-224 112v56h448v-56c0-74.2-149.8-112-224-112z" fill="#82aeff"/></svg>

After

Width:  |  Height:  |  Size: 316 B

View File

@@ -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

View File

@@ -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 {