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>
@@ -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`));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 9.5 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 9.7 KiB |
|
After Width: | Height: | Size: 8.9 KiB |
|
After Width: | Height: | Size: 9.6 KiB |
|
After Width: | Height: | Size: 9.8 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 8.7 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 9.3 KiB |
|
After Width: | Height: | Size: 8.7 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 9.6 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
1
core/src/components/toolbar/test/image.svg
Normal 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 |
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||