diff --git a/BREAKING.md b/BREAKING.md
index a400b99d0b6..24827c58d8a 100644
--- a/BREAKING.md
+++ b/BREAKING.md
@@ -23,6 +23,7 @@ This is a comprehensive list of the breaking changes introduced in the major ver
- [Datetime](#version-10x-datetime)
- [Grid](#version-10x-grid)
- [Input Otp](#version-10x-input-otp)
+ - [Label](#version-10x-label)
- [Radio Group](#version-10x-radio-group)
- [Textarea](#version-10x-textarea)
- [Framework Specific](#version-10x-framework-specific)
@@ -223,6 +224,44 @@ Converted `ion-input-otp` to use [Shadow DOM](https://developer.mozilla.org/en-U
If you were targeting the internals of `ion-input-otp` in your CSS, you will need to target the `group`, `container`, `native`, `separator` or `description` [Shadow Parts](https://ionicframework.com/docs/theming/css-shadow-parts) instead, or use the provided CSS Variables.
+
Label
+
+Converted `ion-label` to use [Shadow DOM](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM).
+
+`ion-label` does not render any internal elements, so there are no Shadow Parts to target. However, the following changes may affect existing styles:
+
+- The default heading (`h1` - `h6`) and paragraph (`p`) styles now only apply to elements that are direct children of `ion-label`. Headings and paragraphs nested inside another element, such as `ion-text`, no longer receive the label styles, and nested headings receive the global typography styles instead. To color the label text and headings, set the `color` property on `ion-label`. To color a paragraph or only part of the label, move `ion-text` inside the heading or paragraph:
+
+ ```diff
+
+ -
+ - Title
+ -
+ + Title
+ Description
+
+ ```
+- Global element styles for headings and paragraphs, such as `h2 { font-size: 20px; }`, now take precedence over the default label styles for those elements. Previously, the label styles took precedence. To keep the label styles, scope the global styles so they do not match elements inside `ion-label`.
+
+ ```diff
+ - h2 {
+ + h2:not(ion-label > h2) {
+ font-size: 20px;
+ }
+ ```
+
+ The `:not()` selector increases the specificity of the global style. To keep the original specificity, wrap the exclusion in `:where()`:
+
+ ```css
+ h2:where(:not(ion-label > h2)) {
+ font-size: 20px;
+ }
+ ```
+
+ Complex selectors inside `:not()` require Chrome 88+, Firefox 84+ and Safari 9+, and `:where()` requires Chrome 88+, Firefox 78+ and Safari 14+.
+- The global typography styles in `typography.css` no longer apply to headings that are direct children of `ion-label`. These headings are styled by `ion-label` instead.
+- App styles that target `ion-label` directly now take precedence over the default `ion-label` styles. For example, a class that sets `color` on an `ion-label` inside of an `ion-item` previously had no effect because the label applied `var(--color)`, and it now changes the label color. Remove these styles if they are no longer wanted, or use the `--color` CSS variable to set the color of a label inside of an item.
+
Radio Group
Converted `ion-radio-group` to use [Shadow DOM](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM).
diff --git a/core/api.txt b/core/api.txt
index 5d12fba32c3..a4c9d281804 100644
--- a/core/api.txt
+++ b/core/api.txt
@@ -1369,7 +1369,7 @@ ion-item-sliding,method,getSlidingRatio,getSlidingRatio() => Promise
ion-item-sliding,method,open,open(side: Side | undefined) => Promise
ion-item-sliding,event,ionDrag,any,true
-ion-label,scoped
+ion-label,shadow
ion-label,prop,color,"danger" | "dark" | "light" | "medium" | "primary" | "secondary" | "success" | "tertiary" | "warning" | string & Record | undefined,undefined,false,true
ion-label,prop,mode,"ios" | "md",undefined,false,false
ion-label,prop,position,"fixed" | "floating" | "stacked" | undefined,undefined,false,false
diff --git a/core/src/components/item/item.common.scss b/core/src/components/item/item.common.scss
index 044bf396531..258d47f165d 100644
--- a/core/src/components/item/item.common.scss
+++ b/core/src/components/item/item.common.scss
@@ -144,6 +144,10 @@
pointer-events: none;
}
+:host(.item-interactive-disabled:not(.item-multiple-inputs)) ::slotted(ion-label) {
+ opacity: 0.3;
+}
+
:host(.item-disabled) {
cursor: default;
opacity: 0.3;
@@ -314,6 +318,29 @@ a {
max-width: 100%;
}
+/**
+ * Item styles win over the label's own styles when both target the
+ * same property, so positioned labels need their sizing set here
+ * to override the slotted label sizing above.
+ */
+::slotted(ion-label.label-fixed) {
+ flex: 0 0 100px;
+
+ width: 100px;
+ max-width: 200px;
+}
+
+::slotted(ion-label.label-stacked),
+::slotted(ion-label.label-floating) {
+ width: auto;
+}
+
+// Overflow hidden is required for the proper
+// margins between skeleton text elements
+:host(.item-skeleton-text) ::slotted(ion-label) {
+ overflow: hidden;
+}
+
// Item Input
// --------------------------------------------------
diff --git a/core/src/components/item/item.ios.scss b/core/src/components/item/item.ios.scss
index b6d2b0447d0..d1ae0c9f8ce 100644
--- a/core/src/components/item/item.ios.scss
+++ b/core/src/components/item/item.ios.scss
@@ -187,6 +187,14 @@
@include margin(10px, 8px, 10px, 0);
}
+::slotted(ion-label.label-stacked) {
+ @include margin(null, null, 4px, null);
+}
+
+::slotted(ion-label.label-floating) {
+ @include margin(null, null, 0, null);
+}
+
// iOS Stacked & Floating Inputs
// --------------------------------------------------
diff --git a/core/src/components/item/item.md.scss b/core/src/components/item/item.md.scss
index 7ecb4dd009e..7074863d759 100644
--- a/core/src/components/item/item.md.scss
+++ b/core/src/components/item/item.md.scss
@@ -213,6 +213,11 @@
// Material Design Floating/Stacked Label
// --------------------------------------------------
+::slotted(ion-label.label-stacked),
+::slotted(ion-label.label-floating) {
+ @include margin(0, 0, 0, 0);
+}
+
:host(.item-label-stacked) ::slotted([slot="end"]),
:host(.item-label-floating) ::slotted([slot="end"]) {
@include margin(
@@ -254,8 +259,6 @@
}
:host(.ion-focused:not(.ion-color)) ::slotted(.label-stacked),
-:host(.ion-focused:not(.ion-color)) ::slotted(.label-floating),
-:host(.item-has-focus:not(.ion-color)) ::slotted(.label-stacked),
-:host(.item-has-focus:not(.ion-color)) ::slotted(.label-floating) {
+:host(.ion-focused:not(.ion-color)) ::slotted(.label-floating) {
color: $label-md-text-color-focused;
}
diff --git a/core/src/components/item/test/basic/index.html b/core/src/components/item/test/basic/index.html
index 49c5efb5f84..d3ec0ca83f5 100644
--- a/core/src/components/item/test/basic/index.html
+++ b/core/src/components/item/test/basic/index.html
@@ -74,13 +74,9 @@ H2 Title Text
-
- H3 Title Text
-
+ H3 Title Text
Paragraph line 1
-
- Paragraph line 2 secondary
-
+ Paragraph line 2 secondary
diff --git a/core/src/components/item/test/basic/item.e2e.ts-snapshots/item-diff-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/item/test/basic/item.e2e.ts-snapshots/item-diff-ios-ltr-Mobile-Chrome-linux.png
index e077b477e74..544e1123223 100644
Binary files a/core/src/components/item/test/basic/item.e2e.ts-snapshots/item-diff-ios-ltr-Mobile-Chrome-linux.png and b/core/src/components/item/test/basic/item.e2e.ts-snapshots/item-diff-ios-ltr-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/item/test/basic/item.e2e.ts-snapshots/item-diff-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/item/test/basic/item.e2e.ts-snapshots/item-diff-ios-ltr-Mobile-Firefox-linux.png
index ce938efdd40..822729b2662 100644
Binary files a/core/src/components/item/test/basic/item.e2e.ts-snapshots/item-diff-ios-ltr-Mobile-Firefox-linux.png and b/core/src/components/item/test/basic/item.e2e.ts-snapshots/item-diff-ios-ltr-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/item/test/basic/item.e2e.ts-snapshots/item-diff-ios-ltr-Mobile-Safari-linux.png b/core/src/components/item/test/basic/item.e2e.ts-snapshots/item-diff-ios-ltr-Mobile-Safari-linux.png
index ca6392fb088..b3475b04f51 100644
Binary files a/core/src/components/item/test/basic/item.e2e.ts-snapshots/item-diff-ios-ltr-Mobile-Safari-linux.png and b/core/src/components/item/test/basic/item.e2e.ts-snapshots/item-diff-ios-ltr-Mobile-Safari-linux.png differ
diff --git a/core/src/components/item/test/basic/item.e2e.ts-snapshots/item-diff-ios-rtl-Mobile-Chrome-linux.png b/core/src/components/item/test/basic/item.e2e.ts-snapshots/item-diff-ios-rtl-Mobile-Chrome-linux.png
index ad5ca9457d2..2c2202f5d14 100644
Binary files a/core/src/components/item/test/basic/item.e2e.ts-snapshots/item-diff-ios-rtl-Mobile-Chrome-linux.png and b/core/src/components/item/test/basic/item.e2e.ts-snapshots/item-diff-ios-rtl-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/item/test/basic/item.e2e.ts-snapshots/item-diff-ios-rtl-Mobile-Firefox-linux.png b/core/src/components/item/test/basic/item.e2e.ts-snapshots/item-diff-ios-rtl-Mobile-Firefox-linux.png
index a19493b5bb6..031d251e7dd 100644
Binary files a/core/src/components/item/test/basic/item.e2e.ts-snapshots/item-diff-ios-rtl-Mobile-Firefox-linux.png and b/core/src/components/item/test/basic/item.e2e.ts-snapshots/item-diff-ios-rtl-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/item/test/basic/item.e2e.ts-snapshots/item-diff-ios-rtl-Mobile-Safari-linux.png b/core/src/components/item/test/basic/item.e2e.ts-snapshots/item-diff-ios-rtl-Mobile-Safari-linux.png
index a18e4fbb059..d51c5c97a6a 100644
Binary files a/core/src/components/item/test/basic/item.e2e.ts-snapshots/item-diff-ios-rtl-Mobile-Safari-linux.png and b/core/src/components/item/test/basic/item.e2e.ts-snapshots/item-diff-ios-rtl-Mobile-Safari-linux.png differ
diff --git a/core/src/components/item/test/colors/index.html b/core/src/components/item/test/colors/index.html
index 8ab8646051d..202614097a2 100644
--- a/core/src/components/item/test/colors/index.html
+++ b/core/src/components/item/test/colors/index.html
@@ -28,9 +28,7 @@
Heading
Paragraph
-
- Secondary paragraph
-
+ Secondary paragraph
@@ -73,9 +71,7 @@ Heading
left icon buttons
-
- Primary paragraph
-
+ Primary paragraph
@@ -86,9 +82,7 @@ Heading
disabled left icon buttons
-
- Primary paragraph
-
+ Primary paragraph
diff --git a/core/src/components/item/test/dividers/index.html b/core/src/components/item/test/dividers/index.html
index 9ae573d551c..8ad7580ab84 100644
--- a/core/src/components/item/test/dividers/index.html
+++ b/core/src/components/item/test/dividers/index.html
@@ -49,9 +49,7 @@
-
- Secondary header
-
+ Secondary header
Plain Ol' div with some text
@@ -81,9 +79,7 @@ Secondary header
Dark
-
- Primary h5
-
+ Primary h5
diff --git a/core/src/components/item/test/theme-ionic/index.html b/core/src/components/item/test/theme-ionic/index.html
index 0e443dbf883..17e2ebda70d 100644
--- a/core/src/components/item/test/theme-ionic/index.html
+++ b/core/src/components/item/test/theme-ionic/index.html
@@ -74,13 +74,9 @@ H2 Title Text
-
- H3 Title Text
-
+ H3 Title Text
Paragraph line 1
-
- Paragraph line 2 secondary
-
+ Paragraph line 2 secondary
diff --git a/core/src/components/label/label.ios.scss b/core/src/components/label/label.ios.scss
index ca2608e1354..910fae293e5 100644
--- a/core/src/components/label/label.ios.scss
+++ b/core/src/components/label/label.ios.scss
@@ -22,47 +22,35 @@
:host(.label-floating) {
@include margin(null, null, 0, null);
@include transform(translate(0, 29px));
- @include transform-origin(start, top);
transition: transform 150ms ease-in-out;
}
-:host-context(.item-textarea).label-floating {
- @include transform(translate(0, 28px));
-}
-
-:host-context(.item-has-focus).label-stacked,
-:host-context(.item-has-focus).label-floating {
- color: $label-ios-text-color-focused;
-}
-
-:host-context(.item-has-focus).label-floating,
-:host-context(.item-has-placeholder:not(.item-input)).label-floating,
-:host-context(.item-has-value).label-floating {
- @include transform(scale(0.82));
-}
-
// iOS Typography
// --------------------------------------------------
-::slotted(*) h1 {
+::slotted(h1) {
@include margin(3px, 0, 2px);
font-size: dynamic-font(22px);
font-weight: normal;
+
+ line-height: 1.2;
}
-::slotted(*) h2 {
+::slotted(h2) {
@include margin(0, 0, 2px);
font-size: dynamic-font(17px);
font-weight: normal;
+
+ line-height: 1.2;
}
-::slotted(*) h3,
-::slotted(*) h4,
-::slotted(*) h5,
-::slotted(*) h6 {
+::slotted(h3),
+::slotted(h4),
+::slotted(h5),
+::slotted(h6) {
@include margin(0, 0, 3px);
font-size: dynamic-font(14px);
@@ -71,7 +59,7 @@
line-height: normal;
}
-::slotted(*) p {
+::slotted(p) {
@include margin(
$item-ios-paragraph-margin-top,
$item-ios-paragraph-margin-end,
@@ -79,6 +67,8 @@
$item-ios-paragraph-margin-start
);
+ color: #{$item-ios-paragraph-text-color};
+
font-size: $item-ios-paragraph-font-size;
line-height: normal;
@@ -88,19 +78,15 @@
overflow: inherit;
}
-::slotted(p) {
- color: #{$item-ios-paragraph-text-color};
-}
-
-:host(.in-item-color)::slotted(p) {
+:host(.in-item-color) ::slotted(p) {
color: inherit;
}
-::slotted(*) h2:last-child,
-::slotted(*) h3:last-child,
-::slotted(*) h4:last-child,
-::slotted(*) h5:last-child,
-::slotted(*) h6:last-child,
-::slotted(*) p:last-child {
+::slotted(h2:last-child),
+::slotted(h3:last-child),
+::slotted(h4:last-child),
+::slotted(h5:last-child),
+::slotted(h6:last-child),
+::slotted(p:last-child) {
@include margin(null, null, 0, null);
}
diff --git a/core/src/components/label/label.ios.vars.scss b/core/src/components/label/label.ios.vars.scss
index 48cadf30ce9..50f7d1996db 100644
--- a/core/src/components/label/label.ios.vars.scss
+++ b/core/src/components/label/label.ios.vars.scss
@@ -4,9 +4,6 @@
// iOS Label
// --------------------------------------------------
-/// @prop - Text color of the stacked/floating label when it is focused
-$label-ios-text-color-focused: null;
-
/// @prop - Font size of the label when the text wraps
$label-ios-text-wrap-font-size: dynamic-font(14px);
diff --git a/core/src/components/label/label.md.scss b/core/src/components/label/label.md.scss
index d4f8dd26bcc..32fd21ee828 100644
--- a/core/src/components/label/label.md.scss
+++ b/core/src/components/label/label.md.scss
@@ -1,3 +1,5 @@
+// TODO(FW-6850): Remove when label is migrated to Modular Ionic.
+@use "../../foundations/ionic.vars" as ionic;
@import "./label";
@import "./label.md.vars";
@@ -18,16 +20,6 @@
:host(.label-stacked),
:host(.label-floating) {
@include margin(0, 0, 0, 0);
- /* stylelint-disable property-disallowed-list */
- transform-origin: top left;
- /* stylelint-enable property-disallowed-list */
-}
-
-:host(.label-stacked.label-rtl),
-:host(.label-floating.label-rtl) {
- /* stylelint-disable property-disallowed-list */
- transform-origin: top right;
- /* stylelint-enable property-disallowed-list */
}
:host(.label-stacked) {
@@ -42,53 +34,46 @@
transition: color 150ms $label-md-transition-timing-function, transform 150ms $label-md-transition-timing-function;
}
-:host-context(.ion-focused).label-floating,
-:host-context(.item-has-focus).label-floating,
-:host-context(.item-has-placeholder:not(.item-input)).label-floating,
-:host-context(.item-has-value).label-floating {
- @include transform(translateY(50%), scale(0.75));
-}
-
-:host-context(.ion-focused).label-stacked:not(.ion-color),
-:host-context(.ion-focused).label-floating:not(.ion-color),
-:host-context(.item-has-focus).label-stacked:not(.ion-color),
-:host-context(.item-has-focus).label-floating:not(.ion-color) {
- color: $label-md-text-color-focused;
-}
-
-:host-context(.ion-focused.ion-color).label-stacked:not(.ion-color),
-:host-context(.ion-focused.ion-color).label-floating:not(.ion-color),
-:host-context(.item-has-focus.ion-color).label-stacked:not(.ion-color),
-:host-context(.item-has-focus.ion-color).label-floating:not(.ion-color) {
- color: #{current-color(contrast)};
-}
-
-:host-context(.ion-invalid.ion-touched).label-stacked:not(.ion-color),
-:host-context(.ion-invalid.ion-touched).label-floating:not(.ion-color) {
- color: var(--highlight-color-invalid);
-}
-
// MD Typography
// --------------------------------------------------
-::slotted(*) h1 {
+::slotted(h1) {
@include margin(0, 0, 2px);
font-size: dynamic-font(24px);
font-weight: normal;
+
+ line-height: 1.2;
}
-::slotted(*) h2 {
+::slotted(h2) {
@include margin(2px, 0);
font-size: dynamic-font(16px);
font-weight: normal;
+
+ line-height: 1.2;
}
-::slotted(*) h3,
-::slotted(*) h4,
-::slotted(*) h5,
-::slotted(*) h6 {
+/**
+ * TODO(FW-6850): Remove when label is migrated to Modular Ionic.
+ *
+ * The ionic theme shares this stylesheet but uses the line heights
+ * from the `$ion-heading-h1-regular` and `$ion-heading-h2-regular`
+ * tokens for its headings.
+ */
+:host(.ionic) ::slotted(h1) {
+ line-height: map-get(ionic.$ion-heading-h1-regular, line-height);
+}
+
+:host(.ionic) ::slotted(h2) {
+ line-height: map-get(ionic.$ion-heading-h2-regular, line-height);
+}
+
+::slotted(h3),
+::slotted(h4),
+::slotted(h5),
+::slotted(h6) {
@include margin(2px, 0);
font-size: dynamic-font(14px);
@@ -97,9 +82,11 @@
line-height: normal;
}
-::slotted(*) p {
+::slotted(p) {
@include margin(0, 0, 2px);
+ color: $item-md-paragraph-text-color;
+
font-size: dynamic-font(14px);
line-height: dynamic-font(20px);
@@ -109,10 +96,6 @@
overflow: inherit;
}
-::slotted(p) {
- color: $item-md-paragraph-text-color;
-}
-
-:host(.in-item-color)::slotted(p) {
+:host(.in-item-color) ::slotted(p) {
color: inherit;
}
diff --git a/core/src/components/label/label.scss b/core/src/components/label/label.scss
index 1fb5ba654e9..44fcb30f01d 100644
--- a/core/src/components/label/label.scss
+++ b/core/src/components/label/label.scss
@@ -3,7 +3,7 @@
// Label
// --------------------------------------------------
-:host-context(.item) {
+:host(.in-item) {
/**
* @prop --color: Color of the label. This property is only available when using `ion-label` inside of an `ion-item`.
*/
@@ -29,29 +29,6 @@
overflow: hidden;
}
-:host-context(.item-interactive-disabled:not(.item-multiple-inputs)) {
- cursor: default;
- opacity: 0.3;
- pointer-events: none;
-}
-
-:host-context(.item-input) {
- flex: initial;
-
- max-width: 200px;
- pointer-events: none;
-}
-
-:host-context(.item-textarea) {
- align-self: baseline;
-}
-
-// Overflow hidden is required for the proper
-// margins between skeleton text elements
-:host-context(.item-skeleton-text) {
- overflow: hidden;
-}
-
// Fixed Inputs
// --------------------------------------------------
@@ -69,6 +46,7 @@
:host(.label-stacked),
:host(.label-floating) {
@include margin(null, null, 0, null);
+ @include transform-origin(start, top);
align-self: stretch;
@@ -83,12 +61,24 @@
// Headings
// --------------------------------------------------
-::slotted(*) h1,
-::slotted(*) h2,
-::slotted(*) h3,
-::slotted(*) h4,
-::slotted(*) h5,
-::slotted(*) h6 {
+/**
+ * Slotted elements inherit from the slot rather than the host.
+ * `overflow` and `text-overflow` are not inherited by default, so the
+ * slot passes the host values through. This lets the slotted headings
+ * and paragraphs below inherit them to truncate with an ellipsis.
+ */
+slot {
+ text-overflow: inherit;
+
+ overflow: inherit;
+}
+
+::slotted(h1),
+::slotted(h2),
+::slotted(h3),
+::slotted(h4),
+::slotted(h5),
+::slotted(h6) {
text-overflow: inherit;
overflow: inherit;
diff --git a/core/src/components/label/label.tsx b/core/src/components/label/label.tsx
index 000f3c0a797..990fb5db4c9 100644
--- a/core/src/components/label/label.tsx
+++ b/core/src/components/label/label.tsx
@@ -16,7 +16,7 @@ import type { Color, StyleEventDetail } from '../../interface';
md: 'label.md.scss',
ionic: 'label.md.scss',
},
- scoped: true,
+ shadow: true,
})
export class Label implements ComponentInterface {
private inRange = false;
@@ -111,10 +111,10 @@ export class Label implements ComponentInterface {
diff --git a/core/src/components/label/test/item/item.e2e.ts b/core/src/components/label/test/item/item.e2e.ts
index aa14200707a..b3b2d8a47be 100644
--- a/core/src/components/label/test/item/item.e2e.ts
+++ b/core/src/components/label/test/item/item.e2e.ts
@@ -23,5 +23,24 @@ configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
await expect(item).toHaveScreenshot(screenshot(`label-item`));
});
+
+ test('should render a dimmed label in an item with a disabled input', async ({ page }) => {
+ await page.setContent(
+ `
+
+ Label
+
+ Apple
+
+
+ `,
+ config
+ );
+
+ const item = page.locator('ion-item');
+
+ await expect(item).toHaveClass(/item-interactive-disabled/);
+ await expect(item).toHaveScreenshot(screenshot(`label-item-disabled`));
+ });
});
});
diff --git a/core/src/components/label/test/item/item.e2e.ts-snapshots/label-item-disabled-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/label/test/item/item.e2e.ts-snapshots/label-item-disabled-ios-ltr-Mobile-Chrome-linux.png
new file mode 100644
index 00000000000..820b3d9e0d2
Binary files /dev/null and b/core/src/components/label/test/item/item.e2e.ts-snapshots/label-item-disabled-ios-ltr-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/label/test/item/item.e2e.ts-snapshots/label-item-disabled-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/label/test/item/item.e2e.ts-snapshots/label-item-disabled-ios-ltr-Mobile-Firefox-linux.png
new file mode 100644
index 00000000000..443108e8e5d
Binary files /dev/null and b/core/src/components/label/test/item/item.e2e.ts-snapshots/label-item-disabled-ios-ltr-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/label/test/item/item.e2e.ts-snapshots/label-item-disabled-ios-ltr-Mobile-Safari-linux.png b/core/src/components/label/test/item/item.e2e.ts-snapshots/label-item-disabled-ios-ltr-Mobile-Safari-linux.png
new file mode 100644
index 00000000000..3a912b3ac21
Binary files /dev/null and b/core/src/components/label/test/item/item.e2e.ts-snapshots/label-item-disabled-ios-ltr-Mobile-Safari-linux.png differ
diff --git a/core/src/components/label/test/item/item.e2e.ts-snapshots/label-item-disabled-md-ltr-Mobile-Chrome-linux.png b/core/src/components/label/test/item/item.e2e.ts-snapshots/label-item-disabled-md-ltr-Mobile-Chrome-linux.png
new file mode 100644
index 00000000000..205d9224ebf
Binary files /dev/null and b/core/src/components/label/test/item/item.e2e.ts-snapshots/label-item-disabled-md-ltr-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/label/test/item/item.e2e.ts-snapshots/label-item-disabled-md-ltr-Mobile-Firefox-linux.png b/core/src/components/label/test/item/item.e2e.ts-snapshots/label-item-disabled-md-ltr-Mobile-Firefox-linux.png
new file mode 100644
index 00000000000..2bdd5709a88
Binary files /dev/null and b/core/src/components/label/test/item/item.e2e.ts-snapshots/label-item-disabled-md-ltr-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/label/test/item/item.e2e.ts-snapshots/label-item-disabled-md-ltr-Mobile-Safari-linux.png b/core/src/components/label/test/item/item.e2e.ts-snapshots/label-item-disabled-md-ltr-Mobile-Safari-linux.png
new file mode 100644
index 00000000000..8f69772aaa1
Binary files /dev/null and b/core/src/components/label/test/item/item.e2e.ts-snapshots/label-item-disabled-md-ltr-Mobile-Safari-linux.png differ
diff --git a/core/src/css/ionic/typography.ionic.scss b/core/src/css/ionic/typography.ionic.scss
index 703579f366c..a6ca234b8af 100644
--- a/core/src/css/ionic/typography.ionic.scss
+++ b/core/src/css/ionic/typography.ionic.scss
@@ -17,27 +17,38 @@ body {
@include globals.typography(globals.$ion-body-md-regular);
}
-/* Override html headings based on heading tokens */
-h1 {
+/**
+ * TODO(FW-7085): Move these heading rules into a stylesheet shared by all
+ * themes so the `ion-label` exclusion is defined once.
+ *
+ * Override html headings based on heading tokens.
+ *
+ * Headings slotted directly into `ion-label` are styled by the label's
+ * shadow stylesheet. Document styles always win over `::slotted()` styles,
+ * so these rules skip those headings to let the label styles apply.
+ * The exclusion is wrapped in `:where()` to keep the specificity of a
+ * plain element selector so app styles can still override these rules.
+ */
+h1:where(:not(ion-label > h1)) {
@include globals.typography(globals.$ion-heading-h1-regular);
}
-h2 {
+h2:where(:not(ion-label > h2)) {
@include globals.typography(globals.$ion-heading-h2-regular);
}
-h3 {
+h3:where(:not(ion-label > h3)) {
@include globals.typography(globals.$ion-heading-h3-regular);
}
-h4 {
+h4:where(:not(ion-label > h4)) {
@include globals.typography(globals.$ion-heading-h4-regular);
}
-h5 {
+h5:where(:not(ion-label > h5)) {
@include globals.typography(globals.$ion-heading-h5-regular);
}
-h6 {
+h6:where(:not(ion-label > h6)) {
@include globals.typography(globals.$ion-heading-h6-regular);
}
diff --git a/core/src/css/typography.scss b/core/src/css/typography.scss
index 7ac2d54f99b..ea855897ebf 100644
--- a/core/src/css/typography.scss
+++ b/core/src/css/typography.scss
@@ -50,12 +50,22 @@ a {
color: ion-color(primary, base);
}
-h1,
-h2,
-h3,
-h4,
-h5,
-h6 {
+/**
+ * TODO(FW-7085): Move these heading rules into a stylesheet shared by all
+ * themes so the `ion-label` exclusion is defined once.
+ *
+ * Headings slotted directly into `ion-label` are styled by the label's
+ * shadow stylesheet. Document styles always win over `::slotted()` styles,
+ * so these rules skip those headings to let the label styles apply.
+ * The exclusion is wrapped in `:where()` to keep the specificity of a
+ * plain element selector so app styles can still override these rules.
+ */
+h1:where(:not(ion-label > h1)),
+h2:where(:not(ion-label > h2)),
+h3:where(:not(ion-label > h3)),
+h4:where(:not(ion-label > h4)),
+h5:where(:not(ion-label > h5)),
+h6:where(:not(ion-label > h6)) {
@include margin(16px, null, 10px, null);
font-weight: $headings-font-weight;
@@ -63,31 +73,31 @@ h6 {
line-height: $headings-line-height;
}
-h1 {
+h1:where(:not(ion-label > h1)) {
@include margin(20px, null, null, null);
font-size: $h1-font-size;
}
-h2 {
+h2:where(:not(ion-label > h2)) {
@include margin(18px, null, null, null);
font-size: $h2-font-size;
}
-h3 {
+h3:where(:not(ion-label > h3)) {
font-size: $h3-font-size;
}
-h4 {
+h4:where(:not(ion-label > h4)) {
font-size: $h4-font-size;
}
-h5 {
+h5:where(:not(ion-label > h5)) {
font-size: $h5-font-size;
}
-h6 {
+h6:where(:not(ion-label > h6)) {
font-size: $h6-font-size;
}
diff --git a/docs/component-guide.md b/docs/component-guide.md
index 85b9ab8a09e..9950f5e3e6b 100644
--- a/docs/component-guide.md
+++ b/docs/component-guide.md
@@ -790,6 +790,44 @@ There will be some CSS issues when converting to shadow. Below are some of the d
:host-context(ion-toolbar:not(.ion-color)):host(:not(.ion-color)) ::slotted(ion-segment-button) {
```
+### Parent Context
+
+In scoped components, Stencil rewrites `:host-context()` into descendant selectors that work in every browser. In shadow components, the browser handles `:host-context()` itself, and only Chromium supports it. Styles that depend on an ancestor need a different approach after converting.
+
+When the ancestor is known at render time, set a class on the host with the `hostContext` utility and target that class with `:host()`. Pass tag names to `hostContext` rather than classes that the ancestor adds when it renders. Tag names state exactly which components the styles apply to, and they do not depend on another component's internal classes:
+
+```tsx
+// ✅ Correct
+'in-item': hostContext('ion-item', this.el),
+
+// ❌ Incorrect: `.item` is an internal class added by several components when they render
+'in-item': hostContext('.item', this.el),
+```
+
+```css
+/* IN SCOPED */
+:host-context(ion-item) {
+
+/* IN SHADOW */
+:host(.in-item) {
+```
+
+Combine a tag name with a class only when the class describes the ancestor's state, such as `hostContext('ion-item.ion-color', this.el)`.
+
+`hostContext` only runs when the component renders, so it does not update when the ancestor's classes change later. When the styles depend on ancestor state that changes after render, such as a disabled state reported by a child, move the styles to the ancestor and target the child with `::slotted()`:
+
+```css
+/* IN SCOPED (label.scss) */
+:host-context(.item-interactive-disabled:not(.item-multiple-inputs)) {
+ opacity: 0.3;
+}
+
+/* IN SHADOW (item.common.scss) */
+:host(.item-interactive-disabled:not(.item-multiple-inputs)) ::slotted(ion-label) {
+ opacity: 0.3;
+}
+```
+
## Sass Variables
For guidelines on when to use Sass Variables, see the [Sass Guidelines](./sass-guidelines.md).
@@ -857,39 +895,35 @@ For guidelines on exposing the icon for styling, see the [CSS Shadow Parts Guide
## RTL
-When you need to support both LTR and RTL modes, try to avoid using values such as `left` and `right`. For certain CSS properties, you can use the appropriate mixin to have this handled for you automatically.
+Components must follow the text direction of the nearest ancestor with a `dir` attribute, and must update when that direction changes. Handle direction in CSS or in JavaScript at the time it is needed. Do not add classes such as `my-cmp-rtl` to the host during render. A class set during render is not updated when `dir` changes on an ancestor, so the styles fall out of date.
-For example, if you wanted `transform-origin` to be RTL-aware, you would use the `transform-origin` mixin:
+Use the first approach that applies:
-```css
-@include transform-origin(start, center);
-```
+1. **Logical properties**: Avoid physical values such as `left` and `right`. When a logical property exists, use it or the mixin that outputs it. The `margin`, `padding` and `position` mixins output logical properties such as `margin-inline-start` and `inset-inline-start`:
-This would output `transform-origin: left center` in LTR mode and `transform-origin: right center` in RTL mode.
+ ```scss
+ @include margin(0, 8px, 0, 16px);
+ ```
-These mixins depend on the `:host-context` pseudo-class when used inside of shadow components, which is not supported in WebKit. As a result, these mixins will not work in Safari for macOS and iOS when applied to shadow components.
+2. **RTL mixins**: When there is no logical property, use the RTL-aware mixin. For example, to make `transform-origin` RTL-aware, use the `transform-origin` mixin:
-To work around this, you should set an RTL class on the host of your component and set your RTL styles by targeting that class:
+ ```scss
+ @include transform-origin(start, center);
+ ```
-```tsx
-
- ...
-
-```
+ This outputs `transform-origin: left center` in LTR and `transform-origin: right center` in RTL. For properties without a dedicated mixin, wrap the RTL styles in the `rtl()` mixin.
-```css
-:host {
- transform-origin: left center;
-}
+ Inside shadow components, these mixins output both a `:host-context([dir=rtl])` selector, which is supported by Chromium, and a `:dir(rtl)` selector, which is supported by Firefox and Safari 16.4+. In Safari 16.4 - 16.x, `:dir()` does not update when the direction changes after the page loads ([WebKit bug 257133](https://bugs.webkit.org/show_bug.cgi?id=257133)). Safari 17+ is not affected.
-:host(.my-cmp-rtl) {
- transform-origin: right center;
-}
-```
+3. **JavaScript**: When direction affects behavior that CSS cannot express, such as gesture math, animations or positioning, call `isRTL(this.el)` from `@utils/rtl` at the time the value is needed. Do not store the result during render.
+
+ ```tsx
+ private onMove(detail: GestureDetail) {
+ const rtl = isRTL(this.el);
+ const delta = rtl ? -detail.deltaX : detail.deltaX;
+ ...
+ }
+ ```
## Themes vs. Modes
diff --git a/docs/sass-guidelines.md b/docs/sass-guidelines.md
index 9aef52d4cd6..f4105210c90 100644
--- a/docs/sass-guidelines.md
+++ b/docs/sass-guidelines.md
@@ -439,7 +439,7 @@ $alert-button-font-size: dynamic-font(14px);
While we usually aim for consistency across different modes, this isn't always necessary when dealing with Sass variables. If certain styles are present in one mode but absent in another, there's no need to include a Sass variable for the mode lacking those styles.
-For example, the color of the label changes when focused in `md` mode. However, in `ios`, the label does not receive different styling when focused, therefore it does not require the same styles or a Sass variable defined:
+For example, the color of a stacked or floating label changes when its item is focused in `md` mode. However, in `ios`, the label does not receive different styling when focused, therefore it does not require the same styles or a Sass variable defined:
@@ -459,12 +459,10 @@ $label-md-text-color-focused: ion-color(primary, base);
```
```scss
-// label.md.scss
+// item.md.scss
-:host-context(.ion-focused).label-stacked:not(.ion-color),
-:host-context(.ion-focused).label-floating:not(.ion-color),
-:host-context(.item-has-focus).label-stacked:not(.ion-color),
-:host-context(.item-has-focus).label-floating:not(.ion-color) {
+:host(.ion-focused:not(.ion-color)) ::slotted(.label-stacked),
+:host(.ion-focused:not(.ion-color)) ::slotted(.label-floating) {
color: $label-md-text-color-focused;
}
```
@@ -488,12 +486,10 @@ $label-ios-text-color-focused: null;
```
```scss
-// label.ios.scss
+// item.ios.scss
-:host-context(.ion-focused).label-stacked:not(.ion-color),
-:host-context(.ion-focused).label-floating:not(.ion-color),
-:host-context(.item-has-focus).label-stacked:not(.ion-color),
-:host-context(.item-has-focus).label-floating:not(.ion-color) {
+:host(.ion-focused:not(.ion-color)) ::slotted(.label-stacked),
+:host(.ion-focused:not(.ion-color)) ::slotted(.label-floating) {
color: $label-ios-text-color-focused;
}
```