Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions news/changelog-1.11.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ All changes included in 1.11:
- ([#14795](https://github.com/quarto-dev/quarto-cli/issues/14795)): Fix keyboard focus in `revealjs` presentations reaching links and controls on nearby slides that are not shown. Tab now stays on the current slide, except in overview, scroll and print views where all slides are shown.
- ([#14952](https://github.com/quarto-dev/quarto-cli/issues/14952)): Fix `revealjs` presentations disabling pinch-zoom on touch devices. The viewport `<meta>` tag no longer sets `maximum-scale=1.0` and `user-scalable=no`, which axe-core flagged (`meta-viewport`).
- ([#14968](https://github.com/quarto-dev/quarto-cli/issues/14968)): The header of notebook preview pages (back link, notebook title, download link) is now a navigation landmark named after the notebook, so it is reachable through landmark navigation and no longer flagged by axe-core (`region`).
- ([#14970](https://github.com/quarto-dev/quarto-cli/issues/14970)): Fix keyboard focus order of tabsets in `revealjs` presentations. Tab now follows the order of the slide: tabs and their panel content no longer come before other links on the slide, and two tabsets on the same slide are no longer mixed together.
- ([#15011](https://github.com/quarto-dev/quarto-cli/issues/15011)): Fix the slide menu of `revealjs` presentations being read by screen readers while it is closed and off screen, and Tab moving keyboard focus to it once it lists more slides than fit. (author: @jooyoungseo)
- ([#15015](https://github.com/quarto-dev/quarto-cli/issues/15015)): Fix the "Back to Article" link of notebook previews in website projects not being reachable or usable with the keyboard.
- ([#15019](https://github.com/quarto-dev/quarto-cli/issues/15019)): Fix screen reader announcements in `revealjs` presentations including incremental content before it is revealed and repeating slides and fragments when browser history is enabled. (author: @jooyoungseo)
Expand Down
174 changes: 155 additions & 19 deletions src/resources/formats/html/tabby/js/tabby.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,13 +84,150 @@
var getKeyboardFocusableElements = function (element) {
return [
...element.querySelectorAll(
'a[href], button, input, textarea, select, details,[tabindex]:not([tabindex="-1"])'
'a[href], button, input, textarea, select, summary,[tabindex]:not([tabindex="-1"])'
),
].filter(
(el) => !el.hasAttribute("disabled") && !el.getAttribute("aria-hidden")
// Shown elements only, as the browser skips the others: hidden content,
// like unselected panels (also of a nested tabset), is not rendered,
// and revealjs fragments not yet shown have `visibility: hidden`
(el) =>
el.getAttribute("tabindex") !== "-1" &&
!el.matches(":disabled") &&
!el.getAttribute("aria-hidden") &&
el.getClientRects().length > 0 &&
window.getComputedStyle(el).visibility === "visible"
);
};

// Tabs set up by tabby, as opposed to other tab widgets in a panel
var tabbyTabs = new WeakSet();

// The tab of a tab or of a tab panel
var getTab = function (tabOrPanel) {
return tabOrPanel.getAttribute("role") === "tab"
? tabOrPanel
: document.getElementById(tabOrPanel.getAttribute("aria-labelledby"));
};

// Selects a tab and focuses the last focusable element of its panel, or
// the tab itself if there is none. The panel content is hidden until the
// tab is selected. When the panel ends with a nested tabset, its end is
// the end of that tabset's last panel, as Tab would reach it.
var focusEndOf = function (tab) {
toggle(tab);
var content = document.querySelector(tab.hash);
var last = getKeyboardFocusableElements(content).pop();
if (!last) return tab.focus();

// Outermost tabby tab or tab panel between the last element and this
// panel; other tab widgets are ordinary panel content
var nestedTab;
for (var node = last; node && node !== content; node = node.parentElement) {
var role = node.getAttribute("role");
var nodeTab =
role === "tab" ? node : role === "tabpanel" ? getTab(node) : null;
if (nodeTab && tabbyTabs.has(nodeTab)) nestedTab = nodeTab;
}
var nested = nestedTab && getTabsMap(nestedTab);
if (nested) return focusEndOf(nested.tabs[nested.tabs.length - 1]);
last.focus();
};

// In revealjs, Tab goes from a tab to its panel content and from the end
// of that content to the next tab; Shift+Tab goes the other way. Tabs come
// before all panels in the DOM, so the browser alone would go tab to tab.
var tabOrderHandler = function (event) {
// Leave Tab to controls that handle it themselves, like code editors
if (event.key !== "Tab" || event.defaultPrevented) return;
var current = event.currentTarget;
var onTab = current.getAttribute("role") === "tab";
var tab = getTab(current);
var map = tab && getTabsMap(tab);
if (!map) return;

var target;
if (onTab && !event.shiftKey) {
target = getKeyboardFocusableElements(
document.querySelector(tab.hash)
)[0];
} else if (onTab) {
var previous = map.tabs[map.index - 1];
if (!previous) return;
event.preventDefault();
focusEndOf(previous);
return;
} else {
var focusables = getKeyboardFocusableElements(current);
if (event.shiftKey && event.target === focusables[0]) {
target = tab;
} else if (
!event.shiftKey &&
event.target === focusables[focusables.length - 1]
) {
target = map.tabs[map.index + 1];
}
}
if (!target) return;

event.preventDefault();
target.focus();
};

// Whether Shift+Tab is being pressed. Focus moves while the key is down, so
// this tells focus changes made by Shift+Tab apart from clicks.
var shiftTabDown = false;
var trackShiftTab = function () {
if (trackShiftTab.done) return;
trackShiftTab.done = true;
document.addEventListener(
"keydown",
function (event) {
shiftTabDown = event.key === "Tab" && event.shiftKey;
},
true
);
// A click, or leaving the window (the keyup then goes elsewhere), ends it
// too
var clear = function () {
shiftTabDown = false;
};
document.addEventListener("keyup", clear, true);
document.addEventListener("pointerdown", clear, true);
window.addEventListener("blur", clear);
};

// In revealjs, Shift+Tab from after a tabset reaches the content of the
// selected panel first, which skips the tabs after it. Start from the end
// of the last panel instead, so Shift+Tab is the reverse of Tab. Only for
// Shift+Tab: a click in a panel must keep that panel.
var backwardEntryHandler = function (event) {
var from = event.relatedTarget;
var tab = getTab(event.currentTarget);
var map = tab && getTabsMap(tab);
if (!from || !map || !shiftTabDown) return;

var tabList = map.tabs[0].closest('[role="tablist"]');
var fromInside = Array.prototype.some.call(map.tabs, function (tab) {
var content = document.querySelector(tab.hash);
return tab === from || (content && content.contains(from));
});
var fromAfter =
from.compareDocumentPosition(tabList) &
Node.DOCUMENT_POSITION_PRECEDING;
var last = map.tabs[map.tabs.length - 1];
var lastContent = document.querySelector(last.hash);
if (
fromInside ||
!fromAfter ||
!lastContent ||
lastContent.contains(event.target)
) {
return;
}

focusEndOf(last);
};

/**
* Remove roles and attributes from a tab and its content
* @param {Node} tab The tab
Expand All @@ -105,6 +242,10 @@

// remove event listener
tab.removeEventListener("focus", focusHandler, true);
tab.removeEventListener("keydown", tabOrderHandler);
content.removeEventListener("keydown", tabOrderHandler);
tab.removeEventListener("focusin", backwardEntryHandler);
content.removeEventListener("focusin", backwardEntryHandler);

// Remove roles
tab.removeAttribute("role");
Expand All @@ -131,6 +272,7 @@

// Add roles
tab.setAttribute("role", "tab");
tabbyTabs.add(tab);
tab.setAttribute("aria-controls", content.id);
tab.closest("li").setAttribute("role", "presentation");
content.setAttribute("role", "tabpanel");
Expand All @@ -145,8 +287,15 @@
content.setAttribute("hidden", "hidden");
}

// add focus event listender
if (isReveal()) tab.addEventListener("focus", focusHandler);
// add focus and Tab order event listeners
if (isReveal()) {
tab.addEventListener("focus", focusHandler);
tab.addEventListener("keydown", tabOrderHandler);
content.addEventListener("keydown", tabOrderHandler);
tab.addEventListener("focusin", backwardEntryHandler);
content.addEventListener("focusin", backwardEntryHandler);
trackShiftTab();
}
};

/**
Expand Down Expand Up @@ -322,27 +471,14 @@
// Add role to wrapper
tabWrapper.setAttribute("role", "tablist");

// Add roles to tabs. provide dynanmic tab indexes if we are within reveal
var contentTabindexes = isReveal();
var nextTabindex = 1;
// Add roles to tabs
Array.prototype.forEach.call(tabs, function (tab) {
if (contentTabindexes) {
tab.setAttribute("tabindex", "" + nextTabindex++);
} else {
tab.setAttribute("tabindex", "0");
}
tab.setAttribute("tabindex", "0");

// Get the tab content
var content = document.querySelector(tab.hash);
if (!content) return;

// set tab indexes for content
if (contentTabindexes) {
getKeyboardFocusableElements(content).forEach(function (el) {
el.setAttribute("tabindex", "" + nextTabindex++);
});
}

// Setup the tab
setupTab(tab, content, settings);
});
Expand Down
Loading
Loading