diff --git a/packages/web/src/scss/tools/__tests__/_dictionaries.test.scss b/packages/web/src/scss/tools/__tests__/_dictionaries.test.scss index 4b81087f38..86b880bc46 100644 --- a/packages/web/src/scss/tools/__tests__/_dictionaries.test.scss +++ b/packages/web/src/scss/tools/__tests__/_dictionaries.test.scss @@ -107,24 +107,24 @@ inset: auto auto 100% 0; translate: var(--test-offset-orthogonal, 0) calc(-1 * var(--test-offset, 0)); - transform-origin: bottom; + transform-origin: bottom left; } } } @include true.assert { @include true.output { - @include dictionaries.generate-placements('Test', ('top-start')); + @include dictionaries.generate-placements('Test', ('right-end')); } @include true.expect { - .Test[data-spirit-placement='top-start'], - .Test--topStart { + .Test[data-spirit-placement='right-end'], + .Test--rightEnd { --test-offset: 0; - inset: auto auto 100% 0; - translate: var(--test-offset-orthogonal, 0) calc(-1 * var(--test-offset, 0)); - transform-origin: bottom; + inset: auto auto 0 100%; + translate: var(--test-offset, 0) var(--test-offset-orthogonal, 0); + transform-origin: left bottom; } } } diff --git a/packages/web/src/scss/tools/__tests__/_placement.test.scss b/packages/web/src/scss/tools/__tests__/_placement.test.scss index 248353f851..97949bb703 100644 --- a/packages/web/src/scss/tools/__tests__/_placement.test.scss +++ b/packages/web/src/scss/tools/__tests__/_placement.test.scss @@ -3,23 +3,143 @@ @use '../placement'; @include true.describe('placement functions and mixins') { - @include true.it('should return correct inverse placements') { + @include true.it('should return correct cross axis direction') { @include true.assert { @include true.output { - $inverse-top: placement.inverse('top'); - $inverse-bottom-start: placement.inverse('bottom-start'); - $inverse-left-end: placement.inverse('left-end'); + --cross-axis-direction-left: #{placement.get-cross-axis-direction('left')}; + --cross-axis-direction-bottom: #{placement.get-cross-axis-direction('bottom')}; + } + @include true.expect { + --cross-axis-direction-left: vertical; + --cross-axis-direction-bottom: horizontal; + } + } + } + + @include true.it('should return true for logical placements, false otherwise') { + @include true.assert { + @include true.output { + --is-logical-left: #{placement.is-logical('left')}; + --is-logical-start: #{placement.is-logical('start')}; + } + @include true.expect { + --is-logical-left: false; + --is-logical-start: true; + } + } + } + + @include true.it('should return correct physical placement') { + @include true.assert { + @include true.output { + --logical-to-physical-start-horizontal: #{placement.translate-logical-to-physical('start', 'horizontal')}; + --logical-to-physical-start-vertical: #{placement.translate-logical-to-physical('start', 'vertical')}; + } + @include true.expect { + --logical-to-physical-start-horizontal: left; + --logical-to-physical-start-vertical: top; + } + } + } - // Testing with main-axis-only and hyphenation - $inverse-top-start-hyphen: placement.inverse('top-start', true, true); - $inverse-top-start-space: placement.inverse('top-start', true, false); + @include true.it('should return transformed axis') { + @include true.assert { + @include true.output { + --transform-axis-left: #{placement.transform-axis('left')}; + --transform-axis-left-inverse: #{placement.transform-axis('left', $inverse: true)}; + --transform-axis-left-physical: #{placement.transform-axis('left', $physical-direction: 'horizontal')}; + --transform-axis-left-physical-inverse: #{placement.transform-axis( + 'left', + $inverse: true, + $physical-direction: 'horizontal' + )}; + --transform-axis-start: #{placement.transform-axis('start')}; + --transform-axis-start-inverse: #{placement.transform-axis('start', $inverse: true)}; + --transform-axis-start-physical: #{placement.transform-axis('start', $physical-direction: 'horizontal')}; + --transform-axis-start-physical-inverse: #{placement.transform-axis( + 'start', + $inverse: true, + $physical-direction: 'horizontal' + )}; } @include true.expect { - $inverse-top: 'bottom'; - $inverse-bottom-start: 'top'; - $inverse-left-end: 'right'; - $inverse-top-start-hyphen: 'bottom-start'; - $inverse-top-start-space: 'bottom start'; + --transform-axis-left: left; + --transform-axis-left-inverse: right; + --transform-axis-left-physical: left; + --transform-axis-left-physical-inverse: right; + --transform-axis-start: start; + --transform-axis-start-inverse: end; + --transform-axis-start-physical: left; + --transform-axis-start-physical-inverse: right; + } + } + } + + @include true.describe('should return transformed placement') { + @include true.it('physical only') { + @include true.assert { + @include true.output { + --transform-left: #{placement.transform('left')}; + --transform-left-top: #{placement.transform('left-top')}; + --transform-left-top-main-axis-inverse: #{placement.transform('left-top', $main-axis-inverse: true)}; + --transform-left-top-main-axis-inverse-cross-axis-inverse: #{placement.transform( + 'left-top', + $main-axis-inverse: true, + $cross-axis-inverse: true + )}; + --transform-left-top-main-axis-inverse-cross-axis-inverse-physical: #{placement.transform( + 'left-top', + $main-axis-inverse: true, + $cross-axis-inverse: true, + $cross-axis-physical: true + )}; + --transform-left-top-spaces: #{placement.transform('left-top', $join-with: ' ')}; + } + @include true.expect { + --transform-left: left; + --transform-left-top: left-top; + --transform-left-top-main-axis-inverse: right-top; + --transform-left-top-main-axis-inverse-cross-axis-inverse: right-bottom; + --transform-left-top-main-axis-inverse-cross-axis-inverse-physical: right-bottom; + --transform-left-top-spaces: left top; + } + } + } + + @include true.it('physical and logical') { + @include true.assert { + @include true.output { + --transform-top-start: #{placement.transform('top-start')}; + --transform-top-start-main-axis-inverse: #{placement.transform( + 'top-start', + $main-axis-inverse: true + )}; + --transform-top-start-main-axis-inverse-cross-axis-inverse: #{placement.transform( + 'top-start', + $main-axis-inverse: true, + $cross-axis-inverse: true + )}; + --transform-top-start-main-axis-inverse-cross-axis-physical: #{placement.transform( + 'top-start', + $main-axis-inverse: true, + $cross-axis-physical: true + )}; + --transform-top-start-main-axis-inverse-cross-axis-inverse-physical: #{placement.transform( + 'top-start', + $main-axis-inverse: true, + $cross-axis-inverse: true, + $cross-axis-physical: true + )}; + --transform-top-start-spaces: #{placement.transform('top-start', $join-with: ' ')}; + } + @include true.expect { + --transform-top-start: top-start; + --transform-top-start-main-axis-inverse: bottom-start; + --transform-top-start-main-axis-inverse-cross-axis-inverse: bottom-end; + --transform-top-start-main-axis-inverse-cross-axis-physical: bottom-left; + --transform-top-start-main-axis-inverse-cross-axis-inverse-physical: bottom-right; + --transform-top-start-spaces: top start; + } } } } diff --git a/packages/web/src/scss/tools/_dictionaries.scss b/packages/web/src/scss/tools/_dictionaries.scss index 391f1b84e8..20f1c57a32 100644 --- a/packages/web/src/scss/tools/_dictionaries.scss +++ b/packages/web/src/scss/tools/_dictionaries.scss @@ -193,7 +193,7 @@ // @deprecated CSS modifier classes are deprecated and will be removed in the next major version // Migration: delete this selector .#{$class-name}--#{$placement-modifier} > .#{$class-name}__arrow { - @include placement.arrow-variant($prefix, placement.inverse($placement, $main-axis-only: true)); + @include placement.arrow-variant($prefix, placement.transform($placement, $main-axis-inverse: true)); } } } diff --git a/packages/web/src/scss/tools/_placement.scss b/packages/web/src/scss/tools/_placement.scss index 8cec4f726b..7d95bb393b 100644 --- a/packages/web/src/scss/tools/_placement.scss +++ b/packages/web/src/scss/tools/_placement.scss @@ -19,6 +19,8 @@ @use 'sass:string'; @use 'list' as spirit-list; +$_hyphen: '-'; + $_arrow-rotate-map: ( top: 0deg, top-start: 0deg, @@ -50,13 +52,29 @@ $_inset-map: ( right-end: auto auto 0 100%, ); -$_placement-inverse-map: ( +$_physical-placement-inverse-map: ( top: bottom, bottom: top, left: right, right: left, ); +$_logical-placement-inverse-map: ( + start: end, + end: start, +); + +$_logical-to-physical-placement-map: ( + horizontal: ( + start: left, + end: right, + ), + vertical: ( + start: top, + end: bottom, + ), +); + @function -get-arrow-translate-map($prefix) { @return ( top: -50% 0%, @@ -102,31 +120,82 @@ $_placement-inverse-map: ( ); } -// Function to return an inverse placement name -// Example: inverse('top') will return 'bottom' -// Example: inverse('top-start') will return 'bottom' -// Example: inverse('top-start', true) will return 'bottom-start' -// Example: inverse('top-start', true, false) will return 'bottom start' -@function inverse($placement, $main-axis-only: false, $hyphenate: true) { - $hyphen: '-'; - $placement-chunks: string.split($placement, $hyphen); - $placement-inverse-chunks: (); - $join-with: if($hyphenate, $hyphen, ' '); - - @for $i from 1 through list.length($placement-chunks) { - $chunk: list.nth($placement-chunks, $i); - $new-chunk: null; - - @if $main-axis-only and $i > 1 { - $new-chunk: $chunk; - } @else { - $new-chunk: map.get($_placement-inverse-map, $chunk); +// Function to get the cross-axis direction +// Example: get-cross-axis-direction('left') will return 'vertical' +// Example: get-cross-axis-direction('bottom') will return 'horizontal' +@function get-cross-axis-direction($main-axis) { + @return if(string.index($main-axis, 'left') or string.index($main-axis, 'right'), 'vertical', 'horizontal'); +} + +// Function to check if a placement is logical +// Example: is-logical('top') will return false +// Example: is-logical('start') will return true +@function is-logical($placement) { + @return if(string.index($placement, 'start') or string.index($placement, 'end'), true, false); +} + +// Function to translate logical placement to its physical equivalent +// Example: translate-logical-to-physical('start', $direction: 'horizontal') will return 'left' +@function translate-logical-to-physical($logical, $direction) { + @return map.get($_logical-to-physical-placement-map, $direction, $logical); +} + +// Function to transform axis +// Example: transform-axis('left') will return 'left' (no transformation) +// Example: transform-axis('left', $inverse: true) will return 'right' +// Example: transform-axis('start', $inverse: false, $physical-direction: 'horizontal') will return 'left' +// Example: transform-axis('start', $inverse: true, $physical-direction: 'horizontal') will return 'right' +@function transform-axis($axis, $inverse: false, $physical-direction: null) { + $axis-transformed: $axis; + + @if is-logical($axis-transformed) { + @if $inverse { + $axis-transformed: map.get($_logical-placement-inverse-map, $axis); + } + + @if $physical-direction { + $axis-transformed: translate-logical-to-physical($axis-transformed, $physical-direction); } + } @else if $inverse { + $axis-transformed: map.get($_physical-placement-inverse-map, $axis); + } + + @return $axis-transformed; +} + +// Function to transform placement +// Example: transform('top-start') will return 'top-start' (no transformation) +// Example: transform('top-start', $main-axis-inverse: true) will return 'bottom-start' +// Example: transform('top-start', $main-axis-inverse: true, $cross-axis-inverse: true) will return 'bottom-end' +// Example: transform('top-start', $main-axis-inverse: true, $cross-axis-inverse: true, $cross-axis-physical: true) will return 'bottom-right' +// Example: transform('top-start', $join-with: ' ') will return 'top start' +@function transform( + $placement, + $main-axis-inverse: false, + $cross-axis-inverse: false, + $cross-axis-physical: false, + $join-with: $_hyphen +) { + $placement-chunks: string.split($placement, $_hyphen); + $main-axis: list.nth($placement-chunks, 1); + $main-axis-transformed: transform-axis( + $axis: $main-axis, + $inverse: $main-axis-inverse, + ); - $placement-inverse-chunks: list.append($placement-inverse-chunks, $new-chunk); + @if list.length($placement-chunks) > 1 { + $cross-axis: list.nth($placement-chunks, 2); + $cross-axis-direction: get-cross-axis-direction($main-axis); + $cross-axis-transformed: transform-axis( + $axis: $cross-axis, + $inverse: $cross-axis-inverse, + $physical-direction: if($cross-axis-physical, $cross-axis-direction, null), + ); + + @return spirit-list.to-string(($main-axis-transformed, $cross-axis-transformed), $join-with); } - @return spirit-list.to-string($placement-inverse-chunks, $join-with); + @return $main-axis-transformed; } @mixin parent() { @@ -143,7 +212,9 @@ $_placement-inverse-map: ( inset: map.get($_inset-map, $placement); // 1.2 translate: map.get(-get-child-translate-map($prefix), $placement); // 1.3 - transform-origin: string.unquote(inverse($placement, $hyphenate: false)); + transform-origin: string.unquote( + transform($placement, $main-axis-inverse: true, $cross-axis-physical: true, $join-with: ' ') + ); } @mixin child-controlled($prefix, $offset) {