Repository navigation
Conversation
Footnote, citation, cross-reference and code-annotation popups must close on Esc (WCAG 1.4.13) and an interactive popup must close once focus leaves both its trigger and the popup (WCAG 2.4.11). These fail on the current tippy configuration, which has no Esc handling and only hides on trigger blur. The fixture disables code-copy because the copy button otherwise intercepts hover on the one-line annotated cell.
code-annotations: select shares the hover tippy config, so its click popups need the same Esc handling. revealjs reuses the same popup code for citations, but reveal.js binds Esc to the slide overview. Without dedicated handling, Esc opens the overview, which hides the popup only as a side effect, so the test asserts that the overview stays closed. Footnotes are not covered there because revealjs renders them as slide asides without a popup.
Fixes #15080. The vendored tippy.js 6.3.7 has no Esc handling, and its focusout listener sits only on the trigger: with interactive: true, focus moving into the popup is ignored and nothing notices focus leaving it afterwards. Footnote, citation and cross-reference popups (tippyHover) and code-annotation popups were affected, failing WCAG 1.4.13 (dismissible) and, at narrow widths, 2.4.11 (focus obscured). A tippy plugin, wired into both configs, listens while a popup is shown: - Esc hides the popup. If focus is inside it, focus returns to the trigger, with the instance disabled meanwhile so the focus trigger does not reopen it. - focusout from the popup to an element outside both popup and trigger hides it. A null relatedTarget is ignored so clicking non-focusable text inside the popup keeps it open. Pending delay timeouts are cleared before hiding: cross-reference popups call show() from onTrigger, ahead of tippy's own 100ms delayed show, which otherwise reopened the popup right after Esc. The keydown listener runs in the capture phase and stops propagation, so in revealjs Esc closes the popup without also toggling the slide overview. tippyOptions.config cannot override plugins: it is serialized with JSON.stringify and only set internally (revealjs offset/maxWidth).
tabUntilFocused was copied verbatim into four specs, and the WebKit Alt+Tab mapping was inlined in three more. Both now live in src/utils.ts, with the WebKit rationale stated once. tabKey takes a shift flag for the Shift+Tab case.
The popup Esc handler stops propagation so that reveal.js does not also toggle the slide overview. With a popup open, pressing M opens the slide menu without hiding the popup or moving focus, so the next Esc closed only the popup and the menu needed a second press. Propagation now continues while the menu is open (RevealMenu isOpen()); the menu handles Esc itself, and the overview still stays closed. The test opens the popup with keyboard focus: a hover-opened popup can close on its own once the menu covers the trigger, which makes the scenario racy.
With code-annotations: select the tippy trigger is click, and tippy installs no blur handling for it. Opening the popup with Enter and pressing Tab left the popup open beside the next focused element. The focus-out check now also runs on the trigger, so focus moving anywhere but into the popup closes it. For focus triggers tippy already hides on trigger blur, so the extra listener changes nothing there.
Esc over a popup was allowed to propagate whenever the reveal.js slide
menu was open, so the menu could close on the same keypress. RevealMenu
installs its Esc handler, and the keyboardCondition guard that stops
reveal.js from toggling the overview, only when its keyboard option is
on. With `menu: {keyboard: false, open-on-init: true}` the menu is open
but handles no keys, so dismissing a popup also opened the slide
overview. Propagation now requires the menu to be open with keyboard
handling enabled (RevealMenu treats an unset option as on).
With code-annotations: select, Enter schedules the popup behind the 50 ms trigger delay. The trigger's focusout listener was attached only in onShow, and tippy has no blur handling for a click trigger, so tabbing away within the delay still opened the popup with focus elsewhere. The listener now lives for the instance's lifetime, so leaving the trigger clears the pending show; the popup's own focusout listener stays scoped to when it is shown.
Collaborator
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Footnote, citation, cross-reference and code-annotation popups cannot be dismissed with Esc, and an interactive popup stays open after focus leaves it, covering the next focused element at narrow widths. This fails WCAG 1.4.13 (dismissible) and 2.4.11 (focus not obscured).
Root cause
The vendored tippy.js 6.3.7 has no Esc handling. Its focusout listener sits only on the trigger, so with
interactive: truefocus moving into the popup is ignored and nothing notices focus leaving it afterwards.Fix
A tippy plugin, wired into both the
tippyHoverand the code-annotation configs, listens while a popup is shown:relatedTargetis ignored, so clicking non-focusable text inside the popup keeps it open.A few cases needed more than the sketch in the issue:
show()fromonTrigger, ahead of tippy's own 100ms delayed show, which reopened the popup right after Esc. Pending delay timeouts are now cleared before hiding.code-annotations: selectthe trigger isclick, and tippy has no blur handling for it. The trigger's focusout now closes the popup, and cancels a pending show when focus leaves within the 50 ms delay.keyboard: falsehandles nothing, so propagation requires keyboard handling to be on.tippyOptions.configcannot override plugins: it is serialized withJSON.stringifyand only set internally (revealjs offset and maxWidth).The Playwright tests cover the four popup types in HTML, select mode, and revealjs. The
tabUntilFocusedhelper was copied into four specs and the WebKit Alt+Tab mapping inlined in three more, so both now live insrc/utils.ts.Fixes #15080
Checklist
I have (if applicable):
AI-assisted PR
Note: autonomous AI agents submitting PRs without human oversight are not permitted — see the Code of Conduct.