From a488e3a5ab3594ce502ac56e685431a40e005e12 Mon Sep 17 00:00:00 2001 From: Jeremy Elder Date: Wed, 26 Mar 2025 17:01:25 -0500 Subject: [PATCH 1/4] feat(FocusVisible): Use focus-visible --- src/scss/mixins.scss | 45 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/src/scss/mixins.scss b/src/scss/mixins.scss index 5afafdd8d1..e56f9f0f40 100644 --- a/src/scss/mixins.scss +++ b/src/scss/mixins.scss @@ -39,7 +39,7 @@ } /** -* Helper function for :focus +* Helper function for focus styles that supports both :focus and :focus-visible * * @param $size is deprecated and should not be used anymore */ @@ -50,14 +50,49 @@ $inset: false, $focus-ring: $focus-ring, $outline: false, - $outline-offset: $outline-offset + $outline-offset: $outline-offset, + $use-focus-visible: true +) { + + // Create selectors based on whether we're using focus-visible + @if $use-focus-visible { + &:focus-visible { + @include gl-focus-styles($size, $color, $important, $inset, $focus-ring, $outline, $outline-offset); + } + + // Always include :focus for browsers that don't support :focus-visible + @supports not selector(:focus-visible) { + &:focus { + @include gl-focus-styles($size, $color, $important, $inset, $focus-ring, $outline, $outline-offset); + } + } + } @else { + &:focus { + @include gl-focus-styles($size, $color, $important, $inset, $focus-ring, $outline, $outline-offset); + } + } +} + +/** +* Internal helper mixin for focus styling +* Not intended to be used directly +* Not nested due to linting rules +*/ +@mixin gl-focus-styles( + $size, + $color, + $important, + $inset, + $focus-ring, + $outline, + $outline-offset ) { @if $inset == true { @if $color { box-shadow: inset 0 0 0 $outline-width var(--gl-focus-ring-outer-color), - inset 0 0 0 #{$outline-width + $outline-offset} var(--gl-focus-ring-inner-color), - inset 0 0 0 #{$outline-width + $outline-offset + 1px} $color, - $focus-ring-inset if-important($important); + inset 0 0 0 #{$outline-width + $outline-offset} var(--gl-focus-ring-inner-color), + inset 0 0 0 #{$outline-width + $outline-offset + 1px} $color, + $focus-ring-inset if-important($important); outline: none if-important($important); @media (forced-colors: active) { outline: 2px solid LinkText if-important($important); -- GitLab From 8d1b36866ae2e5953dc84e7cabc402b558147d1a Mon Sep 17 00:00:00 2001 From: Jeremy Elder Date: Wed, 26 Mar 2025 17:15:35 -0500 Subject: [PATCH 2/4] feat(FocusVisible): Add focus tests --- src/scss/mixins.spec.scss | 101 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) diff --git a/src/scss/mixins.spec.scss b/src/scss/mixins.spec.scss index 4017340a2e..aeff23e047 100644 --- a/src/scss/mixins.spec.scss +++ b/src/scss/mixins.spec.scss @@ -30,6 +30,107 @@ } } +@include describe('gl-focus') { + + // Test default behavior with focus-visible + @include it('applies focus styles to focus-visible by default') { + @include assert { + @include output { + .test-element { + @include gl-focus(); + } + } + + @include expect { + .test-element:focus-visible { + box-shadow: 0 0 0 1px var(--gl-focus-ring-inner-color), 0 0 0 3px var(--gl-focus-ring-outer-color); + outline: none; + } + + @media (forced-colors: active) { + .test-element:focus-visible { + outline: 2px solid LinkText; + } + } + + @supports not selector(:focus-visible) { + .test-element:focus { + box-shadow: 0 0 0 1px var(--gl-focus-ring-inner-color), 0 0 0 3px var(--gl-focus-ring-outer-color); + outline: none; + } + + @media (forced-colors: active) { + .test-element:focus { + outline: 2px solid LinkText; + } + } + } + } + } + } + + // Test with focus-visible disabled + @include it('applies focus styles to focus when focus-visible is disabled') { + @include assert { + @include output { + .test-element { + @include gl-focus($use-focus-visible: false); + } + } + + @include expect { + .test-element:focus { + box-shadow: 0 0 0 1px var(--gl-focus-ring-inner-color), 0 0 0 3px var(--gl-focus-ring-outer-color); + outline: none; + } + + @media (forced-colors: active) { + .test-element:focus { + outline: 2px solid LinkText; + } + } + } + } + } + + // Test with custom color + @include it('applies custom color focus styles with focus-visible') { + @include assert { + @include output { + .test-element { + @include gl-focus($color: #1f75cb); + } + } + + @include expect { + .test-element:focus-visible { + box-shadow: inset 0 0 0 1px #1f75cb, 0 0 0 1px var(--gl-focus-ring-inner-color), 0 0 0 3px var(--gl-focus-ring-outer-color); + outline: none; + } + + @media (forced-colors: active) { + .test-element:focus-visible { + outline: 2px solid LinkText; + } + } + + @supports not selector(:focus-visible) { + .test-element:focus { + box-shadow: inset 0 0 0 1px #1f75cb, 0 0 0 1px var(--gl-focus-ring-inner-color), 0 0 0 3px var(--gl-focus-ring-outer-color); + outline: none; + } + + @media (forced-colors: active) { + .test-element:focus { + outline: 2px solid LinkText; + } + } + } + } + } + } +} + @include describe('gl-media-breakpoint-up') { @include it('returns no media query for xs') { @include assert { -- GitLab From 943a54abfc1d59d769ec14237d8a4f9403a93dbc Mon Sep 17 00:00:00 2001 From: Jeremy Elder Date: Thu, 27 Mar 2025 10:09:03 -0500 Subject: [PATCH 3/4] feat(FocusVisible): Mixin testing --- src/scss/mixins.scss | 137 ++++++++++++++++++++------------------ src/scss/mixins.spec.scss | 26 -------- 2 files changed, 73 insertions(+), 90 deletions(-) diff --git a/src/scss/mixins.scss b/src/scss/mixins.scss index e56f9f0f40..4739d0096d 100644 --- a/src/scss/mixins.scss +++ b/src/scss/mixins.scss @@ -51,78 +51,87 @@ $focus-ring: $focus-ring, $outline: false, $outline-offset: $outline-offset, - $use-focus-visible: true -) { - - // Create selectors based on whether we're using focus-visible + $use-focus-visible: true) { @if $use-focus-visible { &:focus-visible { - @include gl-focus-styles($size, $color, $important, $inset, $focus-ring, $outline, $outline-offset); - } - - // Always include :focus for browsers that don't support :focus-visible - @supports not selector(:focus-visible) { - &:focus { - @include gl-focus-styles($size, $color, $important, $inset, $focus-ring, $outline, $outline-offset); + @if $inset == true { + @if $color { + box-shadow: inset 0 0 0 $outline-width var(--gl-focus-ring-outer-color), + inset 0 0 0 #{$outline-width + $outline-offset} var(--gl-focus-ring-inner-color), + inset 0 0 0 #{$outline-width + $outline-offset + 1px} $color, + $focus-ring-inset if-important($important); + outline: none if-important($important); + @media (forced-colors: active) { + outline: 2px solid LinkText if-important($important); + } + } @else if $outline == true { + outline: $focus-ring-outline if-important($important); + outline-offset: $outline-offset; + } @else { + box-shadow: inset 0 0 0 $outline-width var(--gl-focus-ring-outer-color), + $focus-ring-inset if-important($important); + outline: none if-important($important); + @media (forced-colors: active) { + outline: 2px solid LinkText if-important($important); + } + } + } @else if $color { + box-shadow: inset 0 0 0 $gl-border-size-1 $color, $focus-ring if-important($important); + outline: none if-important($important); + @media (forced-colors: active) { + outline: 2px solid LinkText if-important($important); + } + } @else if $outline == true { + outline: $focus-ring-outline if-important($important); + outline-offset: $outline-offset; + } @else { + box-shadow: $focus-ring if-important($important); + outline: none if-important($important); + @media (forced-colors: active) { + outline: 2px solid LinkText if-important($important); + } } } } @else { &:focus { - @include gl-focus-styles($size, $color, $important, $inset, $focus-ring, $outline, $outline-offset); - } - } -} - -/** -* Internal helper mixin for focus styling -* Not intended to be used directly -* Not nested due to linting rules -*/ -@mixin gl-focus-styles( - $size, - $color, - $important, - $inset, - $focus-ring, - $outline, - $outline-offset -) { - @if $inset == true { - @if $color { - box-shadow: inset 0 0 0 $outline-width var(--gl-focus-ring-outer-color), - inset 0 0 0 #{$outline-width + $outline-offset} var(--gl-focus-ring-inner-color), - inset 0 0 0 #{$outline-width + $outline-offset + 1px} $color, - $focus-ring-inset if-important($important); - outline: none if-important($important); - @media (forced-colors: active) { - outline: 2px solid LinkText if-important($important); - } - } @else if $outline == true { - outline: $focus-ring-outline if-important($important); - outline-offset: $outline-offset; - } @else { - box-shadow: inset 0 0 0 $outline-width var(--gl-focus-ring-outer-color), - $focus-ring-inset if-important($important); - outline: none if-important($important); - @media (forced-colors: active) { - outline: 2px solid LinkText if-important($important); + @if $inset == true { + @if $color { + box-shadow: inset 0 0 0 $outline-width var(--gl-focus-ring-outer-color), + inset 0 0 0 #{$outline-width + $outline-offset} var(--gl-focus-ring-inner-color), + inset 0 0 0 #{$outline-width + $outline-offset + 1px} $color, + $focus-ring-inset if-important($important); + outline: none if-important($important); + @media (forced-colors: active) { + outline: 2px solid LinkText if-important($important); + } + } @else if $outline ==true { + outline: $focus-ring-outline if-important($important); + outline-offset: $outline-offset; + } @else { + box-shadow: inset 0 0 0 $outline-width var(--gl-focus-ring-outer-color), + $focus-ring-inset if-important($important); + outline: none if-important($important); + @media (forced-colors: active) { + outline: 2px solid LinkText if-important($important); + } + } + } @else if $color { + box-shadow: inset 0 0 0 $gl-border-size-1 $color, $focus-ring if-important($important); + outline: none if-important($important); + @media (forced-colors: active) { + outline: 2px solid LinkText if-important($important); + } + } @else if $outline == true { + outline: $focus-ring-outline if-important($important); + outline-offset: $outline-offset; + } @else { + box-shadow: $focus-ring if-important($important); + outline: none if-important($important); + @media (forced-colors: active) { + outline: 2px solid LinkText if-important($important); + } } } - } @else if $color { - box-shadow: inset 0 0 0 $gl-border-size-1 $color, $focus-ring if-important($important); - outline: none if-important($important); - @media (forced-colors: active) { - outline: 2px solid LinkText if-important($important); - } - } @else if $outline == true { - outline: $focus-ring-outline if-important($important); - outline-offset: $outline-offset; - } @else { - box-shadow: $focus-ring if-important($important); - outline: none if-important($important); - @media (forced-colors: active) { - outline: 2px solid LinkText if-important($important); - } } } diff --git a/src/scss/mixins.spec.scss b/src/scss/mixins.spec.scss index aeff23e047..2ad030024f 100644 --- a/src/scss/mixins.spec.scss +++ b/src/scss/mixins.spec.scss @@ -52,19 +52,6 @@ outline: 2px solid LinkText; } } - - @supports not selector(:focus-visible) { - .test-element:focus { - box-shadow: 0 0 0 1px var(--gl-focus-ring-inner-color), 0 0 0 3px var(--gl-focus-ring-outer-color); - outline: none; - } - - @media (forced-colors: active) { - .test-element:focus { - outline: 2px solid LinkText; - } - } - } } } } @@ -113,19 +100,6 @@ outline: 2px solid LinkText; } } - - @supports not selector(:focus-visible) { - .test-element:focus { - box-shadow: inset 0 0 0 1px #1f75cb, 0 0 0 1px var(--gl-focus-ring-inner-color), 0 0 0 3px var(--gl-focus-ring-outer-color); - outline: none; - } - - @media (forced-colors: active) { - .test-element:focus { - outline: 2px solid LinkText; - } - } - } } } } -- GitLab From c6e7afc21599211b0b9e9f83fd971ccff48e4a99 Mon Sep 17 00:00:00 2001 From: Jeremy Elder Date: Thu, 27 Mar 2025 11:30:37 -0500 Subject: [PATCH 4/4] feat(FocusVisible): Reset changes --- src/scss/mixins.scss | 160 +++++++++++++++++--------------------- src/scss/mixins.spec.scss | 87 +++------------------ 2 files changed, 81 insertions(+), 166 deletions(-) diff --git a/src/scss/mixins.scss b/src/scss/mixins.scss index 4739d0096d..e1773d13e4 100644 --- a/src/scss/mixins.scss +++ b/src/scss/mixins.scss @@ -39,100 +39,68 @@ } /** -* Helper function for focus styles that supports both :focus and :focus-visible +* Helper function for :focus * * @param $size is deprecated and should not be used anymore */ -@mixin gl-focus( - $size: null, +@mixin gl-focus($size: null, $color: false, $important: false, $inset: false, $focus-ring: $focus-ring, $outline: false, - $outline-offset: $outline-offset, - $use-focus-visible: true) { - @if $use-focus-visible { - &:focus-visible { - @if $inset == true { - @if $color { - box-shadow: inset 0 0 0 $outline-width var(--gl-focus-ring-outer-color), - inset 0 0 0 #{$outline-width + $outline-offset} var(--gl-focus-ring-inner-color), - inset 0 0 0 #{$outline-width + $outline-offset + 1px} $color, - $focus-ring-inset if-important($important); - outline: none if-important($important); - @media (forced-colors: active) { - outline: 2px solid LinkText if-important($important); - } - } @else if $outline == true { - outline: $focus-ring-outline if-important($important); - outline-offset: $outline-offset; - } @else { - box-shadow: inset 0 0 0 $outline-width var(--gl-focus-ring-outer-color), - $focus-ring-inset if-important($important); - outline: none if-important($important); - @media (forced-colors: active) { - outline: 2px solid LinkText if-important($important); - } - } - } @else if $color { - box-shadow: inset 0 0 0 $gl-border-size-1 $color, $focus-ring if-important($important); - outline: none if-important($important); - @media (forced-colors: active) { - outline: 2px solid LinkText if-important($important); - } - } @else if $outline == true { - outline: $focus-ring-outline if-important($important); - outline-offset: $outline-offset; - } @else { - box-shadow: $focus-ring if-important($important); - outline: none if-important($important); - @media (forced-colors: active) { - outline: 2px solid LinkText if-important($important); - } + $outline-offset: $outline-offset) { + @if $inset ==true { + @if $color { + box-shadow: inset 0 0 0 $outline-width var(--gl-focus-ring-outer-color), + inset 0 0 0 #{$outline-width + $outline-offset} var(--gl-focus-ring-inner-color), + inset 0 0 0 #{$outline-width + $outline-offset + 1px} $color, + $focus-ring-inset if-important($important); + outline: none if-important($important); + + @media (forced-colors: active) { + outline: 2px solid LinkText if-important($important); } } - } @else { - &:focus { - @if $inset == true { - @if $color { - box-shadow: inset 0 0 0 $outline-width var(--gl-focus-ring-outer-color), - inset 0 0 0 #{$outline-width + $outline-offset} var(--gl-focus-ring-inner-color), - inset 0 0 0 #{$outline-width + $outline-offset + 1px} $color, - $focus-ring-inset if-important($important); - outline: none if-important($important); - @media (forced-colors: active) { - outline: 2px solid LinkText if-important($important); - } - } @else if $outline ==true { - outline: $focus-ring-outline if-important($important); - outline-offset: $outline-offset; - } @else { - box-shadow: inset 0 0 0 $outline-width var(--gl-focus-ring-outer-color), - $focus-ring-inset if-important($important); - outline: none if-important($important); - @media (forced-colors: active) { - outline: 2px solid LinkText if-important($important); - } - } - } @else if $color { - box-shadow: inset 0 0 0 $gl-border-size-1 $color, $focus-ring if-important($important); - outline: none if-important($important); - @media (forced-colors: active) { - outline: 2px solid LinkText if-important($important); - } - } @else if $outline == true { - outline: $focus-ring-outline if-important($important); - outline-offset: $outline-offset; - } @else { - box-shadow: $focus-ring if-important($important); - outline: none if-important($important); - @media (forced-colors: active) { - outline: 2px solid LinkText if-important($important); - } + + @else if $outline ==true { + outline: $focus-ring-outline if-important($important); + outline-offset: $outline-offset; + } + + @else { + box-shadow: inset 0 0 0 $outline-width var(--gl-focus-ring-outer-color), + $focus-ring-inset if-important($important); + outline: none if-important($important); + + @media (forced-colors: active) { + outline: 2px solid LinkText if-important($important); } } } + + @else if $color { + box-shadow: inset 0 0 0 $gl-border-size-1 $color, $focus-ring if-important($important); + outline: none if-important($important); + + @media (forced-colors: active) { + outline: 2px solid LinkText if-important($important); + } + } + + @else if $outline ==true { + outline: $focus-ring-outline if-important($important); + outline-offset: $outline-offset; + } + + @else { + box-shadow: $focus-ring if-important($important); + outline: none if-important($important); + + @media (forced-colors: active) { + outline: 2px solid LinkText if-important($important); + } + } } @mixin gl-bg-gradient-blur($direction, $color) { @@ -147,14 +115,18 @@ */ @mixin gl-media-breakpoint-up($name) { $min: map-get($breakpoints, $name); - @if $min == null { + + @if $min ==null { @error "#{$name} is not a valid breakpoint for this @media query."; } - @if $min != 0 { + + @if $min !=0 { @media (min-width: $min) { @content; } - } @else { + } + + @else { @content; } } @@ -172,9 +144,11 @@ */ @mixin gl-media-breakpoint-down($name) { $max: map-get($breakpoints, $name); - @if ($max == null or $max == 0) { + + @if ($max ==null or $max ==0) { @error "#{$name} is not a valid breakpoint for this @media query."; } + // The maximum value is reduced by 0.02px to work around the limitations of // `min-` and `max-` prefixes and with fractional viewport sizes. // See: https://www.w3.org/TR/mediaqueries-4/#mq-min-max @@ -195,17 +169,23 @@ * @param $fixed Boolean toggle default and fixed font size scales */ @function get-font-size-variable($size, $fixed) { - @if $fixed == true { + @if $fixed ==true { @if map-has-key($gl-font-sizes-fixed, $size) { @return map-get($gl-font-sizes-fixed, $size); - } @else { + } + + @else { @error "#{$size} is not a valid fixed font size property"; @return null; } - } @else { + } + + @else { @if map-has-key($gl-font-sizes, $size) { @return map-get($gl-font-sizes, $size); - } @else { + } + + @else { @error "#{$size} is not a valid font size property"; @return null; } @@ -273,4 +253,4 @@ background-color: var(--gl-action-selected-background-color-active); border-color: var(--gl-action-selected-border-color-active); } -} +} \ No newline at end of file diff --git a/src/scss/mixins.spec.scss b/src/scss/mixins.spec.scss index 2ad030024f..06d81dcf44 100644 --- a/src/scss/mixins.spec.scss +++ b/src/scss/mixins.spec.scss @@ -8,6 +8,7 @@ @include output { @include gl-fluid-font-size(2rem, 3.5rem); } + @include expect { // prettier-ignore font-size: clamp(2rem, -0.6666666667rem + 5.5555555556vw, 3.5rem); @@ -22,6 +23,7 @@ @include output { @include gl-fluid-line-height(2rem, 3.5rem); } + @include expect { // prettier-ignore line-height: clamp(2rem, -0.6666666667rem + 5.5555555556vw, 3.5rem); @@ -30,81 +32,6 @@ } } -@include describe('gl-focus') { - - // Test default behavior with focus-visible - @include it('applies focus styles to focus-visible by default') { - @include assert { - @include output { - .test-element { - @include gl-focus(); - } - } - - @include expect { - .test-element:focus-visible { - box-shadow: 0 0 0 1px var(--gl-focus-ring-inner-color), 0 0 0 3px var(--gl-focus-ring-outer-color); - outline: none; - } - - @media (forced-colors: active) { - .test-element:focus-visible { - outline: 2px solid LinkText; - } - } - } - } - } - - // Test with focus-visible disabled - @include it('applies focus styles to focus when focus-visible is disabled') { - @include assert { - @include output { - .test-element { - @include gl-focus($use-focus-visible: false); - } - } - - @include expect { - .test-element:focus { - box-shadow: 0 0 0 1px var(--gl-focus-ring-inner-color), 0 0 0 3px var(--gl-focus-ring-outer-color); - outline: none; - } - - @media (forced-colors: active) { - .test-element:focus { - outline: 2px solid LinkText; - } - } - } - } - } - - // Test with custom color - @include it('applies custom color focus styles with focus-visible') { - @include assert { - @include output { - .test-element { - @include gl-focus($color: #1f75cb); - } - } - - @include expect { - .test-element:focus-visible { - box-shadow: inset 0 0 0 1px #1f75cb, 0 0 0 1px var(--gl-focus-ring-inner-color), 0 0 0 3px var(--gl-focus-ring-outer-color); - outline: none; - } - - @media (forced-colors: active) { - .test-element:focus-visible { - outline: 2px solid LinkText; - } - } - } - } - } -} - @include describe('gl-media-breakpoint-up') { @include it('returns no media query for xs') { @include assert { @@ -113,11 +40,13 @@ color: $green-100; } } + @include expect { color: $green-100; } } } + @include it('returns min-width media query for sm') { @include assert { @include output { @@ -125,6 +54,7 @@ color: $blue-100; } } + @include expect { @media (min-width: '576px') { color: $blue-100; @@ -138,11 +68,13 @@ @include it('returns max-width media query for lg') { @include assert { @include output { + // stylelint-disable-next-line @gitlab/no-gl-media-breakpoint-down @include gl-media-breakpoint-down(lg) { color: $red-100; } } + @include expect { @media (max-width: '991.98px') { color: $red-100; @@ -150,14 +82,17 @@ } } } + @include it('returns max-width media query for md') { @include assert { @include output { + // stylelint-disable-next-line @gitlab/no-gl-media-breakpoint-down @include gl-media-breakpoint-down(md) { color: $orange-100; } } + @include expect { @media (max-width: '767.98px') { color: $orange-100; @@ -165,4 +100,4 @@ } } } -} +} \ No newline at end of file -- GitLab