From f7c29b7ddc5290b9d62bc4341f4d114a9388903e Mon Sep 17 00:00:00 2001 From: adarshsm <24850536+adarshsm@users.noreply.github.com> Date: Mon, 28 Sep 2026 16:56:38 +0530 Subject: [PATCH] fix(core): give the toggle button an accessible name and state Screen readers announced the toggle list / toggle heading button as a bare "button": it had no accessible name, its chevron SVG was not hidden, and nothing exposed whether the nested blocks were shown. The button now gets a localized aria-label, the SVG is aria-hidden, and aria-expanded follows data-show-children in both the vanilla and React toggle wrappers. The vanilla node view ignores the aria-expanded mutation the same way it already ignores data-show-children, so toggling does not re-render the block. The HTML exporter, which forces toggles open, sets aria-expanded="true" to match. Fixes #2811 --- .../exporters/html/internalHTMLSerializer.ts | 3 ++ .../ToggleWrapper/createToggleWrapper.test.ts | 49 +++++++++++++++++++ .../ToggleWrapper/createToggleWrapper.ts | 30 +++++++++--- packages/core/src/i18n/locales/ar.ts | 1 + packages/core/src/i18n/locales/de.ts | 1 + packages/core/src/i18n/locales/en.ts | 1 + packages/core/src/i18n/locales/es.ts | 1 + packages/core/src/i18n/locales/fa.ts | 1 + packages/core/src/i18n/locales/fr.ts | 1 + packages/core/src/i18n/locales/he.ts | 1 + packages/core/src/i18n/locales/hr.ts | 1 + packages/core/src/i18n/locales/is.ts | 1 + packages/core/src/i18n/locales/it.ts | 1 + packages/core/src/i18n/locales/ja.ts | 1 + packages/core/src/i18n/locales/ko.ts | 1 + packages/core/src/i18n/locales/nl.ts | 1 + packages/core/src/i18n/locales/no.ts | 1 + packages/core/src/i18n/locales/pl.ts | 1 + packages/core/src/i18n/locales/pt.ts | 1 + packages/core/src/i18n/locales/ru.ts | 1 + packages/core/src/i18n/locales/sk.ts | 1 + packages/core/src/i18n/locales/uk.ts | 1 + packages/core/src/i18n/locales/uz.ts | 1 + packages/core/src/i18n/locales/vi.ts | 1 + packages/core/src/i18n/locales/zh-tw.ts | 1 + packages/core/src/i18n/locales/zh.ts | 1 + .../blocks/ToggleWrapper/ToggleWrapper.tsx | 3 ++ .../blocknoteHTML/heading/toggleable.html | 8 ++- .../blocknoteHTML/lists/basic.html | 8 ++- .../blocknoteHTML/lists/nested.html | 8 ++- .../lists/toggleWithChildren.html | 16 +++++- 31 files changed, 136 insertions(+), 12 deletions(-) create mode 100644 packages/core/src/blocks/ToggleWrapper/createToggleWrapper.test.ts diff --git a/packages/core/src/api/exporters/html/internalHTMLSerializer.ts b/packages/core/src/api/exporters/html/internalHTMLSerializer.ts index 33376b2835..2860426a1d 100644 --- a/packages/core/src/api/exporters/html/internalHTMLSerializer.ts +++ b/packages/core/src/api/exporters/html/internalHTMLSerializer.ts @@ -65,6 +65,9 @@ const forceToggleBlocksShow = (element: HTMLElement) => { ); hiddenToggleWrappers.forEach((toggleWrapper) => { toggleWrapper.setAttribute("data-show-children", "true"); + toggleWrapper + .querySelector(":scope > .bn-toggle-button") + ?.setAttribute("aria-expanded", "true"); }); return element; diff --git a/packages/core/src/blocks/ToggleWrapper/createToggleWrapper.test.ts b/packages/core/src/blocks/ToggleWrapper/createToggleWrapper.test.ts new file mode 100644 index 0000000000..01f3188413 --- /dev/null +++ b/packages/core/src/blocks/ToggleWrapper/createToggleWrapper.test.ts @@ -0,0 +1,49 @@ +import { afterEach, describe, expect, it } from "vite-plus/test"; +import { BlockNoteEditor } from "../../editor/BlockNoteEditor.js"; + +/** + * @vitest-environment jsdom + */ + +describe("Toggle button accessibility", () => { + let editor: BlockNoteEditor; + + afterEach(() => { + editor._tiptapEditor.destroy(); + window.localStorage.clear(); + }); + + it("has an accessible name and exposes its expanded state", async () => { + editor = BlockNoteEditor.create({ + initialContent: [ + { + id: "toggle", + type: "toggleListItem", + content: "Toggle", + children: [{ type: "paragraph", content: "Child" }], + }, + ], + }); + const div = document.createElement("div"); + editor.mount(div); + + const button = div.querySelector(".bn-toggle-button")!; + expect(button.getAttribute("aria-label")).toBe( + editor.dictionary.toggle_blocks.toggle_button_label, + ); + expect(button.querySelector("svg")!.getAttribute("aria-hidden")).toBe( + "true", + ); + expect(button.getAttribute("aria-expanded")).toBe("false"); + + button.click(); + expect(button.getAttribute("aria-expanded")).toBe("true"); + + // Updating `aria-expanded` must not make ProseMirror re-render the block. + await new Promise((resolve) => setTimeout(resolve)); + expect(div.querySelector(".bn-toggle-button")).toBe(button); + + button.click(); + expect(button.getAttribute("aria-expanded")).toBe("false"); + }); +}); diff --git a/packages/core/src/blocks/ToggleWrapper/createToggleWrapper.ts b/packages/core/src/blocks/ToggleWrapper/createToggleWrapper.ts index 257fd7ce6f..c3745b3acf 100644 --- a/packages/core/src/blocks/ToggleWrapper/createToggleWrapper.ts +++ b/packages/core/src/blocks/ToggleWrapper/createToggleWrapper.ts @@ -42,9 +42,22 @@ export const createToggleWrapper = ( const toggleButton = document.createElement("button"); toggleButton.className = "bn-toggle-button"; toggleButton.type = "button"; + toggleButton.setAttribute( + "aria-label", + editor.dictionary.toggle_blocks.toggle_button_label, + ); toggleButton.innerHTML = // https://fonts.google.com/icons?selected=Material+Symbols+Rounded:chevron_right:FILL@0;wght@700;GRAD@0;opsz@24&icon.query=chevron&icon.style=Rounded&icon.size=24&icon.color=%23e8eaed - ''; + ''; + // Keeps the button's expanded state, which screen readers announce, in sync + // with whether the child blocks are shown. + function setShowChildren(showChildren: boolean) { + toggleWrapper.setAttribute( + "data-show-children", + showChildren ? "true" : "false", + ); + toggleButton.setAttribute("aria-expanded", showChildren ? "true" : "false"); + } const toggleButtonMouseDown = (event: MouseEvent) => event.preventDefault(); toggleButton.addEventListener("mousedown", toggleButtonMouseDown); const toggleButtonOnClick = () => { @@ -56,14 +69,14 @@ export const createToggleWrapper = ( } if (toggleWrapper.getAttribute("data-show-children") === "true") { - toggleWrapper.setAttribute("data-show-children", "false"); + setShowChildren(false); toggledState.set(currentBlock, false); if (dom.contains(toggleAddBlockButton)) { dom.removeChild(toggleAddBlockButton); } } else { - toggleWrapper.setAttribute("data-show-children", "true"); + setShowChildren(true); toggledState.set(currentBlock, true); if ( @@ -115,7 +128,7 @@ export const createToggleWrapper = ( if (newChildCount > childCount) { // If a child block is added while children are hidden, show children. if (toggleWrapper.getAttribute("data-show-children") === "false") { - toggleWrapper.setAttribute("data-show-children", "true"); + setShowChildren(true); const currentBlock = editor.getBlock(block); if (currentBlock) { toggledState.set(currentBlock, true); @@ -131,7 +144,7 @@ export const createToggleWrapper = ( // If the last child block is removed while children are shown, hide // children. if (toggleWrapper.getAttribute("data-show-children") === "true") { - toggleWrapper.setAttribute("data-show-children", "false"); + setShowChildren(false); const currentBlock = editor.getBlock(block); if (currentBlock) { toggledState.set(currentBlock, false); @@ -149,7 +162,7 @@ export const createToggleWrapper = ( }); if (toggledState.get(block)) { - toggleWrapper.setAttribute("data-show-children", "true"); + setShowChildren(true); if (editor.isEditable && block.children.length === 0) { // If the toggle is set to show children, but there are no children, @@ -157,7 +170,7 @@ export const createToggleWrapper = ( dom.appendChild(toggleAddBlockButton); } } else { - toggleWrapper.setAttribute("data-show-children", "false"); + setShowChildren(false); } return { @@ -172,6 +185,9 @@ export const createToggleWrapper = ( ((mutation.type === "attributes" && mutation.target === toggleWrapper && mutation.attributeName === "data-show-children") || + (mutation.type === "attributes" && + mutation.target === toggleButton && + mutation.attributeName === "aria-expanded") || (mutation.type === "childList" && (mutation.addedNodes[0] === toggleAddBlockButton || mutation.removedNodes[0] === toggleAddBlockButton))) diff --git a/packages/core/src/i18n/locales/ar.ts b/packages/core/src/i18n/locales/ar.ts index 1c19b810dd..b769ec1702 100644 --- a/packages/core/src/i18n/locales/ar.ts +++ b/packages/core/src/i18n/locales/ar.ts @@ -184,6 +184,7 @@ export const ar: Dictionary = { }, toggle_blocks: { add_block_button: "تبديل فارغ. انقر لإضافة كتلة.", + toggle_button_label: "توسيع أو طي", }, code_block: { add_source_button_text: "إضافة كود المصدر", diff --git a/packages/core/src/i18n/locales/de.ts b/packages/core/src/i18n/locales/de.ts index 45ff9341d8..67284be374 100644 --- a/packages/core/src/i18n/locales/de.ts +++ b/packages/core/src/i18n/locales/de.ts @@ -220,6 +220,7 @@ export const de: Dictionary = { toggle_blocks: { add_block_button: "Leerer aufklappbarer Bereich. Klicken, um einen Block hinzuzufügen.", + toggle_button_label: "Ein- oder ausklappen", }, code_block: { add_source_button_text: "Quellcode hinzufügen", diff --git a/packages/core/src/i18n/locales/en.ts b/packages/core/src/i18n/locales/en.ts index 307ba90c22..ee90f3a3d9 100644 --- a/packages/core/src/i18n/locales/en.ts +++ b/packages/core/src/i18n/locales/en.ts @@ -199,6 +199,7 @@ export const en = { }, toggle_blocks: { add_block_button: "Empty toggle. Click to add a block.", + toggle_button_label: "Expand or collapse", }, code_block: { add_source_button_text: "Add source code", diff --git a/packages/core/src/i18n/locales/es.ts b/packages/core/src/i18n/locales/es.ts index b2c05ca6b2..8c2102378b 100644 --- a/packages/core/src/i18n/locales/es.ts +++ b/packages/core/src/i18n/locales/es.ts @@ -199,6 +199,7 @@ export const es: Dictionary = { }, toggle_blocks: { add_block_button: "Toggle vacío. Haz clic para añadir un bloque.", + toggle_button_label: "Expandir o contraer", }, code_block: { add_source_button_text: "Agregar código fuente", diff --git a/packages/core/src/i18n/locales/fa.ts b/packages/core/src/i18n/locales/fa.ts index 405cf87ddf..dd3ce26370 100644 --- a/packages/core/src/i18n/locales/fa.ts +++ b/packages/core/src/i18n/locales/fa.ts @@ -167,6 +167,7 @@ export const fa = { }, toggle_blocks: { add_block_button: "تاشوی خالی. برای افزودن بلوک کلیک کنید.", + toggle_button_label: "باز یا بسته کردن", }, code_block: { add_source_button_text: "افزودن کد منبع", diff --git a/packages/core/src/i18n/locales/fr.ts b/packages/core/src/i18n/locales/fr.ts index 4807927655..6fc1a3d7b6 100644 --- a/packages/core/src/i18n/locales/fr.ts +++ b/packages/core/src/i18n/locales/fr.ts @@ -245,6 +245,7 @@ export const fr: Dictionary = { }, toggle_blocks: { add_block_button: "Liste repliable vide. Cliquez pour ajouter un bloc.", + toggle_button_label: "Déplier ou replier", }, code_block: { add_source_button_text: "Ajouter le code source", diff --git a/packages/core/src/i18n/locales/he.ts b/packages/core/src/i18n/locales/he.ts index 1b9338b77b..749aa5412c 100644 --- a/packages/core/src/i18n/locales/he.ts +++ b/packages/core/src/i18n/locales/he.ts @@ -201,6 +201,7 @@ export const he: Dictionary = { }, toggle_blocks: { add_block_button: "מתג ריק. לחץ כדי להוסיף בלוק.", + toggle_button_label: "הרחב או כווץ", }, code_block: { add_source_button_text: "הוסף קוד מקור", diff --git a/packages/core/src/i18n/locales/hr.ts b/packages/core/src/i18n/locales/hr.ts index 998a245f20..9faccc4040 100644 --- a/packages/core/src/i18n/locales/hr.ts +++ b/packages/core/src/i18n/locales/hr.ts @@ -212,6 +212,7 @@ export const hr: Dictionary = { }, toggle_blocks: { add_block_button: "Prazan sklopivi blok. Klikni da dodaš sadržaj.", + toggle_button_label: "Proširi ili sažmi", }, code_block: { add_source_button_text: "Dodaj izvorni kôd", diff --git a/packages/core/src/i18n/locales/is.ts b/packages/core/src/i18n/locales/is.ts index e7effe3827..0b95171e03 100644 --- a/packages/core/src/i18n/locales/is.ts +++ b/packages/core/src/i18n/locales/is.ts @@ -213,6 +213,7 @@ export const is: Dictionary = { }, toggle_blocks: { add_block_button: "Tóm fellilína. Smelltu til að bæta við blokk.", + toggle_button_label: "Fella út eða saman", }, code_block: { add_source_button_text: "Bæta við frumkóða", diff --git a/packages/core/src/i18n/locales/it.ts b/packages/core/src/i18n/locales/it.ts index 782a3c7fc4..2a19a8a224 100644 --- a/packages/core/src/i18n/locales/it.ts +++ b/packages/core/src/i18n/locales/it.ts @@ -221,6 +221,7 @@ export const it: Dictionary = { }, toggle_blocks: { add_block_button: "Toggle vuoto. Clicca per aggiungere un blocco.", + toggle_button_label: "Espandi o comprimi", }, code_block: { add_source_button_text: "Aggiungi codice sorgente", diff --git a/packages/core/src/i18n/locales/ja.ts b/packages/core/src/i18n/locales/ja.ts index 8bac14021d..0b279d68f0 100644 --- a/packages/core/src/i18n/locales/ja.ts +++ b/packages/core/src/i18n/locales/ja.ts @@ -239,6 +239,7 @@ export const ja: Dictionary = { }, toggle_blocks: { add_block_button: "空のトグルです。クリックしてブロックを追加。", + toggle_button_label: "展開または折りたたみ", }, code_block: { add_source_button_text: "ソースコードを追加", diff --git a/packages/core/src/i18n/locales/ko.ts b/packages/core/src/i18n/locales/ko.ts index de94329b19..a4f084e12f 100644 --- a/packages/core/src/i18n/locales/ko.ts +++ b/packages/core/src/i18n/locales/ko.ts @@ -212,6 +212,7 @@ export const ko: Dictionary = { }, toggle_blocks: { add_block_button: "비어 있는 토글입니다. 클릭하여 블록을 추가하세요.", + toggle_button_label: "펼치기 또는 접기", }, code_block: { add_source_button_text: "소스 코드 추가", diff --git a/packages/core/src/i18n/locales/nl.ts b/packages/core/src/i18n/locales/nl.ts index a90210b572..6686e4f05e 100644 --- a/packages/core/src/i18n/locales/nl.ts +++ b/packages/core/src/i18n/locales/nl.ts @@ -200,6 +200,7 @@ export const nl: Dictionary = { }, toggle_blocks: { add_block_button: "Lege uitklapper. Klik om een blok toe te voegen.", + toggle_button_label: "Uitklappen of inklappen", }, code_block: { add_source_button_text: "Broncode toevoegen", diff --git a/packages/core/src/i18n/locales/no.ts b/packages/core/src/i18n/locales/no.ts index 9ed6388dc7..903658a1c3 100644 --- a/packages/core/src/i18n/locales/no.ts +++ b/packages/core/src/i18n/locales/no.ts @@ -218,6 +218,7 @@ export const no: Dictionary = { }, toggle_blocks: { add_block_button: "Tomt toggle. Klikk for å legge til en blokk.", + toggle_button_label: "Utvid eller skjul", }, code_block: { add_source_button_text: "Legg til kildekode", diff --git a/packages/core/src/i18n/locales/pl.ts b/packages/core/src/i18n/locales/pl.ts index 95751640b9..6fc38d552e 100644 --- a/packages/core/src/i18n/locales/pl.ts +++ b/packages/core/src/i18n/locales/pl.ts @@ -191,6 +191,7 @@ export const pl: Dictionary = { toggle_blocks: { add_block_button: "Brak bloków do rozwinięcia. Kliknij, aby dodać pierwszego.", + toggle_button_label: "Rozwiń lub zwiń", }, code_block: { add_source_button_text: "Dodaj kod źródłowy", diff --git a/packages/core/src/i18n/locales/pt.ts b/packages/core/src/i18n/locales/pt.ts index 6914de9d2c..7faee83b77 100644 --- a/packages/core/src/i18n/locales/pt.ts +++ b/packages/core/src/i18n/locales/pt.ts @@ -191,6 +191,7 @@ export const pt: Dictionary = { }, toggle_blocks: { add_block_button: "Toggle vazio. Clique para adicionar um bloco.", + toggle_button_label: "Expandir ou recolher", }, code_block: { add_source_button_text: "Adicionar código-fonte", diff --git a/packages/core/src/i18n/locales/ru.ts b/packages/core/src/i18n/locales/ru.ts index db116a3c4c..3a0050b00e 100644 --- a/packages/core/src/i18n/locales/ru.ts +++ b/packages/core/src/i18n/locales/ru.ts @@ -242,6 +242,7 @@ export const ru: Dictionary = { }, toggle_blocks: { add_block_button: "Пустой переключатель. Нажмите, чтобы добавить блок.", + toggle_button_label: "Развернуть или свернуть", }, code_block: { add_source_button_text: "Добавить исходный код", diff --git a/packages/core/src/i18n/locales/sk.ts b/packages/core/src/i18n/locales/sk.ts index f53c4c39d1..e2b9ba3ee9 100644 --- a/packages/core/src/i18n/locales/sk.ts +++ b/packages/core/src/i18n/locales/sk.ts @@ -199,6 +199,7 @@ export const sk = { }, toggle_blocks: { add_block_button: "Prázdne prepínanie. Kliknite pre pridanie bloku.", + toggle_button_label: "Rozbaliť alebo zbaliť", }, code_block: { add_source_button_text: "Pridať zdrojový kód", diff --git a/packages/core/src/i18n/locales/uk.ts b/packages/core/src/i18n/locales/uk.ts index e6101c8f69..e169bdca25 100644 --- a/packages/core/src/i18n/locales/uk.ts +++ b/packages/core/src/i18n/locales/uk.ts @@ -224,6 +224,7 @@ export const uk: Dictionary = { }, toggle_blocks: { add_block_button: "Порожній перемикач. Натисніть, щоб додати блок.", + toggle_button_label: "Розгорнути або згорнути", }, code_block: { add_source_button_text: "Додати вихідний код", diff --git a/packages/core/src/i18n/locales/uz.ts b/packages/core/src/i18n/locales/uz.ts index 23b0f4f1a7..0226d3eb97 100644 --- a/packages/core/src/i18n/locales/uz.ts +++ b/packages/core/src/i18n/locales/uz.ts @@ -260,6 +260,7 @@ export const uz: Dictionary = { toggle_blocks: { add_block_button: "Bo‘sh toggle. Blok qo‘shish uchun bosing.", + toggle_button_label: "Yoyish yoki yig‘ish", }, code_block: { diff --git a/packages/core/src/i18n/locales/vi.ts b/packages/core/src/i18n/locales/vi.ts index d52db4d48d..1d8b317019 100644 --- a/packages/core/src/i18n/locales/vi.ts +++ b/packages/core/src/i18n/locales/vi.ts @@ -198,6 +198,7 @@ export const vi: Dictionary = { }, toggle_blocks: { add_block_button: "Toggle trống. Nhấp để thêm khối.", + toggle_button_label: "Mở rộng hoặc thu gọn", }, code_block: { add_source_button_text: "Thêm mã nguồn", diff --git a/packages/core/src/i18n/locales/zh-tw.ts b/packages/core/src/i18n/locales/zh-tw.ts index 0aba71ead4..53f285bc73 100644 --- a/packages/core/src/i18n/locales/zh-tw.ts +++ b/packages/core/src/i18n/locales/zh-tw.ts @@ -240,6 +240,7 @@ export const zhTW: Dictionary = { }, toggle_blocks: { add_block_button: "空的切換區。點擊新增區塊。", + toggle_button_label: "展開或收合", }, code_block: { add_source_button_text: "新增原始碼", diff --git a/packages/core/src/i18n/locales/zh.ts b/packages/core/src/i18n/locales/zh.ts index 0017c86672..9fea9191f9 100644 --- a/packages/core/src/i18n/locales/zh.ts +++ b/packages/core/src/i18n/locales/zh.ts @@ -240,6 +240,7 @@ export const zh: Dictionary = { }, toggle_blocks: { add_block_button: "空的切换区。点击添加区块。", + toggle_button_label: "展开或折叠", }, code_block: { add_source_button_text: "添加源代码", diff --git a/packages/react/src/blocks/ToggleWrapper/ToggleWrapper.tsx b/packages/react/src/blocks/ToggleWrapper/ToggleWrapper.tsx index b35fc8d779..b84bb5b24c 100644 --- a/packages/react/src/blocks/ToggleWrapper/ToggleWrapper.tsx +++ b/packages/react/src/blocks/ToggleWrapper/ToggleWrapper.tsx @@ -125,6 +125,8 @@ export const ToggleWrapper = (