@@ -10,6 +10,7 @@ import { Plugin } from '@ckeditor/ckeditor5-core';
1010
1111import { TableCaptionEditing } from '../../src/tablecaption/tablecaptionediting.js' ;
1212import { TableEditing } from '../../src/tableediting.js' ;
13+ import { priorities } from '@ckeditor/ckeditor5-utils' ;
1314
1415describe ( 'TableCaptionEditing' , ( ) => {
1516 let editor , model , view ;
@@ -302,10 +303,13 @@ describe( 'TableCaptionEditing', () => {
302303 '<table><tableRow><tableCell><paragraph>xyz</paragraph></tableCell></tableRow><caption>Foo caption</caption></table>'
303304 ) ;
304305
305- expect ( _getViewData ( view , { withoutSelection : true } ) ) . to . equal (
306- '<figure class="ck-widget ck-widget_with-selection-handle table" contenteditable="false">' +
306+ expect ( maskUIDs ( _getViewData ( view , { withoutSelection : true } ) ) ) . to . equal (
307+ '<figure ' +
308+ 'class="ck-widget ck-widget_with-selection-handle table" ' +
309+ 'contenteditable="false"' +
310+ '>' +
307311 '<div class="ck ck-widget__selection-handle"></div>' +
308- '<table>' +
312+ '<table aria-labelledby="masked-uid-1" >' +
309313 '<tbody>' +
310314 '<tr>' +
311315 '<td class="ck-editor__editable ck-editor__nested-editable" contenteditable="true"' +
@@ -315,13 +319,170 @@ describe( 'TableCaptionEditing', () => {
315319 '</tr>' +
316320 '</tbody>' +
317321 '</table>' +
318- '<figcaption class="ck-editor__editable ck-editor__nested-editable" ' +
319- 'contenteditable="true" data-placeholder="Enter table caption" role="textbox" tabindex="-1">' +
322+ '<figcaption ' +
323+ 'class="ck-editor__editable ck-editor__nested-editable" ' +
324+ 'contenteditable="true" ' +
325+ 'data-placeholder="Enter table caption" ' +
326+ 'id="masked-uid-1" ' +
327+ 'role="textbox" ' +
328+ 'tabindex="-1"' +
329+ '>' +
320330 'Foo caption' +
321331 '</figcaption>' +
322332 '</figure>'
323333 ) ;
324334 } ) ;
335+
336+ it ( 'should set id on caption and aria-labelledby on table' , ( ) => {
337+ _setModelData ( model ,
338+ '<table><tableRow><tableCell><paragraph>xyz</paragraph></tableCell></tableRow><caption>Foo caption</caption></table>'
339+ ) ;
340+
341+ const viewFigure = view . document . getRoot ( ) . getChild ( 0 ) ;
342+ const viewTable = viewFigure . getChild ( 1 ) ;
343+ const viewCaption = viewFigure . getChild ( 2 ) ;
344+
345+ expect ( viewCaption . hasAttribute ( 'id' ) ) . to . be . true ;
346+ expect ( viewTable . hasAttribute ( 'aria-labelledby' ) ) . to . be . true ;
347+ expect ( viewTable . getAttribute ( 'aria-labelledby' ) ) . to . equal ( viewCaption . getAttribute ( 'id' ) ) ;
348+ } ) ;
349+
350+ it ( 'should not crash when view does not contain a <table> element' , ( ) => {
351+ editor . conversion . for ( 'editingDowncast' ) . add ( dispatcher => {
352+ dispatcher . on ( 'insert:table' , ( evt , data , { writer, mapper } ) => {
353+ const viewFigure = mapper . toViewElement ( data . item ) ;
354+
355+ if ( ! viewFigure ) {
356+ return ;
357+ }
358+
359+ const viewTable = Array
360+ . from ( viewFigure . getChildren ( ) )
361+ . find ( child => child . is ( 'element' , 'table' ) ) ;
362+
363+ if ( viewTable ) {
364+ writer . remove ( viewTable ) ;
365+ }
366+ } , { priority : priorities . low + 1 } ) ;
367+ } ) ;
368+
369+ _setModelData ( model ,
370+ '<table><tableRow><tableCell><paragraph>xyz</paragraph></tableCell></tableRow><caption>Foo caption</caption></table>'
371+ ) ;
372+
373+ const viewFigure = view . document . getRoot ( ) . getChild ( 0 ) ;
374+ const viewCaption = Array
375+ . from ( viewFigure . getChildren ( ) )
376+ . find ( child => child . is ( 'element' , 'figcaption' ) ) ;
377+
378+ expect ( viewCaption ) . to . not . be . undefined ;
379+ expect ( viewCaption . hasAttribute ( 'id' ) ) . to . be . false ;
380+ } ) ;
381+
382+ it ( 'should not set aria-labelledby on table when there is no caption' , ( ) => {
383+ _setModelData ( model ,
384+ '<table><tableRow><tableCell><paragraph>xyz</paragraph></tableCell></tableRow></table>'
385+ ) ;
386+
387+ const viewFigure = view . document . getRoot ( ) . getChild ( 0 ) ;
388+ const viewTable = viewFigure . getChild ( 1 ) ;
389+
390+ expect ( viewTable . hasAttribute ( 'aria-labelledby' ) ) . to . be . false ;
391+ } ) ;
392+
393+ it ( 'should remove aria-labelledby when caption is removed' , ( ) => {
394+ _setModelData ( model ,
395+ '<table><tableRow><tableCell><paragraph>xyz</paragraph></tableCell></tableRow><caption>Foo caption</caption></table>'
396+ ) ;
397+
398+ const viewFigure = view . document . getRoot ( ) . getChild ( 0 ) ;
399+ const viewTable = viewFigure . getChild ( 1 ) ;
400+
401+ expect ( viewTable . hasAttribute ( 'aria-labelledby' ) ) . to . be . true ;
402+
403+ model . change ( writer => {
404+ const table = model . document . getRoot ( ) . getChild ( 0 ) ;
405+ const caption = table . getChild ( 1 ) ;
406+
407+ writer . remove ( caption ) ;
408+ } ) ;
409+
410+ const viewFigureAfter = view . document . getRoot ( ) . getChild ( 0 ) ;
411+ const viewTableAfter = viewFigureAfter . getChild ( 1 ) ;
412+
413+ expect ( viewTableAfter . hasAttribute ( 'aria-labelledby' ) ) . to . be . false ;
414+ } ) ;
415+
416+ it ( 'should reuse the same id for caption when table is re-rendered' , ( ) => {
417+ _setModelData ( model ,
418+ '<table><tableRow><tableCell><paragraph>xyz</paragraph></tableCell></tableRow><caption>Foo caption</caption></table>'
419+ ) ;
420+
421+ const viewFigure = view . document . getRoot ( ) . getChild ( 0 ) ;
422+ const viewCaption = viewFigure . getChild ( 2 ) ;
423+ const firstCaptionId = viewCaption . getAttribute ( 'id' ) ;
424+
425+ model . change ( writer => {
426+ const table = model . document . getRoot ( ) . getChild ( 0 ) ;
427+
428+ writer . remove ( table ) ;
429+ writer . insert ( table , model . document . getRoot ( ) , 0 ) ;
430+ } ) ;
431+
432+ const newViewFigure = view . document . getRoot ( ) . getChild ( 0 ) ;
433+ const newViewCaption = newViewFigure . getChild ( 2 ) ;
434+ const secondCaptionId = newViewCaption . getAttribute ( 'id' ) ;
435+
436+ expect ( firstCaptionId ) . to . equal ( secondCaptionId ) ;
437+ } ) ;
438+
439+ it ( 'should not add aria-labelledby when caption is not bound to view' , async ( ) => {
440+ editor . conversion . for ( 'editingDowncast' ) . add ( dispatcher => {
441+ dispatcher . on ( 'insert:table' , ( evt , data , { mapper } ) => {
442+ const modelCaption = Array
443+ . from ( data . item . getChildren ( ) )
444+ . find ( child => child . is ( 'element' , 'caption' ) ) ;
445+
446+ const viewCaption = mapper . toViewElement ( modelCaption ) ;
447+
448+ mapper . unbindViewElement ( viewCaption ) ;
449+ } ) ;
450+ } ) ;
451+
452+ _setModelData ( editor . model ,
453+ '<table><tableRow><tableCell><paragraph>xyz</paragraph></tableCell></tableRow><caption>Foo caption</caption></table>'
454+ ) ;
455+
456+ const viewFigure = editor . editing . view . document . getRoot ( ) . getChild ( 0 ) ;
457+ const viewTable = viewFigure . getChild ( 1 ) ;
458+
459+ expect ( viewTable . hasAttribute ( 'aria-labelledby' ) ) . to . be . false ;
460+ } ) ;
461+
462+ it ( 'should reuse id when caption already has id attribute in view' , async ( ) => {
463+ editor . conversion . for ( 'editingDowncast' ) . add ( dispatcher => {
464+ dispatcher . on ( 'insert:table' , ( evt , data , { writer, mapper } ) => {
465+ const modelCaption = Array
466+ . from ( data . item . getChildren ( ) )
467+ . find ( child => child . is ( 'element' , 'caption' ) ) ;
468+
469+ const viewCaption = mapper . toViewElement ( modelCaption ) ;
470+
471+ writer . setAttribute ( 'id' , 'custom-id-123' , viewCaption ) ;
472+ } ) ;
473+ } ) ;
474+
475+ _setModelData ( editor . model ,
476+ '<table><tableRow><tableCell><paragraph>xyz</paragraph></tableCell></tableRow><caption>Foo caption</caption></table>'
477+ ) ;
478+
479+ const viewFigure = editor . editing . view . document . getRoot ( ) . getChild ( 0 ) ;
480+ const viewTable = viewFigure . getChild ( 1 ) ;
481+ const viewCaption = viewFigure . getChild ( 2 ) ;
482+
483+ expect ( viewCaption . getAttribute ( 'id' ) ) . to . equal ( 'custom-id-123' ) ;
484+ expect ( viewTable . getAttribute ( 'aria-labelledby' ) ) . to . equal ( 'custom-id-123' ) ;
485+ } ) ;
325486 } ) ;
326487 } ) ;
327488} ) ;
@@ -387,10 +548,13 @@ describe( 'TableCaptionEditing - useCaptionElement = true', () => {
387548 '<table><tableRow><tableCell><paragraph>xyz</paragraph></tableCell></tableRow><caption>Foo caption</caption></table>'
388549 ) ;
389550
390- expect ( _getViewData ( view , { withoutSelection : true } ) ) . to . equal (
391- '<figure class="ck-widget ck-widget_with-selection-handle table" contenteditable="false">' +
551+ expect ( maskUIDs ( _getViewData ( view , { withoutSelection : true } ) ) ) . to . equal (
552+ '<figure ' +
553+ 'class="ck-widget ck-widget_with-selection-handle table" ' +
554+ 'contenteditable="false"' +
555+ '>' +
392556 '<div class="ck ck-widget__selection-handle"></div>' +
393- '<table>' +
557+ '<table aria-labelledby="masked-uid-1" >' +
394558 '<tbody>' +
395559 '<tr>' +
396560 '<td class="ck-editor__editable ck-editor__nested-editable" contenteditable="true"' +
@@ -399,8 +563,14 @@ describe( 'TableCaptionEditing - useCaptionElement = true', () => {
399563 '</td>' +
400564 '</tr>' +
401565 '</tbody>' +
402- '<caption class="ck-editor__editable ck-editor__nested-editable" ' +
403- 'contenteditable="true" data-placeholder="Enter table caption" role="textbox" tabindex="-1">' +
566+ '<caption ' +
567+ 'class="ck-editor__editable ck-editor__nested-editable" ' +
568+ 'contenteditable="true" ' +
569+ 'data-placeholder="Enter table caption" ' +
570+ 'id="masked-uid-1" ' +
571+ 'role="textbox" ' +
572+ 'tabindex="-1"' +
573+ '>' +
404574 'Foo caption' +
405575 '</caption>' +
406576 '</table>' +
@@ -410,3 +580,19 @@ describe( 'TableCaptionEditing - useCaptionElement = true', () => {
410580 } ) ;
411581 } ) ;
412582} ) ;
583+
584+ function maskUIDs ( str ) {
585+ const uidMap = new Map ( ) ;
586+
587+ return str . replace ( / c k - e d i t o r _ _ c a p t i o n _ e [ 0 - 9 a - f ] { 32 } / g, uid => {
588+ if ( ! uidMap . has ( uid ) ) {
589+ uidMap . set ( uid , maskedUID ( uidMap . size + 1 ) ) ;
590+ }
591+
592+ return uidMap . get ( uid ) ;
593+ } ) ;
594+ }
595+
596+ function maskedUID ( offset = 1 ) {
597+ return `masked-uid-${ offset } ` ;
598+ }
0 commit comments