diff --git a/core/api.txt b/core/api.txt
index 340dddec0b3..5ae230b8766 100644
--- a/core/api.txt
+++ b/core/api.txt
@@ -2829,7 +2829,6 @@ ion-title,css-prop,--ion-title-size-large-align-items
ion-title,css-prop,--ion-title-size-large-font-size
ion-title,css-prop,--ion-title-size-large-font-weight
ion-title,css-prop,--ion-title-size-large-height
-ion-title,css-prop,--ion-title-size-large-inner-width
ion-title,css-prop,--ion-title-size-large-line-height
ion-title,css-prop,--ion-title-size-large-min-width
ion-title,css-prop,--ion-title-size-large-padding-bottom
@@ -2838,7 +2837,6 @@ ion-title,css-prop,--ion-title-size-large-padding-start
ion-title,css-prop,--ion-title-size-large-padding-top
ion-title,css-prop,--ion-title-size-large-position
ion-title,css-prop,--ion-title-size-large-text-align
-ion-title,css-prop,--ion-title-size-large-transform-origin-x
ion-title,css-prop,--ion-title-size-large-width
ion-title,css-prop,--ion-title-size-medium-font-size
ion-title,css-prop,--ion-title-size-medium-font-weight
diff --git a/core/src/components/header/header.utils.ts b/core/src/components/header/header.utils.ts
index 7c07da07609..f73a91ce059 100644
--- a/core/src/components/header/header.utils.ts
+++ b/core/src/components/header/header.utils.ts
@@ -18,6 +18,7 @@ interface ToolbarIndex {
background: HTMLElement;
ionTitleEl: HTMLIonTitleElement | undefined;
innerTitleEl: HTMLElement;
+ innerTitleOrigin: string | undefined;
ionButtonsEl: HTMLElement[] | [];
}
@@ -35,6 +36,33 @@ export const cloneElement = (tagName: string) => {
return clonedEl;
};
+/**
+ * The large title grows from the edge its text is aligned to, so the
+ * text's start doesn't shift. Centered text grows from the center.
+ *
+ * @internal
+ * @param ionTitleEl - The title whose alignment and direction set the origin.
+ * @returns The horizontal `transform-origin` keyword: `left`, `right` or `center`.
+ */
+const getTitleOrigin = (ionTitleEl: HTMLIonTitleElement): string => {
+ const { textAlign, direction } = getComputedStyle(ionTitleEl);
+ const rtl = direction === 'rtl';
+
+ if (textAlign === 'center' || textAlign === '-webkit-center') {
+ return 'center';
+ }
+
+ if (textAlign === 'left' || textAlign === 'right') {
+ return textAlign;
+ }
+
+ if (textAlign === 'end') {
+ return rtl ? 'left' : 'right';
+ }
+
+ return rtl ? 'right' : 'left';
+};
+
export const createHeaderIndex = (headerEl: HTMLElement | undefined): HeaderIndex | undefined => {
if (!headerEl) {
return;
@@ -51,6 +79,7 @@ export const createHeaderIndex = (headerEl: HTMLElement | undefined): HeaderInde
background: toolbar.shadowRoot!.querySelector('.toolbar-background'),
ionTitleEl,
innerTitleEl: ionTitleEl ? ionTitleEl.shadowRoot!.querySelector('.toolbar-title') : null,
+ innerTitleOrigin: ionTitleEl ? getTitleOrigin(ionTitleEl) : undefined,
ionButtonsEl: Array.from(toolbar.querySelectorAll('ion-buttons')),
} as ToolbarIndex;
}),
@@ -239,6 +268,7 @@ export const scaleLargeTitles = (toolbars: ToolbarIndex[] = [], scale = 1, trans
}
titleDiv.style.transition = transition ? TRANSITION : '';
+ titleDiv.style.transformOrigin = `${toolbar.innerTitleOrigin} center`;
titleDiv.style.transform = `scale3d(${scale}, ${scale}, 1)`;
});
};
diff --git a/core/src/components/title/test/transition/index.html b/core/src/components/title/test/transition/index.html
new file mode 100644
index 00000000000..6eb0c981559
--- /dev/null
+++ b/core/src/components/title/test/transition/index.html
@@ -0,0 +1,177 @@
+
+
+
+
+ Title - Transition
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/core/src/components/title/test/transition/title.e2e.ts b/core/src/components/title/test/transition/title.e2e.ts
new file mode 100644
index 00000000000..bf64942225c
--- /dev/null
+++ b/core/src/components/title/test/transition/title.e2e.ts
@@ -0,0 +1,60 @@
+import { expect } from '@playwright/test';
+import { configs, test } from '@utils/test/playwright';
+
+/**
+ * Measures where the text is drawn, which differs from the
+ * title's box when the text is centered.
+ */
+const getTextBox = (el: Element) => {
+ const range = document.createRange();
+ range.selectNodeContents(el);
+ const { left, right } = range.getBoundingClientRect();
+
+ return { left, right };
+};
+
+/**
+ * The large title transition only runs in ios mode, so every theme is
+ * tested with it. Testing mode turns animations off, so it is disabled.
+ */
+configs({ modes: ['ios'] }).forEach(({ title, config }) => {
+ test.describe(title('title: transition'), () => {
+ ['ios', 'md', 'ionic'].forEach((theme) => {
+ test(`should start the large title copy on the title text in the ${theme} theme`, async ({ page }) => {
+ await page.goto(`/src/components/title/test/transition?ionic:_testing=false&ionic:theme=${theme}`, config);
+
+ const largeTitle = page.locator('page-inbox ion-title[size="large"]');
+ await expect(largeTitle).toHaveClass(/hydrated/);
+ const titleBox = await largeTitle.evaluate(getTextBox);
+
+ // Pauses the transition on its first frame once the copy starts animating
+ const transitionStarted = page.evaluate(
+ () =>
+ new Promise((resolve) => {
+ const check = () => {
+ const copy = document.querySelector('ion-title.ion-cloned-element');
+ if (copy && copy.getAnimations().length > 0) {
+ document.getAnimations().forEach((animation) => {
+ animation.pause();
+ animation.currentTime = 0;
+ });
+ requestAnimationFrame(() => resolve());
+ } else {
+ requestAnimationFrame(check);
+ }
+ };
+ check();
+ })
+ );
+
+ await page.locator('page-inbox button.go[data-back-text="match"]').click();
+ await transitionStarted;
+
+ const copyBox = await page.locator('ion-title.ion-cloned-element').evaluate(getTextBox);
+
+ expect(Math.abs(copyBox.left - titleBox.left)).toBeLessThanOrEqual(1);
+ expect(Math.abs(copyBox.right - titleBox.right)).toBeLessThanOrEqual(1);
+ });
+ });
+ });
+});
diff --git a/core/src/components/title/title.interface.ts b/core/src/components/title/title.interface.ts
index 08fce62496e..1ccde7b9e65 100644
--- a/core/src/components/title/title.interface.ts
+++ b/core/src/components/title/title.interface.ts
@@ -62,18 +62,7 @@ export type IonTitleRecipe = {
large?: IonTitleSizeDefinition & {
alignItems?: string;
-
- inner?: {
- width?: string;
- };
-
minWidth?: string;
-
- transform?: {
- origin?: {
- x?: string;
- };
- };
};
};
};
diff --git a/core/src/components/title/title.scss b/core/src/components/title/title.scss
index 5084b7ad87c..19a9d76a41a 100644
--- a/core/src/components/title/title.scss
+++ b/core/src/components/title/title.scss
@@ -43,7 +43,6 @@
* @prop --ion-title-size-large-font-size: Font size of the large title
* @prop --ion-title-size-large-font-weight: Font weight of the large title
* @prop --ion-title-size-large-height: Height of the large title
- * @prop --ion-title-size-large-inner-width: Width of the large title text, measured by the iOS page transition
* @prop --ion-title-size-large-line-height: Line height of the large title
* @prop --ion-title-size-large-min-width: Minimum width of the large title
* @prop --ion-title-size-large-padding-top: Top padding of the large title
@@ -52,7 +51,6 @@
* @prop --ion-title-size-large-padding-start: Left padding if direction is left-to-right, and right padding if direction is right-to-left of the large title
* @prop --ion-title-size-large-position: Position of the large title
* @prop --ion-title-size-large-text-align: Text alignment of the large title
- * @prop --ion-title-size-large-transform-origin-x: Horizontal origin the large title scales from when the header collapses, in the reading direction
* @prop --ion-title-size-large-width: Width of the large title
*
* @prop --ion-title-hue-bold-semantic-default-color: Text color of the `bold` hue when a semantic color is applied
@@ -184,39 +182,9 @@
text-align: var(--ion-title-size-large-text-align);
}
-/**
- * FW-7295: the header collapse scales the large title text from this
- * origin, and the iOS page transition measures the text's width.
- */
-:host(.title-size-large) {
- @include mixins.transform-origin(var(--ion-title-size-large-transform-origin-x), center);
-}
-
-:host(.title-size-large) .toolbar-title {
- @include mixins.transform-origin(inherit);
-
- width: var(--ion-title-size-large-inner-width);
-}
-
-/**
- * FW-7295: the iOS page transition moves a copy of the large title out of
- * its toolbar, so restore the color and font it would have inherited.
- */
-:host(.title-size-large.ion-cloned-element) {
- color: var(--ion-title-color, var(--ion-text-color, #000));
-
- /**
- * Same tokens as the title, but a different fallback: the copy sits
- * outside the toolbar, so `inherit` would pick up the overlay's font.
- */
- font-family: var(--ion-title-font-family, var(--ion-font-family));
-}
-
// Title: Hue
// --------------------------------------------------
-// Keep these after the cloned rule so a colored large title's copy keeps its color
-
:host(.title-hue-bold.ion-color) {
color: var(--ion-title-hue-bold-semantic-default-color);
}
diff --git a/core/src/components/toolbar/toolbar.tsx b/core/src/components/toolbar/toolbar.tsx
index 09ab315e35f..fb8f7415774 100644
--- a/core/src/components/toolbar/toolbar.tsx
+++ b/core/src/components/toolbar/toolbar.tsx
@@ -38,6 +38,7 @@ export class Toolbar implements ComponentInterface {
];
private readonly showClasses = ['show-start', 'show-end', 'show-primary', 'show-secondary'];
private readonly slotSizeVars = ['--start-end-size', '--primary-secondary-size'];
+ private slotContentObserver?: ResizeObserver;
@Element() el!: HTMLIonToolbarElement;
@@ -85,6 +86,11 @@ export class Toolbar implements ComponentInterface {
this.updateSlotWidths();
}
+ disconnectedCallback() {
+ this.slotContentObserver?.disconnect();
+ this.slotContentObserver = undefined;
+ }
+
@Watch('titlePlacement')
titlePlacementChanged() {
this.updateSlotClasses();
@@ -212,9 +218,12 @@ export class Toolbar implements ComponentInterface {
const titlePlacement = this.getTitlePlacement();
if (titlePlacement !== 'center') {
this.removeSlotClasses();
+ this.slotContentObserver?.disconnect();
return;
}
+ this.observeSlotContent();
+
// Check if slots have content
const slots = ['start', 'end', 'primary', 'secondary'];
@@ -261,6 +270,24 @@ export class Toolbar implements ComponentInterface {
this.updateSlotWidths();
}
+ /**
+ * Re-measures the slots when their content changes size, such as a back
+ * button that renders after the toolbar. The callback runs before paint,
+ * so a centered title never shows off-center.
+ */
+ private observeSlotContent() {
+ if (typeof ResizeObserver === 'undefined') {
+ return;
+ }
+
+ if (this.slotContentObserver === undefined) {
+ this.slotContentObserver = new ResizeObserver(() => this.measureAndUpdateSlots());
+ }
+
+ // The slotted content, not the slots, so setting the slot widths doesn't re-trigger it
+ this.el.querySelectorAll(':scope > [slot]').forEach((el) => this.slotContentObserver!.observe(el));
+ }
+
private hasSlotContent(slotName: string): boolean {
const slotNode = this.el.shadowRoot?.querySelector(`slot[name="${slotName}"]`) as HTMLSlotElement | null;
return !!slotNode && slotNode.assignedNodes().length > 0;
diff --git a/core/src/themes/ionic/default.tokens.ts b/core/src/themes/ionic/default.tokens.ts
index 07094e38407..a796ca0d988 100644
--- a/core/src/themes/ionic/default.tokens.ts
+++ b/core/src/themes/ionic/default.tokens.ts
@@ -1074,19 +1074,9 @@ export const defaultTheme: DefaultTheme = {
weight: 'var(--ion-font-weight-medium)',
},
- inner: {
- width: '100%',
- },
-
line: {
height: dynamicFont(global.root, 36),
},
-
- transform: {
- origin: {
- x: '50%',
- },
- },
},
},
},
diff --git a/core/src/themes/ios/default.tokens.ts b/core/src/themes/ios/default.tokens.ts
index 05fef74f960..128adb39c4e 100644
--- a/core/src/themes/ios/default.tokens.ts
+++ b/core/src/themes/ios/default.tokens.ts
@@ -1131,18 +1131,6 @@ export const defaultTheme: DefaultTheme = {
weight: 'var(--ion-font-weight-bold)',
},
- inner: {
- /**
- * During a page transition
- * if the large title and the back button
- * texts match up, the back button should be
- * scaled to roughly match the dimensions of
- * the large title text. The following line
- * ensures that the scale values are accurate.
- */
- width: 'auto',
- },
-
padding: {
top: 'var(--ion-spacing-xxxs)',
end: 'var(--ion-spacing-md)',
@@ -1153,12 +1141,6 @@ export const defaultTheme: DefaultTheme = {
text: {
align: 'start',
},
-
- transform: {
- origin: {
- x: '0%',
- },
- },
},
},
},
diff --git a/core/src/themes/md/default.tokens.ts b/core/src/themes/md/default.tokens.ts
index 8927debf634..55f8514426f 100644
--- a/core/src/themes/md/default.tokens.ts
+++ b/core/src/themes/md/default.tokens.ts
@@ -1228,22 +1228,12 @@ export const defaultTheme: DefaultTheme = {
weight: 'var(--ion-font-weight-normal)',
},
- inner: {
- width: '100%',
- },
-
padding: {
top: 'var(--ion-spacing-0)',
end: 'var(--ion-spacing-xl)',
bottom: 'var(--ion-spacing-0)',
start: 'var(--ion-spacing-xl)',
},
-
- transform: {
- origin: {
- x: '50%',
- },
- },
},
},
},
diff --git a/core/src/utils/transition/ios.transition.ts b/core/src/utils/transition/ios.transition.ts
index 4e694fe7ebd..1ece5935092 100644
--- a/core/src/utils/transition/ios.transition.ts
+++ b/core/src/utils/transition/ios.transition.ts
@@ -15,6 +15,42 @@ export const shadow = (el: T): ShadowRoot | T => {
return el.shadowRoot || el;
};
+/**
+ * The scale math needs the box around the title's text. Themes can make the
+ * text element wider than its text, so it's measured at its content width.
+ *
+ * @internal
+ * @param largeTitleEl - The large title to measure.
+ * @param rtl - Whether the title is in a right-to-left direction.
+ * @returns The box of the title's text element at its content width, and how
+ * far the rendered text sits from that box's start, such as when it's centered.
+ */
+const getLargeTitleTextBox = (largeTitleEl: HTMLIonTitleElement, rtl: boolean) => {
+ const textEl = shadow(largeTitleEl).querySelector('.toolbar-title')!;
+
+ /**
+ * A range measures where the text is drawn. The text element's own box
+ * can't, since it may be wider than the text, such as when centered.
+ */
+ const range = document.createRange();
+ range.selectNodeContents(largeTitleEl);
+ const text = range.getBoundingClientRect();
+
+ // Shrink the text element to its text for the scale math, then restore it
+ const width = textEl.style.width;
+ textEl.style.width = 'auto';
+ const box = textEl.getBoundingClientRect();
+ textEl.style.width = width;
+
+ /**
+ * How far the drawn text sits from where the shrunk box starts: 0 for a
+ * start-aligned title, more for a centered one. The copies start there.
+ */
+ const startShift = Math.max(0, rtl ? box.right - text.right : text.left - box.left);
+
+ return { box, startShift };
+};
+
const getLargeTitle = (refEl: any) => {
const tabs = refEl.tagName === 'ION-TABS' ? refEl : refEl.querySelector('ion-tabs');
const query = 'ion-content ion-header:not(.header-collapse-condense-inactive) ion-title.title-size-large';
@@ -80,8 +116,10 @@ const createLargeTitleTransition = (
// Text element not rendered if developers pass text="" to the back button
const enteringBackButtonTextBox = enteringBackButtonTextEl?.getBoundingClientRect();
- const leavingLargeTitleTextEl = shadow(leavingLargeTitle).querySelector('.toolbar-title')!;
- const leavingLargeTitleTextBox = leavingLargeTitleTextEl.getBoundingClientRect();
+ const { box: leavingLargeTitleTextBox, startShift: leavingLargeTitleStartShift } = getLargeTitleTextBox(
+ leavingLargeTitle,
+ rtl
+ );
animateLargeTitle(
rootAnimation,
@@ -90,6 +128,7 @@ const createLargeTitleTransition = (
leavingLargeTitle,
leavingLargeTitleBox,
leavingLargeTitleTextBox,
+ leavingLargeTitleStartShift,
enteringBackButtonBox,
enteringBackButtonTextEl,
enteringBackButtonTextBox
@@ -103,7 +142,8 @@ const createLargeTitleTransition = (
enteringBackButtonTextEl,
enteringBackButtonTextBox,
leavingLargeTitle,
- leavingLargeTitleTextBox
+ leavingLargeTitleTextBox,
+ leavingLargeTitleStartShift
);
} else if (shouldAnimationBackward) {
const enteringLargeTitleBox = enteringLargeTitle.getBoundingClientRect();
@@ -114,8 +154,10 @@ const createLargeTitleTransition = (
// Text element not rendered if developers pass text="" to the back button
const leavingBackButtonTextBox = leavingBackButtonTextEl?.getBoundingClientRect();
- const enteringLargeTitleTextEl = shadow(enteringLargeTitle).querySelector('.toolbar-title')!;
- const enteringLargeTitleTextBox = enteringLargeTitleTextEl.getBoundingClientRect();
+ const { box: enteringLargeTitleTextBox, startShift: enteringLargeTitleStartShift } = getLargeTitleTextBox(
+ enteringLargeTitle,
+ rtl
+ );
animateLargeTitle(
rootAnimation,
@@ -124,6 +166,7 @@ const createLargeTitleTransition = (
enteringLargeTitle,
enteringLargeTitleBox,
enteringLargeTitleTextBox,
+ enteringLargeTitleStartShift,
leavingBackButtonBox,
leavingBackButtonTextEl,
leavingBackButtonTextBox
@@ -137,7 +180,8 @@ const createLargeTitleTransition = (
leavingBackButtonTextEl,
leavingBackButtonTextBox,
enteringLargeTitle,
- enteringLargeTitleTextBox
+ enteringLargeTitleTextBox,
+ enteringLargeTitleStartShift
);
}
@@ -156,7 +200,8 @@ const animateBackButton = (
backButtonTextEl: HTMLElement | null,
backButtonTextBox: DOMRect | undefined,
largeTitleEl: HTMLIonTitleElement,
- largeTitleTextBox: DOMRect
+ largeTitleTextBox: DOMRect,
+ largeTitleStartShift: number
) => {
const BACK_BUTTON_START_OFFSET = rtl ? `calc(100% - ${backButtonBox.right + 4}px)` : `${backButtonBox.left - 4}px`;
@@ -202,9 +247,13 @@ const animateBackButton = (
* text. Otherwise, the back button icon will align with the
* large title text but the back button text will not.
*/
+ /**
+ * Start where the large title's text is, so both copies move together
+ * from a title that isn't at the start edge, such as a centered one.
+ */
const CONTAINER_START_TRANSLATE_X = rtl
- ? `${backButtonIconBox.width / 2 - (backButtonIconBox.right - backButtonBox.right)}px`
- : `${backButtonBox.left - backButtonIconBox.width / 2}px`;
+ ? `${backButtonIconBox.width / 2 - (backButtonIconBox.right - backButtonBox.right) - largeTitleStartShift}px`
+ : `${backButtonBox.left - backButtonIconBox.width / 2 + largeTitleStartShift}px`;
const CONTAINER_END_TRANSLATE_X = rtl ? `-${window.innerWidth - backButtonBox.right}px` : `${backButtonBox.left}px`;
/**
@@ -302,6 +351,9 @@ const animateBackButton = (
position: 'absolute',
top: '0px',
[CONTAINER_ORIGIN_X]: '0px',
+
+ // The copy moves under the pointer, so it must not show hover styles
+ 'pointer-events': 'none',
})
/**
* The write hooks must be set on this animation as it is guaranteed to run. Other
@@ -309,11 +361,15 @@ const animateBackButton = (
* has no visible text.
*/
.beforeAddWrite(() => {
- backButtonEl.style.setProperty('display', 'none');
+ /**
+ * Hide the back button without removing it from the layout,
+ * so a title placed next to it doesn't shift into its space.
+ */
+ backButtonEl.style.setProperty('visibility', 'hidden');
clonedBackButtonEl.style.setProperty(TEXT_ORIGIN_X, BACK_BUTTON_START_OFFSET);
})
.afterAddWrite(() => {
- backButtonEl.style.setProperty('display', '');
+ backButtonEl.style.removeProperty('visibility');
clonedBackButtonEl.style.setProperty('display', 'none');
clonedBackButtonEl.style.removeProperty(TEXT_ORIGIN_X);
})
@@ -345,6 +401,7 @@ const animateLargeTitle = (
largeTitleEl: HTMLIonTitleElement,
largeTitleBox: DOMRect,
largeTitleTextBox: DOMRect,
+ largeTitleStartShift: number,
backButtonBox: DOMRect,
backButtonTextEl: HTMLElement | null,
backButtonTextBox: DOMRect | undefined
@@ -354,7 +411,13 @@ const animateLargeTitle = (
*/
const ORIGIN_X = rtl ? 'right' : 'left';
- const TITLE_START_OFFSET = rtl ? `calc(100% - ${largeTitleBox.right}px)` : `${largeTitleBox.left}px`;
+ /**
+ * The copy's text sits at the start of its box, so the copy starts where
+ * the title's text is rendered, such as further in when it's centered.
+ */
+ const TITLE_START_OFFSET = rtl
+ ? `calc(100% - ${largeTitleBox.right - largeTitleStartShift}px)`
+ : `${largeTitleBox.left + largeTitleStartShift}px`;
/**
* The cloned large should align exactly with the
@@ -370,9 +433,10 @@ const animateLargeTitle = (
* title and the back button due to padding and font weight.
*/
const LARGE_TITLE_TRANSLATION_OFFSET = 8;
+ // The copy already starts further in by the shift, so it travels that much less
let END_TRANSLATE_X = rtl
- ? `-${window.innerWidth - backButtonBox.right - LARGE_TITLE_TRANSLATION_OFFSET}px`
- : `${backButtonBox.x + LARGE_TITLE_TRANSLATION_OFFSET}px`;
+ ? `${-(window.innerWidth - backButtonBox.right - LARGE_TITLE_TRANSLATION_OFFSET - largeTitleStartShift)}px`
+ : `${backButtonBox.x + LARGE_TITLE_TRANSLATION_OFFSET - largeTitleStartShift}px`;
/**
* How much to scale the large title up/down by.
@@ -401,8 +465,8 @@ const animateLargeTitle = (
* does not need to be perfect, so approximate values are acceptable here.
*/
END_TRANSLATE_X = rtl
- ? `-${window.innerWidth - backButtonTextBox.right - LARGE_TITLE_TRANSLATION_OFFSET}px`
- : `${backButtonTextBox.x - LARGE_TITLE_TRANSLATION_OFFSET}px`;
+ ? `${-(window.innerWidth - backButtonTextBox.right - LARGE_TITLE_TRANSLATION_OFFSET - largeTitleStartShift)}px`
+ : `${backButtonTextBox.x - LARGE_TITLE_TRANSLATION_OFFSET - largeTitleStartShift}px`;
/**
* In the forward direction, the large title should start at its normal size and
@@ -461,6 +525,12 @@ const animateLargeTitle = (
const clonedTitleEl = getClonedElement('ion-title')!;
const clonedLargeTitleAnimation = createAnimation();
+ /**
+ * The copy sits outside the toolbar, so it loses styles that depend on it,
+ * such as the inherited color and font. Copy them from the title instead.
+ */
+ const { color, fontFamily, paddingTop, paddingRight, paddingBottom, paddingLeft } = getComputedStyle(largeTitleEl);
+
clonedTitleEl.innerText = largeTitleEl.innerText;
clonedTitleEl.size = largeTitleEl.size;
clonedTitleEl.color = largeTitleEl.color;
@@ -481,9 +551,19 @@ const animateLargeTitle = (
display: '',
position: 'relative',
[ORIGIN_X]: TITLE_START_OFFSET,
+ color,
+ 'font-family': fontFamily,
+ 'padding-top': paddingTop,
+ 'padding-right': paddingRight,
+ 'padding-bottom': paddingBottom,
+ 'padding-left': paddingLeft,
+ 'pointer-events': 'none',
})
.beforeAddWrite(() => {
largeTitleEl.style.setProperty('opacity', '0');
+
+ // Scale from the text's start edge, not the edge of a wider text element
+ shadow(clonedTitleEl).querySelector('.toolbar-title')?.style.setProperty('width', 'auto');
})
.afterAddWrite(() => {
largeTitleEl.style.setProperty('opacity', '');