Skip to content

feat(sidebar): opt-in ARIA landmark roles; aria-controls on nav links - #2526

Merged
cpsievert merged 7 commits into
mainfrom
sidebar-landmark-roles
Oct 7, 2026
Merged

cpsievert merged 7 commits into
mainfrom
sidebar-landmark-roles

Conversation

@cpsievert

@cpsievert cpsievert commented Oct 5, 2026 •

Copy link
Copy Markdown
Collaborator

Stacked on #2523

This PR ports the R-side bslib changes that became necessary after #2523 bumped the vendored bslib assets from 97aa1abc to ac1e25f8. Its base is fix-input-date-datesdisabled-format (a copy of #2523's branch pushed to this repo), so the diff shows only this PR's commits. Once #2523 merges to main, delete the copy branch and GitHub will retarget this PR to main.

An audit of the bslib range found 6 commits; the other 4 needed no Python changes: rstudio/bslib#1349 and rstudio/bslib#1352 are TS/SCSS-only (carried by the vendored assets), rstudio/bslib#1353 is a DESCRIPTION change, and rstudio/bslib#1351 fixes a duplicate-id bug py-shiny never had (its toolbar_input_select() already gives the <select> its own {id}-select id).

Port of rstudio/bslib#1359: sidebar landmark roles

  • ui.sidebar() gains a role parameter ("form", "search", "complementary", "region"). The default, None, now renders a neutral <div> instead of an <aside>; "complementary" renders an <aside>, and other roles render a <div> with the corresponding role attribute (e.g. <div role="form">).
  • Landmark roles require an accessible name. An explicit aria_label=/aria_labelledby= (passed via **kwargs) always wins and is hoisted from the content div onto the landmark element; otherwise the landmark is labelled by the sidebar's title (via aria-labelledby, generating a title id when needed). Non-tag titles (e.g. HTML()) are wrapped in a display:contents div so they can hold an id. A landmark sidebar with no name raises ValueError.
  • ui.page_sidebar() now places the whole layout_sidebar() inside the page's <main class="bslib-page-main"> landmark (previously only the main-content children were), and the <main> no longer gets bslib-gap-spacing (page_main_container() gains a gap parameter).
  • playwright.controller.Sidebar now locates the sidebar by class (> .sidebar#{id}) instead of the aside element, since the element is no longer always an <aside>.
  • Express ui.sidebar() gains the same role parameter.

Note the vendored page_sidebar.scss (updated in #2523) already expects this new markup.

Port of rstudio/bslib#1355: aria-controls on nav links

Nav links that target a tab panel now carry aria-controls pointing at the panel's id, alongside the existing href.

Testing

  • New pytest coverage: neutral/complementary/landmark markup, title-derived and explicit accessible names, non-tag title wrapping, the accessible-name error, invalid roles, the page_sidebar() main-landmark structure, and aria-controls on nav links. Existing nav markup expectations updated.
  • tests/pytest: 1226 passed, 6 skipped (the 14 test_app_ui_page_html.py errors are a pre-existing local starlette/httpx environment issue, also present on the base branch).
  • flake8 and pyrefly pass on the changed files (make check-lint/make check-types fail locally on pre-existing sandbox files/missing pyrefly, unrelated to this change).

taekop and others added 5 commits October 5, 2026 13:26
Ran `make upgrade-html-deps`. Pulls rstudio/shiny@main (bbc43849, the
#4434 merge commit) and rstudio/bslib@main (97aa1abc -> ac1e25f8).
Emit data-dates-disabled instead of data-date-dates-disabled so the dates are parsed as yyyy-mm-dd by the client binding rather than by bootstrap-datepicker with the display format. Omit the attribute when datesdisabled is None. Update InputDate.expect_datesdisabled() to match. Needs the shiny.js from rstudio/shiny#4434.
Port of rstudio/bslib#1359 to ui.sidebar() and ui.page_sidebar().

ui.sidebar() gains a role parameter ("form", "search", "complementary",
"region"). The default (None) now renders a neutral <div> instead of an
<aside>; "complementary" renders an <aside>, and other roles render a
<div> with the corresponding role attribute. Landmark roles require an
accessible name: an explicit aria_label/aria_labelledby wins, otherwise the
landmark is labelled by the sidebar's title (non-tag titles are wrapped in
a display:contents div so they can hold an id).

ui.page_sidebar() now places the whole sidebar layout inside the page's
<main> landmark (previously only the main content children), and the
<main> no longer gets bslib-gap-spacing (page_main_container gains a gap
parameter).

The Sidebar playwright controller locates the sidebar by class instead of
the aside element.
Port of rstudio/bslib#1355: nav links that target a tab panel now carry
aria-controls pointing at the panel's id, alongside the existing href.
@cpsievert
cpsievert changed the base branch from main to fix-input-date-datesdisabled-format October 5, 2026 15:21
The 'str' annotation tripped both pyright and pyrefly in CI (the
pyright-ignore comment had also been detached from the call by black).
@cpsievert
cpsievert requested review from gadenbuie and a balanced review from Copilot October 5, 2026 15:29

This comment was marked as resolved.

@cpsievert
cpsievert changed the base branch from fix-input-date-datesdisabled-format to main October 7, 2026 16:38
@cpsievert
cpsievert merged commit 002af90 into main Oct 7, 2026
176 checks passed
@cpsievert
cpsievert deleted the sidebar-landmark-roles branch October 7, 2026 16:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants