From 2d13ad3d397423e527caa6f3eba9df242505dfff Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Sun, 9 Feb 2020 21:42:12 +0000 Subject: [PATCH] Focus trap improvements --- src/js/fullscreen.js | 173 ++++++++++++++++++++++----------------- src/js/utils/elements.js | 35 -------- 2 files changed, 99 insertions(+), 109 deletions(-) diff --git a/src/js/fullscreen.js b/src/js/fullscreen.js index 7ae3ff17..c74b3406 100644 --- a/src/js/fullscreen.js +++ b/src/js/fullscreen.js @@ -5,79 +5,10 @@ // ========================================================================== import browser from './utils/browser'; -import { hasClass, toggleClass, trapFocus } from './utils/elements'; +import { getElements, hasClass, toggleClass } from './utils/elements'; import { on, triggerEvent } from './utils/events'; import is from './utils/is'; -function onChange() { - if (!this.enabled) { - return; - } - - // Update toggle button - const button = this.player.elements.buttons.fullscreen; - if (is.element(button)) { - button.pressed = this.active; - } - - // Trigger an event - triggerEvent.call(this.player, this.target, this.active ? 'enterfullscreen' : 'exitfullscreen', true); - - // Trap focus in container - if (!browser.isIos) { - trapFocus.call(this.player, this.target, this.active); - } -} - -function toggleFallback(toggle = false) { - // Store or restore scroll position - if (toggle) { - this.scrollPosition = { - x: window.scrollX || 0, - y: window.scrollY || 0, - }; - } else { - window.scrollTo(this.scrollPosition.x, this.scrollPosition.y); - } - - // Toggle scroll - document.body.style.overflow = toggle ? 'hidden' : ''; - - // Toggle class hook - toggleClass(this.target, this.player.config.classNames.fullscreen.fallback, toggle); - - // Force full viewport on iPhone X+ - if (browser.isIos) { - let viewport = document.head.querySelector('meta[name="viewport"]'); - const property = 'viewport-fit=cover'; - - // Inject the viewport meta if required - if (!viewport) { - viewport = document.createElement('meta'); - viewport.setAttribute('name', 'viewport'); - } - - // Check if the property already exists - const hasProperty = is.string(viewport.content) && viewport.content.includes(property); - - if (toggle) { - this.cleanupViewport = !hasProperty; - - if (!hasProperty) { - viewport.content += `,${property}`; - } - } else if (this.cleanupViewport) { - viewport.content = viewport.content - .split(',') - .filter(part => part.trim() !== property) - .join(','); - } - } - - // Toggle button and fire events - onChange.call(this); -} - class Fullscreen { constructor(player) { // Keep reference to parent @@ -101,7 +32,7 @@ class Fullscreen { this.prefix === 'ms' ? 'MSFullscreenChange' : `${this.prefix}fullscreenchange`, () => { // TODO: Filter for target?? - onChange.call(this); + this.onChange(); }, ); @@ -115,6 +46,9 @@ class Fullscreen { this.toggle(); }); + // Tap focus when in fullscreen + on.call(this, this.player.elements.container, 'keydown', event => this.trapFocus(event)); + // Update the UI this.update(); } @@ -194,6 +128,97 @@ class Fullscreen { : this.player.elements.container; } + onChange() { + if (!this.enabled) { + return; + } + + // Update toggle button + const button = this.player.elements.buttons.fullscreen; + if (is.element(button)) { + button.pressed = this.active; + } + + // Trigger an event + triggerEvent.call(this.player, this.target, this.active ? 'enterfullscreen' : 'exitfullscreen', true); + } + + toggleFallback(toggle = false) { + // Store or restore scroll position + if (toggle) { + this.scrollPosition = { + x: window.scrollX || 0, + y: window.scrollY || 0, + }; + } else { + window.scrollTo(this.scrollPosition.x, this.scrollPosition.y); + } + + // Toggle scroll + document.body.style.overflow = toggle ? 'hidden' : ''; + + // Toggle class hook + toggleClass(this.target, this.player.config.classNames.fullscreen.fallback, toggle); + + // Force full viewport on iPhone X+ + if (browser.isIos) { + let viewport = document.head.querySelector('meta[name="viewport"]'); + const property = 'viewport-fit=cover'; + + // Inject the viewport meta if required + if (!viewport) { + viewport = document.createElement('meta'); + viewport.setAttribute('name', 'viewport'); + } + + // Check if the property already exists + const hasProperty = is.string(viewport.content) && viewport.content.includes(property); + + if (toggle) { + this.cleanupViewport = !hasProperty; + + if (!hasProperty) { + viewport.content += `,${property}`; + } + } else if (this.cleanupViewport) { + viewport.content = viewport.content + .split(',') + .filter(part => part.trim() !== property) + .join(','); + } + } + + // Toggle button and fire events + this.onChange(); + } + + // Trap focus inside container + trapFocus(event) { + // Bail if iOS, not active, not the tab key + if (browser.isIos || !this.active || event.key !== 'Tab' || event.keyCode !== 9) { + return; + } + + // Get the current focused element + const focused = document.activeElement; + const focusable = getElements.call( + this.player, + 'a[href], button:not(:disabled), input:not(:disabled), [tabindex]', + ); + const [first] = focusable; + const last = focusable[focusable.length - 1]; + + if (focused === last && !event.shiftKey) { + // Move focus to first element that can be tabbed if Shift isn't used + first.focus(); + event.preventDefault(); + } else if (focused === first && event.shiftKey) { + // Move focus to last element that can be tabbed if Shift is used + last.focus(); + event.preventDefault(); + } + } + // Update UI update() { if (this.enabled) { @@ -226,9 +251,9 @@ class Fullscreen { if (browser.isIos && this.player.config.fullscreen.iosNative) { this.target.webkitEnterFullscreen(); } else if (!Fullscreen.native || this.forceFallback) { - toggleFallback.call(this, true); + this.toggleFallback(true); } else if (!this.prefix) { - this.target.requestFullscreen({ navigationUI: "hide" }); + this.target.requestFullscreen({ navigationUI: 'hide' }); } else if (!is.empty(this.prefix)) { this.target[`${this.prefix}Request${this.property}`](); } @@ -245,7 +270,7 @@ class Fullscreen { this.target.webkitExitFullscreen(); this.player.play(); } else if (!Fullscreen.native || this.forceFallback) { - toggleFallback.call(this, false); + this.toggleFallback(false); } else if (!this.prefix) { (document.cancelFullScreen || document.exitFullscreen).call(document); } else if (!is.empty(this.prefix)) { diff --git a/src/js/utils/elements.js b/src/js/utils/elements.js index 921d533a..b88aad0c 100644 --- a/src/js/utils/elements.js +++ b/src/js/utils/elements.js @@ -2,7 +2,6 @@ // Element utils // ========================================================================== -import { toggleListener } from './events'; import is from './is'; import { extend } from './objects'; @@ -248,40 +247,6 @@ export function getElement(selector) { return this.elements.container.querySelector(selector); } -// Trap focus inside container -export function trapFocus(element = null, toggle = false) { - if (!is.element(element)) { - return; - } - - const focusable = getElements.call(this, 'button:not(:disabled), input:not(:disabled), [tabindex]'); - const first = focusable[0]; - const last = focusable[focusable.length - 1]; - const player = this; - - const trap = event => { - // Bail if not tab key or not fullscreen - if (event.key !== 'Tab' || event.keyCode !== 9 || !player.fullscreen.active) { - return; - } - - // Get the current focused element - const focused = document.activeElement; - - if (focused === last && !event.shiftKey) { - // Move focus to first element that can be tabbed if Shift isn't used - first.focus(); - event.preventDefault(); - } else if (focused === first && event.shiftKey) { - // Move focus to last element that can be tabbed if Shift is used - last.focus(); - event.preventDefault(); - } - }; - - toggleListener.call(this, this.elements.container, 'keydown', trap, toggle, false); -} - // Set focus and tab focus class export function setFocus(element = null, tabFocus = false) { if (!is.element(element)) {