Skip to content

Commit 7ecfa51

Browse files
committed
fix(toolbar): fixing dynamic slot sizing to ensure better centered content
1 parent 2720faa commit 7ecfa51

File tree

4 files changed

+118
-32
lines changed

4 files changed

+118
-32
lines changed

core/src/components/select/test/basic/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -345,7 +345,7 @@
345345

346346
var customModalSelect = document.getElementById('customModalSelect');
347347
var customModalSheetOptions = {
348-
header: 'Pizza Toppings',
348+
header: 'Pizza Toppings are really long',
349349
breakpoints: [0.5],
350350
initialBreakpoint: 0.5,
351351
};

core/src/components/toolbar/test/basic/index.html

Lines changed: 14 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -64,41 +64,39 @@
6464
</ion-toolbar>
6565

6666
<ion-toolbar>
67-
<ion-buttons slot="secondary">
67+
<ion-buttons slot="start">
6868
<ion-button>
6969
<ion-icon slot="icon-only" name="person-circle"></ion-icon>
7070
</ion-button>
7171
<ion-button>
7272
<ion-icon slot="icon-only" name="search"></ion-icon>
7373
</ion-button>
74-
</ion-buttons>
75-
<ion-buttons slot="primary">
76-
<ion-button color="secondary">
77-
<ion-icon slot="icon-only" ios="ellipsis-horizontal" md="ellipsis-vertical"></ion-icon>
74+
<ion-button>
75+
<ion-icon slot="icon-only" name="search"></ion-icon>
7876
</ion-button>
7977
</ion-buttons>
80-
<ion-title> This is a long title with buttons. It just goes on and on my friend. </ion-title>
78+
<ion-text slot="end"> Small </ion-text>
79+
<ion-title> Toolbar with start content that's long and end content that's short </ion-title>
8180
</ion-toolbar>
8281

8382
<ion-toolbar>
84-
<ion-buttons slot="start">
83+
<ion-text slot="start"> Small </ion-text>
84+
<ion-buttons slot="end">
8585
<ion-button>
8686
<ion-icon slot="icon-only" name="person-circle"></ion-icon>
8787
</ion-button>
8888
<ion-button>
8989
<ion-icon slot="icon-only" name="search"></ion-icon>
9090
</ion-button>
91-
</ion-buttons>
92-
<ion-buttons slot="end">
93-
<ion-button color="secondary">
94-
<ion-icon slot="icon-only" ios="ellipsis-horizontal" md="ellipsis-vertical"></ion-icon>
91+
<ion-button>
92+
<ion-icon slot="icon-only" name="search"></ion-icon>
9593
</ion-button>
9694
</ion-buttons>
97-
<ion-title> This is a long title with start and end buttons. It just goes on and on my friend. </ion-title>
95+
<ion-title> Toolbar with start content that's long and end content that's short </ion-title>
9896
</ion-toolbar>
9997

10098
<ion-toolbar>
101-
<ion-buttons slot="start">
99+
<ion-buttons slot="secondary">
102100
<ion-button>
103101
<ion-icon slot="icon-only" name="person-circle"></ion-icon>
104102
</ion-button>
@@ -111,13 +109,11 @@
111109
<ion-icon slot="icon-only" ios="ellipsis-horizontal" md="ellipsis-vertical"></ion-icon>
112110
</ion-button>
113111
</ion-buttons>
114-
<ion-title>
115-
This is a long title with start and primary buttons. It just goes on and on my friend.
116-
</ion-title>
112+
<ion-title> This is a long title with buttons. It just goes on and on my friend. </ion-title>
117113
</ion-toolbar>
118114

119115
<ion-toolbar>
120-
<ion-buttons slot="secondary">
116+
<ion-buttons slot="start">
121117
<ion-button>
122118
<ion-icon slot="icon-only" name="person-circle"></ion-icon>
123119
</ion-button>
@@ -130,9 +126,7 @@
130126
<ion-icon slot="icon-only" ios="ellipsis-horizontal" md="ellipsis-vertical"></ion-icon>
131127
</ion-button>
132128
</ion-buttons>
133-
<ion-title>
134-
This is a long title with secondary and end buttons. It just goes on and on my friend.
135-
</ion-title>
129+
<ion-title> This is a long title with start and end buttons. It just goes on and on my friend. </ion-title>
136130
</ion-toolbar>
137131

138132
<ion-toolbar>

core/src/components/toolbar/toolbar.ionic.scss

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
// --------------------------------------------------
4040

4141
::slotted(ion-title) {
42-
@include globals.padding-horizontal(var(--padding-start), var(--padding-end));
42+
@include globals.padding-horizontal(globals.$ion-space-200);
4343
}
4444

4545
:host(.toolbar-title-default) ::slotted(ion-title) {
@@ -52,43 +52,53 @@
5252

5353
// Ionic Toolbar Slot Placement
5454
// --------------------------------------------------
55+
// We're using the slots to force the main toolbar content to be
56+
// cenetered in the toolbar. This is a bit of a hack but it works.
57+
// The main content is placed in the center and then JavaScript evaluates
58+
// the sizes of the different slots and sets what size the pairs should be
59+
// based on the larger one. We then use `flex-basis` to set the expected
60+
// size of the slots and disable `flex-shrink` so that the smaller slot cannot
61+
// shrink and throw off the center. The slots are paired up so the mirroring slots
62+
// always have the same size. That is, start and end are paired and primary
63+
// and secondary are paired. All of this works together to force the main
64+
// content to maintain the center as best as CSS allows while also allowing
65+
// the slots and main content to have fairly dynamic widths.
66+
// --------------------------------------------------
5567

5668
:host(.has-end-content) slot[name="end"],
5769
:host(.show-end) slot[name="end"] {
5870
display: flex;
5971

60-
flex: 1 1 0;
61-
gap: globals.$ion-space-200;
62-
72+
flex: 1 0 var(--start-end-size, 0);
6373
justify-content: flex-end;
6474

6575
text-align: end;
76+
gap: globals.$ion-space-400;
6677
}
6778

6879
:host(.has-start-content) slot[name="start"],
6980
:host(.show-start) slot[name="start"] {
7081
display: flex;
7182

72-
flex: 1 1 0;
73-
gap: globals.$ion-space-200;
83+
flex: 1 0 var(--start-end-size, 0);
84+
gap: globals.$ion-space-400;
7485
}
7586

7687
:host(.has-primary-content) slot[name="primary"],
7788
:host(.show-primary) slot[name="primary"] {
7889
display: flex;
7990

80-
flex: 1 1 0;
81-
gap: globals.$ion-space-200;
82-
91+
flex: 1 0 var(--primary-secondary-size, 0);
8392
justify-content: flex-end;
8493

8594
text-align: end;
95+
gap: globals.$ion-space-400;
8696
}
8797

8898
:host(.has-secondary-content) slot[name="secondary"],
8999
:host(.show-secondary) slot[name="secondary"] {
90100
display: flex;
91101

92-
flex: 1 1 0;
93-
gap: globals.$ion-space-200;
102+
flex: 1 0 var(--primary-secondary-size, 0);
103+
gap: globals.$ion-space-400;
94104
}

core/src/components/toolbar/toolbar.tsx

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,85 @@ export class Toolbar implements ComponentInterface {
6464

6565
componentDidLoad() {
6666
this.updateSlotClasses();
67+
this.updateSlotWidths();
68+
}
69+
70+
/**
71+
* Updates the CSS custom properties for slot widths
72+
* This ensures that slots shown by their met conditions
73+
* have a minimum width matching their required slot
74+
*/
75+
private updateSlotWidths(tries: number = 0) {
76+
// Set timeout to try to execute after everything is rendered
77+
setTimeout(() => {
78+
// Attempt to measure and update
79+
const success = this.measureAndUpdateSlots();
80+
81+
// If not all measurements were successful, try again in 100 ms
82+
// cap recursion at 5 tries for safety
83+
if (!success && tries < 5) {
84+
setTimeout(() => {
85+
this.updateSlotWidths(tries + 1);
86+
}, 100);
87+
}
88+
});
89+
}
90+
91+
/**
92+
* Measure the widths of the slots and update the CSS custom properties
93+
* for the minimum width of each pair of slots based on the largest width in each pair.
94+
* Returns whether we successfully measured all of the slots we expect to have content.
95+
* If not, the content probably hasn't rendered yet and we need to try again.
96+
*/
97+
private measureAndUpdateSlots(): boolean {
98+
// Define the relationship between slots based on the conditions array
99+
// Group slots that should have the same width
100+
const slotPairs = [
101+
{ name: 'start-end', slots: ['start', 'end'] },
102+
{ name: 'primary-secondary', slots: ['primary', 'secondary'] },
103+
];
104+
105+
// First, measure all slot widths
106+
const slotWidths = new Map<string, number>();
107+
let allMeasurementsSuccessful = true;
108+
109+
// Measure all slots with content
110+
const slots = ['start', 'end', 'primary', 'secondary'];
111+
slots.forEach((slot) => {
112+
if (this.el.classList.contains(`has-${slot}-content`)) {
113+
const slotElement = this.el.shadowRoot?.querySelector(`slot[name="${slot}"]`) as HTMLElement | null;
114+
if (slotElement) {
115+
const width = slotElement.offsetWidth;
116+
if (width > 0) {
117+
slotWidths.set(slot, width);
118+
} else {
119+
allMeasurementsSuccessful = false;
120+
}
121+
}
122+
}
123+
});
124+
125+
// Then set the CSS custom properties based on the largest width in each pair
126+
slotPairs.forEach(({ name, slots }) => {
127+
// Find the maximum width among the slots in this pair
128+
let maxWidth = 0;
129+
let hasAnyContent = false;
130+
131+
slots.forEach((slot) => {
132+
if (slotWidths.has(slot)) {
133+
hasAnyContent = true;
134+
maxWidth = Math.max(maxWidth, slotWidths.get(slot) ?? 0);
135+
}
136+
});
137+
138+
// If at least one slot in the pair has content, set the min-width for the pair
139+
if (hasAnyContent && maxWidth > 0) {
140+
// Set a single CSS variable for the pair
141+
this.el.style.setProperty(`--${name}-size`, `${maxWidth}px`);
142+
}
143+
});
144+
145+
return allMeasurementsSuccessful;
67146
}
68147

69148
private updateSlotClasses() {
@@ -108,6 +187,9 @@ export class Toolbar implements ComponentInterface {
108187
// Add classes to the toolbar element
109188
this.el.classList.add(...classesToAdd);
110189
this.el.classList.remove(...classesToRemove);
190+
191+
// Update slot widths after classes have been updated
192+
this.updateSlotWidths();
111193
}
112194

113195
private hasSlotContent(slotName: string): boolean {

0 commit comments

Comments
 (0)