From 045baa9c1152f627037bf5a44f18ca8c2c2bb71d Mon Sep 17 00:00:00 2001 From: Karthikeyan Padaikathu Date: Tue, 20 Oct 2026 16:31:20 -0700 Subject: [PATCH 1/4] fix: accessibility fixes for page title, nav drawer, version menu and mobile search - Recreate Gatsby Head nodes in the HTML namespace so document.title is set - Use theme colors for the mobile nav drawer so nav items have sufficient contrast - Use the default foreground color for CLI version menu links - Constrain the mobile search results panel so all results can be scrolled into view Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- gatsby-browser.js | 26 ++++++++++++++++++++++++ src/components/nav-drawer.module.css | 6 +++--- src/components/search.module.css | 9 ++++++++ src/components/variant-select.js | 4 +++- src/components/variant-select.module.css | 4 ++++ 5 files changed, 45 insertions(+), 4 deletions(-) diff --git a/gatsby-browser.js b/gatsby-browser.js index 12118ae9a49..f867ff05e3b 100644 --- a/gatsby-browser.js +++ b/gatsby-browser.js @@ -3,6 +3,32 @@ import {SKIP_TO_SEARCH_ID} from './src/constants' export {default as wrapPageElement} from './src/page' export {default as wrapRootElement} from './src/root' +const HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml' + +const toHtmlNamespace = node => { + const element = document.createElement(node.nodeName.toLowerCase()) + for (const {name, value} of node.attributes) { + element.setAttribute(name, value) + } + element.textContent = node.textContent + return element +} + +// Gatsby renders the Head export inside an to avoid React 19 metadata hoisting, so the +// / nodes it copies into are created in the SVG namespace. Browsers and +// assistive technology ignore an SVG for document.title, so recreate them as HTML elements. +const fixHeadNamespaces = () => { + for (const node of document.head.querySelectorAll('[data-gatsby-head]')) { + if (node.namespaceURI !== HTML_NAMESPACE) { + node.replaceWith(toHtmlNamespace(node)) + } + } +} + +export const onClientEntry = () => { + new MutationObserver(fixHeadNamespaces).observe(document.head, {childList: true}) +} + export const shouldUpdateScroll = ({routerProps}) => { const {scrollUpdate = true} = routerProps.location.state ?? {} return scrollUpdate diff --git a/src/components/nav-drawer.module.css b/src/components/nav-drawer.module.css index 0a3546212bc..5af65a3edca 100644 --- a/src/components/nav-drawer.module.css +++ b/src/components/nav-drawer.module.css @@ -19,7 +19,7 @@ display: flex; flex-direction: column; height: 100%; - background: #333333; + background: var(--bgColor-default, #ffffff); overflow: auto; } @@ -27,8 +27,8 @@ display: flex; flex-direction: column; flex: 1 0 auto; - color: #e1e4e8; - background: #333333; + color: var(--fgColor-default, #1f2328); + background: var(--bgColor-default, #ffffff); } .Box_4 { diff --git a/src/components/search.module.css b/src/components/search.module.css index d9f834097f1..56a0c70eecf 100644 --- a/src/components/search.module.css +++ b/src/components/search.module.css @@ -42,6 +42,15 @@ flex-direction: column; } +/* Primer's ThemeProvider wraps LightTheme content in a <div data-color-mode>. Let that wrapper + shrink to the remaining height so the results list scrolls instead of overflowing the viewport. */ +.Box_6 > [data-color-mode] { + display: flex; + flex-direction: column; + flex: 1 1 auto; + min-height: 0; +} + .Box_7 { display: flex; color: #1f2328; diff --git a/src/components/variant-select.js b/src/components/variant-select.js index 7308af4ad3d..7e4ad56de8a 100644 --- a/src/components/variant-select.js +++ b/src/components/variant-select.js @@ -19,7 +19,9 @@ const StyledOverlay = styled(ActionMenu.Overlay)` const VariantItem = ({title, shortName, url, active}) => { return ( <ActionList.Item state={{scrollUpdate: false}} id={shortName} active={active} tabIndex={null}> - <LinkNoUnderline to={url}>{title}</LinkNoUnderline> + <LinkNoUnderline to={url} className={styles.variantLink}> + {title} + </LinkNoUnderline> </ActionList.Item> ) } diff --git a/src/components/variant-select.module.css b/src/components/variant-select.module.css index 9bdcfbd24bf..08a18fbf412 100644 --- a/src/components/variant-select.module.css +++ b/src/components/variant-select.module.css @@ -11,6 +11,10 @@ width: 100%; } +a.variantLink { + color: var(--fgColor-default, #1f2328); +} + .menuButton:focus-visible { outline: 2px solid; outline-color: -webkit-focus-ring-color; From ad1d1880d5f39b7bb39627d46534c6a82510e723 Mon Sep 17 00:00:00 2001 From: Karthikeyan Padaikathu <kartykp@github.com> Date: Tue, 20 Oct 2026 16:44:36 -0700 Subject: [PATCH 2/4] fix: remove invalid head element and unknown button prop warnings - Move ThemeProvider from wrapRootElement to wrapPageElement so no div is rendered inside Head - Use leadingVisual instead of the unsupported leadingIcon prop on the mobile table of contents button - Remove code comments Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- gatsby-browser.js | 3 --- src/components/search.module.css | 2 -- src/components/table-of-contents.js | 2 +- src/page.js | 35 ++++++++++++++++------------- src/root.js | 7 +----- 5 files changed, 21 insertions(+), 28 deletions(-) diff --git a/gatsby-browser.js b/gatsby-browser.js index f867ff05e3b..959715647e3 100644 --- a/gatsby-browser.js +++ b/gatsby-browser.js @@ -14,9 +14,6 @@ const toHtmlNamespace = node => { return element } -// Gatsby renders the Head export inside an <svg> to avoid React 19 metadata hoisting, so the -// <title>/<meta> nodes it copies into <head> are created in the SVG namespace. Browsers and -// assistive technology ignore an SVG <title> for document.title, so recreate them as HTML elements. const fixHeadNamespaces = () => { for (const node of document.head.querySelectorAll('[data-gatsby-head]')) { if (node.namespaceURI !== HTML_NAMESPACE) { diff --git a/src/components/search.module.css b/src/components/search.module.css index 56a0c70eecf..b6abf39f2e8 100644 --- a/src/components/search.module.css +++ b/src/components/search.module.css @@ -42,8 +42,6 @@ flex-direction: column; } -/* Primer's ThemeProvider wraps LightTheme content in a <div data-color-mode>. Let that wrapper - shrink to the remaining height so the results list scrolls instead of overflowing the viewport. */ .Box_6 > [data-color-mode] { display: flex; flex-direction: column; diff --git a/src/components/table-of-contents.js b/src/components/table-of-contents.js index e5d20b6f6c6..0f45399b5e1 100644 --- a/src/components/table-of-contents.js +++ b/src/components/table-of-contents.js @@ -48,7 +48,7 @@ export const Mobile = withTableOfContents(({items}) => { <Details {...getDetailsProps()} className={styles.Details}> <Button as="summary" - leadingIcon={open ? ChevronDownIcon : ChevronRightIcon} + leadingVisual={open ? ChevronDownIcon : ChevronRightIcon} className={`${styles.Button} ${open ? styles.buttonOpen : styles.buttonClosed}`} > Table of contents diff --git a/src/page.js b/src/page.js index fbadf5d100f..2f4432eb7ae 100644 --- a/src/page.js +++ b/src/page.js @@ -17,6 +17,7 @@ import { import {PageProvider} from './hooks/use-page' import Layout from './layout' +import {ThemeProvider} from './theme' import * as styles from './page.module.css' @@ -44,24 +45,26 @@ const PageElement = ({element, props}) => { slugger: new Slugger(), } return ( - <BaseStyles> - <GlobalStyles /> - <SkipBox> - <SkipLink href={`#${SKIP_TO_SEARCH_ID}`}>Skip to search</SkipLink> - <SkipLink href={`#${SKIP_TO_CONTENT_ID}`}>Skip to content</SkipLink> - </SkipBox> - <PageProvider value={page}> - <div className={styles.Box}> - <Header /> - <div className={styles.Box_1}> - <div className={styles.sidebarContainer}> - <Sidebar /> + <ThemeProvider> + <BaseStyles> + <GlobalStyles /> + <SkipBox> + <SkipLink href={`#${SKIP_TO_SEARCH_ID}`}>Skip to search</SkipLink> + <SkipLink href={`#${SKIP_TO_CONTENT_ID}`}>Skip to content</SkipLink> + </SkipBox> + <PageProvider value={page}> + <div className={styles.Box}> + <Header /> + <div className={styles.Box_1}> + <div className={styles.sidebarContainer}> + <Sidebar /> + </div> + <Layout>{element}</Layout> </div> - <Layout>{element}</Layout> </div> - </div> - </PageProvider> - </BaseStyles> + </PageProvider> + </BaseStyles> + </ThemeProvider> ) } diff --git a/src/root.js b/src/root.js index fb20d7219f3..e63179958f8 100644 --- a/src/root.js +++ b/src/root.js @@ -1,12 +1,7 @@ import React from 'react' import {MDXProvider} from '@mdx-js/react' -import {ThemeProvider} from './theme' import * as components from './mdx' -const RootElement = ({element}) => ( - <MDXProvider components={components}> - <ThemeProvider>{element}</ThemeProvider> - </MDXProvider> -) +const RootElement = ({element}) => <MDXProvider components={components}>{element}</MDXProvider> export default RootElement From 9e12e557db2244a626977e32501d465d3db0dc23 Mon Sep 17 00:00:00 2001 From: Karthikeyan Padaikathu <kartykp@github.com> Date: Tue, 20 Oct 2026 17:05:27 -0700 Subject: [PATCH 3/4] fix: expose group and position information for CLI version menu and navigation lists - Render CLI version menu entries as ActionList.LinkItem so the focused element is the menuitem inside its labelled group - Add list position to sidebar navigation links and page navigation hierarchy links Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/components/nav-items.js | 8 +++++--- src/components/variant-select.js | 15 ++++++++++----- src/components/variant-select.module.css | 2 +- src/mdx/nav-hierarchy.js | 16 ++++++++++++---- 4 files changed, 28 insertions(+), 13 deletions(-) diff --git a/src/components/nav-items.js b/src/components/nav-items.js index f3c0cf0f8b6..655ed657930 100644 --- a/src/components/nav-items.js +++ b/src/components/nav-items.js @@ -10,7 +10,7 @@ import useSiteMetadata from '../hooks/use-site-metadata' import * as styles from './nav-items.module.css' -const NavItem = ({item, path, depth}) => { +const NavItem = ({item, path, depth, position, total}) => { const isCurrent = getNav.isActiveUrl(path, item.url) const items = getNav.getHierarchy(item, item.url, {hideVariants: true}) @@ -20,6 +20,8 @@ const NavItem = ({item, path, depth}) => { to={item.url} defaultOpen={items && isCurrent} aria-current={isCurrent ? 'page' : null} + aria-label={`${item.title}, ${position} of ${total}`} + aria-labelledby={null} className={depth === 1 ? styles.NavList_Item_depth1 : styles.NavList_Item_depth2} > {item.title} @@ -34,11 +36,11 @@ const NavItem = ({item, path, depth}) => { const NavItems = ({items, path, depth = 1}) => ( <> - {items.map(item => + {items.map((item, index) => React.createElement( depth === 1 ? NavList.Group : React.Fragment, {key: item.title}, - <NavItem item={item} path={path} depth={depth} />, + <NavItem item={item} path={path} depth={depth} position={index + 1} total={items.length} />, ), )} </> diff --git a/src/components/variant-select.js b/src/components/variant-select.js index 7e4ad56de8a..30f9a5db95b 100644 --- a/src/components/variant-select.js +++ b/src/components/variant-select.js @@ -18,11 +18,16 @@ const StyledOverlay = styled(ActionMenu.Overlay)` const VariantItem = ({title, shortName, url, active}) => { return ( - <ActionList.Item state={{scrollUpdate: false}} id={shortName} active={active} tabIndex={null}> - <LinkNoUnderline to={url} className={styles.variantLink}> - {title} - </LinkNoUnderline> - </ActionList.Item> + <ActionList.LinkItem + as={LinkNoUnderline} + to={url} + state={{scrollUpdate: false}} + id={shortName} + active={active} + className={styles.variantItem} + > + {title} + </ActionList.LinkItem> ) } diff --git a/src/components/variant-select.module.css b/src/components/variant-select.module.css index 08a18fbf412..a51057000f8 100644 --- a/src/components/variant-select.module.css +++ b/src/components/variant-select.module.css @@ -11,7 +11,7 @@ width: 100%; } -a.variantLink { +.variantItem a { color: var(--fgColor-default, #1f2328); } diff --git a/src/mdx/nav-hierarchy.js b/src/mdx/nav-hierarchy.js index 1ec7c51dd25..daae9aa5315 100644 --- a/src/mdx/nav-hierarchy.js +++ b/src/mdx/nav-hierarchy.js @@ -5,12 +5,12 @@ import usePage from '../hooks/use-page' import * as styles from './nav-hierarchy.module.css' -const HierarchyItem = ({item, maxDepth, depth, hideVariants}) => { +const HierarchyItem = ({item, maxDepth, depth, hideVariants, position, total}) => { const hierarchy = getNav.getHierarchy(item, null, {hideVariants}) return ( <li key={item.url}> - <Link key={item.title} to={item.url}> + <Link key={item.title} to={item.url} aria-label={`${item.title}, ${position} of ${total}`}> {item.title} </Link> {item.description ? <div className={styles.Box}>{item.description}</div> : null} @@ -28,8 +28,16 @@ const Hierarchy = ({items, maxDepth, depth, hideVariants}) => { return ( <ul> - {items.map(item => ( - <HierarchyItem key={item.url} item={item} maxDepth={maxDepth} depth={depth} hideVariants={hideVariants} /> + {items.map((item, index) => ( + <HierarchyItem + key={item.url} + item={item} + maxDepth={maxDepth} + depth={depth} + hideVariants={hideVariants} + position={index + 1} + total={items.length} + /> ))} </ul> ) From c783f9ae393b1c83f82e070a52b93a83eaf02ca3 Mon Sep 17 00:00:00 2001 From: Karthikeyan Padaikathu <kartykp@github.com> Date: Tue, 20 Oct 2026 17:18:26 -0700 Subject: [PATCH 4/4] fix: apply navigation position labels to leaf items only Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/components/nav-items.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/nav-items.js b/src/components/nav-items.js index 655ed657930..d6fba0979e2 100644 --- a/src/components/nav-items.js +++ b/src/components/nav-items.js @@ -13,6 +13,7 @@ import * as styles from './nav-items.module.css' const NavItem = ({item, path, depth, position, total}) => { const isCurrent = getNav.isActiveUrl(path, item.url) const items = getNav.getHierarchy(item, item.url, {hideVariants: true}) + const positionLabel = items ? {} : {'aria-label': `${item.title}, ${position} of ${total}`, 'aria-labelledby': null} return ( <NavList.Item @@ -20,8 +21,7 @@ const NavItem = ({item, path, depth, position, total}) => { to={item.url} defaultOpen={items && isCurrent} aria-current={isCurrent ? 'page' : null} - aria-label={`${item.title}, ${position} of ${total}`} - aria-labelledby={null} + {...positionLabel} className={depth === 1 ? styles.NavList_Item_depth1 : styles.NavList_Item_depth2} > {item.title}