Skip to content
Open
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
68 changes: 68 additions & 0 deletions BREAKING.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ This is a comprehensive list of the breaking changes introduced in the major ver
- [Text](#version-10x-text)
- [Textarea](#version-10x-textarea)
- [Thumbnail](#version-10x-thumbnail)
- [Title](#version-10x-title)
- [Toolbar](#version-10x-toolbar)
- [Framework Specific](#version-10x-framework-specific)
- [Angular](#version-10x-angular)

Expand Down Expand Up @@ -529,6 +531,72 @@ Use the **parent's** thumbnail tokens instead:

Remove any instances that target the theme classes: `ion-thumbnail.md`, `ion-thumbnail.ios`.

<h4 id="version-10x-title">Title</h4>

The following breaking changes apply to `ion-title`:

1. `--color` has been replaced. <sup>[1](#version-10x-title-replaced-css-variables)</sup>
2. The color applied by the `color` prop is now driven by the centralized Ionic Theming system, scoped to the new `hue` property. <sup>[2](#version-10x-title-hue)</sup>
3. The size classes now include the property name, and a title without `size` is now `medium`. <sup>[3](#version-10x-title-size-classes)</sup>
4. The `title-rtl` class has been removed. <sup>[4](#version-10x-title-rtl-class)</sup>
5. Theme classes (`ion-title.md`, `ion-title.ios`) are no longer supported. <sup>[5](#version-10x-title-theme-classes)</sup>

<h5 id="version-10x-title-replaced-css-variables">Replaced CSS variables</h5>

Use the new token structure for global styles, or the corresponding CSS variable for component-specific overrides:

| Old (9.x) | New token (global) | New CSS variable (component-specific) |
|---|---|---|
| `--color` | `IonTitle.color` | `--ion-title-color` |

<h5 id="version-10x-title-hue">New `hue` property and color tokens</h5>

A new `hue` property selects between vibrant and muted color variants. It defaults to `"bold"`, which preserves prior behavior when `color` is set.

When `color` is set, the title color now reads from a token instead of `--ion-color-base` directly. Global overrides should use the theme tokens; component-specific overrides use the corresponding CSS variables:

| Hue | Token (global) | CSS variable (component-specific) |
|---|---|---|
| `bold` | `IonTitle.hue.bold.semantic.default.color` | `--ion-title-hue-bold-semantic-default-color` |
| `subtle` | `IonTitle.hue.subtle.semantic.default.color` | `--ion-title-hue-subtle-semantic-default-color` |

<h5 id="version-10x-title-size-classes">Size classes</h5>

The classes `ion-title` adds for its size now include the property name. A title without `size` is now `medium`, which can also be set explicitly:

| Old (9.x) | New |
|---|---|
| `title-default` | `title-size-medium` |
| `title-small` | `title-size-small` |
| `title-large` | `title-size-large` |

`ion-toolbar` builds its own classes from these, so they change as well:

| Old (9.x) | New |
|---|---|
| `toolbar-title-default` | `toolbar-title-size-medium` |
| `toolbar-title-small` | `toolbar-title-size-small` |
| `toolbar-title-large` | `toolbar-title-size-large` |

<h5 id="version-10x-title-rtl-class">`title-rtl` class</h5>

`ion-title` no longer adds the `title-rtl` class. Use the `:dir(rtl)` pseudo-class instead:

```diff
-ion-title.title-rtl { }
+ion-title:dir(rtl) { }
```

<h5 id="version-10x-title-theme-classes">Theme classes</h5>

Remove any instances that target the theme classes: `ion-title.md`, `ion-title.ios`.

<h4 id="version-10x-toolbar">Toolbar</h4>

The following breaking changes apply to `ion-toolbar`:

1. The classes it adds for a slotted title's size now include the property name. <sup>[1](#version-10x-title-size-classes)</sup>

<h2 id="version-10x-framework-specific">Framework Specific</h2>

<h4 id="version-10x-angular">Angular</h4>
Expand Down
55 changes: 50 additions & 5 deletions core/api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2811,12 +2811,57 @@ ion-thumbnail,css-prop,--ion-thumbnail-width

ion-title,shadow
ion-title,prop,color,"danger" | "dark" | "light" | "medium" | "primary" | "secondary" | "success" | "tertiary" | "warning" | string & Record<never, never> | undefined,undefined,false,true
ion-title,prop,hue,"bold" | "subtle" | undefined,undefined,false,false
ion-title,prop,mode,"ios" | "md",undefined,false,false
ion-title,prop,size,"large" | "small" | undefined,undefined,false,false
ion-title,prop,theme,"ios" | "md" | "ionic",undefined,false,false
ion-title,css-prop,--color,ionic
ion-title,css-prop,--color,ios
ion-title,css-prop,--color,md
ion-title,prop,size,"large" | "medium" | "small" | undefined,undefined,false,false
ion-title,css-prop,--ion-title-box-sizing
ion-title,css-prop,--ion-title-color
ion-title,css-prop,--ion-title-font-family
ion-title,css-prop,--ion-title-hue-bold-semantic-default-color
ion-title,css-prop,--ion-title-hue-subtle-semantic-default-color
ion-title,css-prop,--ion-title-letter-spacing
ion-title,css-prop,--ion-title-pointer-events
ion-title,css-prop,--ion-title-position-bottom
ion-title,css-prop,--ion-title-position-end
ion-title,css-prop,--ion-title-position-start
ion-title,css-prop,--ion-title-position-top
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
ion-title,css-prop,--ion-title-size-large-padding-end
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
ion-title,css-prop,--ion-title-size-medium-height
ion-title,css-prop,--ion-title-size-medium-line-height
ion-title,css-prop,--ion-title-size-medium-padding-bottom
ion-title,css-prop,--ion-title-size-medium-padding-end
ion-title,css-prop,--ion-title-size-medium-padding-start
ion-title,css-prop,--ion-title-size-medium-padding-top
ion-title,css-prop,--ion-title-size-medium-position
ion-title,css-prop,--ion-title-size-medium-text-align
ion-title,css-prop,--ion-title-size-medium-width
ion-title,css-prop,--ion-title-size-small-font-size
ion-title,css-prop,--ion-title-size-small-font-weight
ion-title,css-prop,--ion-title-size-small-height
ion-title,css-prop,--ion-title-size-small-line-height
ion-title,css-prop,--ion-title-size-small-padding-bottom
ion-title,css-prop,--ion-title-size-small-padding-end
ion-title,css-prop,--ion-title-size-small-padding-start
ion-title,css-prop,--ion-title-size-small-padding-top
ion-title,css-prop,--ion-title-size-small-position
ion-title,css-prop,--ion-title-size-small-text-align
ion-title,css-prop,--ion-title-size-small-width

ion-toast,shadow
ion-toast,prop,animated,boolean,true,false,false
Expand Down
29 changes: 16 additions & 13 deletions core/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import { SelectPopoverOption } from "./components/select-popover/select-popover-
import { SpinnerSize } from "./components/spinner/spinner.interfaces";
import { TabBarChangedEventDetail, TabBarScrollEffect, TabButtonClickEventDetail, TabButtonLayout } from "./components/tab-bar/tab-bar-interface";
import { TextareaChangeEventDetail, TextareaInputEventDetail } from "./components/textarea/textarea-interface";
import { IonTitleSize } from "./components/title/title.interface";
import { ToastButton, ToastDismissOptions, ToastLayout, ToastPosition, ToastPresentOptions, ToastSwipeGestureDirection } from "./components/toast/toast-interface";
import { ToggleChangeEventDetail } from "./components/toggle/toggle-interface";
export { AccordionGroupChangeEventDetail } from "./components/accordion-group/accordion-group-interface";
Expand Down Expand Up @@ -95,6 +96,7 @@ export { SelectPopoverOption } from "./components/select-popover/select-popover-
export { SpinnerSize } from "./components/spinner/spinner.interfaces";
export { TabBarChangedEventDetail, TabBarScrollEffect, TabButtonClickEventDetail, TabButtonLayout } from "./components/tab-bar/tab-bar-interface";
export { TextareaChangeEventDetail, TextareaInputEventDetail } from "./components/textarea/textarea-interface";
export { IonTitleSize } from "./components/title/title.interface";
export { ToastButton, ToastDismissOptions, ToastLayout, ToastPosition, ToastPresentOptions, ToastSwipeGestureDirection } from "./components/toast/toast-interface";
export { ToggleChangeEventDetail } from "./components/toggle/toggle-interface";
export namespace Components {
Expand Down Expand Up @@ -4172,17 +4174,17 @@ export namespace Components {
*/
"color"?: Color;
/**
* The mode determines the platform behaviors of the component.
* Set to `"bold"` for a title with vibrant, bold colors or to `"subtle"` for a title with muted, subtle colors. Defaults to `"bold"` if both the hue property and theme config are unset.
*/
"mode"?: "ios" | "md";
"hue"?: Hue;
/**
* The size of the toolbar title.
* The mode determines the platform behaviors of the component.
*/
"size"?: 'large' | 'small';
"mode"?: "ios" | "md";
/**
* The theme determines the visual appearance of the component.
* The size of the toolbar title. Defaults to `"medium"` if both the size property and theme config are unset.
*/
"theme"?: "ios" | "md" | "ionic";
"size"?: IonTitleSize;
}
interface IonToast {
/**
Expand Down Expand Up @@ -10136,6 +10138,10 @@ declare namespace LocalJSX {
* The color to use from your application's color palette. Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. For more information on colors, see [theming](/docs/theming/basics).
*/
"color"?: Color;
/**
* Set to `"bold"` for a title with vibrant, bold colors or to `"subtle"` for a title with muted, subtle colors. Defaults to `"bold"` if both the hue property and theme config are unset.
*/
"hue"?: Hue;
/**
* The mode determines the platform behaviors of the component.
*/
Expand All @@ -10145,13 +10151,9 @@ declare namespace LocalJSX {
*/
"onIonStyle"?: (event: IonTitleCustomEvent<StyleEventDetail>) => void;
/**
* The size of the toolbar title.
* The size of the toolbar title. Defaults to `"medium"` if both the size property and theme config are unset.
*/
"size"?: 'large' | 'small';
/**
* The theme determines the visual appearance of the component.
*/
"theme"?: "ios" | "md" | "ionic";
"size"?: IonTitleSize;
}
interface IonToast {
/**
Expand Down Expand Up @@ -11146,7 +11148,8 @@ declare namespace LocalJSX {
}
interface IonTitleAttributes {
"color": Color;
"size": 'large' | 'small';
"hue": Hue;
"size": IonTitleSize;
}
interface IonToastAttributes {
"overlayIndex": number;
Expand Down
13 changes: 8 additions & 5 deletions core/src/components/header/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
handleContentScroll,
handleHeaderFade,
handleToolbarIntersection,
isLargeTitle,
setHeaderActive,
setToolbarBackgroundOpacity,
getRoleType,
Expand Down Expand Up @@ -234,6 +235,10 @@ export class Header implements ComponentInterface {
}
}

private hasLargeTitle() {
return Array.from(this.el.querySelectorAll('ion-title')).some(isLargeTitle);
}

private async setupCondenseHeader(contentEl: HTMLElement | null, pageEl: Element | null): Promise<boolean> {
if (!contentEl || !pageEl) {
printIonContentErrorMsg(this.el);
Expand All @@ -258,8 +263,7 @@ export class Header implements ComponentInterface {
* in the condense header. Without it, there is nothing to
* collapse into the main header toolbar.
*/
const hasLargeTitle = this.el.querySelector('ion-title[size="large"]') !== null;
if (!hasLargeTitle) {
if (!this.hasLargeTitle()) {
printIonWarning(
'[ion-header] - The condense scroll effect requires an <ion-title size="large"> in the condense header.',
this.el
Expand Down Expand Up @@ -329,12 +333,11 @@ export class Header implements ComponentInterface {
// condense/fade via the deprecated `collapse` prop are iOS-only.
const isModeRestricted = scrollEffect === undefined && theme !== 'ios';
const hasHide = effect === 'hide';
const hasCondense =
effect === 'condense' && !isModeRestricted && this.el.querySelector('ion-title[size="large"]') !== null;
const hasCondense = effect === 'condense' && !isModeRestricted && this.hasLargeTitle();
const hasFade = effect === 'fade' && !isModeRestricted;
// The condense header should be hidden when
// - deprecated collapse prop is used on non-iOS (mode restricted), or
// - scrollEffect="condense" is set but no ion-title[size="large"] is present
// - scrollEffect="condense" is set but no large ion-title is present
const isHiddenCondense = effect === 'condense' && !hasCondense;
// banner role must be at top level, so remove role if inside a menu
const roleType = isHiddenCondense ? 'none' : getRoleType(hostContext('ion-menu', this.el));
Expand Down
21 changes: 20 additions & 1 deletion core/src/components/header/header.utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { readTask, writeTask } from '@stencil/core';
import { clamp } from '@utils/helpers';

import { config } from '../../global/config';
import type { IonTitleSize } from '../title/title.interface';

const TRANSITION = 'all 0.2s ease-in-out';
const ROLE_NONE = 'none';
const ROLE_BANNER = 'banner';
Expand Down Expand Up @@ -211,11 +214,27 @@ export const setHeaderActive = (headerIndex: HeaderIndex, active = true) => {
}
};

/**
* Checks the `size` prop, then the attribute (the header can render before
* the title is defined), then the `IonTitle.size` config.
*
* @internal
* @param ionTitleEl - The title to check.
* @returns Whether the title is large.
*/
export const isLargeTitle = (ionTitleEl: HTMLIonTitleElement) => {
const attributeSize = ionTitleEl.getAttribute('size') as IonTitleSize | null;
const configSize = config.getObjectValue('IonTitle.size') as IonTitleSize | undefined;
const size = ionTitleEl.size ?? attributeSize ?? configSize;

return size === 'large';
};

export const scaleLargeTitles = (toolbars: ToolbarIndex[] = [], scale = 1, transition = false) => {
toolbars.forEach((toolbar) => {
const ionTitle = toolbar.ionTitleEl;
const titleDiv = toolbar.innerTitleEl;
if (!ionTitle || ionTitle.size !== 'large') {
if (!ionTitle || !isLargeTitle(ionTitle)) {
return;
}

Expand Down

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The title in this snapshot now follows the page's md theme instead of the header's mode="ios", since migrated components take their look from the theme and mode only affects behavior; the header itself is still visible, which is what the test checks.

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.
43 changes: 43 additions & 0 deletions core/src/components/header/test/header.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { newSpecPage } from '@stencil/core/testing';

import { config } from '../../../global/config';
import { ToolbarTitle } from '../../title/title';
import { Header } from '../header';

describe('header: condense', () => {
afterEach(() => {
config.reset({});
});

it('should condense when the title size is large', async () => {
const page = await newSpecPage({
components: [Header, ToolbarTitle],
html: `<ion-header scroll-effect="condense"><ion-title size="large">Title</ion-title></ion-header>`,
});

expect(page.root!.classList.contains('header-collapse-condense')).toBe(true);
});

it('should condense when the config sets a large title size', async () => {
config.reset({
customTheme: { config: { components: { IonTitle: { size: 'large' } } } },
});

const page = await newSpecPage({
components: [Header, ToolbarTitle],
html: `<ion-header scroll-effect="condense"><ion-title>Title</ion-title></ion-header>`,
});

expect(page.root!.classList.contains('header-collapse-condense')).toBe(true);
});

it('should hide the header when the title is not large', async () => {
const page = await newSpecPage({
components: [Header, ToolbarTitle],
html: `<ion-header scroll-effect="condense"><ion-title>Title</ion-title></ion-header>`,
});

expect(page.root!.classList.contains('header-collapse-condense')).toBe(false);
expect(page.root!.classList.contains('header-collapse-condense-hidden')).toBe(true);
});
});
2 changes: 1 addition & 1 deletion core/src/components/title/test/basic/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
</ion-header>

<ion-header>
<ion-toolbar class="toolbar-title-default">
<ion-toolbar class="toolbar-title-size-medium">
<ion-buttons slot="start">
<ion-back-button default-href="#"></ion-back-button>
</ion-buttons>
Expand Down
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
Loading