From bdd4b8d36946ddeb45ec6a8ea5b2ffdbc9235557 Mon Sep 17 00:00:00 2001 From: Anthony Lio Date: Thu, 10 Sep 2026 15:03:55 +0300 Subject: [PATCH] fix(docs): guides sidebar a11y elements (#49942) ## What kind of change does this PR introduce? bug fix (accessibility) + test coverage ## What is the current behavior? the guides sidebar renders invalid list markup: group headers and dividers sit directly under the root `ul`, and accordion links render as `li` elements without an owning list fixes [DOCS-1279](https://linear.app/supabase/issue/DOCS-1279/guides-sidebar-put-li-elements-directly-in-the-ul) ## What is the new behavior? - sidebar renders a semantic hierarchy: every `ul` has only `li` children, every `li` has an immediate list parent, and the menu header sits outside the item list. pure markup change, - docs e2e scans the guide navigation separately from the article and blocks the `list` and `listitem` axe rules there against sample pages that include different usages (flat links, grouped links, nested accordion) ## How to test? run the docs dev server, then the scoped a11y suite: ```bash pnpm dev:docs pnpm e2e:docs:a11y ``` ## Follow up visuals and behavior are unchanged here but better parity between guide/reference is handled in the stacked pr ## Summary by CodeRabbit * **Bug Fixes** * Improved documentation navigation rendering for nested guide items, active states, and disabled entries. * Ensured navigation groups and child links use valid, testable list structures. * **Tests** * Added coverage verifying that guide navigation changes run the appropriate documentation pages. * Confirmed unrelated documentation changes can be skipped by the end-to-end workflow. * **Chores** * Updated documentation test scope detection to include guide navigation changes. --- .../NavigationMenuGuideListItems.tsx | 168 ++++++++---------- e2e/docs/utils/resolve-docs-scope.test.ts | 31 ++++ e2e/docs/utils/resolve-docs-scope.ts | 22 ++- 3 files changed, 130 insertions(+), 91 deletions(-) create mode 100644 e2e/docs/utils/resolve-docs-scope.test.ts diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideListItems.tsx b/apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideListItems.tsx index 3e26b1f62ff..89b64e78b9d 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideListItems.tsx +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideListItems.tsx @@ -43,9 +43,10 @@ const ContentAccordionLink = React.memo(function ContentAccordionLink(props: any const { resolvedTheme } = useTheme() const activeItem = props.subItem.url === pathname const activeItemRef = useRef(null) + const hasChildren = props.subItem.items && props.subItem.items.length > 0 const isChildActive = - props.subItem.items && + hasChildren && props.subItem.items.some((child: NavAccordionItem) => hasActiveDescendant(child, pathname)) const LinkContainer = (props) => { @@ -73,16 +74,8 @@ const ContentAccordionLink = React.memo(function ContentAccordionLink(props: any } }) return ( - <> - {props.subItemIndex === 0 && ( - <> -
- - {props.parent.name} - - - )} - {props.subItem.items && props.subItem.items.length > 0 ? ( +
  • + {hasChildren ? ( - {props.subItem.items - .filter((subItem) => subItem.enabled !== false) - .map((subSubItem) => { - if (subSubItem.items && subSubItem.items.length > 0) { - return ( - - ) - } +
      + {props.subItem.items + .filter((subItem) => subItem.enabled !== false) + .map((subSubItem) => { + if (subSubItem.items && subSubItem.items.length > 0) { + return + } - return ( -
    • - - {subSubItem.name} - -
    • - ) - })} + return ( +
    • + + {subSubItem.name} + +
    • + ) + })} +
    ) : ( -
  • - -
    - {props.subItem.icon && ( - {props.subItem.name} - )} - {props.subItem.name} -
    -
    -
  • + +
    + {props.subItem.icon && ( + {props.subItem.name} + )} + {props.subItem.name} +
    +
    )} - + ) }) @@ -210,7 +196,7 @@ const Content = (props) => { } return ( - + + ) } diff --git a/e2e/docs/utils/resolve-docs-scope.test.ts b/e2e/docs/utils/resolve-docs-scope.test.ts new file mode 100644 index 00000000000..15eadef1560 --- /dev/null +++ b/e2e/docs/utils/resolve-docs-scope.test.ts @@ -0,0 +1,31 @@ +import assert from 'node:assert/strict' +import test from 'node:test' +import { fileURLToPath } from 'node:url' + +import { + GUIDE_LIST_COMPONENT, + GUIDE_LIST_COMPONENT_PAGES, + resolveDocsScope, +} from './resolve-docs-scope.ts' + +const repoRoot = fileURLToPath(new URL('../../..', import.meta.url)) + +test('maps a guide list component change to its sample pages', async () => { + const result = await resolveDocsScope({ + changedFiles: [GUIDE_LIST_COMPONENT], + repoRoot, + }) + + assert.deepEqual(result.pages, [...GUIDE_LIST_COMPONENT_PAGES].sort()) + assert.equal(result.skip, false) +}) + +test('ignores unrelated docs component changes', async () => { + const result = await resolveDocsScope({ + changedFiles: ['apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts'], + repoRoot, + }) + + assert.deepEqual(result.pages, []) + assert.equal(result.skip, true) +}) diff --git a/e2e/docs/utils/resolve-docs-scope.ts b/e2e/docs/utils/resolve-docs-scope.ts index 2c5325cbac9..1a5f66ba488 100644 --- a/e2e/docs/utils/resolve-docs-scope.ts +++ b/e2e/docs/utils/resolve-docs-scope.ts @@ -19,6 +19,14 @@ const PARTIALS_PREFIX = 'apps/docs/content/_partials/' const DOCS_GUIDES_URL_PREFIX = '/docs/guides/' const DOCS_TROUBLESHOOTING_URL_PREFIX = '/docs/guides/troubleshooting/' const FEDERATED_CONTENT_SOURCES_DIR = 'apps/docs/scripts/federated-content/sources' +export const GUIDE_LIST_COMPONENT = + 'apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideListItems.tsx' + +export const GUIDE_LIST_COMPONENT_PAGES = [ + '/docs/guides/getting-started/api-keys', + '/docs/guides/getting-started/quickstarts/nextjs', + '/docs/guides/integrations/build-a-supabase-oauth-integration/oauth-scopes', +] const PARTIAL_PATH_RE = /<\$Partial\b[\s\S]*?\bpath\s*=\s*"([^"]+)"[\s\S]*?\/?>/g const SOURCE_SECTION_RE = /\bsection:\s*'([^']+)'/g @@ -246,9 +254,15 @@ export async function resolveDocsScope( const maxPages = options.maxPages ?? MAX_SCOPED_PAGES const pages = new Set() + const requiredPages = new Set() const changedPartials: string[] = [] for (const file of options.changedFiles) { + if (normalizeRepoPath(file) === GUIDE_LIST_COMPONENT) { + for (const page of GUIDE_LIST_COMPONENT_PAGES) requiredPages.add(page) + continue + } + const page = changedFileToPagePath(file) if (page) { pages.add(page) @@ -271,7 +285,13 @@ export async function resolveDocsScope( } } - const sorted = [...pages].sort().slice(0, maxPages) + const selectedRequiredPages = [...requiredPages].sort().slice(0, maxPages) + const remainingSlots = Math.max(0, maxPages - selectedRequiredPages.length) + const selectedPages = [...pages] + .filter((page) => !requiredPages.has(page)) + .sort() + .slice(0, remainingSlots) + const sorted = [...selectedRequiredPages, ...selectedPages].sort() return { pages: sorted,