From c40892175fdd3624381b07698277e912afa6dc6d Mon Sep 17 00:00:00 2001 From: gabalafou Date: Wed, 24 Apr 2024 11:00:15 -0400 Subject: [PATCH] Only make scrollable code blocks into tab stops (#1777) * Only make scrollable code blocks into tab stops * Check for y-overflow too --- .../assets/scripts/pydata-sphinx-theme.js | 27 +++++++++++++++++++ src/pydata_sphinx_theme/translator.py | 4 +-- tests/test_a11y.py | 22 +++++++++++++++ 3 files changed, 51 insertions(+), 2 deletions(-) diff --git a/src/pydata_sphinx_theme/assets/scripts/pydata-sphinx-theme.js b/src/pydata_sphinx_theme/assets/scripts/pydata-sphinx-theme.js index e151945509..cc97219b69 100644 --- a/src/pydata_sphinx_theme/assets/scripts/pydata-sphinx-theme.js +++ b/src/pydata_sphinx_theme/assets/scripts/pydata-sphinx-theme.js @@ -692,6 +692,32 @@ function setupMobileSidebarKeyboardHandlers() { }); } +/** + * When the page loads or the window resizes check all elements with + * [data-tabindex="0"], and if they have scrollable overflow, set tabIndex = 0. + */ +function setupLiteralBlockTabStops() { + const updateTabStops = () => { + document.querySelectorAll('[data-tabindex="0"]').forEach((el) => { + el.tabIndex = + el.scrollWidth > el.clientWidth || el.scrollHeight > el.clientHeight + ? 0 + : -1; + }); + }; + window.addEventListener("resize", debounce(updateTabStops, 300)); + updateTabStops(); +} +function debounce(callback, wait) { + let timeoutId = null; + return (...args) => { + clearTimeout(timeoutId); + timeoutId = setTimeout(() => { + callback(...args); + }, wait); + }; +} + /******************************************************************************* * Call functions after document loading. */ @@ -703,3 +729,4 @@ documentReady(setupSearchButtons); documentReady(initRTDObserver); documentReady(setupMobileSidebarKeyboardHandlers); documentReady(fixMoreLinksInMobileSidebar); +documentReady(setupLiteralBlockTabStops); diff --git a/src/pydata_sphinx_theme/translator.py b/src/pydata_sphinx_theme/translator.py index b05bc83077..e8ab9ea6e2 100644 --- a/src/pydata_sphinx_theme/translator.py +++ b/src/pydata_sphinx_theme/translator.py @@ -33,7 +33,7 @@ def starttag(self, *args, **kwargs): kwargs["ARIA-LEVEL"] = "2" if "pre" in args: - kwargs["tabindex"] = "0" + kwargs["data-tabindex"] = "0" return super().starttag(*args, **kwargs) @@ -50,7 +50,7 @@ def visit_literal_block(self, node): # executed successfully and appended to self.body a string of HTML # representing the code block, which we then modify. html_string = self.body[-1] - self.body[-1] = html_string.replace(" None: light_mode = "rgb(10, 125, 145)" # pst-color-primary # dark_mode = "rgb(63, 177, 197)" expect(entry).to_have_css("color", light_mode) + + +def test_code_block_tab_stop(page: Page, url_base: str) -> None: + """Code blocks that have scrollable content should be tab stops.""" + page.set_viewport_size({"width": 1440, "height": 720}) + page.goto(urljoin(url_base, "/examples/kitchen-sink/blocks.html")) + code_block = page.locator( + 'css=#code-block pre[data-tabindex="0"]', has_text="from typing import Iterator" + ) + + # Viewport is wide, so code block content fits, no overflow, no tab stop + assert code_block.evaluate("el => el.scrollWidth > el.clientWidth") is False + assert code_block.evaluate("el => el.tabIndex") != 0 + + page.set_viewport_size({"width": 400, "height": 720}) + + # Resize handler is debounced with 300 ms wait time + page.wait_for_timeout(301) + + # Narrow viewport, content overflows and code block should be a tab stop + assert code_block.evaluate("el => el.scrollWidth > el.clientWidth") is True + assert code_block.evaluate("el => el.tabIndex") == 0