Skip to content

Commit aaa4c6a

Browse files
yukito5669IzumiSyCopilot
authored
fix(sidebar): mark active links with aria-current (#634)
* fix(sidebar): expose active page with aria-current * Update SidebarMenuButton to use currentPath for active state Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Seiya IZUMI <IzumiSy@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent 74e9465 commit aaa4c6a

9 files changed

Lines changed: 59 additions & 8 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@tailor-platform/app-shell": patch
3+
---
4+
5+
Mark active internal sidebar links with `aria-current="page"` in custom, grouped, and generated navigation. Screen readers can identify the current page without application code editing the link element.

‎docs-manifest.json‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1294,9 +1294,9 @@
12941294
"symbols": [],
12951295
"hashes": {
12961296
"typeSurface": null,
1297-
"outline": "7e12558d80191549",
1297+
"outline": "ffd040e38c8f8a4b",
12981298
"snapshot": null,
1299-
"outputMd": "e342ef02dc2b9b86",
1299+
"outputMd": "3670e37a3c78abe7",
13001300
"examples": null
13011301
}
13021302
},
@@ -1733,7 +1733,7 @@
17331733
"packages/core/skills/app-shell-patterns/references/concepts/graphql.md": "a59a10293fba053b",
17341734
"packages/core/skills/app-shell-patterns/references/concepts/modules-and-resources.md": "bf15b5460f22d165",
17351735
"packages/core/skills/app-shell-patterns/references/concepts/routing-navigation.md": "8034c7f885994b29",
1736-
"packages/core/skills/app-shell-patterns/references/concepts/sidebar-navigation.md": "e342ef02dc2b9b86",
1736+
"packages/core/skills/app-shell-patterns/references/concepts/sidebar-navigation.md": "3670e37a3c78abe7",
17371737
"packages/core/skills/app-shell-patterns/references/concepts/styling-theming.md": "5c55865c499890a8",
17381738
"packages/core/skills/app-shell-patterns/references/components/action-panel.md": "71794d4b561e4d4b",
17391739
"packages/core/skills/app-shell-patterns/references/components/activity-card.md": "4f386e49380fb0b0",

‎docs-src/concepts/sidebar-navigation.docs.outline.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ const App = () => (
6969
### SidebarItem
7070

7171
A navigation item that automatically resolves title and icon from resource definitions.
72+
Active internal sidebar links expose `aria-current="page"` on the link itself, including
73+
items with custom `render` content. Inactive and external links do not expose it.
7274

7375
#### Props
7476

‎docs/concepts/sidebar-navigation.md‎

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎packages/core/src/components/sidebar/default-sidebar.test.tsx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,7 @@ describe("DefaultSidebar auto-generation", () => {
452452
// Active styling (background, weight and --sidebar-accent-foreground text)
453453
// is keyed on data-active, so the item must carry it — not just a class.
454454
expect(overviewLink.getAttribute("data-active")).toBe("true");
455+
expect(overviewLink.getAttribute("aria-current")).toBe("page");
455456
});
456457

457458
it("excludes componentless resources from sidebar links", async () => {

‎packages/core/src/components/sidebar/default-sidebar.tsx‎

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,14 @@ const AutoSidebarItems = (props: {
253253
{item.url ? (
254254
<>
255255
<SidebarMenuButton
256-
render={<Link to={item.url as string} />}
256+
render={
257+
<Link
258+
to={item.url as string}
259+
aria-current={
260+
isActivePath(item.url, props.currentPath) ? "page" : undefined
261+
}
262+
/>
263+
}
257264
isActive={isActivePath(item.url, props.currentPath)}
258265
tooltip={item.title}
259266
>
@@ -301,7 +308,14 @@ const AutoSidebarItems = (props: {
301308
.map((subItem) => (
302309
<SidebarMenuSubItem key={subItem.title}>
303310
<SidebarMenuSubButton
304-
render={<Link to={subItem.url!} />}
311+
render={
312+
<Link
313+
to={subItem.url!}
314+
aria-current={
315+
isActivePath(subItem.url, props.currentPath) ? "page" : undefined
316+
}
317+
/>
318+
}
305319
isActive={isActivePath(subItem.url, props.currentPath)}
306320
>
307321
<span>{subItem.title}</span>

‎packages/core/src/components/sidebar/sidebar-group.tsx‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,12 @@ export const SidebarGroup = (props: SidebarGroupProps) => {
191191
// explicit accessible name (the hover flyout's aria-label only mounts on
192192
// hover).
193193
const trigger = to ? (
194-
<SidebarMenuButton render={<Link to={to} aria-label={resolvedTitle} />} isActive={isActive}>
194+
<SidebarMenuButton
195+
render={
196+
<Link to={to} aria-label={resolvedTitle} aria-current={isActive ? "page" : undefined} />
197+
}
198+
isActive={isActive}
199+
>
195200
{icon}
196201
<span>{resolvedTitle}</span>
197202
</SidebarMenuButton>
@@ -213,7 +218,11 @@ export const SidebarGroup = (props: SidebarGroupProps) => {
213218
if (to) {
214219
return (
215220
<Collapsible.Root render={<SidebarMenuItem />} defaultOpen={defaultOpen}>
216-
<SidebarMenuButton render={<Link to={to} />} isActive={isActive} tooltip={resolvedTitle}>
221+
<SidebarMenuButton
222+
render={<Link to={to} aria-current={isActive ? "page" : undefined} />}
223+
isActive={isActive}
224+
tooltip={resolvedTitle}
225+
>
217226
{icon}
218227
<span>{resolvedTitle}</span>
219228
</SidebarMenuButton>

‎packages/core/src/components/sidebar/sidebar-item.tsx‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,11 @@ export const SidebarItem = (props: SidebarItemProps) => {
148148
// Internal link
149149
return (
150150
<SidebarMenuItem>
151-
<SidebarMenuButton render={<Link to={to} />} isActive={isActive} tooltip={title}>
151+
<SidebarMenuButton
152+
render={<Link to={to} aria-current={currentPath === to ? "page" : undefined} />}
153+
isActive={isActive}
154+
tooltip={title}
155+
>
152156
{render ? (
153157
render(renderProps)
154158
) : (

‎packages/core/src/components/sidebar/sidebar.test.tsx‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,13 +162,23 @@ describe("SidebarItem", () => {
162162
expect(menu.getByTestId("custom").textContent).toContain("Dashboard Custom");
163163
});
164164

165+
it("marks a custom-rendered active item on its link", () => {
166+
renderWithProviders(
167+
<SidebarItem to="/dashboard/overview" render={({ title }) => <span>{title}</span>} />,
168+
"/dashboard/overview",
169+
);
170+
171+
expect(getSidebarMenu().getByRole("link").getAttribute("aria-current")).toBe("page");
172+
});
173+
165174
it("renders external link with target blank", () => {
166175
renderWithProviders(<SidebarItem to="https://docs.example.com" external />);
167176

168177
const menu = getSidebarMenu();
169178
const link = menu.getByRole("link");
170179
expect(link.getAttribute("target")).toBe("_blank");
171180
expect(link.getAttribute("rel")).toBe("noopener noreferrer");
181+
expect(link.hasAttribute("aria-current")).toBe(false);
172182
});
173183

174184
it("auto-detects external URLs without external prop", () => {
@@ -185,6 +195,7 @@ describe("SidebarItem", () => {
185195
const menu = getSidebarMenu();
186196
const link = menu.getByRole("link");
187197
expect(link.getAttribute("data-active")).toBe("true");
198+
expect(link.getAttribute("aria-current")).toBe("page");
188199
});
189200

190201
it("applies the theme's selected background and text tokens when active, without hover", () => {
@@ -201,6 +212,7 @@ describe("SidebarItem", () => {
201212

202213
const link = getSidebarMenu().getByRole("link");
203214
expect(link.getAttribute("data-active")).toBe("false");
215+
expect(link.hasAttribute("aria-current")).toBe(false);
204216
});
205217

206218
it("falls back to URL-based title for unknown paths", () => {
@@ -270,10 +282,12 @@ describe("SidebarGroup", () => {
270282
const headerLink = links.find((l) => l.getAttribute("href") === "/products")!;
271283
const childLink = links.find((l) => l.getAttribute("href") === "/products/all")!;
272284
expect(headerLink.getAttribute("data-active")).toBe("true");
285+
expect(headerLink.getAttribute("aria-current")).toBe("page");
273286
expect(headerLink.className).toContain(
274287
"astw:data-[active=true]:text-sidebar-accent-foreground",
275288
);
276289
expect(childLink.getAttribute("data-active")).toBe("false");
290+
expect(childLink.hasAttribute("aria-current")).toBe(false);
277291
});
278292

279293
it("supports localized title function", () => {

0 commit comments

Comments
 (0)