From 8dfd35e6119854beaec4743602144f638a16f18c Mon Sep 17 00:00:00 2001 From: Josep Piguillem Date: Wed, 14 Oct 2026 20:50:03 +0200 Subject: [PATCH] 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 --- .../presenters/src/lib/ui/markdown.spec.ts | 14 +++++++++-- .../tracker/presenters/src/lib/ui/markdown.ts | 24 +++++++++++++++---- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/libs/web/tracker/presenters/src/lib/ui/markdown.spec.ts b/libs/web/tracker/presenters/src/lib/ui/markdown.spec.ts index 30cb859..e75cb89 100644 --- a/libs/web/tracker/presenters/src/lib/ui/markdown.spec.ts +++ b/libs/web/tracker/presenters/src/lib/ui/markdown.spec.ts @@ -43,7 +43,17 @@ describe('renderMarkdown', () => { it('shows task lists as ticked boxes nobody can click', () => { const html = renderMarkdown('- [x] Reproducido\n- [ ] Arreglado'); - expect(html).toContain(''); - expect(html).toContain(''); + const boxes = Array.from( + new DOMParser() + .parseFromString(html, 'text/html') + .querySelectorAll('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); + } }); }); diff --git a/libs/web/tracker/presenters/src/lib/ui/markdown.ts b/libs/web/tracker/presenters/src/lib/ui/markdown.ts index 4f8a80c..13b4fb1 100644 --- a/libs/web/tracker/presenters/src/lib/ui/markdown.ts +++ b/libs/web/tracker/presenters/src/lib/ui/markdown.ts @@ -67,9 +67,14 @@ function installHooks(): void { } } 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(); - else node.setAttribute('disabled', ''); + else { + node.removeAttribute('disabled'); + node.setAttribute('aria-disabled', 'true'); + node.setAttribute('tabindex', '-1'); + } } const cls = node.getAttribute('class'); if (cls && !/^ts-mention( is-me)?$/.test(cls)) @@ -117,7 +122,8 @@ export function renderMarkdown(text: string, me: string | null = null): string { 'data-user', 'type', 'checked', - 'disabled', + 'aria-disabled', + 'tabindex', ], // DOMPurify's own rule plus `attachment:`: links and images to http(s), // mail or a pasted file; plain values (`type="checkbox"`) still pass. @@ -179,10 +185,20 @@ export const MARKDOWN_STYLES = ` 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(> p:first-child > input[type='checkbox']) { 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 { color: var(--ts-brand);