Focus trap improvements
This commit is contained in:
parent
74ba6a96fc
commit
2d13ad3d39
@ -5,79 +5,10 @@
|
|||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
|
|
||||||
import browser from './utils/browser';
|
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 { on, triggerEvent } from './utils/events';
|
||||||
import is from './utils/is';
|
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 {
|
class Fullscreen {
|
||||||
constructor(player) {
|
constructor(player) {
|
||||||
// Keep reference to parent
|
// Keep reference to parent
|
||||||
@ -101,7 +32,7 @@ class Fullscreen {
|
|||||||
this.prefix === 'ms' ? 'MSFullscreenChange' : `${this.prefix}fullscreenchange`,
|
this.prefix === 'ms' ? 'MSFullscreenChange' : `${this.prefix}fullscreenchange`,
|
||||||
() => {
|
() => {
|
||||||
// TODO: Filter for target??
|
// TODO: Filter for target??
|
||||||
onChange.call(this);
|
this.onChange();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -115,6 +46,9 @@ class Fullscreen {
|
|||||||
this.toggle();
|
this.toggle();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Tap focus when in fullscreen
|
||||||
|
on.call(this, this.player.elements.container, 'keydown', event => this.trapFocus(event));
|
||||||
|
|
||||||
// Update the UI
|
// Update the UI
|
||||||
this.update();
|
this.update();
|
||||||
}
|
}
|
||||||
@ -194,6 +128,97 @@ class Fullscreen {
|
|||||||
: this.player.elements.container;
|
: 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 UI
|
||||||
update() {
|
update() {
|
||||||
if (this.enabled) {
|
if (this.enabled) {
|
||||||
@ -226,9 +251,9 @@ class Fullscreen {
|
|||||||
if (browser.isIos && this.player.config.fullscreen.iosNative) {
|
if (browser.isIos && this.player.config.fullscreen.iosNative) {
|
||||||
this.target.webkitEnterFullscreen();
|
this.target.webkitEnterFullscreen();
|
||||||
} else if (!Fullscreen.native || this.forceFallback) {
|
} else if (!Fullscreen.native || this.forceFallback) {
|
||||||
toggleFallback.call(this, true);
|
this.toggleFallback(true);
|
||||||
} else if (!this.prefix) {
|
} else if (!this.prefix) {
|
||||||
this.target.requestFullscreen({ navigationUI: "hide" });
|
this.target.requestFullscreen({ navigationUI: 'hide' });
|
||||||
} else if (!is.empty(this.prefix)) {
|
} else if (!is.empty(this.prefix)) {
|
||||||
this.target[`${this.prefix}Request${this.property}`]();
|
this.target[`${this.prefix}Request${this.property}`]();
|
||||||
}
|
}
|
||||||
@ -245,7 +270,7 @@ class Fullscreen {
|
|||||||
this.target.webkitExitFullscreen();
|
this.target.webkitExitFullscreen();
|
||||||
this.player.play();
|
this.player.play();
|
||||||
} else if (!Fullscreen.native || this.forceFallback) {
|
} else if (!Fullscreen.native || this.forceFallback) {
|
||||||
toggleFallback.call(this, false);
|
this.toggleFallback(false);
|
||||||
} else if (!this.prefix) {
|
} else if (!this.prefix) {
|
||||||
(document.cancelFullScreen || document.exitFullscreen).call(document);
|
(document.cancelFullScreen || document.exitFullscreen).call(document);
|
||||||
} else if (!is.empty(this.prefix)) {
|
} else if (!is.empty(this.prefix)) {
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
// Element utils
|
// Element utils
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
|
|
||||||
import { toggleListener } from './events';
|
|
||||||
import is from './is';
|
import is from './is';
|
||||||
import { extend } from './objects';
|
import { extend } from './objects';
|
||||||
|
|
||||||
@ -248,40 +247,6 @@ export function getElement(selector) {
|
|||||||
return this.elements.container.querySelector(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
|
// Set focus and tab focus class
|
||||||
export function setFocus(element = null, tabFocus = false) {
|
export function setFocus(element = null, tabFocus = false) {
|
||||||
if (!is.element(element)) {
|
if (!is.element(element)) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user