From 0202e8efb0127c3be6542cd8267edd9c4efee982 Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Sat, 11 Mar 2023 21:15:32 +1100 Subject: [PATCH] fix(a11y): leverage native :focus-visible in CSS --- README.md | 3 +- demo/src/js/demo.js | 6 +-- demo/src/js/tab-focus.js | 31 --------------- demo/src/js/toggle-class.js | 5 --- demo/src/sass/components/buttons.scss | 4 +- demo/src/sass/components/links.scss | 4 +- demo/src/sass/layout/core.scss | 4 +- demo/src/sass/lib/mixins.scss | 6 +-- demo/src/sass/settings/colors.scss | 2 +- demo/src/sass/utilities/focus.scss | 4 ++ src/js/config/defaults.js | 1 - src/js/controls.js | 8 ++-- src/js/listeners.js | 55 --------------------------- src/js/utils/elements.js | 9 +---- src/sass/components/control.scss | 4 +- src/sass/components/menus.scss | 6 +-- src/sass/components/sliders.scss | 8 ++-- src/sass/components/tooltips.scss | 4 +- src/sass/lib/mixins.scss | 4 +- src/sass/settings/cosmetics.scss | 2 +- src/sass/types/audio.scss | 2 +- src/sass/types/video.scss | 3 +- 22 files changed, 39 insertions(+), 136 deletions(-) delete mode 100644 demo/src/js/tab-focus.js delete mode 100644 demo/src/js/toggle-class.js create mode 100644 demo/src/sass/utilities/focus.scss diff --git a/README.md b/README.md index 3af70650..203499c1 100644 --- a/README.md +++ b/README.md @@ -197,11 +197,10 @@ Here's a list of the properties and what they are used for: | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- | | `--plyr-color-main` | The primary UI color. | ![#f03c15](https://place-hold.it/15/00b3ff/000000?text=+) `#00b3ff` | | `--plyr-video-background` | The background color of video and poster wrappers for using alpha channel videos and poster images. | `rgba(0, 0, 0, 1)` | -| `--plyr-tab-focus-color` | The color used for the dotted outline when an element is `:focus-visible` (equivalent) keyboard focus. | `--plyr-color-main` | +| `--plyr-focus-visible-color` | The color used for the focus styles when an element is `:focus-visible` (keyboard focused). | `--plyr-color-main` | | `--plyr-badge-background` | The background color for badges in the menu. | ![#4a5464](https://place-hold.it/15/4a5464/000000?text=+) `#4a5464` | | `--plyr-badge-text-color` | The text color for badges. | ![#ffffff](https://place-hold.it/15/ffffff/000000?text=+) `#ffffff` | | `--plyr-badge-border-radius` | The border radius used for badges. | `2px` | -| `--plyr-tab-focus-color` | The color used to highlight tab (keyboard) focus. | `--plyr-color-main` | | `--plyr-captions-background` | The color for the background of captions. | `rgba(0, 0, 0, 0.8)` | | `--plyr-captions-text-color` | The color used for the captions text. | ![#ffffff](https://place-hold.it/15/ffffff/000000?text=+) `#ffffff` | | `--plyr-control-icon-size` | The size of the icons used in the controls. | `18px` | diff --git a/demo/src/js/demo.js b/demo/src/js/demo.js index 65e17a7e..35b51a0d 100644 --- a/demo/src/js/demo.js +++ b/demo/src/js/demo.js @@ -4,7 +4,6 @@ // Please see README.md in the root or github.com/sampotts/plyr // ========================================================================== -import './tab-focus'; import 'custom-event-polyfill'; import 'url-polyfill'; @@ -13,7 +12,6 @@ import Shr from 'shr-buttons'; import Plyr from '../../../src/js/plyr'; import sources from './sources'; -import toggleClass from './toggle-class'; (() => { const production = 'plyr.io'; @@ -108,10 +106,10 @@ import toggleClass from './toggle-class'; function render(type) { // Remove active classes - Array.from(buttons).forEach((button) => toggleClass(button.parentElement, 'active', false)); + Array.from(buttons).forEach((button) => button.parentElement.classList.toggle('active', false)); // Set active on parent - toggleClass(document.querySelector(`[data-source="${type}"]`), 'active', true); + document.querySelector(`[data-source="${type}"]`).classList.toggle('active', true); // Show cite Array.from(document.querySelectorAll('.plyr__cite')).forEach((cite) => { diff --git a/demo/src/js/tab-focus.js b/demo/src/js/tab-focus.js deleted file mode 100644 index 0ef1f0d5..00000000 --- a/demo/src/js/tab-focus.js +++ /dev/null @@ -1,31 +0,0 @@ -// Setup tab focus -const container = document.getElementById('container'); -const tabClassName = 'tab-focus'; - -// Remove class on blur -document.addEventListener('focusout', (event) => { - if (!event.target.classList || container.contains(event.target)) { - return; - } - - event.target.classList.remove(tabClassName); -}); - -// Add classname to tabbed elements -document.addEventListener('keydown', (event) => { - if (event.key !== 'Tab') { - return; - } - - // Delay the adding of classname until the focus has changed - // This event fires before the focusin event - setTimeout(() => { - const focused = document.activeElement; - - if (!focused || !focused.classList || container.contains(focused)) { - return; - } - - focused.classList.add(tabClassName); - }, 10); -}); diff --git a/demo/src/js/toggle-class.js b/demo/src/js/toggle-class.js deleted file mode 100644 index bd10c246..00000000 --- a/demo/src/js/toggle-class.js +++ /dev/null @@ -1,5 +0,0 @@ -// Toggle class on an element -const toggleClass = (element, className = '', toggle = false) => - element && element.classList[toggle ? 'add' : 'remove'](className); - -export default toggleClass; diff --git a/demo/src/sass/components/buttons.scss b/demo/src/sass/components/buttons.scss index 7c3ae4f7..2d071eb9 100644 --- a/demo/src/sass/components/buttons.scss +++ b/demo/src/sass/components/buttons.scss @@ -44,8 +44,8 @@ outline: 0; } - &.tab-focus { - @include tab-focus; + &:focus-visible { + @include focus-visible($color-button-background); } &:active { diff --git a/demo/src/sass/components/links.scss b/demo/src/sass/components/links.scss index 4f03c4de..1ec53ee9 100644 --- a/demo/src/sass/components/links.scss +++ b/demo/src/sass/components/links.scss @@ -38,8 +38,8 @@ a { } } - &.tab-focus { - @include tab-focus; + &:focus-visible { + @include focus-visible($color-link); } &.no-border::after { diff --git a/demo/src/sass/layout/core.scss b/demo/src/sass/layout/core.scss index b613a9fc..5a25f95f 100644 --- a/demo/src/sass/layout/core.scss +++ b/demo/src/sass/layout/core.scss @@ -58,8 +58,8 @@ aside { a { color: $color-twitter; - &.tab-focus { - @include tab-focus($color-twitter); + &:focus-visible { + @include focus-visible($color-twitter); } } } diff --git a/demo/src/sass/lib/mixins.scss b/demo/src/sass/lib/mixins.scss index 0f7e66b8..f80e77a1 100644 --- a/demo/src/sass/lib/mixins.scss +++ b/demo/src/sass/lib/mixins.scss @@ -25,9 +25,9 @@ // Nicer focus styles // --------------------------------------- -@mixin tab-focus($color: $tab-focus-default-color) { - box-shadow: 0 0 0 3px rgba($color, 0.35); - outline: 0; +@mixin focus-visible($color: $focus-default-color) { + outline: 2px dashed $color; + outline-offset: 2px; } // Use rems for font sizing diff --git a/demo/src/sass/settings/colors.scss b/demo/src/sass/settings/colors.scss index 451bc29d..1ec54c82 100644 --- a/demo/src/sass/settings/colors.scss +++ b/demo/src/sass/settings/colors.scss @@ -39,4 +39,4 @@ $color-button-count-background: #fff; $color-button-count-text: $color-gray-600; // Focus -$tab-focus-default-color: #fff; +$focus-default-color: $color-brand-primary; diff --git a/demo/src/sass/utilities/focus.scss b/demo/src/sass/utilities/focus.scss new file mode 100644 index 00000000..57e64a2a --- /dev/null +++ b/demo/src/sass/utilities/focus.scss @@ -0,0 +1,4 @@ +*:focus-visible { + outline: 2px dotted $color-brand-primary; + outline-offset: 2px; +} diff --git a/src/js/config/defaults.js b/src/js/config/defaults.js index 4487ba34..b464421d 100644 --- a/src/js/config/defaults.js +++ b/src/js/config/defaults.js @@ -379,7 +379,6 @@ const defaults = { supported: 'plyr--airplay-supported', active: 'plyr--airplay-active', }, - tabFocus: 'plyr__tab-focus', previewThumbnails: { // Tooltip thumbs thumbContainer: 'plyr__preview-thumb', diff --git a/src/js/controls.js b/src/js/controls.js index 1f5c02ea..2d0dda9a 100644 --- a/src/js/controls.js +++ b/src/js/controls.js @@ -1106,7 +1106,7 @@ const controls = { }, // Focus the first menu item in a given (or visible) menu - focusFirstMenuItem(pane, tabFocus = false) { + focusFirstMenuItem(pane, focusVisible = false) { if (this.elements.settings.popup.hidden) { return; } @@ -1119,7 +1119,7 @@ const controls = { const firstItem = target.querySelector('[role^="menuitem"]'); - setFocus.call(this, firstItem, tabFocus); + setFocus.call(this, firstItem, focusVisible); }, // Show/hide menu @@ -1196,7 +1196,7 @@ const controls = { }, // Show a panel in the menu - showMenuPanel(type = '', tabFocus = false) { + showMenuPanel(type = '', focusVisible = false) { const target = this.elements.container.querySelector(`#plyr-settings-${this.id}-${type}`); // Nothing to show, bail @@ -1247,7 +1247,7 @@ const controls = { toggleHidden(target, false); // Focus the first item - controls.focusFirstMenuItem.call(this, target, tabFocus); + controls.focusFirstMenuItem.call(this, target, focusVisible); }, // Set the download URL diff --git a/src/js/listeners.js b/src/js/listeners.js index 697129ea..13dd42a2 100644 --- a/src/js/listeners.js +++ b/src/js/listeners.js @@ -21,7 +21,6 @@ class Listeners { this.handleKey = this.handleKey.bind(this); this.toggleMenu = this.toggleMenu.bind(this); - this.setTabFocus = this.setTabFocus.bind(this); this.firstTouch = this.firstTouch.bind(this); } @@ -194,57 +193,6 @@ class Listeners { toggleClass(elements.container, player.config.classNames.isTouch, true); }; - setTabFocus = (event) => { - const { player } = this; - const { elements } = player; - const { key, type, timeStamp } = event; - - clearTimeout(this.focusTimer); - - // Ignore any key other than tab - if (type === 'keydown' && key !== 'Tab') { - return; - } - - // Store reference to event timeStamp - if (type === 'keydown') { - this.lastKeyDown = timeStamp; - } - - // Remove current classes - const removeCurrent = () => { - const className = player.config.classNames.tabFocus; - const current = getElements.call(player, `.${className}`); - toggleClass(current, className, false); - }; - - // Determine if a key was pressed to trigger this event - const wasKeyDown = timeStamp - this.lastKeyDown <= 20; - - // Ignore focus events if a key was pressed prior - if (type === 'focus' && !wasKeyDown) { - return; - } - - // Remove all current - removeCurrent(); - - // Delay the adding of classname until the focus has changed - // This event fires before the focusin event - if (type !== 'focusout') { - this.focusTimer = setTimeout(() => { - const focused = document.activeElement; - - // Ignore if current focus element isn't inside the player - if (!elements.container.contains(focused)) { - return; - } - - toggleClass(document.activeElement, player.config.classNames.tabFocus, true); - }, 10); - } - }; - // Global window & document listeners global = (toggle = true) => { const { player } = this; @@ -259,9 +207,6 @@ class Listeners { // Detect touch by events once.call(player, document.body, 'touchstart', this.firstTouch); - - // Tab focus detection - toggleListener.call(player, document.body, 'keydown focus blur focusout', this.setTabFocus, toggle, false, true); }; // Container listeners diff --git a/src/js/utils/elements.js b/src/js/utils/elements.js index 62dd5605..3cbd0e0c 100644 --- a/src/js/utils/elements.js +++ b/src/js/utils/elements.js @@ -268,16 +268,11 @@ export function getElement(selector) { } // Set focus and tab focus class -export function setFocus(element = null, tabFocus = false) { +export function setFocus(element = null, focusVisible = false) { if (!is.element(element)) { return; } // Set regular focus - element.focus({ preventScroll: true }); - - // If we want to mimic keyboard focus via tab - if (tabFocus) { - toggleClass(element, this.config.classNames.tabFocus); - } + element.focus({ preventScroll: true, focusVisible }); } diff --git a/src/sass/components/control.scss b/src/sass/components/control.scss index 55d02143..6697d750 100644 --- a/src/sass/components/control.scss +++ b/src/sass/components/control.scss @@ -28,8 +28,8 @@ } // Tab focus - &.plyr__tab-focus { - @include plyr-tab-focus; + &:focus-visible { + @include plyr-focus-visible; } } diff --git a/src/sass/components/menus.scss b/src/sass/components/menus.scss index a2edd8f1..466314a0 100644 --- a/src/sass/components/menus.scss +++ b/src/sass/components/menus.scss @@ -100,7 +100,7 @@ right: calc((#{$plyr-control-padding} * 1.5) - #{$plyr-menu-item-arrow-size}); } - &.plyr__tab-focus::after, + &:focus-visible::after, &:hover::after { border-left-color: currentColor; } @@ -132,7 +132,7 @@ top: 100%; } - &.plyr__tab-focus::after, + &:focus-visible::after, &:hover::after { border-right-color: currentColor; } @@ -181,7 +181,7 @@ } } - &.plyr__tab-focus::before, + &:focus-visible::before, &:hover::before { background: rgba($plyr-color-gray-900, 0.1); } diff --git a/src/sass/components/sliders.scss b/src/sass/components/sliders.scss index db75bd56..fdb3c4c6 100644 --- a/src/sass/components/sliders.scss +++ b/src/sass/components/sliders.scss @@ -83,17 +83,17 @@ outline: 0; } - &.plyr__tab-focus { + &:focus-visible { &::-webkit-slider-runnable-track { - @include plyr-tab-focus; + @include plyr-focus-visible; } &::-moz-range-track { - @include plyr-tab-focus; + @include plyr-focus-visible; } &::-ms-track { - @include plyr-tab-focus; + @include plyr-focus-visible; } } } diff --git a/src/sass/components/tooltips.scss b/src/sass/components/tooltips.scss index b7afd366..020a6ebc 100644 --- a/src/sass/components/tooltips.scss +++ b/src/sass/components/tooltips.scss @@ -42,7 +42,7 @@ // Displaying .plyr .plyr__control:hover .plyr__tooltip, -.plyr .plyr__control.plyr__tab-focus .plyr__tooltip, +.plyr .plyr__control:focus-visible .plyr__tooltip, .plyr__tooltip--visible { opacity: 1; transform: translate(-50%, 0) scale(1); @@ -82,7 +82,7 @@ .plyr__controls > .plyr__control:first-child + .plyr__control, .plyr__controls > .plyr__control:last-child { &:hover .plyr__tooltip, - &.plyr__tab-focus .plyr__tooltip, + &:focus-visible .plyr__tooltip, .plyr__tooltip--visible { transform: translate(0, 0) scale(1); } diff --git a/src/sass/lib/mixins.scss b/src/sass/lib/mixins.scss index 0c1eab6e..5fbe85e7 100644 --- a/src/sass/lib/mixins.scss +++ b/src/sass/lib/mixins.scss @@ -4,8 +4,8 @@ // Nicer focus styles // --------------------------------------- -@mixin plyr-tab-focus($color: $plyr-tab-focus-color) { - outline: $color dotted 3px; +@mixin plyr-focus-visible($color: $plyr-focus-visible-color) { + outline: 2px dashed $color; outline-offset: 2px; } diff --git a/src/sass/settings/cosmetics.scss b/src/sass/settings/cosmetics.scss index 4fc10de5..e9fb7049 100644 --- a/src/sass/settings/cosmetics.scss +++ b/src/sass/settings/cosmetics.scss @@ -2,4 +2,4 @@ // Cosmetic // ========================================================================== -$plyr-tab-focus-color: var(--plyr-tab-focus-color, var(--plyr-color-main, $plyr-color-main)) !default; +$plyr-focus-visible-color: var(--plyr-focus-visible-color, var(--plyr-color-main, $plyr-color-main)) !default; diff --git a/src/sass/types/audio.scss b/src/sass/types/audio.scss index a44244c9..07984f57 100644 --- a/src/sass/types/audio.scss +++ b/src/sass/types/audio.scss @@ -17,7 +17,7 @@ // Control elements .plyr--audio .plyr__control { - &.plyr__tab-focus, + &:focus-visible, &:hover, &[aria-expanded='true'] { background: $plyr-audio-control-background-hover; diff --git a/src/sass/types/video.scss b/src/sass/types/video.scss index 747a2eb7..41ff85d6 100644 --- a/src/sass/types/video.scss +++ b/src/sass/types/video.scss @@ -87,8 +87,7 @@ $embed-padding: (math.div(100, 16) * 9); // Control elements .plyr--video .plyr__control { - // Hover and tab focus - &.plyr__tab-focus, + &:focus-visible, &:hover, &[aria-expanded='true'] { background: $plyr-video-control-background-hover;