From c546d9ff544207f1b438b2a3a595ae21f8a05258 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Mon, 22 Jan 2024 22:48:37 +0100 Subject: [PATCH] fix(material-experimental/theming): implement M3 badge Aligns the badge with the M3 spec. This required a few issues to be fixed: 1. The badge was being sized with `width` and `height`. This is incorrect, because it prevents the badge from scaling with the text. I've resolved it while keeping the old behavior by introducing new tokens that target `min-width` and `min-height` while the old ones still target `width` and `height`. 2. The badge was being positioned purely with `top`, `bottom`, `left` and `right`. This is problematic, because it anchors the badge to the specific point in the host, causing the content to grow inwards instead of outwards. I've fixed it by using the opposite dimensions to position the badge (e.g. `bottom: 100%` instead of `top: -20px`) and then using a negative margin to offset the badge from there. This approach also has the advantage of producing less CSS. 3. The badge didn't have a padding which made the content look off if it's more than one character. --- .../theming/_custom-tokens.scss | 18 +++ src/material/badge/_badge-theme.scss | 7 +- src/material/badge/badge.scss | 127 +++++++++--------- src/material/core/tokens/m2/mat/_badge.scss | 28 +++- 4 files changed, 111 insertions(+), 69 deletions(-) diff --git a/src/material-experimental/theming/_custom-tokens.scss b/src/material-experimental/theming/_custom-tokens.scss index 25e5c0f26821..854381b3bb5d 100644 --- a/src/material-experimental/theming/_custom-tokens.scss +++ b/src/material-experimental/theming/_custom-tokens.scss @@ -59,7 +59,25 @@ text-size: map.get($systems, md-sys-typescale, label-small-size), text-weight: map.get($systems, md-sys-typescale, label-small-weight), small-size-text-size: _hardcode(0, $exclude-hardcoded), + container-shape: map.get($systems, md-sys-shape, corner-full), + container-size: _hardcode(16px, $exclude-hardcoded), + legacy-container-size: _hardcode(unset, $exclude-hardcoded), + legacy-small-size-container-size: _hardcode(unset, $exclude-hardcoded), + small-size-container-size: _hardcode(6px, $exclude-hardcoded), + container-padding: _hardcode(0 4px, $exclude-hardcoded), + small-size-container-padding: _hardcode(0, $exclude-hardcoded), + container-offset: -12px 0, + small-size-container-offset: -6px 0, + container-overlap-offset: -12px, + small-size-container-overlap-offset: -6px, + + // This size isn't in the M3 spec so we emit the same values as for `medium`. + large-size-container-size: _hardcode(16px, $exclude-hardcoded), + large-size-container-offset: -12px 0, + large-size-container-overlap-offset: -12px, large-size-text-size: map.get($systems, md-sys-typescale, label-small-size), + large-size-container-padding: _hardcode(0 4px, $exclude-hardcoded), + legacy-large-size-container-size: _hardcode(unset, $exclude-hardcoded), ), ( primary: ( background-color: map.get($systems, md-sys-color, primary), diff --git a/src/material/badge/_badge-theme.scss b/src/material/badge/_badge-theme.scss index 9e465f92c871..91df4fc15661 100644 --- a/src/material/badge/_badge-theme.scss +++ b/src/material/badge/_badge-theme.scss @@ -13,7 +13,12 @@ @if inspection.get-theme-version($theme) == 1 { @include _theme-from-tokens(inspection.get-theme-tokens($theme, base)); } - @else {} + @else { + @include sass-utils.current-selector-or-root() { + @include token-utils.create-token-values(tokens-mat-badge.$prefix, + tokens-mat-badge.get-unthemable-tokens()); + } + } } /// Outputs color theme styles for the mat-badge. diff --git a/src/material/badge/badge.scss b/src/material/badge/badge.scss index 8aecd84cda02..043505896a82 100644 --- a/src/material/badge/badge.scss +++ b/src/material/badge/badge.scss @@ -1,5 +1,4 @@ @use 'sass:color'; -@use 'sass:math'; @use '@angular/cdk'; @use '../core/tokens/m2/mat/badge' as tokens-mat-badge; @use '../core/tokens/token-utils'; @@ -8,65 +7,33 @@ $default-size: 22px !default; $small-size: $default-size - 6; $large-size: $default-size + 6; - -// Mixin for building offset given different sizes -@mixin _badge-size($size, $font-size-token) { - .mat-badge-content { - width: $size; - height: $size; - line-height: $size; - - @if ($font-size-token) { - @include token-utils.use-tokens(tokens-mat-badge.$prefix, - tokens-mat-badge.get-token-slots()) { - @include token-utils.create-token-slot(font-size, $font-size-token); - } - } - } - - &.mat-badge-above .mat-badge-content { - top: math.div(-$size, 2); - } - - &.mat-badge-below .mat-badge-content { - bottom: math.div(-$size, 2); - } - - &.mat-badge-before .mat-badge-content { - left: -$size; - } - - [dir='rtl'] &.mat-badge-before .mat-badge-content { - left: auto; - right: -$size; - } - - &.mat-badge-after .mat-badge-content { - right: -$size; - } - - [dir='rtl'] &.mat-badge-after .mat-badge-content { - right: auto; - left: -$size; - } - - &.mat-badge-overlap { - &.mat-badge-before .mat-badge-content { - left: math.div(-$size, 2); - } - - [dir='rtl'] &.mat-badge-before .mat-badge-content { - left: auto; - right: math.div(-$size, 2); - } - - &.mat-badge-after .mat-badge-content { - right: math.div(-$size, 2); +@mixin _badge-size($size) { + @include token-utils.use-tokens(tokens-mat-badge.$prefix, tokens-mat-badge.get-token-slots()) { + $prefix: if($size == 'medium', '', $size + '-size-'); + $legacy-size-var: token-utils.get-token-variable('legacy-#{$prefix}container-size'); + $size-var: token-utils.get-token-variable('#{$prefix}container-size'); + + .mat-badge-content { + // The M2 badge is implemented incorrectly because it uses `width` and `height` for its + // size which causes the text to be truncated. For M3 we want to fix this by emitting + // two declarations: + // * `legacy-container-size` token - targets width/height as in M2. In M3 the token is + // emitted as `unset`. + // * `container-size` token - In M2 the token is emitted as `unset` to preserve the legacy + // behavior while in M3 it targets `min-width` and `min-height` which allows the badge to + // grow with the content. + width: var(#{$legacy-size-var}, unset); + height: var(#{$legacy-size-var}, unset); + min-width: var(#{$size-var}, unset); + min-height: var(#{$size-var}, unset); + line-height: var($legacy-size-var, var(#{$size-var})); + @include token-utils.create-token-slot(padding, '#{$prefix}container-padding'); + @include token-utils.create-token-slot(font-size, '#{$prefix}text-size'); + @include token-utils.create-token-slot(margin, '#{$prefix}container-offset'); } - [dir='rtl'] &.mat-badge-after .mat-badge-content { - right: auto; - left: math.div(-$size, 2); + &.mat-badge-overlap .mat-badge-content { + @include token-utils.create-token-slot(margin, '#{$prefix}container-overlap-offset'); } } } @@ -87,25 +54,51 @@ $large-size: $default-size + 6; position: absolute; text-align: center; display: inline-block; - border-radius: 50%; transition: transform 200ms ease-in-out; transform: scale(0.6); overflow: hidden; white-space: nowrap; text-overflow: ellipsis; + box-sizing: border-box; pointer-events: none; + @include cdk.high-contrast(active, off) { + outline: solid 1px; + border-radius: 0; + } + @include token-utils.use-tokens(tokens-mat-badge.$prefix, tokens-mat-badge.get-token-slots()) { @include token-utils.create-token-slot(background-color, background-color); @include token-utils.create-token-slot(color, text-color); @include token-utils.create-token-slot(font-family, text-font); - @include token-utils.create-token-slot(font-size, text-size); @include token-utils.create-token-slot(font-weight, text-weight); - } + @include token-utils.create-token-slot(border-radius, container-shape); - @include cdk.high-contrast(active, off) { - outline: solid 1px; - border-radius: 0; + .mat-badge-above & { + bottom: 100%; + } + + .mat-badge-below & { + top: 100%; + } + + .mat-badge-before & { + right: 100%; + } + + [dir='rtl'] .mat-badge-before & { + right: auto; + left: 100%; + } + + .mat-badge-after & { + left: 100%; + } + + [dir='rtl'] .mat-badge-after & { + left: auto; + right: 100%; + } } } @@ -133,13 +126,13 @@ $large-size: $default-size + 6; } .mat-badge-small { - @include _badge-size($small-size, small-size-text-size); + @include _badge-size('small'); } .mat-badge-medium { - @include _badge-size($default-size, null); + @include _badge-size('medium'); } .mat-badge-large { - @include _badge-size($large-size, large-size-text-size); + @include _badge-size('large'); } diff --git a/src/material/core/tokens/m2/mat/_badge.scss b/src/material/core/tokens/m2/mat/_badge.scss index 72725ad7d3b9..9c28ff0bbb43 100644 --- a/src/material/core/tokens/m2/mat/_badge.scss +++ b/src/material/core/tokens/m2/mat/_badge.scss @@ -1,5 +1,6 @@ @use 'sass:meta'; @use 'sass:map'; +@use 'sass:math'; @use 'sass:color'; @use '../../token-utils'; @use '../../../theming/inspection'; @@ -11,7 +12,32 @@ $prefix: (mat, badge); // Tokens that can't be configured through Angular Material's current theming API, // but may be in a future version of the theming API. @function get-unthemable-tokens() { - @return (); + $default-size: 22px; + $small-size: $default-size - 6; + $large-size: $default-size + 6; + + @return ( + container-shape: 50%, + container-size: unset, + small-size-container-size: unset, + large-size-container-size: unset, + + legacy-container-size: $default-size, + legacy-small-size-container-size: $small-size, + legacy-large-size-container-size: $large-size, + + container-offset: math.div($default-size, -2) 0, + small-size-container-offset: math.div($small-size, -2) 0, + large-size-container-offset: math.div($large-size, -2) 0, + + container-overlap-offset: math.div($default-size, -2), + small-size-container-overlap-offset: math.div($small-size, -2), + large-size-container-overlap-offset: math.div($large-size, -2), + + container-padding: 0, + small-size-container-padding: 0, + large-size-container-padding: 0, + ); } // Tokens that can be configured through Angular Material's color theming API.