From 0257d9a443c7d0e7ee3287bad1aff8ba7d4a2571 Mon Sep 17 00:00:00 2001 From: Alexander Marks Date: Thu, 8 Apr 2021 09:53:08 -0700 Subject: [PATCH] Annote line numbers with aria-hidden --- CHANGELOG.md | 3 +++ src/playground-code-editor.ts | 39 +++++++++++++++++++++++++++ src/test/playground-ide_test.ts | 48 ++++++++++++++++++++++++++++++++- tsconfig.json | 2 +- 4 files changed, 90 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 43a8285cb..6ad6e14b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - The editor now uses the CodeMirror `contenteditable` input style, which has much better screen reader support. +- Line numbers are now annotated with `aria-hidden` so that they are not voiced + by screen readers. + ## [0.8.0] - 2021-04-02 ### Added diff --git a/src/playground-code-editor.ts b/src/playground-code-editor.ts index b0a8442b7..e8ced2aa9 100644 --- a/src/playground-code-editor.ts +++ b/src/playground-code-editor.ts @@ -189,6 +189,7 @@ export class PlaygroundCodeEditor extends LitElement { this._valueChangingFromOutside = false; break; case 'lineNumbers': + this._enableOrDisableAriaLineNumberObserver(); cm.setOption('lineNumbers', this.lineNumbers); break; case 'type': @@ -256,6 +257,7 @@ export class PlaygroundCodeEditor extends LitElement { const cm = CodeMirror( (dom) => { this._cmDom = dom; + this._enableOrDisableAriaLineNumberObserver(); this._resizing = true; requestAnimationFrame(() => { requestAnimationFrame(() => { @@ -296,6 +298,43 @@ export class PlaygroundCodeEditor extends LitElement { this._codemirror = cm; } + private _ariaLineNumberObserver?: MutationObserver; + + /** + * Prevent screen readers from voicing line numbers. + * + * When line numbers are active, watch for lines inserted into the DOM by + * CodeMirror, and add the "aria-hidden" attribute to their line numbers. + * + * See https://github.com/codemirror/CodeMirror/issues/6578 + */ + private _enableOrDisableAriaLineNumberObserver() { + if (this.lineNumbers && !this._ariaLineNumberObserver) { + // Start observing newly added lines. + const linesParent = this._cmDom?.querySelector('.CodeMirror-code'); + if (!linesParent) { + console.error('Internal playground error: .CodeMirror-code missing'); + return; + } + this._ariaLineNumberObserver = new MutationObserver((mutations) => { + for (const mutation of mutations) { + for (const node of mutation.addedNodes) { + // Could be e.g. a text node. Cast so we can check for presence of + // querySelector with optional chaining instead of a typeof test. + (node as Partial) + .querySelector?.('.CodeMirror-gutter-wrapper') + ?.setAttribute('aria-hidden', 'true'); + } + } + }); + this._ariaLineNumberObserver.observe(linesParent, {childList: true}); + } else if (!this.lineNumbers && this._ariaLineNumberObserver) { + // Line numbers are no longer rendering. + this._ariaLineNumberObserver.disconnect(); + this._ariaLineNumberObserver = undefined; + } + } + /** * Create hidden and folded regions for playground-hide and playground-fold * comments. diff --git a/src/test/playground-ide_test.ts b/src/test/playground-ide_test.ts index 2ada1d27b..8c37c8841 100644 --- a/src/test/playground-ide_test.ts +++ b/src/test/playground-ide_test.ts @@ -223,7 +223,7 @@ suite('playground-ide', () => { ide.config = { files: { 'index.html': { - content: 'Foo\nBar', + content: 'Foo', }, }, }; @@ -239,4 +239,50 @@ suite('playground-ide', () => { assert.equal(cmCode.getAttribute('contenteditable'), 'true'); }); + + test.only('a11y: line numbers get aria-hidden attribute', async () => { + const ide = document.createElement('playground-ide'); + ide.lineNumbers = true; + ide.config = { + files: { + 'index.html': { + content: 'Foo\nBar', + }, + }, + }; + container.appendChild(ide); + await assertPreviewContains('Foo\nBar'); + + const editor = (await pierce( + 'playground-ide', + 'playground-file-editor', + 'playground-code-editor' + )) as PlaygroundCodeEditor; + + const queryHiddenLineNumbers = () => + [ + ...editor.shadowRoot!.querySelectorAll('.CodeMirror-gutter-wrapper'), + ].filter((gutter) => gutter.getAttribute('aria-hidden') === 'true'); + + // Initial render with line-numbers enabled. + assert.equal(queryHiddenLineNumbers().length, 2); + + // Disable line numbers. + ide.lineNumbers = false; + await new Promise((r) => requestAnimationFrame(r)); + assert.equal(queryHiddenLineNumbers().length, 0); + + // Re-enable line numbers. + ide.lineNumbers = true; + await new Promise((r) => requestAnimationFrame(r)); + assert.equal(queryHiddenLineNumbers().length, 2); + + // Add a line. + const editorInternals = (editor as unknown) as { + _codemirror: PlaygroundCodeEditor['_codemirror']; + }; + editorInternals._codemirror!.setValue(editor.value + '\nBaz'); + await new Promise((r) => requestAnimationFrame(r)); + assert.equal(queryHiddenLineNumbers().length, 3); + }); }); diff --git a/tsconfig.json b/tsconfig.json index 66488ca81..f1b38d088 100755 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,7 @@ "incremental": true, "target": "es2019", "module": "esnext", - "lib": ["ES2020", "ES2015.iterable", "DOM"], + "lib": ["ES2020", "ES2015.iterable", "DOM", "DOM.Iterable"], "declaration": true, "declarationMap": true, "sourceMap": false,