From 80a45b5a1f1222f6a1370f4931d1041274f5eff3 Mon Sep 17 00:00:00 2001 From: Mateusz Baginski Date: Tue, 19 Mar 2024 14:26:11 +0100 Subject: [PATCH 01/10] Use `aria-live` to read `code block` --- .../ckeditor5-code-block/lang/contexts.json | 6 ++- .../src/codeblockediting.ts | 39 ++++++++++++++++++- packages/ckeditor5-code-block/src/utils.ts | 32 ++++++++++++++- 3 files changed, 73 insertions(+), 4 deletions(-) diff --git a/packages/ckeditor5-code-block/lang/contexts.json b/packages/ckeditor5-code-block/lang/contexts.json index 80bdedc58db..f35201c86c5 100644 --- a/packages/ckeditor5-code-block/lang/contexts.json +++ b/packages/ckeditor5-code-block/lang/contexts.json @@ -1,4 +1,8 @@ { "Insert code block": "A label of the button that allows inserting a new code block into the editor content.", - "Plain text": "A language of the code block in the editor content when no specific programming language is associated with it." + "Plain text": "A language of the code block in the editor content when no specific programming language is associated with it.", + "Leaving %0 code snippet": "Assistive technologies label for entering the code block with specified language in document.", + "Entering %0 code snippet": "Assistive technologies label for leaving the code block with specified language in document.", + "Entering code snippet": "Assistive technologies label for entering the code block with not specified language in document.", + "Leaving code snippet": "Assistive technologies label for leaving the code block with not specified language in document." } diff --git a/packages/ckeditor5-code-block/src/codeblockediting.ts b/packages/ckeditor5-code-block/src/codeblockediting.ts index ad7f560333c..5651db70e2f 100644 --- a/packages/ckeditor5-code-block/src/codeblockediting.ts +++ b/packages/ckeditor5-code-block/src/codeblockediting.ts @@ -19,10 +19,12 @@ import { type DowncastInsertEvent, type UpcastElementEvent, type UpcastTextEvent, - type Element + type Element, + type SelectionChangeRangeEvent } from 'ckeditor5/src/engine.js'; import type { ListEditing } from '@ckeditor/ckeditor5-list'; +import type { CodeBlockLanguageDefinition } from './codeblockconfig.js'; import CodeBlockCommand from './codeblockcommand.js'; import IndentCodeBlockCommand from './indentcodeblockcommand.js'; @@ -30,7 +32,8 @@ import OutdentCodeBlockCommand from './outdentcodeblockcommand.js'; import { getNormalizedAndLocalizedLanguageDefinitions, getLeadingWhiteSpaces, - rawSnippetTextToViewDocumentFragment + rawSnippetTextToViewDocumentFragment, + getCodeBlockAriaAnnouncement } from './utils.js'; import { modelToViewCodeBlockInsertion, @@ -236,6 +239,8 @@ export default class CodeBlockEditing extends Plugin { } } ); } ); + + this._initAriaAnnouncements( normalizedLanguagesDefs ); } /** @@ -278,6 +283,36 @@ export default class CodeBlockEditing extends Plugin { evt.stop(); }, { context: 'pre' } ); } + + /** + * @internal + */ + private _initAriaAnnouncements( languageDefs: Array ) { + const { model, ui, t } = this.editor; + let lastFocusedCodeBlock: Element | null = null; + + model.document.selection.on( 'change:range', () => { + const focusParent = model.document.selection.focus!.parent; + + if ( lastFocusedCodeBlock === focusParent || !focusParent.is( 'element' ) ) { + return; + } + + let announcement: string | null = null; + + if ( focusParent.is( 'element', 'codeBlock' ) ) { + announcement = getCodeBlockAriaAnnouncement( t, languageDefs, focusParent, 'enter' ); + } else if ( lastFocusedCodeBlock && lastFocusedCodeBlock.is( 'element', 'codeBlock' ) ) { + announcement = getCodeBlockAriaAnnouncement( t, languageDefs, focusParent, 'leave' ); + } + + if ( announcement ) { + ui.ariaLiveAnnouncer.announce( 'codeBlocks', announcement, 'assertive' ); + } + + lastFocusedCodeBlock = focusParent; + } ); + } } /** diff --git a/packages/ckeditor5-code-block/src/utils.ts b/packages/ckeditor5-code-block/src/utils.ts index a280556eb4d..cb7eae968d1 100644 --- a/packages/ckeditor5-code-block/src/utils.ts +++ b/packages/ckeditor5-code-block/src/utils.ts @@ -9,7 +9,6 @@ import type { Editor } from 'ckeditor5/src/core.js'; import type { CodeBlockLanguageDefinition } from './codeblockconfig.js'; -import { first } from 'ckeditor5/src/utils.js'; import type { DocumentSelection, Element, @@ -22,6 +21,8 @@ import type { ViewElement } from 'ckeditor5/src/engine.js'; +import { first, type LocaleTranslate } from 'ckeditor5/src/utils.js'; + /** * Returns code block languages as defined in `config.codeBlock.languages` but processed: * @@ -258,3 +259,32 @@ export function canBeCodeBlock( schema: Schema, element: Element ): boolean { return schema.checkChild( element.parent as Element, 'codeBlock' ); } + +/** + * Get the translated message read by the screen reader when you enter or exit an element with your cursor. + */ +export function getCodeBlockAriaAnnouncement( + t: LocaleTranslate, + languageDefs: Array, + element: Element, + direction: 'enter' | 'leave' +): string { + const languagesToLabels = getPropertyAssociation( languageDefs, 'language', 'label' ); + const codeBlockLanguage = element.getAttribute( 'language' ) as string; + + if ( codeBlockLanguage in languagesToLabels ) { + const language = languagesToLabels[ codeBlockLanguage ]; + + if ( direction === 'enter' ) { + return t( 'Entering %0 code snippet', language ); + } + + return t( 'Leaving %0 code snippet', language ); + } + + if ( direction === 'enter' ) { + return t( 'Entering code snippet' ); + } + + return t( 'Leaving code snippet' ); +} From 1a56b5ca6595224c72c87cd8f42dc238e6496657 Mon Sep 17 00:00:00 2001 From: Mateusz Baginski Date: Tue, 19 Mar 2024 14:28:24 +0100 Subject: [PATCH 02/10] Fix incorrect translations descriptions --- packages/ckeditor5-code-block/lang/contexts.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ckeditor5-code-block/lang/contexts.json b/packages/ckeditor5-code-block/lang/contexts.json index f35201c86c5..c44065517c8 100644 --- a/packages/ckeditor5-code-block/lang/contexts.json +++ b/packages/ckeditor5-code-block/lang/contexts.json @@ -1,8 +1,8 @@ { "Insert code block": "A label of the button that allows inserting a new code block into the editor content.", "Plain text": "A language of the code block in the editor content when no specific programming language is associated with it.", - "Leaving %0 code snippet": "Assistive technologies label for entering the code block with specified language in document.", - "Entering %0 code snippet": "Assistive technologies label for leaving the code block with specified language in document.", + "Leaving %0 code snippet": "Assistive technologies label for leaving the code block with specified language in document.", + "Entering %0 code snippet": "Assistive technologies label for entering the code block with specified language in document.", "Entering code snippet": "Assistive technologies label for entering the code block with not specified language in document.", "Leaving code snippet": "Assistive technologies label for leaving the code block with not specified language in document." } From e3c6e63e519f1853f1633b0997ad600fb7f68df7 Mon Sep 17 00:00:00 2001 From: Mateusz Baginski Date: Tue, 19 Mar 2024 14:49:15 +0100 Subject: [PATCH 03/10] Adjust docs for `_initAriaAnnouncements` --- packages/ckeditor5-code-block/src/codeblockediting.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/ckeditor5-code-block/src/codeblockediting.ts b/packages/ckeditor5-code-block/src/codeblockediting.ts index 5651db70e2f..606934a41b4 100644 --- a/packages/ckeditor5-code-block/src/codeblockediting.ts +++ b/packages/ckeditor5-code-block/src/codeblockediting.ts @@ -239,8 +239,6 @@ export default class CodeBlockEditing extends Plugin { } } ); } ); - - this._initAriaAnnouncements( normalizedLanguagesDefs ); } /** @@ -282,13 +280,20 @@ export default class CodeBlockEditing extends Plugin { data.preventDefault(); evt.stop(); }, { context: 'pre' } ); + + this._initAriaAnnouncements( ); } /** + * Observe when user enters or leaves code block and set value of `aria-live' tag. + * This allows screen readers to indicate when the user has entered and left the specified code block. + * * @internal */ - private _initAriaAnnouncements( languageDefs: Array ) { + private _initAriaAnnouncements( ) { const { model, ui, t } = this.editor; + const languageDefs = getNormalizedAndLocalizedLanguageDefinitions( this.editor ); + let lastFocusedCodeBlock: Element | null = null; model.document.selection.on( 'change:range', () => { From af222f23b69af76ee94defcd729bfec31e4828b8 Mon Sep 17 00:00:00 2001 From: Mateusz Baginski Date: Tue, 19 Mar 2024 15:16:50 +0100 Subject: [PATCH 04/10] Add tests --- .../ckeditor5-code-block/lang/contexts.json | 8 +-- .../src/codeblockediting.ts | 4 +- .../tests/codeblockediting.js | 50 ++++++++++++++++++- 3 files changed, 55 insertions(+), 7 deletions(-) diff --git a/packages/ckeditor5-code-block/lang/contexts.json b/packages/ckeditor5-code-block/lang/contexts.json index c44065517c8..6a10d3a027e 100644 --- a/packages/ckeditor5-code-block/lang/contexts.json +++ b/packages/ckeditor5-code-block/lang/contexts.json @@ -1,8 +1,8 @@ { "Insert code block": "A label of the button that allows inserting a new code block into the editor content.", "Plain text": "A language of the code block in the editor content when no specific programming language is associated with it.", - "Leaving %0 code snippet": "Assistive technologies label for leaving the code block with specified language in document.", - "Entering %0 code snippet": "Assistive technologies label for entering the code block with specified language in document.", - "Entering code snippet": "Assistive technologies label for entering the code block with not specified language in document.", - "Leaving code snippet": "Assistive technologies label for leaving the code block with not specified language in document." + "Leaving %0 code snippet": "Assistive technologies label for leaving the code block with specified programming language.", + "Entering %0 code snippet": "Assistive technologies label for entering the code block with specified programming language.", + "Entering code snippet": "Assistive technologies label for entering the code block with not specified programming language.", + "Leaving code snippet": "Assistive technologies label for leaving the code block with not specified programming language." } diff --git a/packages/ckeditor5-code-block/src/codeblockediting.ts b/packages/ckeditor5-code-block/src/codeblockediting.ts index 606934a41b4..cc5609c8baf 100644 --- a/packages/ckeditor5-code-block/src/codeblockediting.ts +++ b/packages/ckeditor5-code-block/src/codeblockediting.ts @@ -308,10 +308,10 @@ export default class CodeBlockEditing extends Plugin { if ( focusParent.is( 'element', 'codeBlock' ) ) { announcement = getCodeBlockAriaAnnouncement( t, languageDefs, focusParent, 'enter' ); } else if ( lastFocusedCodeBlock && lastFocusedCodeBlock.is( 'element', 'codeBlock' ) ) { - announcement = getCodeBlockAriaAnnouncement( t, languageDefs, focusParent, 'leave' ); + announcement = getCodeBlockAriaAnnouncement( t, languageDefs, lastFocusedCodeBlock, 'leave' ); } - if ( announcement ) { + if ( announcement && ui ) { ui.ariaLiveAnnouncer.announce( 'codeBlocks', announcement, 'assertive' ); } diff --git a/packages/ckeditor5-code-block/tests/codeblockediting.js b/packages/ckeditor5-code-block/tests/codeblockediting.js index 3877e7fafb1..1cb02028be7 100644 --- a/packages/ckeditor5-code-block/tests/codeblockediting.js +++ b/packages/ckeditor5-code-block/tests/codeblockediting.js @@ -30,7 +30,7 @@ import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils import { _clear as clearTranslations, add as addTranslations } from '@ckeditor/ckeditor5-utils/src/translation-service.js'; describe( 'CodeBlockEditing', () => { - let editor, element, model, view, viewDoc; + let editor, element, model, view, viewDoc, root; before( () => { addTranslations( 'en', { @@ -60,6 +60,7 @@ describe( 'CodeBlockEditing', () => { model = editor.model; view = editor.editing.view; viewDoc = view.document; + root = model.document.getRoot(); } ); } ); @@ -1788,4 +1789,51 @@ describe( 'CodeBlockEditing', () => { } ); } ); } ); + + describe( 'accessibility', () => { + let announcerSpy; + + beforeEach( () => { + announcerSpy = sinon.spy( editor.ui.ariaLiveAnnouncer, 'announce' ); + } ); + + it( 'should announce enter and leave code block with specified language label', () => { + setModelData( model, 'fooa' ); + + model.change( writer => { + writer.setSelection( createRange( root, [ 0, 0 ], root, [ 0, 1 ] ) ); + } ); + + expect( announcerSpy ).to.be.calledWithExactly( 'codeBlocks', 'Entering CSS code snippet', 'assertive' ); + + model.change( writer => { + writer.setSelection( createRange( root, [ 1, 0 ], root, [ 1, 1 ] ) ); + } ); + + expect( announcerSpy ).to.be.calledWithExactly( 'codeBlocks', 'Leaving CSS code snippet', 'assertive' ); + } ); + + it( 'should announce enter and leave code block without language label', () => { + setModelData( model, 'fooa' ); + + model.change( writer => { + writer.setSelection( createRange( root, [ 0, 0 ], root, [ 0, 1 ] ) ); + } ); + + expect( announcerSpy ).to.be.calledWithExactly( 'codeBlocks', 'Entering code snippet', 'assertive' ); + + model.change( writer => { + writer.setSelection( createRange( root, [ 1, 0 ], root, [ 1, 1 ] ) ); + } ); + + expect( announcerSpy ).to.be.calledWithExactly( 'codeBlocks', 'Leaving code snippet', 'assertive' ); + } ); + } ); + + function createRange( startElement, startPath, endElement, endPath ) { + return model.createRange( + model.createPositionFromPath( startElement, startPath ), + model.createPositionFromPath( endElement, endPath ) + ); + } } ); From eb69e604009b2a74dc977d0b1f798bee0a580b56 Mon Sep 17 00:00:00 2001 From: Mateusz Baginski Date: Tue, 19 Mar 2024 15:21:12 +0100 Subject: [PATCH 05/10] Adjust comment --- packages/ckeditor5-code-block/src/codeblockediting.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ckeditor5-code-block/src/codeblockediting.ts b/packages/ckeditor5-code-block/src/codeblockediting.ts index cc5609c8baf..656aa96ad0b 100644 --- a/packages/ckeditor5-code-block/src/codeblockediting.ts +++ b/packages/ckeditor5-code-block/src/codeblockediting.ts @@ -285,7 +285,7 @@ export default class CodeBlockEditing extends Plugin { } /** - * Observe when user enters or leaves code block and set value of `aria-live' tag. + * Observe when user enters or leaves code block and set proper aria value in global live announcer. * This allows screen readers to indicate when the user has entered and left the specified code block. * * @internal From 84d1ada4e42bbdd3df3f2b4623a77a77db5d863f Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Wed, 20 Mar 2024 15:44:42 +0100 Subject: [PATCH 06/10] Improved entry descriptions in contexts.json. --- packages/ckeditor5-code-block/lang/contexts.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/ckeditor5-code-block/lang/contexts.json b/packages/ckeditor5-code-block/lang/contexts.json index 6a10d3a027e..95f3b5435a1 100644 --- a/packages/ckeditor5-code-block/lang/contexts.json +++ b/packages/ckeditor5-code-block/lang/contexts.json @@ -1,8 +1,8 @@ { "Insert code block": "A label of the button that allows inserting a new code block into the editor content.", "Plain text": "A language of the code block in the editor content when no specific programming language is associated with it.", - "Leaving %0 code snippet": "Assistive technologies label for leaving the code block with specified programming language.", - "Entering %0 code snippet": "Assistive technologies label for entering the code block with specified programming language.", - "Entering code snippet": "Assistive technologies label for entering the code block with not specified programming language.", - "Leaving code snippet": "Assistive technologies label for leaving the code block with not specified programming language." + "Leaving %0 code snippet": "Assistive technologies label for leaving the code block with a specified programming language. Example: 'Leaving JavaScript code snippet'", + "Entering %0 code snippet": "Assistive technologies label for entering the code block with a specified programming language. Example: 'Entering JavaScript code snippet'", + "Entering code snippet": "Assistive technologies label for entering the code block with unspecified programming language.", + "Leaving code snippet": "Assistive technologies label for leaving the code block with unspecified programming language." } From dbcde9a2b27c3f4eaafe8d09aa98230e0bca597d Mon Sep 17 00:00:00 2001 From: Mateusz Baginski Date: Thu, 21 Mar 2024 08:28:44 +0100 Subject: [PATCH 07/10] Add more tests to `codeblockediting` and improve `aria-live` support in `code block` --- .../src/codeblockediting.ts | 21 ++- .../tests/codeblockediting.js | 129 +++++++++++++++++- 2 files changed, 142 insertions(+), 8 deletions(-) diff --git a/packages/ckeditor5-code-block/src/codeblockediting.ts b/packages/ckeditor5-code-block/src/codeblockediting.ts index 656aa96ad0b..e07a21a067b 100644 --- a/packages/ckeditor5-code-block/src/codeblockediting.ts +++ b/packages/ckeditor5-code-block/src/codeblockediting.ts @@ -7,6 +7,8 @@ * @module code-block/codeblockediting */ +import { lowerFirst, upperFirst } from 'lodash-es'; + import { Plugin, type Editor, type MultiCommand } from 'ckeditor5/src/core.js'; import { ShiftEnter, type ViewDocumentEnterEvent } from 'ckeditor5/src/enter.js'; @@ -24,7 +26,6 @@ import { } from 'ckeditor5/src/engine.js'; import type { ListEditing } from '@ckeditor/ckeditor5-list'; -import type { CodeBlockLanguageDefinition } from './codeblockconfig.js'; import CodeBlockCommand from './codeblockcommand.js'; import IndentCodeBlockCommand from './indentcodeblockcommand.js'; @@ -303,15 +304,23 @@ export default class CodeBlockEditing extends Plugin { return; } - let announcement: string | null = null; + const announcements: Array = []; + + if ( lastFocusedCodeBlock && lastFocusedCodeBlock.is( 'element', 'codeBlock' ) ) { + announcements.push( getCodeBlockAriaAnnouncement( t, languageDefs, lastFocusedCodeBlock, 'leave' ) ); + } if ( focusParent.is( 'element', 'codeBlock' ) ) { - announcement = getCodeBlockAriaAnnouncement( t, languageDefs, focusParent, 'enter' ); - } else if ( lastFocusedCodeBlock && lastFocusedCodeBlock.is( 'element', 'codeBlock' ) ) { - announcement = getCodeBlockAriaAnnouncement( t, languageDefs, lastFocusedCodeBlock, 'leave' ); + announcements.push( getCodeBlockAriaAnnouncement( t, languageDefs, focusParent, 'enter' ) ); } - if ( announcement && ui ) { + if ( ui && announcements.length ) { + const announcement = upperFirst( + announcements + .map( lowerFirst ) + .join( ', ' ) + ); + ui.ariaLiveAnnouncer.announce( 'codeBlocks', announcement, 'assertive' ); } diff --git a/packages/ckeditor5-code-block/tests/codeblockediting.js b/packages/ckeditor5-code-block/tests/codeblockediting.js index 1cb02028be7..93f97ae2129 100644 --- a/packages/ckeditor5-code-block/tests/codeblockediting.js +++ b/packages/ckeditor5-code-block/tests/codeblockediting.js @@ -1798,7 +1798,7 @@ describe( 'CodeBlockEditing', () => { } ); it( 'should announce enter and leave code block with specified language label', () => { - setModelData( model, 'fooa' ); + setModelData( model, join( codeblock( 'css' ), tag( 'paragraph' ) ) ); model.change( writer => { writer.setSelection( createRange( root, [ 0, 0 ], root, [ 0, 1 ] ) ); @@ -1814,7 +1814,7 @@ describe( 'CodeBlockEditing', () => { } ); it( 'should announce enter and leave code block without language label', () => { - setModelData( model, 'fooa' ); + setModelData( model, join( codeblock( 'FooBar' ), tag( 'paragraph' ) ) ); model.change( writer => { writer.setSelection( createRange( root, [ 0, 0 ], root, [ 0, 1 ] ) ); @@ -1828,8 +1828,133 @@ describe( 'CodeBlockEditing', () => { expect( announcerSpy ).to.be.calledWithExactly( 'codeBlocks', 'Leaving code snippet', 'assertive' ); } ); + + it( 'should announce sequential entry and exit of a code block with paragraph between', () => { + setModelData( model, join( codeblock( 'php' ), tag( 'paragraph' ), codeblock( 'css' ) ) ); + + model.change( writer => { + writer.setSelection( createRange( root, [ 0, 0 ], root, [ 0, 1 ] ) ); + } ); + + expect( announcerSpy ).to.be.calledWithExactly( 'codeBlocks', 'Entering PHP code snippet', 'assertive' ); + + model.change( writer => { + writer.setSelection( createRange( root, [ 1, 0 ], root, [ 1, 1 ] ) ); + } ); + + expect( announcerSpy ).to.be.calledWithExactly( 'codeBlocks', 'Leaving PHP code snippet', 'assertive' ); + + model.change( writer => { + writer.setSelection( createRange( root, [ 2, 0 ], root, [ 2, 1 ] ) ); + } ); + + expect( announcerSpy ).to.be.calledWithExactly( 'codeBlocks', 'Entering CSS code snippet', 'assertive' ); + } ); + + it( 'should announce sequential entry and exit of a code block that starts immediately after another code block', () => { + setModelData( + model, + join( + codeblock( 'css' ), + codeblock( 'php' ), + tag( 'paragraph' ) + ) + ); + + model.change( writer => { + writer.setSelection( createRange( root, [ 0, 0 ], root, [ 0, 1 ] ) ); + } ); + + expect( announcerSpy ).to.be.calledWithExactly( 'codeBlocks', 'Entering CSS code snippet', 'assertive' ); + + model.change( writer => { + writer.setSelection( createRange( root, [ 1, 0 ], root, [ 1, 1 ] ) ); + } ); + + expect( announcerSpy ).to.be.calledWithExactly( + 'codeBlocks', + 'Leaving CSS code snippet, entering PHP code snippet', + 'assertive' + ); + + model.change( writer => { + writer.setSelection( createRange( root, [ 2, 0 ], root, [ 2, 1 ] ) ); + } ); + + expect( announcerSpy ).to.be.calledWithExactly( + 'codeBlocks', + 'Leaving PHP code snippet', + 'assertive' + ); + } ); + + it( 'should announce random enter and exit of a code block that starts immediately after another code block', () => { + setModelData( + model, + join( + codeblock( 'css' ), + codeblock( 'php' ), + codeblock( 'ruby' ), + codeblock( 'xml' ), + codeblock( 'FooBar' ) + ) + ); + + model.change( writer => { + writer.setSelection( createRange( root, [ 2, 0 ], root, [ 2, 1 ] ) ); + } ); + + expect( announcerSpy ).to.be.calledWithExactly( 'codeBlocks', 'Entering Ruby code snippet', 'assertive' ); + + model.change( writer => { + writer.setSelection( createRange( root, [ 0, 0 ], root, [ 0, 1 ] ) ); + } ); + + expect( announcerSpy ).to.be.calledWithExactly( + 'codeBlocks', + 'Leaving Ruby code snippet, entering CSS code snippet', + 'assertive' + ); + + model.change( writer => { + writer.setSelection( createRange( root, [ 3, 0 ], root, [ 3, 1 ] ) ); + } ); + + expect( announcerSpy ).to.be.calledWithExactly( + 'codeBlocks', + 'Leaving CSS code snippet, entering XML code snippet', + 'assertive' + ); + + model.change( writer => { + writer.setSelection( createRange( root, [ 4, 0 ], root, [ 4, 1 ] ) ); + } ); + + expect( announcerSpy ).to.be.calledWithExactly( + 'codeBlocks', + 'Leaving XML code snippet, entering code snippet', + 'assertive' + ); + } ); } ); + function join( ...lines ) { + return lines.filter( Boolean ).join( '' ); + } + + function tag( name, attributes = {}, content = 'Example' ) { + const formattedAttributes = Object + .entries( attributes || {} ) + .map( ( [ key, value ] ) => `${ key }="${ value }"` ) + .join( ' ' ); + + return `<${ name }${ formattedAttributes ? ` ${ formattedAttributes }` : '' }>${ content }`; + } + + function codeblock( language, content = 'Example code' ) { + return tag( 'codeBlock', { language }, content ); + } + function createRange( startElement, startPath, endElement, endPath ) { return model.createRange( model.createPositionFromPath( startElement, startPath ), From 6acc8502590a93adf17e6b7b87e3745ebfe4ee48 Mon Sep 17 00:00:00 2001 From: Mateusz Baginski Date: Thu, 21 Mar 2024 08:36:09 +0100 Subject: [PATCH 08/10] Add missing dependency to `code-block` package --- packages/ckeditor5-code-block/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/ckeditor5-code-block/package.json b/packages/ckeditor5-code-block/package.json index 07fe3acbd84..2bb0dd6943f 100644 --- a/packages/ckeditor5-code-block/package.json +++ b/packages/ckeditor5-code-block/package.json @@ -13,7 +13,8 @@ "type": "module", "main": "src/index.ts", "dependencies": { - "ckeditor5": "41.2.1" + "ckeditor5": "41.2.1", + "lodash-es": "4.17.21" }, "devDependencies": { "@ckeditor/ckeditor5-alignment": "41.2.1", From 0f875e3536f629747cd2d885c4b00cb32c6fd4a3 Mon Sep 17 00:00:00 2001 From: Mateusz Baginski Date: Thu, 21 Mar 2024 09:09:25 +0100 Subject: [PATCH 09/10] Add edge case with identical announcement --- .../src/codeblockediting.ts | 29 +++++++++++--- .../tests/codeblockediting.js | 40 +++++++++++++++++++ 2 files changed, 64 insertions(+), 5 deletions(-) diff --git a/packages/ckeditor5-code-block/src/codeblockediting.ts b/packages/ckeditor5-code-block/src/codeblockediting.ts index e07a21a067b..28500babeda 100644 --- a/packages/ckeditor5-code-block/src/codeblockediting.ts +++ b/packages/ckeditor5-code-block/src/codeblockediting.ts @@ -296,6 +296,13 @@ export default class CodeBlockEditing extends Plugin { const languageDefs = getNormalizedAndLocalizedLanguageDefinitions( this.editor ); let lastFocusedCodeBlock: Element | null = null; + let prevAnnouncement: string | null = null; + + const joinAnnouncements = ( announcements: Array ) => upperFirst( + announcements + .map( lowerFirst ) + .join( ', ' ) + ); model.document.selection.on( 'change:range', () => { const focusParent = model.document.selection.focus!.parent; @@ -315,13 +322,25 @@ export default class CodeBlockEditing extends Plugin { } if ( ui && announcements.length ) { - const announcement = upperFirst( - announcements - .map( lowerFirst ) - .join( ', ' ) - ); + let announcement = joinAnnouncements( announcements ); + + // Handle edge case when: + // + // 1. user enters code block #1 with PHP language. + // 2. user enters code block #2 with PHP language. + // 3. user leaves code block #2 and comes back to code block #1 with identical language + // + // In this scenario `announcement` will be identical (`Leaving PHP code block, entering PHP code block`) + // Screen reader will not detect this change because `aria-live` is identical with previous one and + // will skip reading the label. + // + // Try to bypass this issue by toggling non readable character at the end of phrase. + if ( prevAnnouncement === announcement ) { + announcement += '.'; + } ui.ariaLiveAnnouncer.announce( 'codeBlocks', announcement, 'assertive' ); + prevAnnouncement = announcement; } lastFocusedCodeBlock = focusParent; diff --git a/packages/ckeditor5-code-block/tests/codeblockediting.js b/packages/ckeditor5-code-block/tests/codeblockediting.js index 93f97ae2129..2fa901a45a5 100644 --- a/packages/ckeditor5-code-block/tests/codeblockediting.js +++ b/packages/ckeditor5-code-block/tests/codeblockediting.js @@ -1936,6 +1936,46 @@ describe( 'CodeBlockEditing', () => { 'assertive' ); } ); + + it( 'should force trigger announce if leaving and entering again code block with the same language', () => { + setModelData( model, join( codeblock( 'css' ), codeblock( 'css' ) ) ); + + model.change( writer => { + writer.setSelection( createRange( root, [ 0, 0 ], root, [ 0, 1 ] ) ); + } ); + + expect( announcerSpy ).to.be.calledWithExactly( 'codeBlocks', 'Entering CSS code snippet', 'assertive' ); + + model.change( writer => { + writer.setSelection( createRange( root, [ 1, 0 ], root, [ 1, 1 ] ) ); + } ); + + expect( announcerSpy ).to.be.calledWithExactly( + 'codeBlocks', + 'Leaving CSS code snippet, entering CSS code snippet', + 'assertive' + ); + + model.change( writer => { + writer.setSelection( createRange( root, [ 0, 0 ], root, [ 0, 1 ] ) ); + } ); + + expect( announcerSpy ).to.be.calledWithExactly( + 'codeBlocks', + 'Leaving CSS code snippet, entering CSS code snippet.', + 'assertive' + ); + + model.change( writer => { + writer.setSelection( createRange( root, [ 1, 0 ], root, [ 1, 1 ] ) ); + } ); + + expect( announcerSpy ).to.be.calledWithExactly( + 'codeBlocks', + 'Leaving CSS code snippet, entering CSS code snippet', + 'assertive' + ); + } ); } ); function join( ...lines ) { From 91153772648256d2c4ad99723d4d7388d3007a28 Mon Sep 17 00:00:00 2001 From: Mateusz Baginski Date: Thu, 21 Mar 2024 09:37:41 +0100 Subject: [PATCH 10/10] Adjust variable name in code block editing --- packages/ckeditor5-code-block/src/codeblockediting.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/ckeditor5-code-block/src/codeblockediting.ts b/packages/ckeditor5-code-block/src/codeblockediting.ts index 28500babeda..850ff5a25d1 100644 --- a/packages/ckeditor5-code-block/src/codeblockediting.ts +++ b/packages/ckeditor5-code-block/src/codeblockediting.ts @@ -296,7 +296,7 @@ export default class CodeBlockEditing extends Plugin { const languageDefs = getNormalizedAndLocalizedLanguageDefinitions( this.editor ); let lastFocusedCodeBlock: Element | null = null; - let prevAnnouncement: string | null = null; + let lastAnnouncement: string | null = null; const joinAnnouncements = ( announcements: Array ) => upperFirst( announcements @@ -335,12 +335,12 @@ export default class CodeBlockEditing extends Plugin { // will skip reading the label. // // Try to bypass this issue by toggling non readable character at the end of phrase. - if ( prevAnnouncement === announcement ) { + if ( lastAnnouncement === announcement ) { announcement += '.'; } ui.ariaLiveAnnouncer.announce( 'codeBlocks', announcement, 'assertive' ); - prevAnnouncement = announcement; + lastAnnouncement = announcement; } lastFocusedCodeBlock = focusParent;