Skip to content

Commit a4eaf77

Browse files
authored
Only make scrollable code blocks into tab stops (#1777)
* Only make scrollable code blocks into tab stops * Check for y-overflow too
1 parent a78a066 commit a4eaf77

File tree

3 files changed

+51
-2
lines changed

3 files changed

+51
-2
lines changed

src/pydata_sphinx_theme/assets/scripts/pydata-sphinx-theme.js

+27
Original file line numberDiff line numberDiff line change
@@ -692,6 +692,32 @@ function setupMobileSidebarKeyboardHandlers() {
692692
});
693693
}
694694

695+
/**
696+
* When the page loads or the window resizes check all elements with
697+
* [data-tabindex="0"], and if they have scrollable overflow, set tabIndex = 0.
698+
*/
699+
function setupLiteralBlockTabStops() {
700+
const updateTabStops = () => {
701+
document.querySelectorAll('[data-tabindex="0"]').forEach((el) => {
702+
el.tabIndex =
703+
el.scrollWidth > el.clientWidth || el.scrollHeight > el.clientHeight
704+
? 0
705+
: -1;
706+
});
707+
};
708+
window.addEventListener("resize", debounce(updateTabStops, 300));
709+
updateTabStops();
710+
}
711+
function debounce(callback, wait) {
712+
let timeoutId = null;
713+
return (...args) => {
714+
clearTimeout(timeoutId);
715+
timeoutId = setTimeout(() => {
716+
callback(...args);
717+
}, wait);
718+
};
719+
}
720+
695721
/*******************************************************************************
696722
* Call functions after document loading.
697723
*/
@@ -703,3 +729,4 @@ documentReady(setupSearchButtons);
703729
documentReady(initRTDObserver);
704730
documentReady(setupMobileSidebarKeyboardHandlers);
705731
documentReady(fixMoreLinksInMobileSidebar);
732+
documentReady(setupLiteralBlockTabStops);

src/pydata_sphinx_theme/translator.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ def starttag(self, *args, **kwargs):
3333
kwargs["ARIA-LEVEL"] = "2"
3434

3535
if "pre" in args:
36-
kwargs["tabindex"] = "0"
36+
kwargs["data-tabindex"] = "0"
3737

3838
return super().starttag(*args, **kwargs)
3939

@@ -50,7 +50,7 @@ def visit_literal_block(self, node):
5050
# executed successfully and appended to self.body a string of HTML
5151
# representing the code block, which we then modify.
5252
html_string = self.body[-1]
53-
self.body[-1] = html_string.replace("<pre", '<pre tabindex="0"')
53+
self.body[-1] = html_string.replace("<pre", '<pre data-tabindex="0"')
5454
raise nodes.SkipNode
5555

5656
def visit_table(self, node):

tests/test_a11y.py

+22
Original file line numberDiff line numberDiff line change
@@ -240,3 +240,25 @@ def test_version_switcher_highlighting(page: Page, url_base: str) -> None:
240240
light_mode = "rgb(10, 125, 145)" # pst-color-primary
241241
# dark_mode = "rgb(63, 177, 197)"
242242
expect(entry).to_have_css("color", light_mode)
243+
244+
245+
def test_code_block_tab_stop(page: Page, url_base: str) -> None:
246+
"""Code blocks that have scrollable content should be tab stops."""
247+
page.set_viewport_size({"width": 1440, "height": 720})
248+
page.goto(urljoin(url_base, "/examples/kitchen-sink/blocks.html"))
249+
code_block = page.locator(
250+
'css=#code-block pre[data-tabindex="0"]', has_text="from typing import Iterator"
251+
)
252+
253+
# Viewport is wide, so code block content fits, no overflow, no tab stop
254+
assert code_block.evaluate("el => el.scrollWidth > el.clientWidth") is False
255+
assert code_block.evaluate("el => el.tabIndex") != 0
256+
257+
page.set_viewport_size({"width": 400, "height": 720})
258+
259+
# Resize handler is debounced with 300 ms wait time
260+
page.wait_for_timeout(301)
261+
262+
# Narrow viewport, content overflows and code block should be a tab stop
263+
assert code_block.evaluate("el => el.scrollWidth > el.clientWidth") is True
264+
assert code_block.evaluate("el => el.tabIndex") == 0

0 commit comments

Comments
 (0)