Browse Source

Reorganize focus management

pull/188/head
Jānis Skarnelis 3 years ago
parent
commit
a758857269
  1. 2
      dist/fancybox.css
  2. 2
      dist/fancybox.esm.js
  3. 2
      dist/fancybox.umd.js
  4. 7
      package.json
  5. 162
      src/Fancybox/Fancybox.js
  6. 17
      src/Fancybox/plugins/Toolbar/Toolbar.js
  7. 4
      src/Fancybox/scss/base.scss
  8. 60
      src/shared/utils/setFocusOn.js
  9. 4
      tests/4_fancybox_test.js

2
dist/fancybox.css

File diff suppressed because one or more lines are too long

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

7
package.json

@ -23,15 +23,16 @@
"keywords": [
"javascript",
"lightbox",
"fancybox",
"carousel",
"panzoom",
"pinchzoom",
"pinch",
"zoom",
"gesture",
"touch",
"drag",
"gallery",
"carousel",
"panzoom"
"gallery"
],
"license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://fancyapps.com/",

162
src/Fancybox/Fancybox.js

@ -1,5 +1,6 @@
import { extend } from "../shared/utils/extend.js";
import { canUseDOM } from "../shared/utils/canUseDOM.js";
import { FOCUSABLE_ELEMENTS, setFocusOn } from "../shared/utils/setFocusOn.js";
import { Base } from "../shared/Base/Base.js";
@ -143,7 +144,7 @@ class Fancybox extends Base {
// Reveal container
this.$container.setAttribute("aria-hidden", "false");
// Focus on the first focus element in this instance
// Set focus on the first focusable element inside this instance
if (this.option("trapFocus")) {
this.focus();
}
@ -161,6 +162,7 @@ class Fancybox extends Base {
"onFocus",
"onCreateSlide",
"onSettle",
"onTouchMove",
"onTouchEnd",
@ -424,6 +426,7 @@ class Fancybox extends Base {
"*": (name, ...details) => this.trigger(`Carousel.${name}`, ...details),
init: (carousel) => (this.Carousel = carousel),
createSlide: this.onCreateSlide,
settle: this.onSettle,
},
},
@ -474,6 +477,15 @@ class Fancybox extends Base {
}
}
/**
* Handle Carousel `settle` event
*/
onSettle() {
if (this.option("autoFocus")) {
this.focus();
}
}
/**
* Handle focus event
* @param {Event} event - Focus event
@ -564,7 +576,9 @@ class Fancybox extends Base {
* Handle `mousedown` event to mark that the mouse is in use
*/
onMousedown() {
document.body.classList.add("is-using-mouse");
if (this.state === "ready") {
document.body.classList.add("is-using-mouse");
}
}
/**
@ -633,45 +647,6 @@ class Fancybox extends Base {
* @param {Event} [event] - Focus event
*/
focus(event) {
const setFocusOn = (node) => {
if (!node) {
return;
}
Fancybox.ignoreFocusChange = true;
try {
if (node.setActive) {
// IE/Edge
node.setActive();
} else if (Fancybox.preventScrollSupported) {
// Modern browsers
node.focus({ preventScroll: true });
} else {
// Safari
node.focus();
}
} catch (e) {}
Fancybox.ignoreFocusChange = false;
};
const FOCUSABLE_ELEMENTS = [
"a[href]",
"area[href]",
'input:not([disabled]):not([type="hidden"]):not([aria-hidden])',
"select:not([disabled]):not([aria-hidden])",
"textarea:not([disabled]):not([aria-hidden])",
"button:not([disabled]):not([aria-hidden])",
"iframe",
"object",
"embed",
"video",
"audio",
"[contenteditable]",
'[tabindex]:not([tabindex^="-"]):not([disabled]):not([aria-hidden])',
];
if (Fancybox.ignoreFocusChange) {
return;
}
@ -680,38 +655,44 @@ class Fancybox extends Base {
return;
}
const $currentSlide = this.getSlide().$el;
if (!$currentSlide) {
return;
}
if (event) {
event.preventDefault();
}
const allFocusableElems = Array.from(this.$container.querySelectorAll(FOCUSABLE_ELEMENTS));
Fancybox.ignoreFocusChange = true;
const $container = this.$container;
const currentSlide = this.getSlide();
const $currentSlide = currentSlide.state === "done" ? currentSlide.$el : null;
const allFocusableElems = Array.from($container.querySelectorAll(FOCUSABLE_ELEMENTS));
let enabledElems = [this.$container];
let enabledElems = [];
let $firstEl;
for (let node of allFocusableElems) {
const isInsideSlide = $currentSlide.contains(node);
// Enable element if it's visible and
// is inside the current slide or is outside main carousel (for example, inside the toolbar)
const isNodeVisible = node.offsetParent;
const isNodeInsideCurrentSlide = $currentSlide && $currentSlide.contains(node);
const isNodeOutsideCarousel = !this.Carousel.$viewport.contains(node);
// Enable element if it's visible and is inside current slide or
// not inside main carousel , e.g., not inside previous/next slide, but located, for example, inside the toolbar
if (node.offsetParent && (isInsideSlide || !this.Carousel.$viewport.contains(node))) {
if (isNodeVisible && (isNodeInsideCurrentSlide || isNodeOutsideCarousel)) {
enabledElems.push(node);
if (node.dataset.origTabindex !== undefined) {
node.tabIndex = node.dataset.tabindex;
node.removeAttribute("data-tabindex");
node.tabIndex = node.dataset.origTabindex;
node.removeAttribute("data-orig-tabindex");
}
if (node.hasAttribute("autoFocus") || (!$firstEl && isInsideSlide)) {
if (
node.hasAttribute("autoFocus") ||
(!$firstEl && isNodeInsideCurrentSlide && !node.classList.contains("carousel__button"))
) {
$firstEl = node;
}
} else {
// Element is either hidden or is inside preloaded slide (e.g., not inside current slide, but next/prev)
node.dataset.origTabindex =
node.dataset.origTabindex === undefined ? node.getAttribute("tabindex") : node.dataset.origTabindex;
@ -719,31 +700,27 @@ class Fancybox extends Base {
}
}
if (enabledElems.indexOf(document.activeElement) > -1) {
this.lastFocus = document.activeElement;
return;
}
if (!event) {
setFocusOn($firstEl || enabledElems[0]);
return;
}
if (!$currentSlide.contains(document.activeElement)) {
if (this.lastFocus === enabledElems[1]) {
setFocusOn(enabledElems[enabledElems.length - 1]);
if (this.option("autoFocus") && $firstEl) {
setFocusOn($firstEl);
} else if (enabledElems.indexOf(document.activeElement) < 0) {
setFocusOn($container);
}
} else {
if (enabledElems.indexOf(event.target) > -1) {
this.lastFocus = event.target;
} else {
let idx = enabledElems.indexOf(this.lastFocus);
if (idx < enabledElems.length - 2) {
setFocusOn(enabledElems[idx + 1]);
if (this.lastFocus === $container) {
setFocusOn(enabledElems[enabledElems.length - 1]);
} else {
setFocusOn(enabledElems[0]);
setFocusOn($container);
}
}
this.lastFocus = document.activeElement;
}
this.lastFocus = document.activeElement;
Fancybox.ignoreFocusChange = false;
}
/**
@ -1185,16 +1162,7 @@ class Fancybox extends Base {
this.$container = this.$backdrop = this.$carousel = null;
if ($trigger) {
// `preventScroll` option is not yet supported by Safari
// https://bugs.webkit.org/show_bug.cgi?id=178583
if (Fancybox.preventScrollSupported) {
$trigger.focus({ preventScroll: true });
} else {
const scrollTop = document.body.scrollTop; // Save position
$trigger.focus();
document.body.scrollTop = scrollTop;
}
setFocusOn($trigger);
}
delete instances[this.id];
@ -1371,22 +1339,26 @@ class Fancybox extends Base {
target = options.target || null;
// Get options
// ===
options = extend({}, options, Fancybox.openers.get(opener));
// Get matching nodes
const groupAttr = options.groupAttr === undefined ? "data-fancybox" : options.groupAttr;
const groupValue = groupAttr && target && target.getAttribute(`${groupAttr}`);
// ===
const groupAll = options.groupAll === undefined ? false : options.groupAll;
if (groupAll || groupValue) {
const groupAttr = options.groupAttr === undefined ? "data-fancybox" : options.groupAttr;
const groupValue = groupAttr && target ? target.getAttribute(`${groupAttr}`) : "";
if (!target || groupValue || groupAll) {
items = [].slice.call(document.querySelectorAll(opener));
}
if (!groupAll) {
if (target && !groupAll) {
if (groupValue) {
items = items.filter((el) => el.getAttribute(`${groupAttr}`) === groupValue);
} else {
items = [target];
}
} else {
items = [target];
}
if (!items.length) {
@ -1394,13 +1366,17 @@ class Fancybox extends Base {
}
// Exit if current instance is triggered from the same element
// ===
const currentInstance = Fancybox.getInstance();
if (currentInstance && items.indexOf(currentInstance.options.$trigger) > -1) {
return false;
}
// Index of current item in the gallery
// Start Fancybox
// ===
// Get index of current item in the gallery
index = target ? items.indexOf(target) : index;
// Convert items in a format supported by fancybox

17
src/Fancybox/plugins/Toolbar/Toolbar.js

@ -6,7 +6,17 @@ import { Slideshow } from "../../../shared/utils/Slideshow.js";
const defaults = {
// What toolbar items to display
display: ["counter", "zoom", "slideshow", "fullscreen", "thumbs", "close"],
display: [
"counter",
//"prev",
//"next",
//"download",
"zoom",
"slideshow",
"fullscreen",
"thumbs",
"close",
],
// Only create a toolbar item if there is at least one image in the group
autoEnable: true,
@ -458,7 +468,7 @@ export class Toolbar {
}
}
// Zoom buttons
// Zoom button
// ===
const panzoom = slide.Panzoom;
const canZoom = panzoom && panzoom.option("maxScale") > panzoom.option("baseScale");
@ -481,7 +491,8 @@ export class Toolbar {
$el.innerHTML = cnt;
}
// Disable prev/next links if gallery is not infinite and reached start/end
// Disable previous/next links if gallery is not infinite and has reached start/end
// ===
if (!this.fancybox.option("infinite")) {
for (const $el of this.fancybox.$container.querySelectorAll("[data-fancybox-prev]")) {
if (idx === 0) {

4
src/Fancybox/scss/base.scss

@ -1,3 +1,7 @@
html.with-fancybox {
scroll-behavior: auto;
}
body.compensate-for-scrollbar {
overflow: hidden !important;
touch-action: none;

60
src/shared/utils/setFocusOn.js

@ -0,0 +1,60 @@
import { canUseDOM } from "./canUseDOM.js";
let preventScrollSupported = null;
export const FOCUSABLE_ELEMENTS = [
"a[href]",
"area[href]",
'input:not([disabled]):not([type="hidden"]):not([aria-hidden])',
"select:not([disabled]):not([aria-hidden])",
"textarea:not([disabled]):not([aria-hidden])",
"button:not([disabled]):not([aria-hidden])",
"iframe",
"object",
"embed",
"video",
"audio",
"[contenteditable]",
'[tabindex]:not([tabindex^="-"]):not([disabled]):not([aria-hidden])',
];
export const setFocusOn = (node) => {
if (!node || !canUseDOM) {
return;
}
if (preventScrollSupported === null) {
document.createElement("div").focus({
get preventScroll() {
preventScrollSupported = true;
return false;
},
});
}
try {
if (node.setActive) {
// IE/Edge
node.setActive();
} else if (preventScrollSupported) {
// Modern browsers
node.focus({ preventScroll: true });
} else {
// Safari does not support `preventScroll` option
// https://bugs.webkit.org/show_bug.cgi?id=178583
// Save position
const scrollTop = window.pageXOffset || document.body.scrollTop;
const scrollLeft = window.pageYOffset || document.body.scrollLeft;
node.focus();
document.body.scrollTo({
top: scrollTop,
left: scrollLeft,
behavior: "auto",
});
}
} catch (e) {}
};

4
tests/4_fancybox_test.js

@ -306,7 +306,7 @@ describe("Fancybox", function () {
expect(instance.state).to.be.equal("destroy");
});
it("sets focus on close button by default", async function () {
it("sets focus on container by default", async function () {
const instance = new Fancybox(
[
{
@ -320,7 +320,7 @@ describe("Fancybox", function () {
}
);
expect(document.activeElement.classList.contains("is-close")).to.be.true;
expect(document.activeElement.classList.contains("fancybox__container")).to.be.true;
instance.close();
});

Loading…
Cancel
Save