Skip to content

Commit

Permalink
Merge pull request #16792 from ckeditor/ck/15208-editor-title
Browse files Browse the repository at this point in the history
Feature: Allowed to configure the accessible editable area label via `config.label` property. Closes #15208. Closes #11863. Closes #9731.

MINOR BREAKING CHANGE (ui): The default `aria-label` provided by `InlineEditableUIView` is now `'Rich Text Editor. Editing area: [root name]'` (previously `'Editor editing area: [root name]'`). You can use the `options.label` constructor property to adjust the label.
  • Loading branch information
oleq authored Jul 31, 2024
2 parents 5182fda + 54efa99 commit b50ddbb
Show file tree
Hide file tree
Showing 31 changed files with 1,041 additions and 63 deletions.
42 changes: 42 additions & 0 deletions packages/ckeditor5-core/src/editor/editorconfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -821,6 +821,48 @@ export interface EditorConfig {
* Translations to be used in the editor.
*/
translations?: ArrayOrItem<Translations>;

/**
* Label text for the `aria-label` attribute set on editor editing area. Used by assistive technologies
* to tell apart multiple editor instances (editing areas) on the page. If not set, a default
* "Rich Text Editor. Editing area [name of the area]" is used instead.
*
* ```ts
* ClassicEditor
* .create( document.querySelector( '#editor' ), {
* label: 'My editor'
* } )
* .then( ... )
* .catch( ... );
* ```
*
* If your editor implementation uses multiple roots, you should pass an object with keys corresponding to the editor
* roots names and values equal to the label that should be used for each root:
*
* ```ts
* MultiRootEditor.create(
* // Roots for the editor:
* {
* header: document.querySelector( '#header' ),
* content: document.querySelector( '#content' ),
* leftSide: document.querySelector( '#left-side' ),
* rightSide: document.querySelector( '#right-side' )
* },
* // Config:
* {
* label: {
* header: 'Header label',
* content: 'Content label',
* leftSide: 'Left side label',
* rightSide: 'Right side label'
* }
* }
* )
* .then( ... )
* .catch( ... );
* ```
*/
label?: string | Record<string, string>;
}

/**
Expand Down
4 changes: 3 additions & 1 deletion packages/ckeditor5-core/tests/_utils/classictesteditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@ export default class ClassicTestEditor extends ElementApiMixin( Editor ) {
this.ui = new ClassicTestEditorUI( this, new BoxedEditorUIView( this.locale ) );

// Expose properties normally exposed by the ClassicEditorUI.
this.ui.view.editable = new InlineEditableUIView( this.ui.view.locale, this.editing.view );
this.ui.view.editable = new InlineEditableUIView( this.ui.view.locale, this.editing.view, undefined, {
label: this.config.get( 'label' )
} );
}

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/ckeditor5-editor-balloon/src/ballooneditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export default class BalloonEditor extends /* #__PURE__ */ ElementApiMixin( Edit

this.model.document.createRoot();

const view = new BalloonEditorUIView( this.locale, this.editing.view, this.sourceElement );
const view = new BalloonEditorUIView( this.locale, this.editing.view, this.sourceElement, this.config.get( 'label' ) );
this.ui = new BalloonEditorUI( this, view );

attachToForm( this );
Expand Down
11 changes: 5 additions & 6 deletions packages/ckeditor5-editor-balloon/src/ballooneditoruiview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,20 +32,19 @@ export default class BalloonEditorUIView extends EditorUIView {
* @param editingView The editing view instance this view is related to.
* @param editableElement The editable element. If not specified, it will be automatically created by
* {@link module:ui/editableui/editableuiview~EditableUIView}. Otherwise, the given element will be used.
* @param label When set, this value will be used as an accessible `aria-label` of the
* {@link module:ui/editableui/editableuiview~EditableUIView editable view}.
*/
constructor(
locale: Locale,
editingView: EditingView,
editableElement?: HTMLElement
editableElement?: HTMLElement,
label?: string | Record<string, string>
) {
super( locale );

const t = locale.t;

this.editable = new InlineEditableUIView( locale, editingView, editableElement, {
label: editableView => {
return t( 'Rich Text Editor. Editing area: %0', editableView.name! );
}
label
} );

this.menuBarView = new MenuBarView( locale );
Expand Down
106 changes: 104 additions & 2 deletions packages/ckeditor5-editor-balloon/tests/ballooneditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -146,8 +146,10 @@ describe( 'BalloonEditor', () => {
} );
} );

afterEach( () => {
return editor.destroy();
afterEach( async () => {
if ( editor.state !== 'destroyed' ) {
await editor.destroy();
}
} );

it( 'creates an instance which inherits from the BalloonEditor', () => {
Expand Down Expand Up @@ -275,6 +277,106 @@ describe( 'BalloonEditor', () => {
.then( done )
.catch( done );
} );

describe( 'configurable editor label (aria-label)', () => {
it( 'should be set to the defaut value if not configured', () => {
expect( editor.editing.view.getDomRoot().getAttribute( 'aria-label' ) ).to.equal(
'Rich Text Editor. Editing area: main'
);
} );

it( 'should support the string format', async () => {
await editor.destroy();

editor = await BalloonEditor.create( editorElement, {
plugins: [ Paragraph, Bold ],
label: 'Custom label'
} );

expect( editor.editing.view.getDomRoot().getAttribute( 'aria-label' ) ).to.equal(
'Custom label'
);
} );

it( 'should support object format', async () => {
await editor.destroy();

editor = await BalloonEditor.create( editorElement, {
plugins: [ Paragraph, Bold ],
label: {
main: 'Custom label'
}
} );

expect( editor.editing.view.getDomRoot().getAttribute( 'aria-label' ) ).to.equal(
'Custom label'
);
} );

it( 'should keep an existing value from the source DOM element', async () => {
await editor.destroy();

editorElement.setAttribute( 'aria-label', 'Pre-existing value' );
const newEditor = await BalloonEditor.create( editorElement, {
plugins: [ Paragraph, Bold ]
} );

expect( newEditor.editing.view.getDomRoot().getAttribute( 'aria-label' ), 'Keep value' ).to.equal(
'Pre-existing value'
);

await newEditor.destroy();

expect( editorElement.getAttribute( 'aria-label' ), 'Restore value' ).to.equal( 'Pre-existing value' );
} );

it( 'should override the existing value from the source DOM element', async () => {
await editor.destroy();

editorElement.setAttribute( 'aria-label', 'Pre-existing value' );
editor = await BalloonEditor.create( editorElement, {
plugins: [ Paragraph, Bold ],
label: 'Custom label'
} );

expect( editor.editing.view.getDomRoot().getAttribute( 'aria-label' ), 'Override value' ).to.equal(
'Custom label'
);

await editor.destroy();

expect( editorElement.getAttribute( 'aria-label' ), 'Restore value' ).to.equal( 'Pre-existing value' );
} );

it( 'should use default label when creating an editor from initial data rather than a DOM element', async () => {
await editor.destroy();

editor = await BalloonEditor.create( '<p>Initial data</p>', {
plugins: [ Paragraph, Bold ]
} );

expect( editor.editing.view.getDomRoot().getAttribute( 'aria-label' ), 'Override value' ).to.equal(
'Rich Text Editor. Editing area: main'
);

await editor.destroy();
} );

it( 'should set custom label when creating an editor from initial data rather than a DOM element', async () => {
await editor.destroy();

editor = await BalloonEditor.create( '<p>Initial data</p>', {
plugins: [ Paragraph, Bold ],
label: 'Custom label'
} );

expect( editor.editing.view.getDomRoot().getAttribute( 'aria-label' ), 'Override value' ).to.equal(
'Custom label'
);

await editor.destroy();
} );
} );
} );

describe( 'create - events', () => {
Expand Down
28 changes: 27 additions & 1 deletion packages/ckeditor5-editor-balloon/tests/ballooneditoruiview.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,39 @@ describe( 'BalloonEditorUIView', () => {
expect( view.editable.isRendered ).to.be.false;
} );

it( 'is given an accessible aria label', () => {
it( 'creates an editing root with the default aria-label', () => {
view.render();

expect( editingViewRoot.getAttribute( 'aria-label' ) ).to.equal( 'Rich Text Editor. Editing area: main' );

view.destroy();
} );

it( 'creates an editing root with the configured aria-label (string format)', () => {
const editingView = new EditingView();
const editingViewRoot = createRoot( editingView.document );
const view = new BalloonEditorUIView( locale, editingView, undefined, 'Foo' );
view.editable.name = editingViewRoot.rootName;
view.render();

expect( editingViewRoot.getAttribute( 'aria-label' ) ).to.equal( 'Foo' );

view.destroy();
} );

it( 'creates an editing root with the configured aria-label (object format)', () => {
const editingView = new EditingView();
const editingViewRoot = createRoot( editingView.document );
const view = new BalloonEditorUIView( locale, editingView, undefined, {
main: 'Foo'
} );
view.editable.name = editingViewRoot.rootName;
view.render();

expect( editingViewRoot.getAttribute( 'aria-label' ) ).to.equal( 'Foo' );

view.destroy();
} );
} );

describe( '#menuBarView', () => {
Expand Down
3 changes: 2 additions & 1 deletion packages/ckeditor5-editor-classic/src/classiceditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ export default class ClassicEditor extends /* #__PURE__ */ ElementApiMixin( Edit

const view = new ClassicEditorUIView( this.locale, this.editing.view, {
shouldToolbarGroupWhenFull,
useMenuBar: menuBarConfig.isVisible
useMenuBar: menuBarConfig.isVisible,
label: this.config.get( 'label' )
} );

this.ui = new ClassicEditorUI( this, view );
Expand Down
7 changes: 6 additions & 1 deletion packages/ckeditor5-editor-classic/src/classiceditoruiview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,16 @@ export default class ClassicEditorUIView extends BoxedEditorUIView {
* @param options.shouldToolbarGroupWhenFull When set `true` enables automatic items grouping
* in the main {@link module:editor-classic/classiceditoruiview~ClassicEditorUIView#toolbar toolbar}.
* See {@link module:ui/toolbar/toolbarview~ToolbarOptions#shouldGroupWhenFull} to learn more.
* @param options.label When set, this value will be used as an accessible `aria-label` of the
* {@link module:ui/editableui/editableuiview~EditableUIView editable view}.
*/
constructor(
locale: Locale,
editingView: EditingView,
options: {
shouldToolbarGroupWhenFull?: boolean;
useMenuBar?: boolean;
label?: string | Record<string, string>;
} = {}
) {
super( locale );
Expand All @@ -64,7 +67,9 @@ export default class ClassicEditorUIView extends BoxedEditorUIView {
this.menuBarView = new MenuBarView( locale );
}

this.editable = new InlineEditableUIView( locale, editingView );
this.editable = new InlineEditableUIView( locale, editingView, undefined, {
label: options.label
} );
}

/**
Expand Down
71 changes: 69 additions & 2 deletions packages/ckeditor5-editor-classic/tests/classiceditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -171,8 +171,10 @@ describe( 'ClassicEditor', () => {
} );
} );

afterEach( () => {
return editor.destroy();
afterEach( async () => {
if ( editor.state !== 'destroyed' ) {
await editor.destroy();
}
} );

it( 'creates an instance which inherits from the ClassicEditor', () => {
Expand Down Expand Up @@ -267,6 +269,71 @@ describe( 'ClassicEditor', () => {
it( 'attaches editable UI as view\'s DOM root', () => {
expect( editor.editing.view.getDomRoot() ).to.equal( editor.ui.view.editable.element );
} );

describe( 'configurable editor label (aria-label)', () => {
it( 'should be set to the defaut value if not configured', () => {
expect( editor.editing.view.getDomRoot().getAttribute( 'aria-label' ) ).to.equal(
'Rich Text Editor. Editing area: main'
);
} );

it( 'should support the string format', async () => {
await editor.destroy();

editor = await ClassicEditor.create( editorElement, {
plugins: [ Paragraph, Bold ],
label: 'Custom label'
} );

expect( editor.editing.view.getDomRoot().getAttribute( 'aria-label' ) ).to.equal(
'Custom label'
);
} );

it( 'should support object format', async () => {
await editor.destroy();

editor = await ClassicEditor.create( editorElement, {
plugins: [ Paragraph, Bold ],
label: {
main: 'Custom label'
}
} );

expect( editor.editing.view.getDomRoot().getAttribute( 'aria-label' ) ).to.equal(
'Custom label'
);
} );

it( 'should use default label when creating an editor from initial data rather than a DOM element', async () => {
await editor.destroy();

editor = await ClassicEditor.create( '<p>Initial data</p>', {
plugins: [ Paragraph, Bold ]
} );

expect( editor.editing.view.getDomRoot().getAttribute( 'aria-label' ), 'Override value' ).to.equal(
'Rich Text Editor. Editing area: main'
);

await editor.destroy();
} );

it( 'should set custom label when creating an editor from initial data rather than a DOM element', async () => {
await editor.destroy();

editor = await ClassicEditor.create( '<p>Initial data</p>', {
plugins: [ Paragraph, Bold ],
label: 'Custom label'
} );

expect( editor.editing.view.getDomRoot().getAttribute( 'aria-label' ), 'Override value' ).to.equal(
'Custom label'
);

await editor.destroy();
} );
} );
} );
} );

Expand Down
Loading

0 comments on commit b50ddbb

Please sign in to comment.