diff --git a/docs/accessibility/bf-036-bf-038-keyboard-visual-qa.md b/docs/accessibility/bf-036-bf-038-keyboard-visual-qa.md
index 97cc32f0..cf0c32b6 100644
--- a/docs/accessibility/bf-036-bf-038-keyboard-visual-qa.md
+++ b/docs/accessibility/bf-036-bf-038-keyboard-visual-qa.md
@@ -31,7 +31,7 @@ Scope: representative navigation, discovery, tool action, upload, and output sur
## Automated Coverage
- `tests/guards/focus-visible-guard.test.ts` prevents removing visible focus rings from app/component surfaces.
-- `tests/guards/shared-a11y-surfaces.test.ts` guards All Tools dialog focus trap/restore behavior, shared action toolbar names, and Pipeline Builder run status.
+- `tests/guards/shared-a11y-surfaces.test.ts` guards All Tools dialog focus trap/restore behavior, shared action-group names, and Pipeline Builder run status.
- `tests/guards/a11y-mobile-baseline.test.ts` guards skip-link behavior, live toast feedback, mobile touch target baseline, and core mobile routes.
- `tests/guards/all-tools-performance-a11y-guard.test.ts` guards disabled explanations plus long-text/mobile containment on All Tools.
- `tests/guards/theme-contrast-token-guard.test.ts` checks light/dark foreground and muted text contrast tokens against WCAG AA.
diff --git a/docs/accessibility/bf-037-screen-reader-qa.md b/docs/accessibility/bf-037-screen-reader-qa.md
index c428fd43..5f4bf46b 100644
--- a/docs/accessibility/bf-037-screen-reader-qa.md
+++ b/docs/accessibility/bf-037-screen-reader-qa.md
@@ -17,7 +17,7 @@ Scope: representative dynamic tool interfaces for BF-037.
- All Tools: search exposes the result-count status as its accessible description; filtered result counts announce through a polite live region; the mobile filter drawer has a dialog name, description, trapped focus, Escape close, and focus restoration.
- Pipeline Builder: final output is label-associated; run-log status announces through a polite live region; the run-log table has an accessible name; run failures expose an alert.
- External-request media flow: confirmation panel identifies the destination/purpose/data sent; preview/download actions remain disabled with accessible descriptions until explicit confirmation.
-- Shared tool actions: action groups expose a toolbar label; disabled actions use visible button names plus programmatic descriptions, without exposing tool payloads.
+- Shared tool actions: action groups expose a named group container; disabled actions use visible button names plus programmatic descriptions, without exposing tool payloads.
- Copy, download, export, save, and share feedback: toast title/description text is mirrored into the shared polite status region so success and failure states are announced consistently.
## Automated Coverage
diff --git a/src/app/[lang]/all-tools/page.tsx b/src/app/[lang]/all-tools/page.tsx
index 86ba8b5a..5956fd26 100644
--- a/src/app/[lang]/all-tools/page.tsx
+++ b/src/app/[lang]/all-tools/page.tsx
@@ -142,6 +142,7 @@ export default async function AllToolsPage({ params }: { params: Promise<{ lang:
popularTags: requireTranslationValue(t.common.popular_tags, "common.popular_tags"),
removeFilter: requireTranslationValue(t.common.remove_filter, "common.remove_filter"),
showFilters: requireTranslationValue(t.common.show_filters, "common.show_filters"),
+ showFiltersWithCount: requireTranslationValue(t.common.show_filters_with_count, "common.show_filters_with_count"),
showFewerTools: requireTranslationValue(t.common.show_fewer_tools, "common.show_fewer_tools"),
showMoreTools: requireTranslationValue(t.common.show_more_tools, "common.show_more_tools"),
closeFilters: requireTranslationValue(t.common.close_filters, "common.close_filters"),
diff --git a/src/core/i18n/translations/de.json b/src/core/i18n/translations/de.json
index a3de38f1..c68ca4ce 100644
--- a/src/core/i18n/translations/de.json
+++ b/src/core/i18n/translations/de.json
@@ -458,6 +458,7 @@
"input_url_domain": "URL/Domain",
"remove_filter": "Filter entfernen",
"show_filters": "Filter anzeigen",
+ "show_filters_with_count": "Filter anzeigen. Aktive Filter: {count}",
"show_fewer_tools": "Weniger anzeigen",
"show_more_tools": "Mehr anzeigen",
"use_case_encode": "Kodieren/dekodieren",
diff --git a/src/core/i18n/translations/en.json b/src/core/i18n/translations/en.json
index ce706587..2219b44a 100644
--- a/src/core/i18n/translations/en.json
+++ b/src/core/i18n/translations/en.json
@@ -458,6 +458,7 @@
"input_url_domain": "URL/domain",
"remove_filter": "Remove filter",
"show_filters": "Show filters",
+ "show_filters_with_count": "Show filters. Active filters: {count}",
"show_fewer_tools": "Show fewer",
"show_more_tools": "Show more",
"use_case_encode": "Encode/decode",
diff --git a/src/core/i18n/translations/fr.json b/src/core/i18n/translations/fr.json
index 5673e964..86a2b0f4 100644
--- a/src/core/i18n/translations/fr.json
+++ b/src/core/i18n/translations/fr.json
@@ -458,6 +458,7 @@
"input_url_domain": "URL/domaine",
"remove_filter": "Retirer le filtre",
"show_filters": "Afficher les filtres",
+ "show_filters_with_count": "Afficher les filtres. Filtres actifs : {count}",
"show_fewer_tools": "Afficher moins",
"show_more_tools": "Afficher plus",
"use_case_encode": "Encoder/décoder",
diff --git a/src/core/i18n/translations/ja.json b/src/core/i18n/translations/ja.json
index e233c312..257df54a 100644
--- a/src/core/i18n/translations/ja.json
+++ b/src/core/i18n/translations/ja.json
@@ -458,6 +458,7 @@
"input_url_domain": "URL/ドメイン",
"remove_filter": "フィルターを削除",
"show_filters": "フィルターを表示",
+ "show_filters_with_count": "フィルターを表示(適用中: {count} 件)",
"show_fewer_tools": "少なく表示",
"show_more_tools": "もっと表示",
"use_case_encode": "エンコード/デコード",
diff --git a/src/core/i18n/translations/ko.json b/src/core/i18n/translations/ko.json
index 5b5f23a0..eb2d279c 100644
--- a/src/core/i18n/translations/ko.json
+++ b/src/core/i18n/translations/ko.json
@@ -458,6 +458,7 @@
"input_url_domain": "URL/도메인",
"remove_filter": "필터 제거",
"show_filters": "필터 표시",
+ "show_filters_with_count": "필터 표시, 적용됨 {count}개",
"show_fewer_tools": "줄여 보기",
"show_more_tools": "더 보기",
"use_case_encode": "인코드/디코드",
diff --git a/src/core/i18n/translations/zh-CN.json b/src/core/i18n/translations/zh-CN.json
index d6e03c0f..a1c45b02 100644
--- a/src/core/i18n/translations/zh-CN.json
+++ b/src/core/i18n/translations/zh-CN.json
@@ -458,6 +458,7 @@
"input_url_domain": "URL/域名",
"remove_filter": "移除筛选",
"show_filters": "显示筛选",
+ "show_filters_with_count": "显示筛选,已选 {count} 项",
"show_fewer_tools": "收起",
"show_more_tools": "显示更多",
"use_case_encode": "编码/解码",
diff --git a/src/core/i18n/translations/zh-TW.json b/src/core/i18n/translations/zh-TW.json
index 220ab1bd..01609af7 100644
--- a/src/core/i18n/translations/zh-TW.json
+++ b/src/core/i18n/translations/zh-TW.json
@@ -458,6 +458,7 @@
"input_url_domain": "URL/網域",
"remove_filter": "移除篩選",
"show_filters": "顯示篩選",
+ "show_filters_with_count": "顯示篩選,已選 {count} 項",
"show_fewer_tools": "收合",
"show_more_tools": "顯示更多",
"use_case_encode": "編碼/解碼",
diff --git a/src/features/tool-discovery/all-tools-discovery.tsx b/src/features/tool-discovery/all-tools-discovery.tsx
index dbd1d549..7906d982 100644
--- a/src/features/tool-discovery/all-tools-discovery.tsx
+++ b/src/features/tool-discovery/all-tools-discovery.tsx
@@ -102,6 +102,7 @@ type AllToolsDiscoveryLabels = {
removeFilter: string
closeFilters: string
showFilters: string
+ showFiltersWithCount: string
showFewerTools: string
showMoreTools: string
clearRecentTools: string
@@ -925,6 +926,7 @@ export function AllToolsDiscovery({
type="button"
className="mt-4 flex min-h-11 w-full items-center justify-between gap-3 rounded-lg border border-border/70 bg-background/45 px-3 text-sm font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/45 lg:hidden"
onClick={() => setShowMobileFilters(true)}
+ aria-label={labels.showFiltersWithCount.replace("{count}", String(activeFilters.length))}
aria-expanded={showMobileFilters}
aria-controls="all-tools-filter-drawer"
aria-describedby="all-tools-result-status"
diff --git a/src/features/tool-shell/external-request-status.tsx b/src/features/tool-shell/external-request-status.tsx
index 884f32cc..90974ca0 100644
--- a/src/features/tool-shell/external-request-status.tsx
+++ b/src/features/tool-shell/external-request-status.tsx
@@ -92,11 +92,17 @@ export function ExternalRequestStatus({
{announcement}
+ data-external-request-announcement={role === "status" ? true : undefined}
+ >{role === "status" ? announcement : ""}
+ {role === "alert" ? announcement : ""}
)
}
diff --git a/src/features/tool-shell/tool-action-bar.tsx b/src/features/tool-shell/tool-action-bar.tsx
index 3adac4d3..397cf98c 100644
--- a/src/features/tool-shell/tool-action-bar.tsx
+++ b/src/features/tool-shell/tool-action-bar.tsx
@@ -273,7 +273,7 @@ export function ToolActionBar({
return (
{primaryActions.map((action) => {
diff --git a/src/lib/sitemap-lastmod.json b/src/lib/sitemap-lastmod.json
index 0deb4718..2179b7c3 100644
--- a/src/lib/sitemap-lastmod.json
+++ b/src/lib/sitemap-lastmod.json
@@ -184,13 +184,13 @@
},
"static": {
"all-tools": {
- "en": "2026-07-16T00:00:00.000Z",
- "zh-CN": "2026-07-16T00:00:00.000Z",
- "zh-TW": "2026-07-16T00:00:00.000Z",
- "ja": "2026-07-16T00:00:00.000Z",
- "ko": "2026-07-16T00:00:00.000Z",
- "de": "2026-07-16T00:00:00.000Z",
- "fr": "2026-07-16T00:00:00.000Z"
+ "en": "2026-07-17T00:00:00.000Z",
+ "zh-CN": "2026-07-17T00:00:00.000Z",
+ "zh-TW": "2026-07-17T00:00:00.000Z",
+ "ja": "2026-07-17T00:00:00.000Z",
+ "ko": "2026-07-17T00:00:00.000Z",
+ "de": "2026-07-17T00:00:00.000Z",
+ "fr": "2026-07-17T00:00:00.000Z"
},
"about": {
"en": "2026-07-16T00:00:00.000Z",
diff --git a/tests/component/all-tools-discovery.test.tsx b/tests/component/all-tools-discovery.test.tsx
index fbfe5794..ef5397e1 100644
--- a/tests/component/all-tools-discovery.test.tsx
+++ b/tests/component/all-tools-discovery.test.tsx
@@ -40,6 +40,7 @@ const labels = {
popularTags: "Popular tags",
removeFilter: "Remove filter",
showFilters: "Show filters",
+ showFiltersWithCount: "Show filters. Active filters: {count}",
showFewerTools: "Show fewer",
showMoreTools: "Show more",
closeFilters: "Done",
@@ -387,6 +388,7 @@ describe("AllToolsDiscovery", () => {
renderDiscovery()
const trigger = screen.getByRole("button", { name: /Show filters/ })
+ expect(trigger).toHaveAccessibleName("Show filters. Active filters: 0")
trigger.focus()
fireEvent.click(trigger)
@@ -396,9 +398,11 @@ describe("AllToolsDiscovery", () => {
expect(within(drawer).getByText(/10 tools/)).toBeInTheDocument()
fireEvent.click(within(drawer).getAllByRole("button", { name: "File" })[0])
expect(within(drawer).getByText(/1 tools/)).toBeInTheDocument()
+ expect(trigger).toHaveAccessibleName("Show filters. Active filters: 1")
fireEvent.click(within(drawer).getByRole("button", { name: "Clear filters" }))
expect(within(drawer).getByText(/10 tools/)).toBeInTheDocument()
+ expect(trigger).toHaveAccessibleName("Show filters. Active filters: 0")
const doneButtons = within(drawer).getAllByRole("button", { name: "Done" })
fireEvent.click(doneButtons[doneButtons.length - 1])
diff --git a/tests/component/http-request-builder-page.test.tsx b/tests/component/http-request-builder-page.test.tsx
index a71f0d54..bc654234 100644
--- a/tests/component/http-request-builder-page.test.tsx
+++ b/tests/component/http-request-builder-page.test.tsx
@@ -126,7 +126,7 @@ describe("HttpRequestBuilderPage", () => {
expect(screen.getByRole("group", { name: "Add to favorites / Remove from favorites" })).toHaveAttribute("data-tool-global-actions")
expect(screen.getByRole("button", { name: "Add to favorites" })).toBeInTheDocument()
- expect(screen.getByRole("toolbar", { name: "Tool actions" })).toBeInTheDocument()
+ expect(screen.getByRole("group", { name: "Tool actions" })).toBeInTheDocument()
fireEvent.click(screen.getByRole("button", { name: "Add Header" }))
fireEvent.change(screen.getAllByRole("textbox", { name: "Header name" })[1], {
diff --git a/tests/component/phase3-pipeline-builder-page.test.tsx b/tests/component/phase3-pipeline-builder-page.test.tsx
index 51ceac49..6a489728 100644
--- a/tests/component/phase3-pipeline-builder-page.test.tsx
+++ b/tests/component/phase3-pipeline-builder-page.test.tsx
@@ -104,7 +104,7 @@ describe("phase 3 pipeline builder page", () => {
expect(screen.getByRole("heading", { name: "Pipeline Builder" })).toBeInTheDocument()
expect(screen.getByRole("heading", { name: "Build a recipe in three moves" })).toBeInTheDocument()
- expect(screen.getByRole("toolbar", { name: "Tool actions" })).toBeInTheDocument()
+ expect(screen.getByRole("group", { name: "Tool actions" })).toBeInTheDocument()
expect(screen.getByLabelText("Initial input")).toBeInTheDocument()
expect(screen.getByLabelText("Final output")).toBeInTheDocument()
expect(screen.getAllByText("Steps").length).toBeGreaterThan(0)
@@ -310,13 +310,13 @@ describe("phase 3 pipeline builder page", () => {
expect(screen.getByRole("button", { name: "Export structure only" })).toBeInTheDocument()
}, 10_000)
- it("restores privacy dialog focus for toolbar and inspector actions", async () => {
+ it("restores privacy dialog focus for action-bar and inspector actions", async () => {
renderWithEnglish(
)
- const toolbar = screen.getByRole("toolbar", { name: "Tool actions" })
- const toolbarExport = within(toolbar).getByRole("button", { name: "Export JSON" })
- toolbarExport.focus()
- fireEvent.click(toolbarExport)
+ const actionGroup = screen.getByRole("group", { name: "Tool actions" })
+ const actionGroupExport = within(actionGroup).getByRole("button", { name: "Export JSON" })
+ actionGroupExport.focus()
+ fireEvent.click(actionGroupExport)
const firstDialog = await screen.findByRole("dialog", { name: "Privacy preview" })
expect(firstDialog).toHaveAccessibleDescription("Export a structure-only JSON recipe file.")
@@ -339,14 +339,14 @@ describe("phase 3 pipeline builder page", () => {
fireEvent.keyDown(document, { key: "Escape" })
await waitFor(() => expect(screen.queryByRole("dialog", { name: "Privacy preview" })).not.toBeInTheDocument())
- await waitFor(() => expect(toolbarExport).toHaveFocus())
+ await waitFor(() => expect(actionGroupExport).toHaveFocus())
- fireEvent.click(toolbarExport)
+ fireEvent.click(actionGroupExport)
const cancelDialog = await screen.findByRole("dialog", { name: "Privacy preview" })
fireEvent.click(within(cancelDialog).getByRole("button", { name: "Cancel" }))
await waitFor(() => expect(screen.queryByRole("dialog", { name: "Privacy preview" })).not.toBeInTheDocument())
- await waitFor(() => expect(toolbarExport).toHaveFocus())
+ await waitFor(() => expect(actionGroupExport).toHaveFocus())
const inspector = screen.getByRole("complementary", { name: "Recipe inspector" })
const inspectorExport = within(inspector).getByRole("button", { name: "Export JSON" })
diff --git a/tests/component/representative-a11y-axe.test.tsx b/tests/component/representative-a11y-axe.test.tsx
index 9a040e93..9cd2f72b 100644
--- a/tests/component/representative-a11y-axe.test.tsx
+++ b/tests/component/representative-a11y-axe.test.tsx
@@ -112,6 +112,7 @@ const discoveryLabels = {
popularTags: "Popular tags",
removeFilter: "Remove filter",
showFilters: "Show filters",
+ showFiltersWithCount: "Show filters. Active filters: {count}",
showFewerTools: "Show fewer",
showMoreTools: "Show more",
closeFilters: "Done",
diff --git a/tests/component/tool-action-bar.test.tsx b/tests/component/tool-action-bar.test.tsx
index ee887dc9..18f677d3 100644
--- a/tests/component/tool-action-bar.test.tsx
+++ b/tests/component/tool-action-bar.test.tsx
@@ -45,9 +45,12 @@ describe("ToolActionBar", () => {
render(
)
- const toolbar = screen.getByRole("toolbar", { name: "Tool actions" })
- const labels = within(toolbar).getAllByRole("button").map((button) => button.textContent)
+ const actionGroup = screen.getByRole("group", { name: "Tool actions" })
+ const actionButtons = within(actionGroup).getAllByRole("button")
+ const labels = actionButtons.map((button) => button.textContent)
+ expect(screen.queryByRole("toolbar", { name: "Tool actions" })).not.toBeInTheDocument()
+ expect(actionButtons.filter((button) => !button.hasAttribute("disabled")).every((button) => button.tabIndex === 0)).toBe(true)
expect(labels).toEqual(["Sample", "Clear", "Format", "Copy", "Download"])
expect(screen.getByRole("button", { name: "Download", description: "Nothing to download." })).toHaveAttribute("title", "Download: Nothing to download.")
})
diff --git a/tests/component/tool-shell-status-output.test.tsx b/tests/component/tool-shell-status-output.test.tsx
index 93e5755f..9c79a802 100644
--- a/tests/component/tool-shell-status-output.test.tsx
+++ b/tests/component/tool-shell-status-output.test.tsx
@@ -167,10 +167,12 @@ describe("shared tool shell status and output components", () => {
)
const permissionStatus = screen.getByRole("status")
+ const failureAlert = screen.getByRole("alert")
expect(permissionStatus.closest('[data-external-request-status="permission"]')).not.toBeNull()
expect(permissionStatus).toHaveAttribute("aria-live", "polite")
expect(permissionStatus.childNodes).toHaveLength(1)
expect(permissionStatus.firstChild?.nodeType).toBe(Node.TEXT_NODE)
+ expect(failureAlert).toBeEmptyDOMElement()
rerender(
@@ -184,7 +186,9 @@ describe("shared tool shell status and output components", () => {
)
expect(screen.getByRole("alert").closest('[data-external-request-status="offline"]')).not.toBeNull()
- expect(screen.getByRole("alert")).toHaveAttribute("aria-live", "assertive")
+ expect(screen.getByRole("alert")).not.toHaveAttribute("aria-live")
+ expect(screen.getByRole("alert")).toHaveAttribute("data-external-request-announcement")
+ expect(permissionStatus).toBeEmptyDOMElement()
})
it("exposes external request status, next step, hosts, and alert semantics for failures", () => {
@@ -201,5 +205,6 @@ describe("shared tool shell status and output components", () => {
expect(screen.getByRole("alert")).toHaveTextContent("What to do next: Reconnect and retry.")
expect(screen.getByRole("alert")).toHaveTextContent("i.ytimg.com")
expect(screen.getByRole("alert")).toHaveTextContent("Network access starts only after you choose the external-request action.")
+ expect(screen.getByRole("status")).toBeEmptyDOMElement()
})
})
diff --git a/tests/guards/shared-a11y-surfaces.test.ts b/tests/guards/shared-a11y-surfaces.test.ts
index 601b7845..b58498cf 100644
--- a/tests/guards/shared-a11y-surfaces.test.ts
+++ b/tests/guards/shared-a11y-surfaces.test.ts
@@ -23,7 +23,8 @@ describe("shared accessibility surfaces", () => {
it("keeps the shared tool action bar named and described", () => {
const source = readSource("src/features/tool-shell/tool-action-bar.tsx")
- expect(source).toContain("role=\"toolbar\"")
+ expect(source).toContain("role=\"group\"")
+ expect(source).not.toContain("role=\"toolbar\"")
expect(source).toContain("aria-label={t.common.tool_actions}")
expect(source).toContain("aria-describedby={disabledDescriptionId}")
expect(source).toContain("aria-describedby={handoffDisabledDescriptionId}")
@@ -66,8 +67,10 @@ describe("shared accessibility surfaces", () => {
for (const source of [youtube, vimeo, instagram]) {
expect(source).toContain("ExternalRequestStatus")
}
- expect(externalRequestStatus).toContain('role={role}')
- expect(externalRequestStatus).toContain('aria-live={role === "alert" ? "assertive" : "polite"}')
+ expect(externalRequestStatus).toContain('role="status"')
+ expect(externalRequestStatus).toContain('aria-live="polite"')
+ expect(externalRequestStatus).toContain('role="alert"')
+ expect(externalRequestStatus).toContain('{role === "alert" ? announcement : ""}')
expect(externalRequestStatus).toContain("data-external-request-status")
})
})