From 694df80fe99bb0940702a85b8fbea6c169e12825 Mon Sep 17 00:00:00 2001 From: Ahmed Date: Thu, 5 Nov 2026 22:25:45 +0000 Subject: [PATCH] fix(material/form-field): support a different direction than the page The form field styles used `[dir=rtl]` selectors, which match any ancestor with `dir="rtl"`. This meant that a form field with `dir="ltr"` inside an RTL page still got the RTL styles: the outline lost its leading border and the label, padding and native select arrow were placed for RTL. These changes switch the styles to logical properties, so they follow the direction of the form field itself. `transform-origin` and `clip-path` have no logical equivalents, so those two rules use `:dir(rtl)`, which also resolves against the closest element with a `dir` attribute. Fixes #26778. --- .../form-field/_form-field-native-select.scss | 12 +- .../_mdc-text-field-structure-overrides.scss | 27 +---- .../form-field/_mdc-text-field-structure.scss | 104 +++++------------- src/material/form-field/form-field.scss | 26 ++--- 4 files changed, 44 insertions(+), 125 deletions(-) diff --git a/src/material/form-field/_form-field-native-select.scss b/src/material/form-field/_form-field-native-select.scss index f41291505ee5..c089b9d32c26 100644 --- a/src/material/form-field/_form-field-native-select.scss +++ b/src/material/form-field/_form-field-native-select.scss @@ -55,7 +55,7 @@ $mat-form-field-select-horizontal-end-padding: $mat-form-field-select-arrow-widt border-right: math.div($mat-form-field-select-arrow-width, 2) solid transparent; border-top: $mat-form-field-select-arrow-height solid; position: absolute; - right: 0; + inset-inline-end: 0; top: 50%; margin-top: -#{math.div($mat-form-field-select-arrow-height, 2)}; @@ -64,10 +64,6 @@ $mat-form-field-select-horizontal-end-padding: $mat-form-field-select-arrow-widt color: token-utils.slot(form-field-enabled-select-arrow-color, $fallbacks); - [dir='rtl'] & { - right: auto; - left: 0; - } } &.mat-focused .mat-mdc-form-field-infix::after { @@ -81,11 +77,7 @@ $mat-form-field-select-horizontal-end-padding: $mat-form-field-select-arrow-widt // Add padding on the end of the native select so that the content does not // overlap with the Material Design arrow. .mat-mdc-form-field-input-control { - padding-right: $mat-form-field-select-horizontal-end-padding; - [dir='rtl'] & { - padding-right: 0; - padding-left: $mat-form-field-select-horizontal-end-padding; - } + padding-inline-end: $mat-form-field-select-horizontal-end-padding; } } } diff --git a/src/material/form-field/_mdc-text-field-structure-overrides.scss b/src/material/form-field/_mdc-text-field-structure-overrides.scss index 9bcc1572612f..0c5a2b7fb45f 100644 --- a/src/material/form-field/_mdc-text-field-structure-overrides.scss +++ b/src/material/form-field/_mdc-text-field-structure-overrides.scss @@ -91,27 +91,13 @@ $_is-external-build: true; // an icon prefix or suffix. We can't rely on MDC's styles for this because we use a different // structure for our form-field in order to support arbitrary height input elements. .mat-mdc-form-field-has-icon-prefix .mat-mdc-text-field-wrapper { - padding-left: 0; + padding-inline-start: 0; // Signal to the TypeScript label calculation code that the label should be offset 16px less // due to resetting the padding above. --mat-mdc-form-field-label-offset-x: -16px; } .mat-mdc-form-field-has-icon-suffix .mat-mdc-text-field-wrapper { - padding-right: 0; - } - [dir='rtl'] { - // Undo the above padding removals which only apply in LTR languages. - .mat-mdc-text-field-wrapper { - padding-left: 16px; - padding-right: 16px; - } - // ...and apply the correct padding resets for RTL languages. - .mat-mdc-form-field-has-icon-suffix .mat-mdc-text-field-wrapper { - padding-left: 0; - } - .mat-mdc-form-field-has-icon-prefix .mat-mdc-text-field-wrapper { - padding-right: 0; - } + padding-inline-end: 0; } // Before the switch to the tokens MDC was setting a specific placeholder color when the input @@ -172,14 +158,7 @@ $_is-external-build: true; // TODO(mmalerba): port this fix into MDC .mat-mdc-form-field.mat-mdc-form-field.mat-mdc-form-field.mat-mdc-form-field { &.mat-mdc-form-field.mat-mdc-form-field .mdc-notched-outline__notch { - border-left: 1px solid transparent; - } - } - - [dir='rtl'] .mat-mdc-form-field.mat-mdc-form-field.mat-mdc-form-field.mat-mdc-form-field { - &.mat-mdc-form-field.mat-mdc-form-field .mdc-notched-outline__notch { - border-left: none; - border-right: 1px solid transparent; + border-inline-start: 1px solid transparent; } } } diff --git a/src/material/form-field/_mdc-text-field-structure.scss b/src/material/form-field/_mdc-text-field-structure.scss index e84c27a5a683..9c50d5aab646 100644 --- a/src/material/form-field/_mdc-text-field-structure.scss +++ b/src/material/form-field/_mdc-text-field-structure.scss @@ -155,32 +155,26 @@ $fallbacks: m3-form-field.get-tokens(); height: 56px; overflow: visible; $shape-var: token-utils.slot(form-field-outlined-container-shape, $fallbacks); - padding-right: max(16px, #{$shape-var}); - padding-left: max(16px, calc(#{$shape-var} + 4px)); - - [dir='rtl'] & { - padding-right: max(16px, calc(#{$shape-var} + 4px)); - padding-left: max(16px, #{$shape-var}); - } + padding-inline-end: max(16px, #{$shape-var}); + padding-inline-start: max(16px, calc(#{$shape-var} + 4px)); } .mdc-floating-label { position: absolute; - left: 0; + inset-inline-start: 0; transform-origin: left top; line-height: 1.15rem; - text-align: left; + text-align: start; text-overflow: ellipsis; white-space: nowrap; cursor: text; overflow: hidden; will-change: transform; - [dir='rtl'] & { - right: 0; - left: auto; + // `transform-origin` has no logical equivalent. Use `:dir()` rather than `[dir='rtl']` so + // that the direction of the closest element with a `dir` attribute is used. + &:dir(rtl) { transform-origin: right top; - text-align: right; } .mdc-text-field & { @@ -196,23 +190,13 @@ $fallbacks: m3-form-field.get-tokens(); } .mdc-text-field--outlined & { - left: 4px; - right: auto; - } - - [dir='rtl'] .mdc-text-field--outlined & { - left: auto; - right: 4px; + inset-inline-start: 4px; + inset-inline-end: auto; } .mdc-text-field--filled & { - left: 16px; - right: auto; - } - - [dir='rtl'] .mdc-text-field--filled & { - left: auto; - right: 16px; + inset-inline-start: 16px; + inset-inline-end: auto; } .mdc-text-field--disabled & { @@ -332,14 +316,9 @@ $fallbacks: m3-form-field.get-tokens(); .mdc-floating-label--required { &:not(.mdc-floating-label--hide-required-marker)::after { - margin-left: 1px; - margin-right: 0; + margin-inline-start: 1px; + margin-inline-end: 0; content: '*'; - - [dir='rtl'] & { - margin-left: 0; - margin-right: 1px; - } } } @@ -353,13 +332,9 @@ $fallbacks: m3-form-field.get-tokens(); width: 100%; max-width: 100%; height: 100%; - text-align: left; + text-align: start; pointer-events: none; - [dir='rtl'] & { - text-align: right; - } - .mdc-text-field--outlined & { z-index: 1; } @@ -417,45 +392,27 @@ $fallbacks: m3-form-field.get-tokens(); } .mdc-notched-outline__leading { - border-left: 1px solid; - border-right: none; - border-top-right-radius: 0; - border-bottom-right-radius: 0; - border-top-left-radius: token-utils.slot(form-field-outlined-container-shape, $fallbacks); - border-bottom-left-radius: token-utils.slot(form-field-outlined-container-shape, $fallbacks); + border-inline-start: 1px solid; + border-inline-end: none; + border-start-end-radius: 0; + border-end-end-radius: 0; + border-start-start-radius: token-utils.slot(form-field-outlined-container-shape, $fallbacks); + border-end-start-radius: token-utils.slot(form-field-outlined-container-shape, $fallbacks); .mdc-text-field--outlined .mdc-notched-outline & { $shape-var: token-utils.slot(form-field-outlined-container-shape, $fallbacks); width: max(12px, #{$shape-var}); } - - [dir='rtl'] & { - border-left: none; - border-right: 1px solid; - border-bottom-left-radius: 0; - border-top-left-radius: 0; - border-top-right-radius: token-utils.slot(form-field-outlined-container-shape, $fallbacks); - border-bottom-right-radius: token-utils.slot(form-field-outlined-container-shape, $fallbacks); - } } .mdc-notched-outline__trailing { flex-grow: 1; - border-left: none; - border-right: 1px solid; - border-top-left-radius: 0; - border-bottom-left-radius: 0; - border-top-right-radius: token-utils.slot(form-field-outlined-container-shape, $fallbacks); - border-bottom-right-radius: token-utils.slot(form-field-outlined-container-shape, $fallbacks); - - [dir='rtl'] & { - border-left: 1px solid; - border-right: none; - border-top-right-radius: 0; - border-bottom-right-radius: 0; - border-top-left-radius: token-utils.slot(form-field-outlined-container-shape, $fallbacks); - border-bottom-left-radius: token-utils.slot(form-field-outlined-container-shape, $fallbacks); - } + border-inline-start: none; + border-inline-end: 1px solid; + border-start-start-radius: 0; + border-end-start-radius: 0; + border-start-end-radius: token-utils.slot(form-field-outlined-container-shape, $fallbacks); + border-end-end-radius: token-utils.slot(form-field-outlined-container-shape, $fallbacks); } .mdc-notched-outline__notch { @@ -480,16 +437,11 @@ $fallbacks: m3-form-field.get-tokens(); } .mdc-notched-outline--notched & { - padding-left: 0; - padding-right: 8px; + padding-inline-start: 0; + padding-inline-end: 8px; border-top: none; } - [dir='rtl'] .mdc-notched-outline--notched & { - padding-left: 8px; - padding-right: 0; - } - .mdc-notched-outline--no-label & { display: none; } diff --git a/src/material/form-field/form-field.scss b/src/material/form-field/form-field.scss index 00b2c60540e4..6dcf14da0f5e 100644 --- a/src/material/form-field/form-field.scss +++ b/src/material/form-field/form-field.scss @@ -45,7 +45,7 @@ $_icon-prefix-infix-padding: 4px; // This allows the form-field to shrink down when used inside flex or grid layouts. min-width: 0; // To avoid problems with text-align. - text-align: left; + text-align: start; @include vendor-prefixes.smooth-font(); font-family: token-utils.slot(form-field-container-text-font, $fallbacks); @@ -67,10 +67,6 @@ $_icon-prefix-infix-padding: 4px; font-size: $token-value; } } - - [dir='rtl'] & { - text-align: right; - } } // Container that contains the prefixes, infix and suffixes. These elements should @@ -145,13 +141,13 @@ $_icon-prefix-infix-padding: 4px; // The prefix/suffix needs a little extra padding between the icon and the infix. Because we need to // support arbitrary height input elements, we use a different DOM structure for prefix and suffix // icons, and therefore can't rely on MDC for these styles. -.mat-mdc-form-field-icon-prefix, -[dir='rtl'] .mat-mdc-form-field-icon-suffix { - padding: 0 $_icon-prefix-infix-padding 0 0; +.mat-mdc-form-field-icon-prefix { + padding: 0; + padding-inline-end: $_icon-prefix-infix-padding; } -.mat-mdc-form-field-icon-suffix, -[dir='rtl'] .mat-mdc-form-field-icon-prefix { - padding: 0 0 0 $_icon-prefix-infix-padding; +.mat-mdc-form-field-icon-suffix { + padding: 0; + padding-inline-start: $_icon-prefix-infix-padding; } // Scale down icons in the subscript and floating label to be the same size @@ -193,12 +189,12 @@ $_icon-prefix-infix-padding: 4px; // left border is completely hidden. (Though the border is transparent, it still creates a triangle // shaped artifact where it meets the top and bottom borders.) .mat-mdc-form-field .mdc-notched-outline__notch { - margin-left: -1px; + margin-inline-start: -1px; @include vendor-prefixes.clip-path(inset(-9em -999em -9em 1px)); - [dir='rtl'] & { - margin-left: 0; - margin-right: -1px; + // `clip-path` has no logical equivalent. Use `:dir()` rather than `[dir='rtl']` so that the + // direction of the closest element with a `dir` attribute is used. + &:dir(rtl) { @include vendor-prefixes.clip-path(inset(-9em 1px -9em -999em)); } }