diff --git a/.changeset/focus-ring-mixin.md b/.changeset/focus-ring-mixin.md new file mode 100644 index 000000000..7dce5c2e9 --- /dev/null +++ b/.changeset/focus-ring-mixin.md @@ -0,0 +1,5 @@ +--- +"@tabler/core": patch +--- + +Updated `focus-ring()` to be the single focus mixin and added a runtime `--focus-ring-box-shadow` token. diff --git a/BOOTSTRAP-V6-MIGRATION.md b/BOOTSTRAP-V6-MIGRATION.md index 3e6d81dff..458d3b667 100644 --- a/BOOTSTRAP-V6-MIGRATION.md +++ b/BOOTSTRAP-V6-MIGRATION.md @@ -253,6 +253,15 @@ of `color-scheme`, the token-dump verification harness) but not its code. - Decide on `$spacers`: adopting the v6 0–9 scale silently changes every `.p-3` / `.m-4` in every Tabler template. Recommendation: keep the v5 scale, add finer steps at new keys. - Replace all `$*-focus-box-shadow` with one `focus-ring()` mixin over `--focus-ring*` tokens. + Done (#2972). Step 1: `focus-ring()` is the one implementation and every focus consumer calls it; + the `$*-focus-box-shadow` variables stay as `!default` aliases; CSS byte-identical. Step 2: `:root` + emits `--focus-ring-inner-width` and `--focus-ring-box-shadow`, the mixin reads the token so the + ring retunes at runtime and per theme, and the `.focus-ring` helper stops referencing the phantom + `--focus-ring-x/-y/-blur`; the Step 2 CSS diff is limited to focus declarations and the `:root` + tokens. Tabler keeps the two-layer `box-shadow` look, not v6's `outline`, and the `forced-colors` + fallback in `layout/_accessibility.scss` stays. `.form-select` stays a single-layer exception, and + the three `outline: 2px solid` rules (`.btn-action`, `.date-item`, `.switch-icon`) are not + converted — both are follow-ups. - Consider the layered shadow scale (`.shadow-xs`…`.shadow-xl`) and `.border-keyline`. - Skip the `.fs-*` renaming — it inverts the scale direction and breaks every page. diff --git a/core/scss/_variables.scss b/core/scss/_variables.scss index 9dd885397..747cf18d7 100644 --- a/core/scss/_variables.scss +++ b/core/scss/_variables.scss @@ -1017,11 +1017,12 @@ $focus-ring-width: 0.25rem !default; $focus-ring-opacity: 0.25 !default; $focus-ring-color: color-mix(in srgb, var(--primary) #{math.percentage($focus-ring-opacity)}, transparent) !default; $focus-ring-blur: 0 !default; -// Inner 2px solid ring keeps the indicator above the 3:1 non-text contrast minimum; -// the translucent halo preserves the soft look -$focus-ring-box-shadow: - 0 0 0 2px var(--primary), - 0 0 $focus-ring-blur $focus-ring-width $focus-ring-color !default; +$focus-ring-inner-width: 2px !default; +// The literal, emitted once as `--focus-ring-box-shadow` (`bootstrap/_root.scss`). +$focus-ring-box-shadow-value: + 0 0 0 var(--focus-ring-inner-width) var(--primary), + 0 0 $focus-ring-blur var(--focus-ring-width) var(--focus-ring-color) !default; +$focus-ring-box-shadow: var(--focus-ring-box-shadow) !default; // Transitions $transition-base: all 0.2s ease-in-out !default; diff --git a/core/scss/bootstrap/_nav.scss b/core/scss/bootstrap/_nav.scss index 43b576a5d..b0ab8f98c 100644 --- a/core/scss/bootstrap/_nav.scss +++ b/core/scss/bootstrap/_nav.scss @@ -41,8 +41,7 @@ } &:focus-visible { - outline: 0; - box-shadow: $nav-link-focus-box-shadow; + @include focus-ring($shadow: $nav-link-focus-box-shadow); } // Disabled state lightens text diff --git a/core/scss/bootstrap/_root.scss b/core/scss/bootstrap/_root.scss index 0e9e2e16a..1a7a26fc8 100644 --- a/core/scss/bootstrap/_root.scss +++ b/core/scss/bootstrap/_root.scss @@ -142,6 +142,8 @@ --focus-ring-width: #{$focus-ring-width}; --focus-ring-opacity: #{$focus-ring-opacity}; --focus-ring-color: #{$focus-ring-color}; + --focus-ring-inner-width: #{$focus-ring-inner-width}; + --focus-ring-box-shadow: #{$focus-ring-box-shadow-value}; // scss-docs-end root-focus-variables // scss-docs-start root-form-validation-variables diff --git a/core/scss/bootstrap/forms/_form-check.scss b/core/scss/bootstrap/forms/_form-check.scss index 04e6e12a4..62f774189 100644 --- a/core/scss/bootstrap/forms/_form-check.scss +++ b/core/scss/bootstrap/forms/_form-check.scss @@ -61,8 +61,7 @@ &:focus { border-color: $form-check-input-focus-border; - outline: 0; - box-shadow: $form-check-input-focus-box-shadow; + @include focus-ring($shadow: $form-check-input-focus-box-shadow); } &:checked { diff --git a/core/scss/bootstrap/forms/_form-range.scss b/core/scss/bootstrap/forms/_form-range.scss index 63f7e0091..cae7ae3e1 100644 --- a/core/scss/bootstrap/forms/_form-range.scss +++ b/core/scss/bootstrap/forms/_form-range.scss @@ -19,10 +19,10 @@ // Pseudo-elements must be split across multiple rulesets to have an effect. // No box-shadow() mixin for focus accessibility. &::-webkit-slider-thumb { - box-shadow: $form-range-thumb-focus-box-shadow; + @include focus-ring($shadow: $form-range-thumb-focus-box-shadow, $outline: null); } &::-moz-range-thumb { - box-shadow: $form-range-thumb-focus-box-shadow; + @include focus-ring($shadow: $form-range-thumb-focus-box-shadow, $outline: null); } } diff --git a/core/scss/helpers/_index.scss b/core/scss/helpers/_index.scss index 2e27153e1..85759cf30 100644 --- a/core/scss/helpers/_index.scss +++ b/core/scss/helpers/_index.scss @@ -141,6 +141,5 @@ // .focus-ring:focus { outline: 0; - // By default, there is no `--bs-focus-ring-x`, `--bs-focus-ring-y`, or `--bs-focus-ring-blur`, but we provide CSS variables with fallbacks to initial `0` values - box-shadow: var(--focus-ring-x, 0) var(--focus-ring-y, 0) var(--focus-ring-blur, 0) var(--focus-ring-width) var(--focus-ring-color); + box-shadow: 0 0 0 var(--focus-ring-width) var(--focus-ring-color); } diff --git a/core/scss/mixins/_mixins.scss b/core/scss/mixins/_mixins.scss index 16fcf7337..63b24cce7 100644 --- a/core/scss/mixins/_mixins.scss +++ b/core/scss/mixins/_mixins.scss @@ -77,14 +77,15 @@ gap: var(--list-gap); } -@mixin focus-ring($show-border: false) { - outline: 0; - box-shadow: - 0 0 0 2px var(--primary), - 0 0 $focus-ring-blur $focus-ring-width color-transparent(var(--primary), 0.25); +@mixin focus-ring($shadow: $focus-ring-box-shadow, $show-border: false, $outline: 0) { + @if $outline != null { + outline: $outline; + } - @if ($show-border) { - border-color: color-transparent(var(--primary), 0.25); + box-shadow: $shadow; + + @if $show-border { + border-color: var(--focus-ring-color); } } diff --git a/core/scss/mixins/bootstrap/_forms.scss b/core/scss/mixins/bootstrap/_forms.scss index 7955b143e..74e4d61b4 100644 --- a/core/scss/mixins/bootstrap/_forms.scss +++ b/core/scss/mixins/bootstrap/_forms.scss @@ -4,6 +4,7 @@ @use '../../settings' as *; @use '../../variables' as *; @use '../functions' as *; +@use '../mixins' as *; @use 'breakpoints' as *; // This mixin uses an `if()` technique to be compatible with Dart Sass // See https://github.com/sass/sass/issues/1873#issuecomment-152293725 for more details @@ -70,7 +71,7 @@ @if $enable-shadows { @include box-shadow($input-box-shadow, $focus-box-shadow); } @else { - box-shadow: $focus-box-shadow; + @include focus-ring($shadow: $focus-box-shadow, $outline: null); } } } @@ -105,7 +106,7 @@ @if $enable-shadows { @include box-shadow($form-select-box-shadow, $focus-box-shadow); } @else { - box-shadow: $focus-box-shadow; + @include focus-ring($shadow: $focus-box-shadow, $outline: null); } } } @@ -128,7 +129,7 @@ } &:focus { - box-shadow: $focus-box-shadow; + @include focus-ring($shadow: $focus-box-shadow, $outline: null); } ~ .form-check-label { diff --git a/core/scss/tests/__snapshots__/css-var-prefix.test.mjs.snap b/core/scss/tests/__snapshots__/css-var-prefix.test.mjs.snap index 924931fbe..ce76818e0 100644 --- a/core/scss/tests/__snapshots__/css-var-prefix.test.mjs.snap +++ b/core/scss/tests/__snapshots__/css-var-prefix.test.mjs.snap @@ -55,6 +55,8 @@ exports[`css custom-property prefixing > leaves vendor-owned names alone and pre "--tblr-chart-3", "--tblr-chart-4", "--tblr-chart-5", + "--tblr-focus-ring-box-shadow", + "--tblr-focus-ring-color", "--tblr-font-sans-serif", "--tblr-font-size-h3", "--tblr-font-size-h5", diff --git a/core/scss/tests/_mixins.test.scss b/core/scss/tests/_mixins.test.scss index 7770b9c8c..126231f86 100644 --- a/core/scss/tests/_mixins.test.scss +++ b/core/scss/tests/_mixins.test.scss @@ -104,7 +104,7 @@ } @include describe('focus-ring mixin') { - @include it('outputs the ring without a border by default') { + @include it('outputs the ring token without a border by default') { @include assert() { @include output() { .t { @@ -114,9 +114,7 @@ @include expect() { .t { outline: 0; - box-shadow: - 0 0 0 2px var(--primary), - 0 0 0 0.25rem color-mix(in srgb, var(--primary) 25%, transparent); + box-shadow: var(--focus-ring-box-shadow); } } } @@ -132,10 +130,23 @@ @include expect() { .t { outline: 0; - box-shadow: - 0 0 0 2px var(--primary), - 0 0 0 0.25rem color-mix(in srgb, var(--primary) 25%, transparent); - border-color: color-mix(in srgb, var(--primary) 25%, transparent); + box-shadow: var(--focus-ring-box-shadow); + border-color: var(--focus-ring-color); + } + } + } + } + + @include it('passes $shadow through and skips the outline reset with $outline: null') { + @include assert() { + @include output() { + .t { + @include focus-ring($shadow: 0 0 0 1px red, $outline: null); + } + } + @include expect() { + .t { + box-shadow: 0 0 0 1px red; } } } diff --git a/core/scss/ui/_forms.scss b/core/scss/ui/_forms.scss index 60e52bed9..aa593bff1 100644 --- a/core/scss/ui/_forms.scss +++ b/core/scss/ui/_forms.scss @@ -207,7 +207,7 @@ Input group .input-group-flat { &:focus-within { - box-shadow: $input-focus-box-shadow; + @include focus-ring($shadow: $input-focus-box-shadow, $outline: null); @include border-radius($input-border-radius); .form-control, diff --git a/core/scss/ui/forms/_form-colorinput.scss b/core/scss/ui/forms/_form-colorinput.scss index e8f94dcea..b75c36107 100644 --- a/core/scss/ui/forms/_form-colorinput.scss +++ b/core/scss/ui/forms/_form-colorinput.scss @@ -46,7 +46,7 @@ Color Input .form-colorinput-input:focus ~ & { border-color: var(--primary); - box-shadow: $input-btn-focus-box-shadow; + @include focus-ring($shadow: $input-btn-focus-box-shadow, $outline: null); } @at-root .form-colorinput-light & { diff --git a/core/scss/ui/forms/_form-imagecheck.scss b/core/scss/ui/forms/_form-imagecheck.scss index ece924180..fa0403811 100644 --- a/core/scss/ui/forms/_form-imagecheck.scss +++ b/core/scss/ui/forms/_form-imagecheck.scss @@ -28,7 +28,7 @@ Image check .form-imagecheck-input:focus ~ & { border-color: var(--primary); - box-shadow: $input-btn-focus-box-shadow; + @include focus-ring($shadow: $input-btn-focus-box-shadow, $outline: null); } .form-imagecheck-input:checked ~ & { diff --git a/core/scss/ui/forms/_form-selectgroup.scss b/core/scss/ui/forms/_form-selectgroup.scss index a789db0d9..f47ed2f4c 100644 --- a/core/scss/ui/forms/_form-selectgroup.scss +++ b/core/scss/ui/forms/_form-selectgroup.scss @@ -111,7 +111,7 @@ Select group z-index: 2; color: var(--primary); border-color: var(--primary); - box-shadow: $input-btn-focus-box-shadow; + @include focus-ring($shadow: $input-btn-focus-box-shadow, $outline: null); } /** diff --git a/core/scss/vendor/_coloris.scss b/core/scss/vendor/_coloris.scss index 396dbb9c3..6ad16dce9 100644 --- a/core/scss/vendor/_coloris.scss +++ b/core/scss/vendor/_coloris.scss @@ -17,7 +17,7 @@ input.clr-color { border-color: var(--border-color); &:focus { - @include focus-ring(true); + @include focus-ring($show-border: true); } } diff --git a/core/scss/vendor/_nouislider.scss b/core/scss/vendor/_nouislider.scss index cc2531598..b68d36686 100644 --- a/core/scss/vendor/_nouislider.scss +++ b/core/scss/vendor/_nouislider.scss @@ -43,7 +43,7 @@ &.noUi-active, &:focus { - box-shadow: $form-range-thumb-focus-box-shadow; + @include focus-ring($shadow: $form-range-thumb-focus-box-shadow, $outline: null); } }