diff --git a/packages/web/src/scss/components/Flex/_Flex.scss b/packages/web/src/scss/components/Flex/_Flex.scss index af8df5272e..379c94041c 100644 --- a/packages/web/src/scss/components/Flex/_Flex.scss +++ b/packages/web/src/scss/components/Flex/_Flex.scss @@ -5,24 +5,22 @@ @use '../../tools/breakpoint'; @use '../../tools/dictionaries'; @use '../../tools/reset'; -@use '../../tools/spacing'; +@use '../../tools/responsive-properties'; @use 'theme'; .Flex { @include reset.list(); - @include spacing.create( - $input-property-base-name: '--flex-spacing-x', - $output-property-name: '--flex-column-gap', + @include responsive-properties.create-cascade( $property: 'column-gap', + $input-custom-property-base-name: 'flex-spacing-x', $breakpoints: theme.$breakpoints, - $default-spacing: theme.$gap + $fallback-value: theme.$default-spacing ); - @include spacing.create( - $input-property-base-name: '--flex-spacing-y', - $output-property-name: '--flex-row-gap', + @include responsive-properties.create-cascade( $property: 'row-gap', + $input-custom-property-base-name: 'flex-spacing-y', $breakpoints: theme.$breakpoints, - $default-spacing: theme.$gap + $fallback-value: theme.$default-spacing ); } @@ -30,17 +28,6 @@ flex: none; // 1. } -// stylelint-disable-next-line selector-max-universal -- Let's be bold and tweak all direct descendants regardless of their type to avoid inheritance of spacing for nested Flex. -.Flex > * { - @include spacing.prevent-inheritance( - $input-property-base-names: ( - '--flex-spacing-x', - '--flex-spacing-y', - ), - $breakpoints: theme.$breakpoints - ); -} - @each $breakpoint-name, $breakpoint-value in theme.$breakpoints { $infix: breakpoint.get-modifier('infix', $breakpoint-name, $breakpoint-value); diff --git a/packages/web/src/scss/components/Flex/_theme.scss b/packages/web/src/scss/components/Flex/_theme.scss index 60c558027f..119fe23cde 100644 --- a/packages/web/src/scss/components/Flex/_theme.scss +++ b/packages/web/src/scss/components/Flex/_theme.scss @@ -5,4 +5,4 @@ $alignment-x-dictionary: list.join(dictionaries.$alignments-x-extended, space-between); $alignment-y-dictionary: list.join(dictionaries.$alignments-y-extended, baseline); $breakpoints: tokens.$breakpoints; -$gap: tokens.$space-700; +$default-spacing: tokens.$space-700; diff --git a/packages/web/src/scss/components/Grid/_Grid.scss b/packages/web/src/scss/components/Grid/_Grid.scss index bab6ff3292..3f09d22e6c 100644 --- a/packages/web/src/scss/components/Grid/_Grid.scss +++ b/packages/web/src/scss/components/Grid/_Grid.scss @@ -1,55 +1,33 @@ -// 1. For each breakpoint, generate: -// -// a) responsive alignment classes for both axes, -// b) `grid-template-columns` values for the equal-column Grid variant, -// c) responsive GridItem classes. - @use '../../tools/breakpoint'; @use '../../tools/dictionaries'; @use '../../tools/reset'; -@use '../../tools/spacing'; +@use '../../tools/responsive-properties'; @use 'theme'; -@use 'tools'; .Grid { display: grid; - grid-template-columns: repeat(theme.$grid-columns, 1fr); + grid-template-columns: repeat(theme.$columns, 1fr); width: 100%; @include reset.list(); - @include spacing.create( - $input-property-base-name: '--grid-spacing-x', - $output-property-name: '--grid-column-gap', + @include responsive-properties.create-cascade( $property: 'column-gap', + $input-custom-property-base-name: 'grid-spacing-x', $breakpoints: theme.$breakpoints, - $default-spacing: theme.$grid-spacings + $fallback-value: theme.$default-spacings ); - @include spacing.create( - $input-property-base-name: '--grid-spacing-y', - $output-property-name: '--grid-row-gap', + @include responsive-properties.create-cascade( $property: 'row-gap', + $input-custom-property-base-name: 'grid-spacing-y', $breakpoints: theme.$breakpoints, - $default-spacing: theme.$grid-spacings - ); -} - -// stylelint-disable-next-line selector-max-universal -- Let's be bold and reset spacing for all direct descendants regardless of their type to avoid inheritance of spacing for nested Grid. -.Grid > * { - @include spacing.prevent-inheritance( - $input-property-base-names: ( - '--grid-spacing-x', - '--grid-spacing-y', - ), - $breakpoints: theme.$breakpoints + $fallback-value: theme.$default-spacings ); } -// 1. @each $breakpoint-name, $breakpoint-value in theme.$breakpoints { $infix: breakpoint.get-modifier('infix', $breakpoint-name, $breakpoint-value); @include breakpoint.up($breakpoint-value) { - // 1.a @include dictionaries.generate-alignments( $class-name: 'Grid', $dictionary-values: theme.$alignment-x-dictionary, @@ -64,27 +42,10 @@ $infix: $infix ); - // 1.b - @each $column in theme.$grid-equal-columns { + @each $column in theme.$equal-columns { .Grid--#{breakpoint.get-modifier('infix', $breakpoint-name, $breakpoint-value)}cols-#{$column} { grid-template-columns: repeat(#{$column}, 1fr); } } } } - -// 1.c -@include tools.item(theme.$breakpoints); - -// stylelint-disable-next-line selector-max-universal -- Let's be bold and reset spacing for all direct descendants regardless of their type to avoid inheritance of spacing for nested GridItem. -.GridItem > * { - @include spacing.prevent-inheritance( - $input-property-base-names: ( - '--grid-item-column-start', - '--grid-item-column-end', - '--grid-item-row-start', - '--grid-item-row-end', - ), - $breakpoints: theme.$breakpoints - ); -} diff --git a/packages/web/src/scss/components/Grid/_GridItem.scss b/packages/web/src/scss/components/Grid/_GridItem.scss new file mode 100644 index 0000000000..114f65a44a --- /dev/null +++ b/packages/web/src/scss/components/Grid/_GridItem.scss @@ -0,0 +1,29 @@ +@use '../../tools/responsive-properties'; +@use 'theme'; + +.GridItem { + @include responsive-properties.create-cascade( + $property: 'grid-column-start', + $input-custom-property-base-name: 'grid-item-column-start', + $breakpoints: theme.$breakpoints, + $fallback-value: 'initial' + ); + @include responsive-properties.create-cascade( + $property: 'grid-column-end', + $input-custom-property-base-name: 'grid-item-column-end', + $breakpoints: theme.$breakpoints, + $fallback-value: 'initial' + ); + @include responsive-properties.create-cascade( + $property: 'grid-row-start', + $input-custom-property-base-name: 'grid-item-row-start', + $breakpoints: theme.$breakpoints, + $fallback-value: 'initial' + ); + @include responsive-properties.create-cascade( + $property: 'grid-row-end', + $input-custom-property-base-name: 'grid-item-row-end', + $breakpoints: theme.$breakpoints, + $fallback-value: 'initial' + ); +} diff --git a/packages/web/src/scss/components/Grid/_theme.scss b/packages/web/src/scss/components/Grid/_theme.scss index ca336d6755..872d9051e4 100644 --- a/packages/web/src/scss/components/Grid/_theme.scss +++ b/packages/web/src/scss/components/Grid/_theme.scss @@ -7,6 +7,6 @@ $alignment-y-dictionary: dictionaries.$alignments-y-extended; $breakpoints: tokens.$breakpoints; -$grid-columns: tokens.$grid-columns; -$grid-spacings: map.get(tokens.$grids, spacing); -$grid-equal-columns: 1, 2, 3, 4, 5, 6, 12; +$columns: tokens.$grid-columns; +$equal-columns: 1, 2, 3, 4, 5, 6, 12; +$default-spacings: map.get(tokens.$grids, spacing); diff --git a/packages/web/src/scss/components/Grid/_tools.scss b/packages/web/src/scss/components/Grid/_tools.scss deleted file mode 100644 index 06676698fc..0000000000 --- a/packages/web/src/scss/components/Grid/_tools.scss +++ /dev/null @@ -1,29 +0,0 @@ -@use '../../tools/breakpoint'; - -// Generates grid-column values for each breakpoint with fallbacks to the previous breakpoint. -// Parameters are: -// * $breakpoints: the breakpoints map -@mixin item($breakpoints) { - $column-start: initial; - $column-end: 'initial'; - $row-start: initial; - $row-end: 'initial'; - - @each $breakpoint-name, $breakpoint-value in $breakpoints { - $suffix: breakpoint.get-modifier('suffix', $breakpoint-name, $breakpoint-value); - - @include breakpoint.up($breakpoint-value) { - $column-start: var(--grid-item-column-start#{$suffix}, $column-start); - $column-end: var(--grid-item-column-end#{$suffix}, $column-end); - $row-start: var(--grid-item-row-start#{$suffix}, $row-start); - $row-end: var(--grid-item-row-end#{$suffix}, $row-end); - - .GridItem { - grid-column-start: $column-start; - grid-column-end: $column-end; - grid-row-start: $row-start; - grid-row-end: $row-end; - } - } - } -} diff --git a/packages/web/src/scss/components/Grid/index.scss b/packages/web/src/scss/components/Grid/index.scss index faed805be6..172dca4e73 100644 --- a/packages/web/src/scss/components/Grid/index.scss +++ b/packages/web/src/scss/components/Grid/index.scss @@ -1 +1,2 @@ @forward 'Grid'; +@forward 'GridItem'; diff --git a/packages/web/src/scss/components/Stack/_Stack.scss b/packages/web/src/scss/components/Stack/_Stack.scss index c6f6225dc0..172b7f09b3 100644 --- a/packages/web/src/scss/components/Stack/_Stack.scss +++ b/packages/web/src/scss/components/Stack/_Stack.scss @@ -1,5 +1,5 @@ @use '../../tools/reset'; -@use '../../tools/spacing'; +@use '../../tools/responsive-properties'; @use 'theme'; .Stack { @@ -9,11 +9,11 @@ } .Stack--hasSpacing { - @include spacing.create( - $input-property-base-name: '--stack-spacing', - $output-property-name: '--stack-gap', + @include responsive-properties.create-cascade( + $property: 'gap', + $input-custom-property-base-name: 'stack-spacing', $breakpoints: theme.$breakpoints, - $default-spacing: theme.$spacing-fallback + $fallback-value: theme.$default-spacing ); } @@ -23,7 +23,7 @@ gap: 0; } -// stylelint-disable selector-max-universal -- Let's be bold and tweak all direct descendants regardless of their type to avoid inheritance of spacing for nested Stack. +// stylelint-disable selector-max-universal -- Remove any margins from all direct descendants regardless of their type. .Stack > * { margin-block: 0; } @@ -32,20 +32,13 @@ border-block-start: theme.$border; } -.Stack--hasSpacing > * { - @include spacing.prevent-inheritance( - $input-property-base-names: '--stack-spacing', - $breakpoints: theme.$breakpoints - ); -} - .Stack--hasSpacing.Stack--hasStartDivider > *, .Stack--hasSpacing.Stack--hasEndDivider > * { - padding-block: calc(var(--stack-gap) / 2); + padding-block: calc(var(--stack-spacing-internal) / 2); } .Stack--hasSpacing.Stack--hasIntermediateDividers > * { - padding-block: var(--stack-gap); + padding-block: var(--stack-spacing-internal); } // stylelint-enable @@ -74,9 +67,9 @@ } .Stack--hasSpacing.Stack--hasStartDivider > :first-child { - padding-block-start: var(--stack-gap); + padding-block-start: var(--stack-spacing-internal); } .Stack--hasSpacing.Stack--hasEndDivider > :last-child { - padding-block-end: var(--stack-gap); + padding-block-end: var(--stack-spacing-internal); } diff --git a/packages/web/src/scss/components/Stack/_theme.scss b/packages/web/src/scss/components/Stack/_theme.scss index 60e67f1899..6c3f1cdea1 100644 --- a/packages/web/src/scss/components/Stack/_theme.scss +++ b/packages/web/src/scss/components/Stack/_theme.scss @@ -2,4 +2,4 @@ $breakpoints: tokens.$breakpoints; $border: tokens.$border-width-100 solid tokens.$border-basic; -$spacing-fallback: tokens.$space-600; +$default-spacing: tokens.$space-600; diff --git a/packages/web/src/scss/tools/__tests__/_responsive-properties.test.scss b/packages/web/src/scss/tools/__tests__/_responsive-properties.test.scss new file mode 100644 index 0000000000..7b0e731bfe --- /dev/null +++ b/packages/web/src/scss/tools/__tests__/_responsive-properties.test.scss @@ -0,0 +1,118 @@ +@use 'true' as test; +@use '../responsive-properties'; + +@include test.describe('responsive properties functions and mixins') { + @include test.it('should return correct property with custom property cascade') { + @include test.assert() { + @include test.output() { + .test { + @include responsive-properties.create-cascade( + $property: 'row-gap', + $input-custom-property-base-name: 'gap', + $breakpoints: ( + 'mobile': 0, + 'tablet': 768px, + ), + $fallback-value: 16px + ); + } + } + + @include test.expect() { + .test { + --gap-internal: var(--gap, 16px); + + row-gap: var(--gap-internal); + } + + @media (width >= 768px) { + .test { + --gap-internal: var(--gap-tablet, var(--gap, 16px)); + } + } + + // stylelint-disable-next-line selector-max-universal -- We need the universal selector to reset the custom properties. + .test > * { + --gap: initial; + --gap-tablet: initial; + } + } + } + } + + @include test.it('should return correct property with custom property cascade and no inheritance prevention') { + @include test.assert() { + @include test.output() { + .test { + @include responsive-properties.create-cascade( + $property: 'row-gap', + $input-custom-property-base-name: 'gap', + $breakpoints: ( + 'mobile': 0, + 'tablet': 768px, + ), + $fallback-value: 16px, + $prevent-inheritance: false + ); + } + } + + @include test.expect() { + .test { + --gap-internal: var(--gap, 16px); + + row-gap: var(--gap-internal); + } + + @media (width >= 768px) { + .test { + --gap-internal: var(--gap-tablet, var(--gap, 16px)); + } + } + } + } + } + + @include test.it('should return correct property with custom property cascade and responsive fallbacks') { + @include test.assert() { + @include test.output() { + .test { + @include responsive-properties.create-cascade( + $property: 'row-gap', + $input-custom-property-base-name: 'gap', + $breakpoints: ( + 'mobile': 0, + 'tablet': 768px, + ), + $fallback-value: ( + 'mobile': 16px, + 'tablet': 32px, + ) + ); + } + } + + @include test.expect() { + .test { + --gap-default-internal: 16px; + --gap-internal: var(--gap, var(--gap-default-internal, 16px)); + + row-gap: var(--gap-internal); + } + + @media (width >= 768px) { + .test { + --gap-default-internal: 32px; + --gap-internal: var(--gap-tablet, var(--gap, var(--gap-default-internal, 16px))); + } + } + + // stylelint-disable-next-line selector-max-universal -- We need the universal selector to reset the custom properties. + .test > * { + --gap: initial; + --gap-tablet: initial; + } + } + } + } +} diff --git a/packages/web/src/scss/tools/__tests__/_spacing.test.scss b/packages/web/src/scss/tools/__tests__/_spacing.test.scss deleted file mode 100644 index 8dff6badaf..0000000000 --- a/packages/web/src/scss/tools/__tests__/_spacing.test.scss +++ /dev/null @@ -1,125 +0,0 @@ -@use 'true' as test; -@use '../spacing'; - -@include test.describe('spacing functions and mixins') { - @include test.it('should return correct spacing cascade') { - @include test.assert() { - @include test.output() { - .test { - @include spacing.create( - $input-property-base-name: '--test-input', - $output-property-name: '--test-output', - $breakpoints: ( - 'mobile': 0, - 'tablet': 768px, - ), - $default-spacing: 16px - ); - } - } - - @include test.expect() { - .test { - --test-output: var(--test-input, 16px); - - gap: var(--test-output); - } - - @media (width >= 768px) { - .test { - --test-output: var(--test-input-tablet, var(--test-input, 16px)); - } - } - } - } - } - - @include test.it('should return correct spacing cascade with responsive default spacing') { - @include test.assert() { - @include test.output() { - .test { - @include spacing.create( - $input-property-base-name: '--test-input', - $output-property-name: '--test-output', - $property: 'column-gap', - $breakpoints: ( - 'mobile': 0, - 'tablet': 768px, - ), - $default-spacing: ( - 'mobile': 16px, - 'tablet': 32px, - ) - ); - } - } - - @include test.expect() { - .test { - --test-output-default: 16px; - --test-output: var(--test-input, var(--test-output-default, 16px)); - - column-gap: var(--test-output); - } - - @media (width >= 768px) { - .test { - --test-output-default: 32px; - --test-output: var(--test-input-tablet, var(--test-input, var(--test-output-default, 16px))); - } - } - } - } - } - - @include test.it('should set up correct prevention of spacing inheritance') { - @include test.assert() { - @include test.output() { - .nested { - @include spacing.prevent-inheritance( - $input-property-base-names: '--test-spacing', - $breakpoints: ( - 'mobile': 0, - 'tablet': 768px, - ) - ); - } - } - - @include test.expect() { - .nested { - --test-spacing: initial; - --test-spacing-tablet: initial; - } - } - } - } - - @include test.it('should set up correct prevention of spacing inheritance with multiple properties') { - @include test.assert() { - @include test.output() { - .nested { - @include spacing.prevent-inheritance( - $input-property-base-names: ( - '--test-spacing-x', - '--test-spacing-y', - ), - $breakpoints: ( - 'mobile': 0, - 'tablet': 768px, - ) - ); - } - } - - @include test.expect() { - .nested { - --test-spacing-x: initial; - --test-spacing-y: initial; - --test-spacing-x-tablet: initial; - --test-spacing-y-tablet: initial; - } - } - } - } -} diff --git a/packages/web/src/scss/tools/_responsive-properties.scss b/packages/web/src/scss/tools/_responsive-properties.scss new file mode 100644 index 0000000000..b0219f28a0 --- /dev/null +++ b/packages/web/src/scss/tools/_responsive-properties.scss @@ -0,0 +1,88 @@ +@use 'sass:list'; +@use 'sass:map'; +@use 'sass:meta'; +@use 'sass:string'; +@use 'breakpoint'; + +// Mixin to generate custom properties cascade with fallbacks +// +// Parameters are: +// * $property: the CSS property to apply the value to +// * $input-custom-property-base-name: the base name of the responsive CSS custom property passed in via inline styles (API), without the `--` prefix +// * $breakpoints: the breakpoints map +// * $fallback-value: the default value in case no input properties have been set; CSS value or breakpoint-to-value map +// * $prevent-inheritance: whether to prevent the custom property value from being inherited by the child elements in case of nesting +// +// Implementation Notes +// ==================== +// +// 1. Internal variables: +// +// a) Internal custom property which the input custom properties are applied to. +// b) Default custom property to store the default value (needed for responsive `$fallback-value`). +// c) Responsive custom property name for the current breakpoint, i.e. with the breakpoint suffix. +// d) Dynamic list of all responsive custom property names c). +// +// 2. Create custom property cascade with fallbacks. This is where the magic happens! 🎩 +// 3. Assign the custom property cascade (stored in the internal custom property 1.a) to the CSS property `$property`. +// 4. Prevent the custom property value from being inherited by **any child elements** in case of nesting. Write out all +// custom properties at once to reduce the number of declaration blocks. +// +@mixin create-cascade( + $property, + $input-custom-property-base-name, + $breakpoints, + $fallback-value, + $prevent-inheritance: true +) { + $internal-custom-property-name: --#{$input-custom-property-base-name}-internal; // 1.a + $default-custom-property-name: --#{$input-custom-property-base-name}-default-internal; // 1.b + $responsive-custom-property-name: --#{$input-custom-property-base-name}; // 1.c + $responsive-custom-property-names: ($responsive-custom-property-name); // 1.d + + // 2. + $fallback: if( + meta.type-of($fallback-value) == 'map', + var(#{$default-custom-property-name}, #{map.get($fallback-value, 'mobile')}), + $fallback-value + ); + + @each $breakpoint-name, $breakpoint-value in $breakpoints { + @if $breakpoint-value > 0 { + $responsive-custom-property-name: '--#{$input-custom-property-base-name}-#{$breakpoint-name}'; + + @if $prevent-inheritance { + $responsive-custom-property-names: list.append( + $responsive-custom-property-names, + $responsive-custom-property-name + ); + } + } + + @include breakpoint.up($breakpoint-value) { + @if meta.type-of($fallback-value) == 'map' { + #{$default-custom-property-name}: #{map.get($fallback-value, $breakpoint-name)}; + } + + // 2. + #{$internal-custom-property-name}: var(#{string.unquote($responsive-custom-property-name)}, #{$fallback}); + + // 3. + @if $breakpoint-value == 0 { + #{$property}: var(#{$internal-custom-property-name}); + } + } + + // 2. + $fallback: var(#{string.unquote($responsive-custom-property-name)}, #{$fallback}); + } + + @if $prevent-inheritance { + // stylelint-disable-next-line selector-max-universal -- 4. + & > * { + @each $property-name in $responsive-custom-property-names { + #{$property-name}: initial; + } + } + } +} diff --git a/packages/web/src/scss/tools/_spacing.scss b/packages/web/src/scss/tools/_spacing.scss deleted file mode 100644 index 7a79fedd7f..0000000000 --- a/packages/web/src/scss/tools/_spacing.scss +++ /dev/null @@ -1,57 +0,0 @@ -@use 'sass:map'; -@use 'sass:meta'; -@use 'sass:string'; -@use 'breakpoint'; - -// Mixin to generate responsive spacing -// Parameters are: -// * $input-property-base-name: the base name of the responsive CSS custom property set via inline styles (API) -// * $output-property-name: the name of the output CSS custom property consumed by CSS -// * $property: the CSS property to apply the spacing to -// * $breakpoints: the breakpoints map -// * $default-spacing: the default spacing value; CSS length or breakpoint-to-length map -@mixin create($input-property-base-name, $output-property-name, $property: 'gap', $breakpoints, $default-spacing: 0) { - $responsive-spacing-default-name: #{$output-property-name}-default; - $fallback: if( - meta.type-of($default-spacing) == 'map', - var(#{$responsive-spacing-default-name}, #{map.get($default-spacing, 'mobile')}), - $default-spacing - ); - $new-property-name: $input-property-base-name; - - @each $breakpoint-name, $breakpoint-value in $breakpoints { - @if $breakpoint-value > 0 { - $new-property-name: '#{$input-property-base-name}-#{$breakpoint-name}'; - } - - @include breakpoint.up($breakpoint-value) { - @if meta.type-of($default-spacing) == 'map' { - #{$responsive-spacing-default-name}: #{map.get($default-spacing, $breakpoint-name)}; - } - - #{$output-property-name}: var(#{string.unquote($new-property-name)}, #{$fallback}); - - @if $breakpoint-value == 0 { - #{$property}: var(#{$output-property-name}); - } - } - - $fallback: var(#{string.unquote($new-property-name)}, #{$fallback}); - } -} - -// Mixin to prevent inheritance of spacing -// Parameters are: -// * $input-property-base-names: list of base names (i.e. without responsive suffix) of the CSS custom properties to reset -// * $breakpoints: the breakpoints map -@mixin prevent-inheritance($input-property-base-names, $breakpoints) { - @each $breakpoint-name, $breakpoint-value in $breakpoints { - @each $property-base-name in $input-property-base-names { - @if $breakpoint-value > 0 { - #{$property-base-name}-#{$breakpoint-name}: initial; - } @else { - #{$property-base-name}: initial; - } - } - } -}