fix(web): task lists in formatted text line up with the other items

The box takes the bullet's place instead of the whole item moving left,
and it is no longer `disabled` (browsers grey those out): it keeps the
brand colour, out of reach of clicks and the keyboard.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-14 20:50:03 +02:00
parent 1550332d41
commit 8dfd35e611
2 changed files with 32 additions and 6 deletions

View File

@@ -43,7 +43,17 @@ describe('renderMarkdown', () => {
it('shows task lists as ticked boxes nobody can click', () => { it('shows task lists as ticked boxes nobody can click', () => {
const html = renderMarkdown('- [x] Reproducido\n- [ ] Arreglado'); const html = renderMarkdown('- [x] Reproducido\n- [ ] Arreglado');
expect(html).toContain('<input checked="" disabled="" type="checkbox">'); const boxes = Array.from(
expect(html).toContain('<input disabled="" type="checkbox">'); new DOMParser()
.parseFromString(html, 'text/html')
.querySelectorAll<HTMLInputElement>('input[type="checkbox"]'),
);
expect(boxes.map((box) => box.checked)).toEqual([true, false]);
// Not `disabled` (browsers grey those out): out of reach instead.
for (const box of boxes) {
expect(box.disabled).toBe(false);
expect(box.getAttribute('aria-disabled')).toBe('true');
expect(box.tabIndex).toBe(-1);
}
}); });
}); });

View File

@@ -67,9 +67,14 @@ function installHooks(): void {
} }
} }
if (node.tagName === 'INPUT') { if (node.tagName === 'INPUT') {
// Task lists: `- [x] done` renders as a ticked, read-only box. // Task lists: `- [x] done` renders as a ticked box nobody can change.
// Not `disabled`: browsers grey those out; CSS takes the clicks away.
if (node.getAttribute('type') !== 'checkbox') node.remove(); if (node.getAttribute('type') !== 'checkbox') node.remove();
else node.setAttribute('disabled', ''); else {
node.removeAttribute('disabled');
node.setAttribute('aria-disabled', 'true');
node.setAttribute('tabindex', '-1');
}
} }
const cls = node.getAttribute('class'); const cls = node.getAttribute('class');
if (cls && !/^ts-mention( is-me)?$/.test(cls)) if (cls && !/^ts-mention( is-me)?$/.test(cls))
@@ -117,7 +122,8 @@ export function renderMarkdown(text: string, me: string | null = null): string {
'data-user', 'data-user',
'type', 'type',
'checked', 'checked',
'disabled', 'aria-disabled',
'tabindex',
], ],
// DOMPurify's own rule plus `attachment:`: links and images to http(s), // DOMPurify's own rule plus `attachment:`: links and images to http(s),
// mail or a pasted file; plain values (`type="checkbox"`) still pass. // mail or a pasted file; plain values (`type="checkbox"`) still pass.
@@ -179,10 +185,20 @@ export const MARKDOWN_STYLES = `
margin: 0; margin: 0;
} }
} }
/* Task items: the box takes the bullet's place, so their text lines
up with the other items' (as in the editor). */
li:has(> input[type='checkbox']), li:has(> input[type='checkbox']),
li:has(> p:first-child > input[type='checkbox']) { li:has(> p:first-child > input[type='checkbox']) {
list-style: none; list-style: none;
margin-left: -1.375rem; }
li > input[type='checkbox'],
li > p:first-child > input[type='checkbox'] {
width: 0.875rem;
height: 0.875rem;
margin: 0 0.25rem 0 -1.375rem;
vertical-align: -0.125em;
accent-color: var(--ts-brand);
pointer-events: none;
} }
a { a {
color: var(--ts-brand); color: var(--ts-brand);