diff --git a/media_source/com_media/scss/components/_media-browser.scss b/media_source/com_media/scss/components/_media-browser.scss index f191bf0613317..15502e2757797 100644 --- a/media_source/com_media/scss/components/_media-browser.scss +++ b/media_source/com_media/scss/components/_media-browser.scss @@ -110,6 +110,12 @@ } } +// These controls sit in the corner of a clipped tile, so draw the indicator inside them +.media-browser-select:focus-visible, +.media-browser-actions button:focus-visible { + @include focus-ring(true); +} + .media-browser-actions { position: absolute; top: 4px; diff --git a/media_source/com_media/scss/components/_media-modal.scss b/media_source/com_media/scss/components/_media-modal.scss index d5c3064217a7c..7210116a1b402 100644 --- a/media_source/com_media/scss/components/_media-modal.scss +++ b/media_source/com_media/scss/components/_media-modal.scss @@ -73,6 +73,11 @@ cursor: pointer; opacity: 1; } + &:focus-visible { + opacity: 1; + + @include focus-ring(true); + } } @media (min-width: 576px) { diff --git a/media_source/com_media/scss/components/_media-toolbar.scss b/media_source/com_media/scss/components/_media-toolbar.scss index 14e070f8dbedb..ffc1ab9405fc2 100644 --- a/media_source/com_media/scss/components/_media-toolbar.scss +++ b/media_source/com_media/scss/components/_media-toolbar.scss @@ -10,6 +10,7 @@ border-inline-start: 1px solid $border-color; border-radius: 0 $border-radius 0 0; box-shadow: 0 -1px 0 0 $border-color; + input { padding: .3rem .75rem; } @@ -41,6 +42,11 @@ } } +// Toolbar buttons sit edge to edge, so draw the focus indicator inside each one +.media-toolbar button.media-toolbar-icon:focus-visible { + @include focus-ring(true); +} + .media-view-icons { display: flex; .disabled { diff --git a/media_source/com_media/scss/components/_media-tree.scss b/media_source/com_media/scss/components/_media-tree.scss index 64b262f2c9ed6..5e6dc9d2670a5 100644 --- a/media_source/com_media/scss/components/_media-tree.scss +++ b/media_source/com_media/scss/components/_media-tree.scss @@ -108,9 +108,14 @@ ul.media-tree { text-overflow: ellipsis; white-space: nowrap; cursor: pointer; - &:hover, + &:hover { + background-color: $sidebar-tree-item-hover-bg; + } + // The tree clips overflow, so draw the indicator inside the item &:focus-visible { background-color: $sidebar-tree-item-hover-bg; + + @include focus-ring(true); } } diff --git a/media_source/com_media/scss/media-manager.scss b/media_source/com_media/scss/media-manager.scss index c3325e23779ce..905f94a85a4f8 100644 --- a/media_source/com_media/scss/media-manager.scss +++ b/media_source/com_media/scss/media-manager.scss @@ -1,5 +1,6 @@ // Imports @import "variables"; +@import "../../system/scss/focus-indicator"; // Components @import "components/animations"; diff --git a/media_source/installation/template/scss/template.scss b/media_source/installation/template/scss/template.scss index a42198955f357..54be2ea393ccf 100644 --- a/media_source/installation/template/scss/template.scss +++ b/media_source/installation/template/scss/template.scss @@ -347,6 +347,11 @@ body { display: none; } +.btn { + // Variants only set the shadow colour, so this applies the focus indicator to all of them + --btn-focus-box-shadow: #{$focus-indicator-ring}; +} + .btn-primary { color: var(--btn-primary-color); background-color: var(--btn-primary-bg); diff --git a/media_source/system/scss/_focus-indicator.scss b/media_source/system/scss/_focus-indicator.scss new file mode 100644 index 0000000000000..88ddd9989b7d5 --- /dev/null +++ b/media_source/system/scss/_focus-indicator.scss @@ -0,0 +1,42 @@ +// Keyboard focus indicator shared by the core templates and extensions. +// The values come from the --focus-indicator-* custom properties each template defines, with +// fallbacks for templates that don't define them. The coloured ring is separated from the +// control by a contrasting ring so that it stays visible on both light and dark backgrounds. +$focus-indicator-color: var(--focus-indicator-color, #0b5ed7); +$focus-indicator-contrast: var(--focus-indicator-contrast, #fff); +$focus-indicator-width: var(--focus-indicator-width, 2px); +$focus-indicator-offset: var(--focus-indicator-offset, 2px); + +// Box shadow form, for components that remove the outline (Bootstrap buttons, inputs, etc.) +$focus-indicator-ring: 0 0 0 $focus-indicator-offset $focus-indicator-contrast, 0 0 0 calc(#{$focus-indicator-offset} + #{$focus-indicator-width}) $focus-indicator-color; + +// Outset by default. Use the inset variant where neighbouring controls or overflow would cover +// or clip the outline. Transitions are removed so that the indicator is never drawn part way +// through animating into place. +@mixin focus-ring($inset: false) { + outline: $focus-indicator-width solid $focus-indicator-color; + transition: none; + + @if $inset { + outline-offset: calc(#{$focus-indicator-width} * -1); + box-shadow: inset 0 0 0 calc(#{$focus-indicator-width} + #{$focus-indicator-offset}) $focus-indicator-contrast; + } @else { + outline-offset: $focus-indicator-offset; + box-shadow: 0 0 0 $focus-indicator-offset $focus-indicator-contrast; + } +} + +// Default focus highlighting, with an outline fallback for forced colours mode where box shadows are not drawn. +// !important is needed because Bootstrap components set outline: 0 with higher specificity. The offset is +// left alone so that inset indicators stay inside clipping containers. +@mixin focus-indicator-global() { + :focus-visible { + @include focus-ring(); + } + + @media (forced-colors: active) { + :focus-visible { + outline: $focus-indicator-width solid Highlight !important; + } + } +} diff --git a/media_source/system/scss/fields/switcher.scss b/media_source/system/scss/fields/switcher.scss index 3535e2ccc1f26..aa675ba6dc893 100644 --- a/media_source/system/scss/fields/switcher.scss +++ b/media_source/system/scss/fields/switcher.scss @@ -1,3 +1,5 @@ +@import "../focus-indicator"; + $off-background-colour: #d3d3d3; $on-background-colour: #2f7d32; $switcher-width: 62px; @@ -41,6 +43,10 @@ $switcher-height: 28px; box-shadow: 0 0 0 .2rem rgba(26, 70, 107, .25); } +.switcher input:focus-visible ~ .toggle-outside { + @include focus-ring(); +} + .switcher label { position: absolute; inset-inline-start: 0; diff --git a/media_source/templates/administrator/atum/scss/_variables-dark.scss b/media_source/templates/administrator/atum/scss/_variables-dark.scss index 12f7b58aec6fc..f1b3d4a2e8841 100644 --- a/media_source/templates/administrator/atum/scss/_variables-dark.scss +++ b/media_source/templates/administrator/atum/scss/_variables-dark.scss @@ -29,6 +29,8 @@ $atum-colors-dark: ( button-and-icon-color: $btn-primary-dark, template-quickicon-color: var(--template-bg-dark-5), // fix this focus-shadow: var(--gray-800), + focus-indicator-color: $link-color-dark, // contrast ratio: 8.86 on #0f151d + focus-indicator-contrast: #0f151d, media-manager-bg: var(--body-bg), media-manager-content-bg: var(--body-bg), @@ -213,11 +215,9 @@ $atum-colors-dark: ( quickicon-box-shadow-danger: none, quickicon-box-shadow-warning: none, login-box-shadow: none, - login-focus-box-shadow: none, modal-btn-box-shadow: none, modal-joomla-dialog-box-shadow: none, form-aria-grabbed-box-shadow: none, - choices__button_joomla-focus-box-shadow: none, joomla-tab-accordion-box-shadow: none, joomla-tab-btn-hvr: var(--template-text-light), diff --git a/media_source/templates/administrator/atum/scss/_variables.scss b/media_source/templates/administrator/atum/scss/_variables.scss index b4b272baf527c..0d3f8a88d4df3 100644 --- a/media_source/templates/administrator/atum/scss/_variables.scss +++ b/media_source/templates/administrator/atum/scss/_variables.scss @@ -1,5 +1,8 @@ @use "sass:color"; +// Keyboard focus indicator mixins and box shadow +@import "../../../../system/scss/focus-indicator"; + // Remove prefix added in BS5 for compat with Joomla beta's shipped with Betas $prefix: ""; @@ -262,14 +265,14 @@ $atum-colors: ( quickicon-box-shadow-warning: 0 0 3px 0 var(--warning), login-box-shadow: 0 4px 20px -10px var(--template-bg-dark-50), - login-focus-box-shadow: inset 0 0 1px 1px var(--template-contrast), + login-focus-box-shadow: $focus-indicator-ring, modal-btn-box-shadow: 1px 1px 1px 0 rgba(0, 0, 0, .25), modal-joomla-dialog-box-shadow: 0 .5rem 1rem rgba(0, 0, 0, .15), form-aria-grabbed-box-shadow: 0 0 2px 1px var(--template-bg-dark), - choices__button_joomla-focus-box-shadow: 0 0 0 2px #00bcd4, + choices__button_joomla-focus-box-shadow: $focus-indicator-ring, joomla-tab-accordion-box-shadow: (0 1px $white inset, 0 0 3px rgba(0, 0, 0, .04)), joomla-tab-btn-hvr: var(--template-bg-dark-60), @@ -552,6 +555,12 @@ $atum-colors: ( warning: $warning, warninghvr: color.adjust($warning, $lightness: -15%), + // Keyboard focus indicator: a coloured outline separated from the control by a contrasting ring + focus-indicator-color: #0b5ed7, // contrast ratio: 5.84 on #fff + focus-indicator-contrast: $white, // contrast ratio: 12.63 on the dark header and sidebar + focus-indicator-width: 2px, + focus-indicator-offset: 2px, + ); $colors: ( @@ -921,6 +930,11 @@ $input-box-shadow: var(--atum-box-shadow); $input-max-width: 100%; $input-btn-focus-width: .2rem; $input-btn-focus-color: var(--focus-shadow); + +// Keyboard focus indicator for Bootstrap components that remove the outline +$focus-ring-box-shadow: $focus-indicator-ring; +$input-btn-focus-box-shadow: $focus-indicator-ring; +$form-select-focus-box-shadow: $focus-indicator-ring; $input-btn-padding-y: .5rem; $input-btn-padding-x: 1rem; diff --git a/media_source/templates/administrator/atum/scss/blocks/_global.scss b/media_source/templates/administrator/atum/scss/blocks/_global.scss index acbbb2d629eec..854bcf59992b6 100644 --- a/media_source/templates/administrator/atum/scss/blocks/_global.scss +++ b/media_source/templates/administrator/atum/scss/blocks/_global.scss @@ -91,21 +91,8 @@ legend { font-weight: $font-weight-bold; } -// focus-visible polyfill rule -.js-focus-visible :focus:not(.focus-visible) { - outline: none; -} - // Default focus highlighting -a, -button, -input, -textarea { - - &.focus-visible { - outline: auto 2px var(--focus); - } -} +@include focus-indicator-global(); // Break up text in cells to prevent overflow .break-word { diff --git a/media_source/templates/administrator/atum/scss/blocks/_header.scss b/media_source/templates/administrator/atum/scss/blocks/_header.scss index bd9b17abd1fb2..e9bf0ca923c24 100644 --- a/media_source/templates/administrator/atum/scss/blocks/_header.scss +++ b/media_source/templates/administrator/atum/scss/blocks/_header.scss @@ -28,6 +28,11 @@ background-color: $header-logo-bg; transition: all .3s ease-in-out; + // The logo sits in the viewport corner and clips overflow, so draw the indicator inside the control + &:focus-visible { + @include focus-ring(true); + } + &.small { justify-content: center; inline-size: $sidebar-width-closed; diff --git a/media_source/templates/administrator/atum/scss/blocks/_quickicons.scss b/media_source/templates/administrator/atum/scss/blocks/_quickicons.scss index 94a641430fd6b..d9e5fc070970f 100644 --- a/media_source/templates/administrator/atum/scss/blocks/_quickicons.scss +++ b/media_source/templates/administrator/atum/scss/blocks/_quickicons.scss @@ -39,9 +39,9 @@ background-color: var(--bg-color); transition: all .25s ease; + // The quick icon groups clip overflow, so draw the indicator inside the control &:focus-visible { - border: 1px solid var(--link-color); - outline: 0; + @include focus-ring(true); } .quickicon-icon { @@ -153,9 +153,9 @@ color: $quickicon-linkadd-color; } + // The quick icon groups clip overflow, so draw the indicator inside the control &:focus-visible { - border: 1px solid var(--link-color); - outline: 0; + @include focus-ring(true); } &:hover, diff --git a/media_source/templates/administrator/atum/scss/blocks/_sidebar.scss b/media_source/templates/administrator/atum/scss/blocks/_sidebar.scss index 071caf6b62584..521009be10bf2 100644 --- a/media_source/templates/administrator/atum/scss/blocks/_sidebar.scss +++ b/media_source/templates/administrator/atum/scss/blocks/_sidebar.scss @@ -31,6 +31,11 @@ } } + // The sidebar clips overflow, so draw the indicator inside the control + a:focus-visible { + @include focus-ring(true); + } + > a { position: relative; display: flex; @@ -72,6 +77,11 @@ a { color: $sidebar-toggle-link; /* $sidebar-toggle-link $white */ + + // The sidebar clips overflow, so draw the indicator inside the control + &:focus-visible { + @include focus-ring(true); + } } .sidebar-item-title { @@ -253,6 +263,11 @@ box-shadow: none; } + // The toggle sits in the viewport corner, so draw the indicator inside the control + &.navbar-toggler:focus-visible { + @include focus-ring(true); + } + .navbar-toggler-icon { background-image: none; } diff --git a/media_source/templates/administrator/atum/scss/vendor/bootstrap/_buttons.scss b/media_source/templates/administrator/atum/scss/vendor/bootstrap/_buttons.scss index d74b740eadeae..a7043f47c850a 100644 --- a/media_source/templates/administrator/atum/scss/vendor/bootstrap/_buttons.scss +++ b/media_source/templates/administrator/atum/scss/vendor/bootstrap/_buttons.scss @@ -1,9 +1,15 @@ // Buttons .btn { + // Variants only set the shadow colour, so this applies the focus indicator to all of them + --btn-focus-box-shadow: #{$focus-indicator-ring}; transition: none; } +.navbar-toggler:focus-visible { + @include focus-ring(); +} + @include media-breakpoint-down(sm) { .btn { margin-block-end: .25rem; diff --git a/media_source/templates/administrator/atum/scss/vendor/choicesjs/choices.scss b/media_source/templates/administrator/atum/scss/vendor/choicesjs/choices.scss index cbd3abed8dae0..b270aa2e3ce39 100644 --- a/media_source/templates/administrator/atum/scss/vendor/choicesjs/choices.scss +++ b/media_source/templates/administrator/atum/scss/vendor/choicesjs/choices.scss @@ -137,6 +137,10 @@ &:focus { outline: none; } + + &:focus-visible { + @include focus-ring(); + } } .choices[data-type*="select-one"], diff --git a/media_source/templates/administrator/atum/scss/vendor/joomla-custom-elements/joomla-tab.scss b/media_source/templates/administrator/atum/scss/vendor/joomla-custom-elements/joomla-tab.scss index f6d2507e350c5..c8d867060e25a 100644 --- a/media_source/templates/administrator/atum/scss/vendor/joomla-custom-elements/joomla-tab.scss +++ b/media_source/templates/administrator/atum/scss/vendor/joomla-custom-elements/joomla-tab.scss @@ -48,8 +48,15 @@ joomla-tab { border-start-start-radius: .25rem; } + // Tabs sit edge to edge, so draw the indicator inside each one, above the active bar &:focus-visible { z-index: 1; + + @include focus-ring(true); + + &::after { + z-index: -1; + } } &[aria-selected=true], @@ -145,6 +152,11 @@ joomla-tab[orientation=vertical] { border-radius: 0; box-shadow: none; + // Long tab labels overflow the list, which would clip the focus indicator + &:has(> button:focus-visible) { + overflow: visible; + } + @include media-breakpoint-down(lg) { flex: 0 0 100%; max-inline-size: 100%; @@ -173,6 +185,17 @@ joomla-tab[orientation=vertical] { border-block-end: $joomla-tab-vert-border-bottom; // 1px solid $gray-300; box-shadow: none; + // Draw the indicator inside each tab, above the active bar + &:focus-visible { + z-index: 1; + + @include focus-ring(true); + + &::after { + z-index: -1; + } + } + &[aria-selected=true], &:focus, &:hover { diff --git a/media_source/templates/site/cassiopeia/scss/blocks/_global.scss b/media_source/templates/site/cassiopeia/scss/blocks/_global.scss index c6beebb56fb03..fd1db8bda059b 100644 --- a/media_source/templates/site/cassiopeia/scss/blocks/_global.scss +++ b/media_source/templates/site/cassiopeia/scss/blocks/_global.scss @@ -25,6 +25,9 @@ html { background-color: $white; } +// Default focus highlighting +@include focus-indicator-global(); + body { position: relative; min-height: 100vh; @@ -109,6 +112,11 @@ a { ol, ul { overflow: hidden; + + // Links on the first or last line would lose part of the focus indicator + &:has(:focus-visible) { + overflow: visible; + } } } diff --git a/media_source/templates/site/cassiopeia/scss/blocks/_modifiers.scss b/media_source/templates/site/cassiopeia/scss/blocks/_modifiers.scss index 59ae922946b52..a1e044c3345e4 100644 --- a/media_source/templates/site/cassiopeia/scss/blocks/_modifiers.scss +++ b/media_source/templates/site/cassiopeia/scss/blocks/_modifiers.scss @@ -336,8 +336,7 @@ color: currentColor; } &:focus-visible { - outline: 2px solid currentColor; - outline-offset: 2px; + @include focus-ring(); } } diff --git a/media_source/templates/site/cassiopeia/scss/tools/variables/_variables.scss b/media_source/templates/site/cassiopeia/scss/tools/variables/_variables.scss index 8f7d9081bb9c7..5199fbfef3f70 100644 --- a/media_source/templates/site/cassiopeia/scss/tools/variables/_variables.scss +++ b/media_source/templates/site/cassiopeia/scss/tools/variables/_variables.scss @@ -1,5 +1,8 @@ @use "sass:color"; +// Keyboard focus indicator mixins and box shadow +@import "../../../../../../system/scss/focus-indicator"; + // Remove prefix added in BS5 for compat with Joomla beta's shipped with Betas $prefix: ""; @@ -140,7 +143,13 @@ $cassiopeia-colors: ( template-bg-dark-70: hsl(var(--hue), 40%, 30%), template-bg-dark-75: hsl(var(--hue), 40%, 25%), template-bg-dark-80: hsl(var(--hue), 40%, 20%), - template-bg-dark-90: hsl(var(--hue), 40%, 10%) + template-bg-dark-90: hsl(var(--hue), 40%, 10%), + + // Keyboard focus indicator: a coloured outline separated from the control by a contrasting ring + focus-indicator-color: #0b5ed7, // contrast ratio: 5.84 on #fff + focus-indicator-contrast: $white, // keeps the indicator visible on the dark header and footer + focus-indicator-width: 2px, + focus-indicator-offset: 2px ) !default; // Links @@ -198,6 +207,11 @@ $input-btn-padding-x: 1rem !default; $input-max-width: 100% !default; $input-focus-border-color: tint-color($standard-color-primary, 50%) !default; +// Keyboard focus indicator for Bootstrap components that remove the outline +$focus-ring-box-shadow: $focus-indicator-ring !default; +$input-btn-focus-box-shadow: $focus-indicator-ring !default; +$form-select-focus-box-shadow: $focus-indicator-ring !default; + $form-select-indicator-padding: 3rem !default; $form-select-bg: $gray-200 !default; $form-select-bg-size: 116rem !default; diff --git a/media_source/templates/site/cassiopeia/scss/vendor/_awesomplete.scss b/media_source/templates/site/cassiopeia/scss/vendor/_awesomplete.scss index abf0436c1f0dc..77bb7e4dc4ab2 100644 --- a/media_source/templates/site/cassiopeia/scss/vendor/_awesomplete.scss +++ b/media_source/templates/site/cassiopeia/scss/vendor/_awesomplete.scss @@ -3,6 +3,10 @@ .mod-finder__search.input-group { flex-wrap: nowrap; .awesomplete { + // The wrapper stops Bootstrap's input group from raising the focused input above the button + &:focus-within { + z-index: 5; + } input { @include border-end-radius(0); } diff --git a/media_source/templates/site/cassiopeia/scss/vendor/bootstrap/_buttons.scss b/media_source/templates/site/cassiopeia/scss/vendor/bootstrap/_buttons.scss index f312ec0afa07d..25db8f47378c8 100644 --- a/media_source/templates/site/cassiopeia/scss/vendor/bootstrap/_buttons.scss +++ b/media_source/templates/site/cassiopeia/scss/vendor/bootstrap/_buttons.scss @@ -1,5 +1,14 @@ // Buttons +.btn { + // Variants only set the shadow colour, so this applies the focus indicator to all of them + --btn-focus-box-shadow: #{$focus-indicator-ring}; +} + +.navbar-toggler:focus-visible { + @include focus-ring(); +} + .btn:focus, .btn.focus, .btn:active:focus, diff --git a/media_source/templates/site/cassiopeia/scss/vendor/bootstrap/_lists.scss b/media_source/templates/site/cassiopeia/scss/vendor/bootstrap/_lists.scss index 29245c5361182..462111de95d24 100644 --- a/media_source/templates/site/cassiopeia/scss/vendor/bootstrap/_lists.scss +++ b/media_source/templates/site/cassiopeia/scss/vendor/bootstrap/_lists.scss @@ -2,6 +2,12 @@ .list-group-item { background-color: $list-group-bg; + + // Keep the focus indicator above the neighbouring items + &:focus-visible { + position: relative; + z-index: 2; + } } .list-unstyled .list-unstyled { diff --git a/media_source/templates/site/cassiopeia/scss/vendor/choicesjs/choices.scss b/media_source/templates/site/cassiopeia/scss/vendor/choicesjs/choices.scss index 6dff049869151..12941c4d50906 100644 --- a/media_source/templates/site/cassiopeia/scss/vendor/choicesjs/choices.scss +++ b/media_source/templates/site/cassiopeia/scss/vendor/choicesjs/choices.scss @@ -107,6 +107,10 @@ &:focus { outline: none; } + + &:focus-visible { + @include focus-ring(); + } } .choices[data-type*="select-one"], diff --git a/media_source/templates/site/cassiopeia/scss/vendor/metismenu/_metismenu.scss b/media_source/templates/site/cassiopeia/scss/vendor/metismenu/_metismenu.scss index 2be2b6867444e..368bc21175aa3 100644 --- a/media_source/templates/site/cassiopeia/scss/vendor/metismenu/_metismenu.scss +++ b/media_source/templates/site/cassiopeia/scss/vendor/metismenu/_metismenu.scss @@ -57,10 +57,9 @@ white-space: nowrap; } - > a:focus, - > button:focus { - outline: 1px dotted $gray-400; - outline-offset: 2px; + > a:focus-visible, + > button:focus-visible { + @include focus-ring(); } &.active > a,