Browse Source

Reorganize focus management

pull/111/head
Jānis Skarnelis 3 years ago
parent
commit
ae1b84f35d
  1. 2
      dist/fancybox.esm.js
  2. 2
      dist/fancybox.umd.js
  3. 171
      src/Fancybox/Fancybox.js
  4. 30
      src/Fancybox/plugins/Html/Html.js
  5. 6
      src/Fancybox/plugins/Toolbar/Toolbar.js

2
dist/fancybox.esm.js

File diff suppressed because one or more lines are too long

2
dist/fancybox.umd.js

File diff suppressed because one or more lines are too long

171
src/Fancybox/Fancybox.js

@ -152,6 +152,8 @@ class Fancybox extends Base {
"onKeydown", "onKeydown",
"onClick", "onClick",
"onFocus",
"onCreateSlide", "onCreateSlide",
"onTouchMove", "onTouchMove",
@ -168,7 +170,12 @@ class Fancybox extends Base {
*/ */
attachEvents() { attachEvents() {
document.addEventListener("mousedown", this.onMousedown); document.addEventListener("mousedown", this.onMousedown);
document.addEventListener("keydown", this.onKeydown); document.addEventListener("keydown", this.onKeydown, true);
// Trap keyboard focus inside of the modal
if (this.option("trapFocus")) {
document.addEventListener("focus", this.onFocus, true);
}
this.$container.addEventListener("click", this.onClick); this.$container.addEventListener("click", this.onClick);
} }
@ -178,7 +185,9 @@ class Fancybox extends Base {
*/ */
detachEvents() { detachEvents() {
document.removeEventListener("mousedown", this.onMousedown); document.removeEventListener("mousedown", this.onMousedown);
document.removeEventListener("keydown", this.onKeydown); document.removeEventListener("keydown", this.onKeydown, true);
document.removeEventListener("focus", this.onFocus, true);
this.$container.removeEventListener("click", this.onClick); this.$container.removeEventListener("click", this.onClick);
} }
@ -454,6 +463,14 @@ class Fancybox extends Base {
} }
} }
/**
* Handle focus event
* @param {Event} event - Focus event
*/
onFocus(event) {
this.focus(event);
}
/** /**
* Handle click event on the container * Handle click event on the container
* @param {Event} event - Click event * @param {Event} event - Click event
@ -551,14 +568,6 @@ class Fancybox extends Base {
document.body.classList.remove("is-using-mouse"); document.body.classList.remove("is-using-mouse");
const key = event.key; const key = event.key;
// Trap keyboard focus inside of the modal
if (key === "Tab" && this.option("trapFocus")) {
this.focus(event);
return;
}
const keyboard = this.option("keyboard"); const keyboard = this.option("keyboard");
if (!keyboard || event.ctrlKey || event.altKey || event.shiftKey) { if (!keyboard || event.ctrlKey || event.altKey || event.shiftKey) {
@ -613,43 +622,28 @@ class Fancybox extends Base {
* @param {Event} [event] - Focus event * @param {Event} [event] - Focus event
*/ */
focus(event) { focus(event) {
if (Fancybox.preventScrollSupported === undefined) {
// Detect if .focus() method supports `preventScroll` option,
// see https://developer.mozilla.org/en-US/docs/Web/API/HTMLOrForeignElement/focus
Fancybox.preventScrollSupported = (function () {
let rez = false;
document.createElement("div").focus({
get preventScroll() {
rez = true;
return false;
},
});
return rez;
})();
}
const setFocusOn = (node) => { const setFocusOn = (node) => {
if (node.setActive) { if (!node) {
// IE/Edge return;
node.setActive();
} else if (Fancybox.preventScrollSupported) {
// Modern browsers
node.focus({ preventScroll: true });
} else {
// Safari
node.focus();
} }
};
if (["init", "closing", "customClosing", "destroy"].indexOf(this.state) > -1) { this.ignoreFocusChange = true;
return;
}
if (event) { try {
event.preventDefault(); if (node.setActive) {
} // IE/Edge
node.setActive();
} else if (Fancybox.preventScrollSupported) {
// Modern browsers
node.focus({ preventScroll: true });
} else {
// Safari
node.focus();
}
} catch (e) {}
this.ignoreFocusChange = false;
};
const FOCUSABLE_ELEMENTS = [ const FOCUSABLE_ELEMENTS = [
"a[href]", "a[href]",
@ -667,75 +661,80 @@ class Fancybox extends Base {
'[tabindex]:not([tabindex^="-"]):not([disabled]):not([aria-hidden])', '[tabindex]:not([tabindex^="-"]):not([disabled]):not([aria-hidden])',
]; ];
if (this.ignoreFocusChange) {
return;
}
if (["init", "closing", "customClosing", "destroy"].indexOf(this.state) > -1) {
return;
}
const $currentSlide = this.getSlide().$el; const $currentSlide = this.getSlide().$el;
if (!$currentSlide) { if (!$currentSlide) {
return; return;
} }
// Setting `tabIndex` here helps to avoid Safari issues with random focusing and scrolling if (event) {
$currentSlide.tabIndex = 0; event.preventDefault();
}
const allFocusableElems = [].slice.call(this.$container.querySelectorAll(FOCUSABLE_ELEMENTS)); const allFocusableElems = Array.from(this.$container.querySelectorAll(FOCUSABLE_ELEMENTS));
let enabledElems = []; let enabledElems = [];
let $firstEl;
let $firstBtn;
for (let node of allFocusableElems) { for (let node of allFocusableElems) {
// Slide element will be the last one, the highest priority has elements having `autofocus` attribute const isInsideSlide = $currentSlide.contains(node);
if (node.classList && node.classList.contains("fancybox__slide")) {
continue;
}
const $closestSlide = node.closest(".fancybox__slide"); if (isInsideSlide || !this.Carousel.$viewport.contains(node)) {
enabledElems.push(node);
if (node.dataset.origTabindex !== undefined) {
node.tabIndex = node.dataset.tabindex;
node.removeAttribute("data-tabindex");
}
if ($closestSlide) { if (node.hasAttribute("autoFocus") || (!$firstEl && isInsideSlide)) {
if ($closestSlide === $currentSlide) { $firstEl = node;
enabledElems[node.hasAttribute("autofocus") ? "unshift" : "push"](node); } else if (node.matches(".fancybox__button--close")) {
$firstBtn = node;
} }
} else { } else {
enabledElems.push(node); node.dataset.origTabindex =
node.dataset.origTabindex === undefined ? node.getAttribute("tabindex") : node.dataset.origTabindex;
node.tabIndex = -1;
} }
} }
if (!enabledElems.length) { if (enabledElems.indexOf(document.activeElement) > -1) {
this.lastFocus = document.activeElement;
return; return;
} }
if (this.Carousel.pages.length > 1) { if (!event) {
enabledElems.push($currentSlide); setFocusOn($firstEl || $firstBtn || enabledElems[0]);
return;
} }
// Sort by tabindex if (!$currentSlide.contains(document.activeElement)) {
enabledElems.sort(function (a, b) { if (this.lastFocus === enabledElems[0]) {
if (a.tabIndex > b.tabIndex) return -1; setFocusOn(enabledElems[enabledElems.length - 1]);
if (a.tabIndex < b.tabIndex) return 1; } else {
let idx = enabledElems.indexOf(this.lastFocus);
return 0;
});
const focusedElementIndex = enabledElems.indexOf(document.activeElement);
const moveForward = event && !event.shiftKey;
const moveBackward = event && event.shiftKey;
if (moveForward) {
if (focusedElementIndex === enabledElems.length - 1) {
return setFocusOn(enabledElems[0]);
}
return setFocusOn(enabledElems[focusedElementIndex + 1]);
}
if (moveBackward) { if (idx < enabledElems.length - 2) {
if (focusedElementIndex === 0) { setFocusOn(enabledElems[idx + 1]);
return setFocusOn(enabledElems[enabledElems.length - 1]); } else {
setFocusOn(enabledElems[0]);
}
} }
return setFocusOn(enabledElems[focusedElementIndex - 1]); this.lastFocus = document.activeElement;
}
if (focusedElementIndex < 0) {
return setFocusOn(enabledElems[0]);
} }
} }

30
src/Fancybox/plugins/Html/Html.js

@ -33,9 +33,7 @@ const defaults = {
// HTML5 video parameters // HTML5 video parameters
html5video: { html5video: {
tpl: `<video class="fancybox__html5video" playsinline controls controlsList="nodownload" poster="{{poster}}"> tpl: `<video class="fancybox__html5video" playsinline controls controlsList="nodownload" poster="{{poster}}">
<source src="{{src}}" type="{{format}}" /> <source src="{{src}}" type="{{format}}" />Sorry, your browser doesn't support embedded videos.</video>`,
Sorry, your browser doesn\'t support embedded videos, <a href="{{src}}">download</a> and watch with your favorite video player!
</video>`,
format: "", format: "",
}, },
}; };
@ -526,13 +524,25 @@ export class Html {
*/ */
playVideo(slide) { playVideo(slide) {
if (slide.type === "html5video" && slide.video.autoplay) { if (slide.type === "html5video" && slide.video.autoplay) {
const $video = slide.$el.querySelector("video"); try {
const $video = slide.$el.querySelector("video");
if ($video) {
try { if ($video) {
$video.play(); const promise = $video.play();
} catch (err) {}
} if (promise !== undefined) {
promise
.then(() => {
// Autoplay started
})
.catch((error) => {
// Autoplay was prevented.
$video.muted = true;
$video.play();
});
}
}
} catch (err) {}
} }
if (slide.type !== "video" || !(slide.$iframe && slide.$iframe.contentWindow)) { if (slide.type !== "video" || !(slide.$iframe && slide.$iframe.contentWindow)) {

6
src/Fancybox/plugins/Toolbar/Toolbar.js

@ -118,7 +118,7 @@ const defaults = {
label: "CLOSE", label: "CLOSE",
class: "fancybox__button--close", class: "fancybox__button--close",
html: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" tabindex="-1"><path d="M20 20L4 4m16 0L4 20"></path></svg>', html: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" tabindex="-1"><path d="M20 20L4 4m16 0L4 20"></path></svg>',
tabindex: 1, tabindex: 0,
click: function (event) { click: function (event) {
event.stopPropagation(); event.stopPropagation();
event.preventDefault(); event.preventDefault();
@ -275,7 +275,7 @@ export class Toolbar {
} }
onKeydown(fancybox, key, event) { onKeydown(fancybox, key, event) {
if (key === " ") { if (key === " " && this.Slideshow) {
this.Slideshow.toggle(); this.Slideshow.toggle();
event.preventDefault(); event.preventDefault();
@ -436,12 +436,14 @@ export class Toolbar {
for (const $el of this.fancybox.$container.querySelectorAll("a.fancybox__button--download")) { for (const $el of this.fancybox.$container.querySelectorAll("a.fancybox__button--download")) {
if (src) { if (src) {
$el.removeAttribute("disabled"); $el.removeAttribute("disabled");
$el.removeAttribute("tabindex");
$el.setAttribute("href", src); $el.setAttribute("href", src);
$el.setAttribute("download", src); $el.setAttribute("download", src);
$el.setAttribute("target", "_blank"); $el.setAttribute("target", "_blank");
} else { } else {
$el.setAttribute("disabled", ""); $el.setAttribute("disabled", "");
$el.setAttribute("tabindex", -1);
$el.removeAttribute("href"); $el.removeAttribute("href");
$el.removeAttribute("download"); $el.removeAttribute("download");

Loading…
Cancel
Save