Skip to content

Commit b23fc50

Browse files
authored
Keep focus in place when a revealjs presentation is shown again (#15090)
Each time a revealjs presentation became visible again, such as after switching tabs, reveal.js moved focus to the body. Readers lost their place on a link or chart, and screen readers such as NVDA and JAWS, which pass arrow keys on only while focus is in the deck, dropped back to browse mode. A new patch moves focus out of a frame and onto the deck, and leaves focus where it is everywhere else. - Add the revealjs-0003 patch, applied after 0001 and 0002 - Rebuild the reveal.js bundles and source maps from 5.1.0 - Add a Playwright spec for focus after the page is shown again Fixes #15079 See #15020 and #15034 for the earlier revealjs screen reader fixes.
1 parent 63571e3 commit b23fc50

9 files changed

Lines changed: 114 additions & 7 deletions

File tree

‎news/changelog-1.11.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ All changes included in 1.11:
3535
- ([#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)
3636
- ([#15030](https://github.com/quarto-dev/quarto-cli/pull/15030)): Fix `axe` checks in `revealjs` presentations hiding the current slide from screen readers when moving to another slide while the scan runs.
3737
- ([#15033](https://github.com/quarto-dev/quarto-cli/issues/15033)): Fix screen reader announcements in `revealjs` presentations reading the CSS inside Mermaid diagrams, omitting image alternative text, and reading the contents of `role="img"` graphics instead of their accessible names. (author: @jooyoungseo)
38+
- ([#15079](https://github.com/quarto-dev/quarto-cli/issues/15079)): Fix keyboard focus in `revealjs` presentations moving to the page when coming back to the presentation's browser tab, which took readers off the link or chart they were on and made screen readers stop passing the arrow keys to the presentation. (author: @jooyoungseo)
3839
- ([#15080](https://github.com/quarto-dev/quarto-cli/issues/15080)): Fix footnote, citation, cross-reference and code annotation popups not closing with Esc, and staying open over the next focused element after keyboard focus leaves them.
3940

4041
## Formats
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
diff --git a/js/reveal.js b/js/reveal.js
2+
index 8e5f5ee..82f66f6 100644
3+
--- a/js/reveal.js
4+
+++ b/js/reveal.js
5+
@@ -2841,14 +2841,20 @@ export default function( revealElement, options ) {
6+
*/
7+
function onPageVisibilityChange( event ) {
8+
9+
- // If, after clicking a link or similar and we're coming back,
10+
- // focus the document.body to ensure we can use keyboard shortcuts
11+
- if( document.hidden === false && document.activeElement !== document.body ) {
12+
- // Not all elements support .blur() - SVGs among them.
13+
- if( typeof document.activeElement.blur === 'function' ) {
14+
- document.activeElement.blur();
15+
- }
16+
- document.body.focus();
17+
+ // If, after clicking into a frame such as an embedded video, we're
18+
+ // coming back, move the focus to the presentation so that keyboard
19+
+ // shortcuts work: keys pressed in a frame never reach reveal.js.
20+
+ // Anywhere else they do, so leave the focus where the reader left it.
21+
+ // On the body it would take them out of what they were on, and screen
22+
+ // readers in browse mode do not pass the arrow keys on from there.
23+
+ if( document.hidden === false && document.activeElement && document.activeElement.tagName === 'IFRAME' ) {
24+
+ // Blur the frame first, as reveal.js did: in Playwright's Firefox,
25+
+ // focusing the deck straight from a frame leaves the focus there.
26+
+ document.activeElement.blur();
27+
+ if( !dom.wrapper.hasAttribute( 'tabindex' ) ) {
28+
+ dom.wrapper.setAttribute( 'tabindex', '-1' );
29+
+ }
30+
+ dom.wrapper.focus( { preventScroll: true } );
31+
}
32+
33+
}

‎package/src/common/update-html-dependencies.ts‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -306,13 +306,16 @@ export async function updateHtmlDependencies(config: Configuration) {
306306
workingDir,
307307
async (dir: string, version: string) => {
308308
// Backport the unmerged PRs hakimel/reveal.js#3946 and #3949, plus
309-
// native image alt text from reveal.js 6, to 5.1.0. Before removing these
310-
// source patches and rebuild step, verify all fixes are in the release
311-
// we bundle. The image patch applies after the screen reader patch.
309+
// native image alt text from reveal.js 6, to 5.1.0, and keep the focus
310+
// where it was when the page is shown again (#15079), which no reveal.js
311+
// release does yet. Before removing these source patches and rebuild
312+
// step, verify all fixes are in the release we bundle. Each patch
313+
// applies after the one before it.
312314
const sourceDir = join(dir, `reveal.js-${version}`);
313315
for (const patch of [
314316
"revealjs-0001-screen-reader-announcements.patch",
315317
"revealjs-0002-image-announcements.patch",
318+
"revealjs-0003-focus-on-return.patch",
316319
]) {
317320
await runCmd("git", [
318321
"-C", sourceDir, "apply", "--ignore-space-change",

‎src/resources/formats/revealjs/reveal/dist/reveal.esm.js‎

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎src/resources/formats/revealjs/reveal/dist/reveal.esm.js.map‎

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎src/resources/formats/revealjs/reveal/dist/reveal.js‎

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎src/resources/formats/revealjs/reveal/dist/reveal.js.map‎

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
pagetitle: Focus when the presentation is shown again
3+
format: revealjs
4+
---
5+
6+
## Slide 1 {#slide-1}
7+
8+
[A link on slide-1](https://example.com/)
9+
10+
<iframe srcdoc="<button type='button'>Button in a frame</button>" title="A frame" height="80"></iframe>
11+
12+
## Slide 2 {#slide-2}
13+
14+
Text only.
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { expect, Page, test } from '@playwright/test';
2+
3+
// Playwright does not hide the page when another tab is brought to the front,
4+
// so mark it hidden and then visible again, as switching tabs does.
5+
const hideAndShow = (page: Page) =>
6+
page.evaluate(() => {
7+
const set = (hidden: boolean) => {
8+
Object.defineProperty(document, 'hidden', { configurable: true, get: () => hidden });
9+
Object.defineProperty(document, 'visibilityState', {
10+
configurable: true,
11+
get: () => (hidden ? 'hidden' : 'visible'),
12+
});
13+
document.dispatchEvent(new Event('visibilitychange'));
14+
};
15+
set(true);
16+
set(false);
17+
});
18+
19+
test.describe('the focus stays where it was when the presentation is shown again (#15079)', () => {
20+
test.beforeEach(async ({ page }) => {
21+
await page.goto('./revealjs/visibility-focus.html#/slide-1');
22+
await expect(page.locator('.reveal.ready')).toBeAttached();
23+
});
24+
25+
test('a link on a slide keeps the focus', async ({ page }) => {
26+
const link = page.getByRole('link', { name: 'A link on slide-1' });
27+
await link.focus();
28+
await hideAndShow(page);
29+
await expect(link).toBeFocused();
30+
await page.keyboard.press('ArrowRight');
31+
await expect(page).toHaveURL(/#\/slide-2$/);
32+
});
33+
34+
test('a frame hands the focus to the deck, where keys reach reveal.js', async ({ page }) => {
35+
await page.frameLocator('iframe').getByRole('button', { name: 'Button in a frame' }).focus();
36+
await hideAndShow(page);
37+
await expect(page.locator('.reveal')).toBeFocused();
38+
await page.keyboard.press('ArrowRight');
39+
await expect(page).toHaveURL(/#\/slide-2$/);
40+
});
41+
42+
test('the deck keeps the focus', async ({ page }) => {
43+
const deck = page.locator('.reveal');
44+
await deck.evaluate((element) => {
45+
element.setAttribute('tabindex', '-1');
46+
(element as HTMLElement).focus();
47+
});
48+
await hideAndShow(page);
49+
await expect(deck).toBeFocused();
50+
});
51+
52+
test('a focus on the body stays there', async ({ page }) => {
53+
await hideAndShow(page);
54+
await expect(page.locator('body')).toBeFocused();
55+
});
56+
});

0 commit comments

Comments
 (0)