From e3402bd61b4e2344c69f0fc1bb3f0cbe02be3c36 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Wed, 13 Dec 2023 09:38:15 +0100 Subject: [PATCH] refactor(material/form-field): tokenize density overrides **Note:** this is a resubmit of #28249. Switches the custom density implementation of the form field to be based on tokens. --- .../core/tokens/m2/mat/_form-field.scss | 51 +++++++- src/material/form-field/BUILD.bazel | 3 +- .../form-field/_form-field-density.scss | 118 ------------------ .../form-field/_form-field-sizing.scss | 40 ------ .../form-field/_form-field-subscript.scss | 3 +- .../form-field/_form-field-theme.scss | 6 +- .../_mdc-text-field-density-overrides.scss | 60 +++++++++ src/material/form-field/form-field.scss | 18 ++- src/material/paginator/BUILD.bazel | 1 - src/material/paginator/_paginator-theme.scss | 9 +- 10 files changed, 135 insertions(+), 174 deletions(-) delete mode 100644 src/material/form-field/_form-field-density.scss delete mode 100644 src/material/form-field/_form-field-sizing.scss create mode 100644 src/material/form-field/_mdc-text-field-density-overrides.scss diff --git a/src/material/core/tokens/m2/mat/_form-field.scss b/src/material/core/tokens/m2/mat/_form-field.scss index 592c828fee99..c49fbb9826ee 100644 --- a/src/material/core/tokens/m2/mat/_form-field.scss +++ b/src/material/core/tokens/m2/mat/_form-field.scss @@ -1,6 +1,10 @@ +@use 'sass:math'; @use 'sass:map'; +@use '@material/textfield' as mdc-textfield; +@use '@material/density' as mdc-density; @use '../../token-utils'; @use '../../../style/sass-utils'; +@use '../../../theming/theming'; @use '../../../theming/inspection'; @use '../../../theming/palette'; @@ -86,7 +90,52 @@ $prefix: (mat, form-field); // Tokens that can be configured through Angular Material's density theming API. @function get-density-tokens($theme) { - @return (); + $density-scale: theming.clamp-density(inspection.get-theme-density($theme), -4); + $height: mdc-density.prop-value( + $density-config: mdc-textfield.$density-config, + $density-scale: inspection.get-theme-density($theme), + $property-name: height, + ); + $hide-label: $height < mdc-textfield.$minimum-height-for-filled-label; + + // We computed the desired height of the form-field using the density configuration. The + // spec only describes vertical spacing/alignment in non-dense mode. This means that we + // cannot update the spacing to explicit numbers based on the density scale. Instead, we + // determine the height reduction and equally subtract it from the default `top` and `bottom` + // padding that is provided by the Material Design specification. + $vertical-deduction: math.div(mdc-textfield.$height - $height, 2); + + // Note: these calculations are trivial enough that we could do them at runtime with `calc` + // and the value of the `height` token. The problem is that because we need to hide the label + // if the container becomes too short, we have to change the padding calculation. This is + // complicated further by the fact that filled form fields without labels have the same + // vertical padding as outlined ones. Alternatives: + // 1. Using container queries to hide the label and change the padding - this doesn't work + // because size container queries require setting the `container-type` property which breaks + // the form field layout. We could use style queries, but they're only supported in Chrome. + // 2. Monitoring the size of the label - we already have a `ResizeObserver` on the label so we + // could reuse it to also check when it becomes `display: none`. This would allows us to remove + // the three padding tokens. We don't do it, because it would require us to always set up + // the resize observer, as opposed to currently where it's only set up for outlined form fields. + // This may lead to performance regressions. + // 3. Conditionally adding `::before` and `::after` to the infix with positive and negative + // margin respectively - this works, but is likely to break a lot of overrides that are targeting + // a specific padding. It also runs the risk of overflowing the container. + // TODO: switch the padding tokens to style-based container queries + // when they become available in all the browsers we support. + $filled-with-label-padding-top: 24px - $vertical-deduction; + $filled-with-label-padding-bottom: 8px - $vertical-deduction; + $vertical-padding: 16px - $vertical-deduction; + + @return ( + container-height: $height, + filled-label-display: if($hide-label, none, block), + container-vertical-padding: $vertical-padding, + filled-with-label-container-padding-top: + if($hide-label, $vertical-padding, $filled-with-label-padding-top), + filled-with-label-container-padding-bottom: + if($hide-label, $vertical-padding, $filled-with-label-padding-bottom), + ); } // Combines the tokens generated by the above functions into a single map with placeholder values. diff --git a/src/material/form-field/BUILD.bazel b/src/material/form-field/BUILD.bazel index 9208f937992e..2f5dd654738e 100644 --- a/src/material/form-field/BUILD.bazel +++ b/src/material/form-field/BUILD.bazel @@ -52,12 +52,11 @@ sass_binary( sass_library( name = "form_field_partials", srcs = [ - "_form-field-density.scss", "_form-field-focus-overlay.scss", "_form-field-high-contrast.scss", "_form-field-native-select.scss", - "_form-field-sizing.scss", "_form-field-subscript.scss", + "_mdc-text-field-density-overrides.scss", "_mdc-text-field-structure-overrides.scss", "_mdc-text-field-textarea-overrides.scss", "_user-agent-overrides.scss", diff --git a/src/material/form-field/_form-field-density.scss b/src/material/form-field/_form-field-density.scss deleted file mode 100644 index e679668c27f0..000000000000 --- a/src/material/form-field/_form-field-density.scss +++ /dev/null @@ -1,118 +0,0 @@ -@use 'sass:map'; -@use 'sass:math'; -@use '@material/density' as mdc-density; -@use '@material/textfield' as mdc-textfield; -@use '../core/theming/inspection'; - -@use './form-field-sizing'; - -// Mixin that sets the vertical spacing for the infix container of filled form fields. -// We need to apply spacing to the infix container because we removed the input padding -// provided by MDC in order to support arbitrary form-field controls. -@mixin _infix-vertical-spacing-filled($with-label-padding, $no-label-padding) { - .mat-mdc-text-field-wrapper:not(.mdc-text-field--outlined) .mat-mdc-form-field-infix { - padding-top: map.get($with-label-padding, top); - padding-bottom: map.get($with-label-padding, bottom); - } - - .mdc-text-field--no-label:not(.mdc-text-field--outlined):not(.mdc-text-field--textarea) - .mat-mdc-form-field-infix { - padding-top: map.get($no-label-padding, top); - padding-bottom: map.get($no-label-padding, bottom); - } -} - -// Mixin that sets the vertical spacing for the infix container of outlined form fields. -// We need to apply spacing to the infix container because we removed the input padding -// provided by MDC in order to support arbitrary form-field controls. -@mixin _infix-vertical-spacing-outlined($padding) { - .mat-mdc-text-field-wrapper.mdc-text-field--outlined .mat-mdc-form-field-infix { - padding-top: map.get($padding, top); - padding-bottom: map.get($padding, bottom); - } -} - -// Mixin that includes the density styles for form fields. MDC provides their own density -// styles for MDC text-field which we cannot use. MDC relies on input elements to stretch -// vertically when the height is reduced as per density scale. This doesn't work for our -// form field since we support custom form field controls without a fixed height. Instead, we -// provide spacing that makes arbitrary controls align as specified in the Material Design -// specification. In order to support density, we need to adjust the vertical spacing to be -// based on the density scale. -@mixin private-form-field-density($theme) { - // Height of the form field that is based on the current density scale. - $height: mdc-density.prop-value( - $density-config: mdc-textfield.$density-config, - $density-scale: inspection.get-theme-density($theme), - $property-name: height, - ); - - // Whether floating labels for filled form fields should be hidden. MDC hides the label in - // their density styles when the height decreases too much. We match their density styles. - $hide-filled-floating-label: $height < mdc-textfield.$minimum-height-for-filled-label; - // We computed the desired height of the form-field using the density configuration. The - // spec only describes vertical spacing/alignment in non-dense mode. This means that we - // cannot update the spacing to explicit numbers based on the density scale. Instead, we - // determine the height reduction and equally subtract it from the default `top` and `bottom` - // padding that is provided by the Material Design specification. - $vertical-deduction: math.div(mdc-textfield.$height - $height, 2); - // Map that describes the padding for form-fields with label. - $with-label-padding: ( - top: form-field-sizing.$mat-form-field-with-label-input-padding-top - $vertical-deduction, - bottom: form-field-sizing.$mat-form-field-with-label-input-padding-bottom - $vertical-deduction, - ); - // Map that describes the padding for form-fields without label. - $no-label-padding: ( - top: form-field-sizing.$mat-form-field-no-label-padding-top - $vertical-deduction, - bottom: form-field-sizing.$mat-form-field-no-label-padding-bottom - $vertical-deduction, - ); - - // We add a minimum height to the infix container in order to ensure that custom controls have - // the same default vertical space as text-field inputs (with respect to the vertical padding). - .mat-mdc-form-field-infix { - min-height: $height; - } - - // By default, MDC aligns the label using percentage. This will be overwritten based - // on whether a textarea is used. This is not possible in our implementation of the - // form-field because we do not know what type of form-field control is set up. Hence - // we always use a fixed position for the label. This does not have any implications. - .mat-mdc-text-field-wrapper .mat-mdc-form-field-flex .mat-mdc-floating-label { - top: math.div($height, 2); - } - - // For the outline appearance, we re-create the active floating label transform. This is - // necessary because the transform for docked floating labels can be updated to account for - // the width of prefix container. - .mat-mdc-text-field-wrapper.mdc-text-field--outlined .mdc-notched-outline--upgraded - .mdc-floating-label--float-above { - --mat-mdc-form-field-label-transform: translateY( - -#{mdc-textfield.get-outlined-label-position-y($height)}) - scale(var(--mat-mdc-form-field-floating-label-scale, 0.75)); - transform: var(--mat-mdc-form-field-label-transform); - } - - // Add vertical spacing to the infix to ensure that outlined form fields have their controls - // aligned as if there is no label. This is done similarly in MDC and is specified in the - // Material Design specification. Outline form fields position the control as if there is no - // label. This is because the label overflows the form-field and doesn't need space at the top. - @include _infix-vertical-spacing-outlined($no-label-padding); - - // MDC hides labels for filled form fields when the form field height decreases. We match - // this behavior in our custom density styles. - @if $hide-filled-floating-label { - // Update the spacing for filled form fields to account for the hidden floating label. - @include _infix-vertical-spacing-filled( - $no-label-padding, $no-label-padding); - .mat-mdc-text-field-wrapper:not(.mdc-text-field--outlined) .mat-mdc-floating-label { - display: none; - } - } - @else { - // By default, filled form fields align their controls differently based on whether there - // is a label or not. MDC does this too, but we cannot rely on their styles as we support - // arbitrary form field controls and MDC only applies their spacing to the `` elements. - @include _infix-vertical-spacing-filled( - $with-label-padding, $no-label-padding); - } -} diff --git a/src/material/form-field/_form-field-sizing.scss b/src/material/form-field/_form-field-sizing.scss deleted file mode 100644 index 70b78c8d17a5..000000000000 --- a/src/material/form-field/_form-field-sizing.scss +++ /dev/null @@ -1,40 +0,0 @@ -// Top spacing of the form-field outline. MDC does not have a variable for this -// and just hard-codes it into their styles. -$mat-form-field-outline-top-spacing: 12px; - -// Infix stretches to fit the container, but naturally wants to be this wide. We set -// this in order to have a consistent natural size for the various types of controls -// that can go in a form field. -$mat-form-field-default-infix-width: 180px !default; - -// Minimum amount of space between start and end hints in the subscript. MDC does not -// have built-in support for hints. -$mat-form-field-hint-min-space: 1em !default; - -// Vertical spacing of the text-field if there is no label. MDC hard-codes the spacing -// into their styles, but their spacing variables would not work for our form-field -// structure anyway. This is because MDC's input elements are larger than the text, and -// their padding variables are calculated with respect to the vertical empty space of the -// inputs. We take the explicit numbers provided by the Material Design specification. -// https://material.io/components/text-fields/#specs - -// Vertical spacing of the text-field if there is a label. MDC hard-codes the spacing into -// their styles, but their spacing variables would not work for our form-field structure anyway. -// This is because MDC's alignment depends on the input element to expand to full infix height. -// We allow for arbitrary form controls and support dynamic height, so we manage the control -// infix alignment through padding on the infix that works for any control. We manually measure -// spacing as provided by the Material Design specification. The outlined dimensions in the -// spec section do not match with the text fields shown in the overview or the ones implemented -// by MDC. Note that we need to account for the input box offset. See above for more context. -$mat-form-field-with-label-input-padding-top: 24px; -$mat-form-field-with-label-input-padding-bottom: 8px; - -// Vertical spacing of the text-field if there is no label. We manually measure the -// spacing in the specs. See comment above for padding for text fields with label. The -// same reasoning applies to the padding for text fields without label. -$mat-form-field-no-label-padding-bottom: 16px; -$mat-form-field-no-label-padding-top: 16px; - -// The amount of padding between the icon prefix/suffix and the infix. -// This assumes that the icon will be a 24px square with 12px padding. -$mat-form-field-icon-prefix-infix-padding: 4px; diff --git a/src/material/form-field/_form-field-subscript.scss b/src/material/form-field/_form-field-subscript.scss index ff0795c929ed..d65aa058d89e 100644 --- a/src/material/form-field/_form-field-subscript.scss +++ b/src/material/form-field/_form-field-subscript.scss @@ -3,7 +3,6 @@ @use '../core/tokens/m2/mat/form-field' as tokens-mat-form-field; @use '../core/tokens/token-utils'; -@use './form-field-sizing'; @mixin private-form-field-subscript() { // Wrapper for the hints and error messages. @@ -50,7 +49,7 @@ // Spacer used to make sure start and end hints have enough space between them. .mat-mdc-form-field-hint-spacer { - flex: 1 0 form-field-sizing.$mat-form-field-hint-min-space; + flex: 1 0 1em; } // Single error message displayed beneath the form field underline. diff --git a/src/material/form-field/_form-field-theme.scss b/src/material/form-field/_form-field-theme.scss index 026d3f2f6b67..13052c26e177 100644 --- a/src/material/form-field/_form-field-theme.scss +++ b/src/material/form-field/_form-field-theme.scss @@ -9,7 +9,6 @@ @use '../core/typography/typography'; @use '../core/style/sass-utils'; @use '../core/tokens/token-utils'; -@use './form-field-density'; @mixin base($theme) { @if inspection.get-theme-version($theme) == 1 { @@ -85,7 +84,10 @@ @include _theme-from-tokens(inspection.get-theme-tokens($theme, density)); } @else { - @include form-field-density.private-form-field-density($theme); + @include sass-utils.current-selector-or-root() { + @include token-utils.create-token-values(tokens-mat-form-field.$prefix, + tokens-mat-form-field.get-density-tokens($theme)); + } } } diff --git a/src/material/form-field/_mdc-text-field-density-overrides.scss b/src/material/form-field/_mdc-text-field-density-overrides.scss new file mode 100644 index 000000000000..41ca017072d8 --- /dev/null +++ b/src/material/form-field/_mdc-text-field-density-overrides.scss @@ -0,0 +1,60 @@ +@use '@material/textfield' as mdc-textfield; +@use '../core/tokens/m2/mat/form-field' as tokens-mat-form-field; +@use '../core/tokens/token-utils'; + +// Mixin that includes the density styles for form fields. MDC provides their own density +// styles for MDC text-field which we cannot use. MDC relies on input elements to stretch +// vertically when the height is reduced as per density scale. This doesn't work for our +// form field since we support custom form field controls without a fixed height. Instead, we +// provide spacing that makes arbitrary controls align as specified in the Material Design +// specification. In order to support density, we need to adjust the vertical spacing to be +// based on the density scale. +@mixin private-text-field-density-overrides() { + @include token-utils.use-tokens( + tokens-mat-form-field.$prefix, tokens-mat-form-field.get-token-slots()) { + $height: token-utils.get-token-variable(container-height); + + .mat-mdc-form-field-infix { + // We add a minimum height to the infix container to ensure that custom controls have the + // same default vertical space as text-field inputs (with respect to the vertical padding). + min-height: var(#{$height}); + + @include token-utils.create-token-slot(padding-top, + filled-with-label-container-padding-top); + @include token-utils.create-token-slot(padding-bottom, + filled-with-label-container-padding-bottom); + + .mdc-text-field--outlined &, + .mdc-text-field--no-label & { + @include token-utils.create-token-slot(padding-top, container-vertical-padding); + @include token-utils.create-token-slot(padding-bottom, container-vertical-padding); + } + } + + // By default, MDC aligns the label using percentage. This will be overwritten based + // on whether a textarea is used. This is not possible in our implementation of the + // form-field because we do not know what type of form-field control is set up. Hence + // we always use a fixed position for the label. This does not have any implications. + .mat-mdc-text-field-wrapper .mat-mdc-form-field-flex .mat-mdc-floating-label { + top: calc(var(#{$height}) / 2); + } + + // We need to conditionally hide the floating label based on the height of the form field. + .mdc-text-field--filled .mat-mdc-floating-label { + display: var(#{token-utils.get-token-variable(filled-label-display)}, block); + } + + // For the outline appearance, we re-create the active floating label transform. This is + // necessary because the transform for docked floating labels can be updated to account for + // the width of prefix container. + .mat-mdc-text-field-wrapper.mdc-text-field--outlined .mdc-notched-outline--upgraded + .mdc-floating-label--float-above { + // Needs to be in a string form to work around an internal check that incorrectly flags this + // interpolation in `calc` as unnecessary. If we don't have it, Sass won't evaluate it. + $translate: 'calc(#{mdc-textfield.get-outlined-label-position-y(var(#{$height}))} * -1)'; + --mat-mdc-form-field-label-transform: translateY(#{$translate}) + scale(var(--mat-mdc-form-field-floating-label-scale, 0.75)); + transform: var(--mat-mdc-form-field-label-transform); + } + } +} diff --git a/src/material/form-field/form-field.scss b/src/material/form-field/form-field.scss index d4171f351946..efb89ee23fdb 100644 --- a/src/material/form-field/form-field.scss +++ b/src/material/form-field/form-field.scss @@ -15,7 +15,6 @@ @use '../core/tokens/m2/mat/form-field' as tokens-mat-form-field; @use '../core/tokens/m2/mdc/filled-text-field' as tokens-mdc-filled-text-field; @use '../core/tokens/m2/mdc/outlined-text-field' as tokens-mdc-outlined-text-field; -@use './form-field-sizing'; @use './form-field-subscript'; @use './form-field-focus-overlay'; @use './form-field-high-contrast'; @@ -23,6 +22,7 @@ @use './user-agent-overrides'; @use './mdc-text-field-textarea-overrides'; @use './mdc-text-field-structure-overrides'; +@use './mdc-text-field-density-overrides'; // Includes the structural styles of the components that the form field is composed of. @mixin _static-styles($query) { @@ -57,6 +57,7 @@ // MDC text-field overwrites. @include mdc-text-field-textarea-overrides.private-text-field-textarea-overrides(); @include mdc-text-field-structure-overrides.private-text-field-structure-overrides(); +@include mdc-text-field-density-overrides.private-text-field-density-overrides(); // Include the subscript, focus-overlay, native select and high-contrast styles. @include form-field-subscript.private-form-field-subscript(); @@ -65,6 +66,10 @@ @include form-field-high-contrast.private-form-field-high-contrast(); @include user-agent-overrides.private-form-field-user-agent-overrides(); +// The amount of padding between the icon prefix/suffix and the infix. +// This assumes that the icon will be a 24px square with 12px padding. +$_icon-prefix-infix-padding: 4px; + // Host element of the form-field. It contains the mdc-text-field wrapper // and the subscript wrapper. .mat-mdc-form-field { @@ -144,11 +149,11 @@ // 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 form-field-sizing.$mat-form-field-icon-prefix-infix-padding 0 0; + padding: 0 $_icon-prefix-infix-padding 0 0; } .mat-mdc-form-field-icon-suffix, [dir='rtl'] .mat-mdc-form-field-icon-prefix { - padding: 0 0 0 form-field-sizing.$mat-form-field-icon-prefix-infix-padding; + padding: 0 0 0 $_icon-prefix-infix-padding; } .mat-mdc-form-field-icon-prefix, @@ -178,7 +183,12 @@ .mat-mdc-form-field-infix { flex: auto; min-width: 0; - width: form-field-sizing.$mat-form-field-default-infix-width; + + // Infix stretches to fit the container, but naturally wants to be this wide. We set + // this in order to have a consistent natural size for the various types of controls + // that can go in a form field. + width: 180px; + // Needed so that the floating label does not overlap with prefixes or suffixes. position: relative; box-sizing: border-box; diff --git a/src/material/paginator/BUILD.bazel b/src/material/paginator/BUILD.bazel index 49fc8994b6c7..c55b93cda9e9 100644 --- a/src/material/paginator/BUILD.bazel +++ b/src/material/paginator/BUILD.bazel @@ -34,7 +34,6 @@ sass_library( deps = [ "//:mdc_sass_lib", "//src/material/core:core_scss_lib", - "//src/material/form-field:form_field_scss_lib", ], ) diff --git a/src/material/paginator/_paginator-theme.scss b/src/material/paginator/_paginator-theme.scss index 367f95d1a926..89a7e75928d6 100644 --- a/src/material/paginator/_paginator-theme.scss +++ b/src/material/paginator/_paginator-theme.scss @@ -1,12 +1,12 @@ @use 'sass:map'; @use 'sass:meta'; @use '../core/tokens/m2/mat/paginator' as tokens-mat-paginator; +@use '../core/tokens/m2/mat/form-field' as tokens-mat-form-field; @use '../core/style/sass-utils'; @use '../core/typography/typography'; @use '../core/theming/theming'; @use '../core/theming/inspection'; @use '../core/tokens/token-utils'; -@use '../form-field/form-field-density'; @mixin base($theme) { @if inspection.get-theme-version($theme) == 1 { @@ -51,16 +51,17 @@ tokens-mat-paginator.get-density-tokens($theme)); } - // TODO: this should be done through tokens once the form field has been switched over. .mat-mdc-paginator { // We need the form field to be narrower in order to fit into the paginator, // so we set its density to be -4 or denser. @if ((meta.type-of($density-scale) == 'number' and $density-scale >= -4) or $density-scale == maximum) { - @include form-field-density.private-form-field-density(-4); + @include token-utils.create-token-values(tokens-mat-form-field.$prefix, + tokens-mat-form-field.get-density-tokens((density: -4))); } @else { - @include form-field-density.private-form-field-density($density-scale); + @include token-utils.create-token-values(tokens-mat-form-field.$prefix, + tokens-mat-form-field.get-density-tokens((density: $density-scale))); } } }