Commit f217a530 authored by Antoine Derouin's avatar Antoine Derouin 🤘
Browse files

#1 [A11Y] install focus-visible.js

parent f68ed89e
Pipeline #425 passed with stages
in 39 seconds
/**
* Applies the :focus-visible polyfill at the given scope.
* A scope in this case is either the top-level Document or a Shadow Root.
*
* @param {(Document|ShadowRoot)} scope
* @see https://github.com/WICG/focus-visible
*/
function applyFocusVisiblePolyfill(scope) {
var hadKeyboardEvent = true;
var hadFocusVisibleRecently = false;
var hadFocusVisibleRecentlyTimeout = null;
var inputTypesAllowlist = {
text: false,
search: false,
url: false,
tel: false,
email: false,
password: false,
number: false,
date: false,
month: false,
week: false,
time: false,
datetime: false,
'datetime-local': false
};
/**
* Helper function for legacy browsers and iframes which sometimes focus
* elements like document, body, and non-interactive SVG.
* @param {Element} el
*/
function isValidFocusTarget(el) {
if (
el &&
el !== document &&
el.nodeName !== 'HTML' &&
el.nodeName !== 'BODY' &&
'classList' in el &&
'contains' in el.classList
) {
return true;
}
return false;
}
/**
* Computes whether the given element should automatically trigger the
* `focus-visible` class being added, i.e. whether it should always match
* `:focus-visible` when focused.
* @param {Element} el
* @return {boolean}
*/
function focusTriggersKeyboardModality(el) {
var type = el.type;
var tagName = el.tagName;
if (tagName === 'INPUT' && inputTypesAllowlist[type] && !el.readOnly) {
return true;
}
if (tagName === 'TEXTAREA' && !el.readOnly) {
return true;
}
if (el.isContentEditable) {
return true;
}
return false;
}
/**
* Add the `focus-visible` class to the given element if it was not added by
* the author.
* @param {Element} el
*/
function addFocusVisibleClass(el) {
if (el.classList.contains('focus-visible')) {
return;
}
el.classList.add('focus-visible');
el.setAttribute('data-focus-visible-added', '');
}
/**
* Remove the `focus-visible` class from the given element if it was not
* originally added by the author.
* @param {Element} el
*/
function removeFocusVisibleClass(el) {
if (!el.hasAttribute('data-focus-visible-added')) {
return;
}
el.classList.remove('focus-visible');
el.removeAttribute('data-focus-visible-added');
}
/**
* If the most recent user interaction was via the keyboard;
* and the key press did not include a meta, alt/option, or control key;
* then the modality is keyboard. Otherwise, the modality is not keyboard.
* Apply `focus-visible` to any current active element and keep track
* of our keyboard modality state with `hadKeyboardEvent`.
* @param {KeyboardEvent} e
*/
function onKeyDown(e) {
if (e.metaKey || e.altKey || e.ctrlKey) {
return;
}
if (isValidFocusTarget(scope.activeElement)) {
addFocusVisibleClass(scope.activeElement);
}
hadKeyboardEvent = true;
}
/**
* If at any point a user clicks with a pointing device, ensure that we change
* the modality away from keyboard.
* This avoids the situation where a user presses a key on an already focused
* element, and then clicks on a different element, focusing it with a
* pointing device, while we still think we're in keyboard modality.
* @param {Event} e
*/
function onPointerDown(e) {
hadKeyboardEvent = false;
}
/**
* On `focus`, add the `focus-visible` class to the target if:
* - the target received focus as a result of keyboard navigation, or
* - the event target is an element that will likely require interaction
* via the keyboard (e.g. a text box)
* @param {Event} e
*/
function onFocus(e) {
// Prevent IE from focusing the document or HTML element.
if (!isValidFocusTarget(e.target)) {
return;
}
if (hadKeyboardEvent || focusTriggersKeyboardModality(e.target)) {
addFocusVisibleClass(e.target);
}
}
/**
* On `blur`, remove the `focus-visible` class from the target.
* @param {Event} e
*/
function onBlur(e) {
if (!isValidFocusTarget(e.target)) {
return;
}
if (
e.target.classList.contains('focus-visible') ||
e.target.hasAttribute('data-focus-visible-added')
) {
// To detect a tab/window switch, we look for a blur event followed
// rapidly by a visibility change.
// If we don't see a visibility change within 100ms, it's probably a
// regular focus change.
hadFocusVisibleRecently = true;
window.clearTimeout(hadFocusVisibleRecentlyTimeout);
hadFocusVisibleRecentlyTimeout = window.setTimeout(function() {
hadFocusVisibleRecently = false;
}, 100);
removeFocusVisibleClass(e.target);
}
}
/**
* If the user changes tabs, keep track of whether or not the previously
* focused element had .focus-visible.
* @param {Event} e
*/
function onVisibilityChange(e) {
if (document.visibilityState === 'hidden') {
// If the tab becomes active again, the browser will handle calling focus
// on the element (Safari actually calls it twice).
// If this tab change caused a blur on an element with focus-visible,
// re-apply the class when the user switches back to the tab.
if (hadFocusVisibleRecently) {
hadKeyboardEvent = true;
}
addInitialPointerMoveListeners();
}
}
/**
* Add a group of listeners to detect usage of any pointing devices.
* These listeners will be added when the polyfill first loads, and anytime
* the window is blurred, so that they are active when the window regains
* focus.
*/
function addInitialPointerMoveListeners() {
document.addEventListener('mousemove', onInitialPointerMove);
document.addEventListener('mousedown', onInitialPointerMove);
document.addEventListener('mouseup', onInitialPointerMove);
document.addEventListener('pointermove', onInitialPointerMove);
document.addEventListener('pointerdown', onInitialPointerMove);
document.addEventListener('pointerup', onInitialPointerMove);
document.addEventListener('touchmove', onInitialPointerMove);
document.addEventListener('touchstart', onInitialPointerMove);
document.addEventListener('touchend', onInitialPointerMove);
}
function removeInitialPointerMoveListeners() {
document.removeEventListener('mousemove', onInitialPointerMove);
document.removeEventListener('mousedown', onInitialPointerMove);
document.removeEventListener('mouseup', onInitialPointerMove);
document.removeEventListener('pointermove', onInitialPointerMove);
document.removeEventListener('pointerdown', onInitialPointerMove);
document.removeEventListener('pointerup', onInitialPointerMove);
document.removeEventListener('touchmove', onInitialPointerMove);
document.removeEventListener('touchstart', onInitialPointerMove);
document.removeEventListener('touchend', onInitialPointerMove);
}
/**
* When the polfyill first loads, assume the user is in keyboard modality.
* If any event is received from a pointing device (e.g. mouse, pointer,
* touch), turn off keyboard modality.
* This accounts for situations where focus enters the page from the URL bar.
* @param {Event} e
*/
function onInitialPointerMove(e) {
// Work around a Safari quirk that fires a mousemove on <html> whenever the
// window blurs, even if you're tabbing out of the page. ¯\_(ツ)_/¯
if (e.target.nodeName && e.target.nodeName.toLowerCase() === 'html') {
return;
}
hadKeyboardEvent = false;
removeInitialPointerMoveListeners();
}
// For some kinds of state, we are interested in changes at the global scope
// only. For example, global pointer input, global key presses and global
// visibility change should affect the state at every scope:
document.addEventListener('keydown', onKeyDown, true);
document.addEventListener('mousedown', onPointerDown, true);
document.addEventListener('pointerdown', onPointerDown, true);
document.addEventListener('touchstart', onPointerDown, true);
document.addEventListener('visibilitychange', onVisibilityChange, true);
addInitialPointerMoveListeners();
// For focus and blur, we specifically care about state changes in the local
// scope. This is because focus / blur events that originate from within a
// shadow root are not re-dispatched from the host element if it was already
// the active element in its own scope:
scope.addEventListener('focus', onFocus, true);
scope.addEventListener('blur', onBlur, true);
// We detect that a node is a ShadowRoot by ensuring that it is a
// DocumentFragment and also has a host property. This check covers native
// implementation and polyfill implementation transparently. If we only cared
// about the native implementation, we could just check if the scope was
// an instance of a ShadowRoot.
if (scope.nodeType === Node.DOCUMENT_FRAGMENT_NODE && scope.host) {
// Since a ShadowRoot is a special kind of DocumentFragment, it does not
// have a root element to add a class to. So, we add this attribute to the
// host element instead:
scope.host.setAttribute('data-js-focus-visible', '');
} else if (scope.nodeType === Node.DOCUMENT_NODE) {
document.documentElement.classList.add('js-focus-visible');
document.documentElement.setAttribute('data-js-focus-visible', '');
}
}
// It is important to wrap all references to global window and document in
// these checks to support server-side rendering use cases
// @see https://github.com/WICG/focus-visible/issues/199
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
// Make the polyfill helper globally available. This can be used as a signal
// to interested libraries that wish to coordinate with the polyfill for e.g.,
// applying the polyfill to a shadow root:
window.applyFocusVisiblePolyfill = applyFocusVisiblePolyfill;
// Notify interested libraries of the polyfill's presence, in case the
// polyfill was loaded lazily:
var event;
try {
event = new CustomEvent('focus-visible-polyfill-ready');
} catch (error) {
// IE11 does not support using CustomEvent as a constructor directly:
event = document.createEvent('CustomEvent');
event.initCustomEvent('focus-visible-polyfill-ready', false, false, {});
}
window.dispatchEvent(event);
}
if (typeof document !== 'undefined') {
// Apply the polyfill to the global document, so that no JavaScript
// coordination is required to use the polyfill in the top-level document:
applyFocusVisiblePolyfill(document);
}
......@@ -37,6 +37,7 @@
color: $discord;
border-color: $discord;
&:focus,
&:hover {
background-color: $discord;
}
......@@ -46,6 +47,7 @@
color: $yellow;
border-color: $yellow;
&:focus,
&:hover {
background-color: $yellow;
}
......@@ -55,6 +57,7 @@
color: $green;
border-color: $green;
&:focus,
&:hover {
background-color: $green;
}
......
/*
Création d'un extend `%focused-style` pour pouvoir réutiliser notre nouveau style de l'outline un peu partout où on veut
*/
%focused-style {
outline: rem(2px) dotted $color-font;
outline-offset: rem(4px);
transition: none;
}
/*
Si la librairie est active (donc si mon body possède la classe `js-focus-visible`), alors on masque l'outline pour le focus souris, et pour le focus clavier, on appelle l'extend crée précédemment)
*/
.js-focus-visible {
.btn,
a,
button,
.form-group {
// Focus souris, on désactive l'outline
&:focus:not(.focus-visible) {
outline: none;
}
// Focus clavier, on personnalise l'outline
&.focus-visible {
@extend %focused-style;
}
}
}
......@@ -36,6 +36,7 @@ $breakpoints: $grid-breakpoints;
@import "overrides/carousel";
@import "overrides/nojs";
@import "overrides/focus-visible";
// PRINT
......
......@@ -5,6 +5,7 @@
</footer>
{{ $slider := resources.Get "js/libs/tiny-slider.js" }}
{{ $focus := resources.Get "js/libs/focus-visible.js" }}
{{ $userscript := resources.Get "js/script.js" }}
{{ $script := slice $slider $userscript | resources.Concat "js/bundle.js" | minify | fingerprint }}
{{ $script := slice $slider $focus $userscript | resources.Concat "js/bundle.js" | minify | fingerprint }}
<script src="{{ $script.Permalink }}" {{ printf "integrity=%q" $script.Data.Integrity | safeHTMLAttr }}></script>
\ No newline at end of file
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment