Skip to content

Commit 43122b1

Browse files
committed
fixup! fixup! fixup! ♿️(frontend) make html export accessible to screen reader users
1 parent 5d60b53 commit 43122b1

File tree

2 files changed

+173
-20
lines changed

2 files changed

+173
-20
lines changed

src/frontend/apps/impress/src/features/docs/doc-export/assets/export-html-styles.txt

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,75 @@ s {
184184
margin: 0;
185185
}
186186

187+
/* Remove bullet points from checkbox lists */
188+
ul.checklist,
189+
ul:has(li input[type='checkbox']) {
190+
list-style: none;
191+
padding-left: 0;
192+
margin-left: 0;
193+
}
194+
195+
ul.checklist li,
196+
ul:has(li input[type='checkbox']) li {
197+
list-style: none;
198+
display: flex;
199+
align-items: center;
200+
gap: 8px;
201+
}
202+
203+
ul.checklist li input[type='checkbox'],
204+
ul:has(li input[type='checkbox']) li input[type='checkbox'] {
205+
margin: 0;
206+
width: 16px;
207+
height: 16px;
208+
cursor: pointer;
209+
flex-shrink: 0;
210+
}
211+
212+
ul.checklist li p,
213+
ul:has(li input[type='checkbox']) li p {
214+
margin: 0;
215+
flex: 1;
216+
}
217+
218+
/* Native HTML Lists - remove default margins */
219+
ol,
220+
ul {
221+
margin: 0;
222+
padding-left: 24px;
223+
}
224+
225+
ol {
226+
list-style-type: decimal;
227+
}
228+
229+
ul {
230+
list-style-type: disc;
231+
}
232+
233+
/* Nested lists */
234+
ul ul {
235+
list-style-type: circle;
236+
}
237+
238+
/* Keep decimal numbering for nested ol (remove this if you want letters) */
239+
ol ol {
240+
list-style-type: decimal;
241+
}
242+
243+
li {
244+
margin: 0;
245+
padding: 0;
246+
line-height: 24px;
247+
}
248+
249+
li p {
250+
margin: 0;
251+
display: inline;
252+
}
253+
254+
255+
187256
/* Quotes */
188257
blockquote,
189258
.bn-block-content[data-content-type='quote'] blockquote {

src/frontend/apps/impress/src/features/docs/doc-export/utils_html.ts

Lines changed: 104 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -142,38 +142,121 @@ export const improveHtmlAccessibility = (
142142
block.replaceWith(heading);
143143
});
144144

145-
// 2) Lists: group consecutive list items into UL/OL with LI children
145+
// 2) Lists: convert to semantic OL/UL/LI elements for accessibility
146146
const listItemSelector =
147147
"[data-content-type='bulletListItem'], [data-content-type='numberedListItem']";
148-
const listItems = Array.from(
149-
body.querySelectorAll<HTMLElement>(listItemSelector),
148+
149+
// Helper function to get nesting level by counting block-group ancestors
150+
const getNestingLevel = (blockOuter: HTMLElement): number => {
151+
let level = 0;
152+
let parent = blockOuter.parentElement;
153+
while (parent) {
154+
if (parent.classList.contains('bn-block-group')) {
155+
level++;
156+
}
157+
parent = parent.parentElement;
158+
}
159+
return level;
160+
};
161+
162+
// Find all block-outer elements in document order
163+
const allBlockOuters = Array.from(
164+
body.querySelectorAll<HTMLElement>('.bn-block-outer'),
150165
);
151166

152-
listItems.forEach((item) => {
153-
const parent = item.parentElement;
154-
if (!parent) {
155-
return;
167+
// Collect list items with their info before modifying DOM
168+
interface ListItemInfo {
169+
blockOuter: HTMLElement;
170+
listItem: HTMLElement;
171+
contentType: string;
172+
level: number;
173+
}
174+
175+
const listItemsInfo: ListItemInfo[] = [];
176+
allBlockOuters.forEach((blockOuter) => {
177+
const listItem = blockOuter.querySelector<HTMLElement>(listItemSelector);
178+
if (listItem) {
179+
const contentType = listItem.getAttribute('data-content-type');
180+
if (contentType) {
181+
const level = getNestingLevel(blockOuter);
182+
listItemsInfo.push({
183+
blockOuter,
184+
listItem,
185+
contentType,
186+
level,
187+
});
188+
}
156189
}
190+
});
191+
192+
// Stack to track lists at each nesting level
193+
const listStack: Array<{ list: HTMLElement; type: string; level: number }> =
194+
[];
157195

158-
const isBullet =
159-
item.getAttribute('data-content-type') === 'bulletListItem';
196+
listItemsInfo.forEach((info, idx) => {
197+
const { blockOuter, listItem, contentType, level } = info;
198+
const isBullet = contentType === 'bulletListItem';
160199
const listTag = isBullet ? 'ul' : 'ol';
161200

162-
// If the previous sibling is already the right list, reuse it; otherwise create a new one.
163-
let previousSibling = item.previousElementSibling;
164-
let listContainer: HTMLElement | null = null;
201+
// Check if previous item continues the same list (same type and level)
202+
const previousInfo = idx > 0 ? listItemsInfo[idx - 1] : null;
203+
const continuesPreviousList =
204+
previousInfo &&
205+
previousInfo.contentType === contentType &&
206+
previousInfo.level === level;
165207

166-
if (previousSibling?.tagName.toLowerCase() === listTag) {
167-
listContainer = previousSibling as HTMLElement;
168-
} else {
169-
listContainer = parsedDocument.createElement(listTag);
170-
parent.insertBefore(listContainer, item);
208+
// Find or create the appropriate list
209+
let targetList: HTMLElement | null = null;
210+
211+
if (continuesPreviousList) {
212+
// Continue with the list at this level from stack
213+
const listAtLevel = listStack.find((item) => item.level === level);
214+
targetList = listAtLevel?.list || null;
215+
}
216+
217+
// If no list found, create a new one
218+
if (!targetList) {
219+
targetList = parsedDocument.createElement(listTag);
220+
221+
// Remove lists from stack that are at same or deeper level
222+
while (
223+
listStack.length > 0 &&
224+
listStack[listStack.length - 1].level >= level
225+
) {
226+
listStack.pop();
227+
}
228+
229+
// If we have a parent list, nest this list inside its last li
230+
if (
231+
listStack.length > 0 &&
232+
listStack[listStack.length - 1].level < level
233+
) {
234+
const parentList = listStack[listStack.length - 1].list;
235+
const lastLi = parentList.querySelector('li:last-child');
236+
if (lastLi) {
237+
lastLi.appendChild(targetList);
238+
} else {
239+
// No li yet, create one and add the nested list
240+
const li = parsedDocument.createElement('li');
241+
parentList.appendChild(li);
242+
li.appendChild(targetList);
243+
}
244+
} else {
245+
// Top-level list
246+
blockOuter.parentElement?.insertBefore(targetList, blockOuter);
247+
}
248+
249+
// Add to stack
250+
listStack.push({ list: targetList, type: contentType, level });
171251
}
172252

253+
// Create list item and add content
173254
const li = parsedDocument.createElement('li');
174-
li.innerHTML = item.innerHTML;
175-
listContainer.appendChild(li);
176-
parent.removeChild(item);
255+
li.innerHTML = listItem.innerHTML;
256+
targetList.appendChild(li);
257+
258+
// Remove original block-outer
259+
blockOuter.remove();
177260
});
178261

179262
// 3) Quotes -> <blockquote>
@@ -215,6 +298,7 @@ export const improveHtmlAccessibility = (
215298
} else {
216299
listContainer = parsedDocument.createElement('ul');
217300
listContainer.setAttribute('role', 'list');
301+
listContainer.classList.add('checklist');
218302
parent.insertBefore(listContainer, item);
219303
}
220304

0 commit comments

Comments
 (0)