// // Copyright 2018 Google Inc. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // @use 'sass:list'; @use 'sass:map'; @use 'sass:math'; @use 'sass:meta'; @use '@material/feature-targeting/feature-targeting'; @use '@material/rtl/rtl'; @use '@material/theme/custom-properties'; @use '@material/theme/css'; @use '@material/theme/keys'; @use '@material/theme/theme'; // Shape categories $small-component-radius: 4px !default; $medium-component-radius: 4px !default; $large-component-radius: 0 !default; @include keys.set-values( ( small: $small-component-radius, medium: $medium-component-radius, large: $large-component-radius, ), $options: (custom-property-prefix: shape) ); // @deprecated use shape.resolve-radius() to retrieve a value or // shape.get-shape-keys() and shape.is-shape-key() to manipulate keys. $category-keywords: ( small: keys.create-custom-property(small), medium: keys.create-custom-property(medium), large: keys.create-custom-property(large), ) !default; @function is-shape-key($radius) { @return map.has-key($category-keywords, $radius); } @function get-shape-keys() { @return map.keys($category-keywords); } // // Flips the radius values based on RTL context. // // Examples: // // 1. mdc-shape-flip-radius((0, 4px, 4px, 0)) => 4px 0 0 4px // 2. mdc-shape-flip-radius((0, 8px)) => 8px 0 // @function flip-radius($radius) { @if meta.type-of($radius) == 'list' { @if list.length($radius) > 4 { @error "Invalid radius: '#{$radius}' is more than 4 values"; } } @if list.length($radius) == 4 { @return list.nth($radius, 2) list.nth($radius, 1) list.nth($radius, 4) list.nth($radius, 3); } @else if list.length($radius) == 3 { @return list.nth($radius, 2) list.nth($radius, 1) list.nth($radius, 2) list.nth($radius, 3); } @else if list.length($radius) == 2 { @return list.nth($radius, 2) list.nth($radius, 1); } @else { @return $radius; } } /// Returns the resolved radius value of a shape category - `large`, `medium`, /// or `small`. If $radius is not a category, this function returns the value /// itself if valid. Valid values are numbers or percentages. /// /// If a percentage is provided, $component-height should be specified if the /// width of the component does not match the height. /// /// $radius may be a single value or a list of 1 to 4 values. /// /// @example - scss /// resolve-radius(small); // (varname: --mdc-shape-small, fallback: 4px) /// resolve-radius((varname: --custom-shape, fallback: small)); /// // ( /// // varname: --custom-shape, /// // fallback: (varname: --mdc-shape-small, fallback: 4px) /// // ) /// resolve-radius(8px); // 8px /// resolve-radius(50%, $component-height: 36px); // 16px /// /// $shape: ( /// family: 'rounded', /// radius: ( /// 8px, /// 8px, /// 8px, /// 8px, /// ), /// ); /// resolve-radius($shape); // 8px /// /// @param {String | Number | Map | List} $radius - the radius shape category or /// radius value to resolve. May be a number, custom property Map, shape /// Map, or a List of those values. /// @return {Number | Map | List} the resolved radius value. May be a number, /// a custom property Map, or a List if $radius was a List. @function resolve-radius($radius, $component-height: null) { @if $radius == null { @return null; } @if meta.type-of($radius) == 'list' { // $radius is a List @if list.length($radius) > 4 or list.length($radius) < 1 { @error "mdc-shape: Invalid radius: #{$radius}. Radius must be between 1 and 4 values."; } $radii: (); @each $corner in $radius { $radii: list.append( $radii, resolve-radius($corner, $component-height: $component-height) ); } @return $radii; } @if is-shape-key($radius) { // $radius is a shape key @return resolve-radius( keys.create-custom-property($radius), $component-height: $component-height ); } @else if custom-properties.is-custom-prop($radius) { // $radius is a custom property Map $fallback: resolve-radius( custom-properties.get-fallback($radius, $shallow: true), $component-height: $component-height ); @return custom-properties.set-fallback($radius, $fallback, $shallow: true); } @else if meta.type-of($radius) == 'map' { // $radius is a shape Map $family: map.get($radius, family); @if custom-properties.is-custom-prop($family) { // Shape Map may have been passed through keys.create-custom-properties() $family: custom-properties.get-fallback($family); } @if $family != 'rounded' { @error 'mdc-shape: Invalid shape family: "#{$family}". Only "rounded" is supported.'; } @return resolve-radius( map.get($radius, radius), $component-height: $component-height ); } @else { // $radius is a value @if meta.type-of($radius) != 'number' { @error "mdc-shape: Invalid radius: #{$radius}. Must be a number."; } $radius-unit: math.unit($radius); $component-height-type: meta.type-of($component-height); @if $radius-unit == '%' and $component-height-type == 'number' { $radius: _resolve-radius-percentage($radius, $component-height); } @return $radius; } } // // Accepts radius number or list of 2-4 radius values and returns 4 value list with // masked corners as mentioned in `$masked-corners` // // Example: // // 1. mdc-shape-mask-radius(2px 3px, 1 1 0 0) => 2px 3px 0 0 // 2. mdc-shape-mask-radius(8px, 0 0 1 1) => 0 0 8px 8px // 3. mdc-shape-mask-radius(4px 4px 4px 4px, 0 1 1 0) => 0 4px 4px 0 // @function mask-radius($radius, $masked-corners) { @if meta.type-of($radius) == 'list' { @if list.length($radius) > 4 { @error "Invalid radius: '#{$radius}' is more than 4 values"; } } @if list.length($masked-corners) != 4 { @error "Expected masked-corners of length 4 but got '#{list.length($masked-corners)}'."; } $radius: unpack-radius($radius); @return if(list.nth($masked-corners, 1) == 1, list.nth($radius, 1), 0) if(list.nth($masked-corners, 2) == 1, list.nth($radius, 2), 0) if(list.nth($masked-corners, 3) == 1, list.nth($radius, 3), 0) if(list.nth($masked-corners, 4) == 1, list.nth($radius, 4), 0); } /// Unpacks shorthand values for border-radius (i.e. lists of 1-3 values). /// If a list of 4 values is given, it is returned as-is. /// /// Examples: /// /// shape.unpack-radius(4px) => 4px 4px 4px 4px /// shape.unpack-radius(4px 2px) => 4px 2px 4px 2px /// shape.unpack-radius(4px 2px 2px) => 4px 2px 2px 2px /// shape.unpack-radius(4px 2px 0 2px) => 4px 2px 0 2px /// /// @param {Number | Map | List} $radius - List of 1 to 4 radius numbers. /// @return {List} a List of 4 radius numbers. @function unpack-radius($radius) { @return css.unpack-value($radius); } /// Resolve a percentage radius into a number. /// /// @param {Number} $percentage - the radius percentage. /// @param {Number} $component-height - the height of the component. /// @return {Number} the resolved radius as a number. @function _resolve-radius-percentage($percentage, $component-height) { // Converts the percentage to number without unit. Example: 50% => 50. $percentage: math.div($percentage, $percentage * 0 + 1); @return $component-height * math.div($percentage, 100); } @mixin radius( $radius, $rtl-reflexive: false, $component-height: null, $query: feature-targeting.all() ) { $feat-structure: feature-targeting.create-target($query, structure); @include feature-targeting.targets($feat-structure) { $has-multiple-corners: meta.type-of($radius) == 'list' and list.length($radius) > 1; // Even if $rtl-reflexive is true, only emit RTL styles if we can't easily tell that the given radius is symmetrical $needs-flip: $rtl-reflexive and $has-multiple-corners; $radius: resolve-radius($radius, $component-height: $component-height); @if not $has-multiple-corners { @include theme.property(border-radius, $radius); } @else { $gss: ( noflip: $needs-flip, ); $radii: unpack-radius($radius); @include theme.property( border-top-left-radius, list.nth($radii, 1), $gss: $gss ); @include theme.property( border-top-right-radius, list.nth($radii, 2), $gss: $gss ); @include theme.property( border-bottom-right-radius, list.nth($radii, 3), $gss: $gss ); @include theme.property( border-bottom-left-radius, list.nth($radii, 4), $gss: $gss ); } @if ($needs-flip) { @include rtl.rtl { // If it needs to be flipped, it will always have multiple corners $gss: ( noflip: true, ); $radii: flip-radius(unpack-radius($radius)); @include theme.property( border-top-left-radius, list.nth($radii, 1), $gss: $gss ); @include theme.property( border-top-right-radius, list.nth($radii, 2), $gss: $gss ); @include theme.property( border-bottom-right-radius, list.nth($radii, 3), $gss: $gss ); @include theme.property( border-bottom-left-radius, list.nth($radii, 4), $gss: $gss ); } } } } /// Resolves one or more shape tokens and expands them into 4 separate logical /// tokens for each corner. /// /// @example - scss /// $theme: (container-shape: (4px 4px 0 0)); /// $theme: resolve-theme( /// $theme, /// map.get($resolvers, shape), /// container-shape, /// ); /// // ( /// // container-shape-start-start: 4px, /// // container-shape-start-end: 4px, /// // container-shape-end-end: 0, /// // container-shape-end-start: 0, /// // ) /// /// @param {Map} $theme - The theme to resolve tokens for. /// @param {Function} $resolver - The shape resolver to use. /// @param {String...} $shape-tokens - The shape tokens to resolve. /// @return {Map} The theme with resolved shape tokens. @function resolve-theme($theme, $resolver, $shape-tokens...) { @if $resolver == null { @return $theme; } @each $token in $shape-tokens { $shape-theme: meta.call($resolver, $shape: map.get($theme, $token)); // Add resolved values, but allow $theme to override the results if needed. $theme: map.merge( ( '#{$token}-start-start': map.get($shape-theme, start-start), '#{$token}-start-end': map.get($shape-theme, start-end), '#{$token}-end-end': map.get($shape-theme, end-end), '#{$token}-end-start': map.get($shape-theme, end-start), ), $theme ); $theme: map.remove($theme, $token); } @return $theme; } /// Resolves a shape value by expanding it into logical values for each corner. /// /// @example - scss /// /// resolver($shape: (4px 4px 0 0)) => /// ( /// start-start: 4px, /// start-end: 4px, /// end-end: 0, /// end-start: 0, /// ) /// /// resolver($shape: 4px) => /// ( /// start-start: 4px, /// start-end: 4px, /// end-end: 4px, /// end-start: 4px, /// ) /// @param {Number|List} $shape - The shape token's value. /// @return {Map} A map with logical tokens for each corner's value. @function resolver($shape) { @if meta.type-of($shape) != 'list' { @return ( start-start: $shape, start-end: $shape, end-end: $shape, end-start: $shape ); } @if list.length($shape) == 1 { @return ( start-start: list.nth($shape, 1), start-end: list.nth($shape, 1), end-end: list.nth($shape, 1), end-start: list.nth($shape, 1) ); } @if list.length($shape) == 2 { @return ( start-start: list.nth($shape, 1), start-end: list.nth($shape, 2), end-end: list.nth($shape, 1), end-start: list.nth($shape, 2) ); } @if list.length($shape) == 3 { @return ( start-start: list.nth($shape, 1), start-end: list.nth($shape, 2), end-end: list.nth($shape, 3), end-start: list.nth($shape, 2) ); } @return ( start-start: list.nth($shape, 1), start-end: list.nth($shape, 2), end-end: list.nth($shape, 3), end-start: list.nth($shape, 4) ); }