Commit 9e283414 authored by Michael Telgkamp's avatar Michael Telgkamp Committed by Benni Mack
Browse files

[FEATURE] Make context menu usable via keyboard

The context menu shows on right click and pressing the context
menu button as well as Shift+F10 where available. Inside the
menu you are able to navigate using the arrow keys, the home
and end keys. Items are activated with ENTER or SPACE key.

Using the ESC key you can hide the current context menu.

Resolves: #89496
Releases: master
Change-Id: Icb10bdb927d1891785e82929e05e90475a846f08
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/66258


Tested-by: default avatarTYPO3com <noreply@typo3.com>
Tested-by: Oliver Bartsch's avatarOliver Bartsch <bo@cedev.de>
Tested-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
Reviewed-by: Oliver Bartsch's avatarOliver Bartsch <bo@cedev.de>
Reviewed-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
parent b7893863
......@@ -15,6 +15,7 @@ div#contentMenu1 {
min-width: 150px;
&-item {
margin-bottom: 0;
cursor: pointer;
padding: 5px;
border-bottom-color: transparent;
......@@ -35,6 +36,18 @@ div#contentMenu1 {
width: 100%;
border-bottom-color: $gray-light;
}
&:hover,
&:focus {
color: $list-group-link-hover-color;
background-color: $list-group-hover-bg;
}
&:focus {
outline: 1px auto Highlight;
outline: 1px auto -webkit-focus-ring-color;
outline-offset: -3px;
}
}
&-item-icon {
......
......@@ -48,6 +48,7 @@ class ContextMenu {
private mousePos: MousePosition = {X: null, Y: null};
private delayContextMenuHide: boolean = false;
private record: ActiveRecord = {uid: null, table: null};
private eventSources: Element[] = [];
/**
* @param {MenuItem} item
......@@ -61,9 +62,9 @@ class ContextMenu {
attributesString += ' ' + k + '="' + v + '"';
}
return '<a class="list-group-item"'
return '<li role="menuitem" class="list-group-item" tabindex="-1"'
+ ' data-callback-action="' + item.callbackAction + '"'
+ attributesString + '><span class="list-group-item-icon">' + item.icon + '</span> ' + item.label + '</a>';
+ attributesString + '><span class="list-group-item-icon">' + item.icon + '</span> ' + item.label + '</li>';
}
/**
......@@ -105,7 +106,6 @@ class ContextMenu {
if ($me.prop('onclick') && e.type === 'click') {
return;
}
e.preventDefault();
this.show(
$me.data('table'),
......@@ -113,6 +113,7 @@ class ContextMenu {
$me.data('context'),
$me.data('iteminfo'),
$me.data('parameters'),
e.target
);
});
......@@ -128,9 +129,13 @@ class ContextMenu {
* @param {string} context Context of the item
* @param {string} enDisItems Items to disable / enable
* @param {string} addParams Additional params
* @param {Element} eventSource Source Element
*/
private show(table: string, uid: number, context: string, enDisItems: string, addParams: string): void {
private show(table: string, uid: number, context: string, enDisItems: string, addParams: string, eventSource: Element = null): void {
this.record = {table: table, uid: uid};
// fix: [tabindex=-1] is not focusable!!!
const focusableSource = eventSource.matches('a, button, [tabindex]') ? eventSource : eventSource.closest('a, button, [tabindex]');
this.eventSources.push(focusableSource);
let parameters = '';
......@@ -158,7 +163,7 @@ class ContextMenu {
* @param {string} parameters Parameters sent to the server
*/
private fetch(parameters: string): void {
let url = TYPO3.settings.ajaxUrls.contextmenu;
const url = TYPO3.settings.ajaxUrls.contextmenu;
(new AjaxRequest(url)).withQueryArguments(parameters).get().then(async (response: AjaxResponse): Promise<any> => {
const data: MenuItems = await response.resolve();
if (typeof response !== 'undefined' && Object.keys(response).length > 0) {
......@@ -171,7 +176,7 @@ class ContextMenu {
* Fills the context menu with content and displays it correctly
* depending on the mouse position
*
* @param {Array<MenuItem>} items The data that will be put in the menu
* @param {MenuItems} items The data that will be put in the menu
* @param {number} level The depth of the context menu
*/
private populateData(items: MenuItems, level: number): void {
......@@ -181,9 +186,9 @@ class ContextMenu {
if ($obj.length && (level === 0 || $('#contentMenu' + (level - 1)).is(':visible'))) {
const elements = this.drawMenu(items, level);
$obj.html('<div class="list-group">' + elements + '</div>');
$obj.html('<ul class="list-group">' + elements + '</ul>');
$('a.list-group-item', $obj).on('click', (event: JQueryEventObject): void => {
$('li.list-group-item', $obj).on('click', (event: JQueryEventObject): void => {
event.preventDefault();
const $me = $(event.currentTarget);
......@@ -205,9 +210,114 @@ class ContextMenu {
}
this.hideAll();
});
$('li.list-group-item', $obj).on('keydown', (event: JQueryEventObject): void => {
const $currentItem = $(event.currentTarget);
switch (event.key) {
case 'Down': // IE/Edge specific value
case 'ArrowDown':
this.setFocusToNextItem($currentItem.get(0));
break;
case 'Up': // IE/Edge specific value
case 'ArrowUp':
this.setFocusToPreviousItem($currentItem.get(0));
break;
case 'Right': // IE/Edge specific value
case 'ArrowRight':
if ($currentItem.hasClass('list-group-item-submenu')) {
this.openSubmenu(level, $currentItem);
} else {
return; // allow default behaviour of right key
}
break;
case 'Home':
this.setFocusToFirstItem($currentItem.get(0));
break;
case 'End':
this.setFocusToLastItem($currentItem.get(0));
break;
case 'Enter':
case 'Space':
$currentItem.click();
break;
case 'Esc': // IE/Edge specific value
case 'Escape':
case 'Left': // IE/Edge specific value
case 'ArrowLeft':
this.hide('#' + $currentItem.parents('.context-menu').first().attr('id'));
break;
case 'Tab':
this.hideAll();
break;
default:
return; // return to allow default keypress behaviour
}
// if not returned yet, prevent the default action of the event.
event.preventDefault();
});
$obj.css(this.getPosition($obj)).show();
// focus the first element on creation to enable keyboard shortcuts
$('li.list-group-item[tabindex=-1]', $obj).first().focus();
}
}
private setFocusToPreviousItem(currentItem: HTMLElement): void {
let previousItem = this.getItemBackward(currentItem.previousElementSibling);
if (!previousItem) {
previousItem = this.getLastItem(currentItem);
}
previousItem.focus();
}
private setFocusToNextItem(currentItem: HTMLElement): void {
let nextItem = this.getItemForward(currentItem.nextElementSibling);
if (!nextItem) {
nextItem = this.getFirstItem(currentItem);
}
nextItem.focus();
}
private setFocusToFirstItem(currentItem: HTMLElement): void {
let firstItem = this.getFirstItem(currentItem);
if (firstItem) {
firstItem.focus();
}
}
private setFocusToLastItem(currentItem: HTMLElement): void {
let lastItem = this.getLastItem(currentItem);
if (lastItem) {
lastItem.focus();
}
}
/**
* Returns passed element if it is a menu item, if not checks the previous elements until one is found.
*/
private getItemBackward(element: Element): HTMLElement | null {
while (element &&
(!element.classList.contains('list-group-item') || (element.getAttribute('tabindex') !== '-1'))) {
element = element.previousElementSibling;
}
return <HTMLElement>element;
}
/**
* Returns passed element if it is a menu item, if not checks the previous elements until one is found.
*/
private getItemForward(item: Element): HTMLElement | null {
while (item &&
(!item.classList.contains('list-group-item') || (item.getAttribute('tabindex') !== '-1'))) {
item = item.nextElementSibling;
}
return <HTMLElement>item;
}
private getFirstItem(item: Element): HTMLElement | null {
return this.getItemForward(item.parentElement.firstElementChild);
}
private getLastItem(item: Element): HTMLElement | null {
return this.getItemBackward(item.parentElement.lastElementChild);
}
/**
......@@ -215,14 +325,24 @@ class ContextMenu {
* @param {JQuery} $item
*/
private openSubmenu(level: number, $item: JQuery): void {
this.eventSources.push($item[0]);
const $obj = $('#contentMenu' + (level + 1)).html('');
$item.next().find('.list-group').clone(true).appendTo($obj);
$obj.css(this.getPosition($obj)).show();
$('.list-group-item[tabindex=-1]',$obj).first().focus();
}
private getPosition($obj: JQuery): {[key: string]: string} {
let x = this.mousePos.X;
let y = this.mousePos.Y;
let x = 0, y = 0;
let source = this.eventSources[this.eventSources.length - 1]
if (source) {
const boundingRect = source.getBoundingClientRect();
x = boundingRect.right;
y = boundingRect.top;
} else {
x = this.mousePos.X;
y = this.mousePos.Y;
}
const dimsWindow = {
width: $(window).width() - 20, // saving margin for scrollbars
height: $(window).height(),
......@@ -235,8 +355,8 @@ class ContextMenu {
};
const relative = {
X: this.mousePos.X - $(document).scrollLeft(),
Y: this.mousePos.Y - $(document).scrollTop(),
X: x - $(document).scrollLeft(),
Y: y - $(document).scrollTop(),
};
// adjusting the Y position of the layer to fit it into the window frame
......@@ -273,20 +393,20 @@ class ContextMenu {
*/
private drawMenu(items: MenuItems, level: number): string {
let elements: string = '';
for (let item of Object.values(items)) {
for (const item of Object.values(items)) {
if (item.type === 'item') {
elements += ContextMenu.drawActionItem(item);
} else if (item.type === 'divider') {
elements += '<a class="list-group-item list-group-item-divider"></a>';
elements += '<li role="separator" class="list-group-item list-group-item-divider"></li>';
} else if (item.type === 'submenu' || item.childItems) {
elements += '<a class="list-group-item list-group-item-submenu">'
elements += '<li role="menuitem" aria-haspopup="true" class="list-group-item list-group-item-submenu" tabindex="-1">'
+ '<span class="list-group-item-icon">' + item.icon + '</span> '
+ item.label + '&nbsp;&nbsp;<span class="fa fa-caret-right"></span>'
+ '</a>';
+ '</li>';
const childElements = this.drawMenu(item.childItems, 1);
elements += '<div class="context-menu contentMenu' + (level + 1) + '" style="display:none;">'
+ '<div class="list-group">' + childElements + '</div>'
+ '<ul role="menu" class="list-group">' + childElements + '</ul>'
+ '</div>';
}
}
......@@ -331,9 +451,13 @@ class ContextMenu {
(): void => {
if (!this.delayContextMenuHide) {
$(obj).hide();
const source = this.eventSources.pop();
if (source) {
$(source).focus();
}
}
},
500,
500
);
}
......
......@@ -10,4 +10,4 @@
*
* The TYPO3 project - inspiring people to share!
*/
var __importDefault=this&&this.__importDefault||function(t){return t&&t.__esModule?t:{default:t}};define(["require","exports","jquery","TYPO3/CMS/Core/Ajax/AjaxRequest","./ContextMenuActions","TYPO3/CMS/Core/Event/ThrottleEvent"],(function(t,e,i,n,s,o){"use strict";i=__importDefault(i);class l{constructor(){this.mousePos={X:null,Y:null},this.delayContextMenuHide=!1,this.record={uid:null,table:null},this.storeMousePositionEvent=t=>{this.mousePos={X:t.pageX,Y:t.pageY},this.mouseOutFromMenu("#contentMenu0"),this.mouseOutFromMenu("#contentMenu1")},this.initializeEvents()}static drawActionItem(t){const e=t.additionalAttributes||{};let i="";for(const t of Object.entries(e)){const[e,n]=t;i+=" "+e+'="'+n+'"'}return'<a class="list-group-item" data-callback-action="'+t.callbackAction+'"'+i+'><span class="list-group-item-icon">'+t.icon+"</span> "+t.label+"</a>"}static within(t,e,i){const n=t.offset();return i>=n.top&&i<n.top+t.height()&&e>=n.left&&e<n.left+t.width()}static initializeContextMenuContainer(){if(0===i.default("#contentMenu0").length){const t='<div id="contentMenu0" class="context-menu"></div><div id="contentMenu1" class="context-menu" style="display: block;"></div>';i.default("body").append(t)}}initializeEvents(){i.default(document).on("click contextmenu",".t3js-contextmenutrigger",t=>{const e=i.default(t.currentTarget);e.prop("onclick")&&"click"===t.type||(t.preventDefault(),this.show(e.data("table"),e.data("uid"),e.data("context"),e.data("iteminfo"),e.data("parameters")))}),new o("mousemove",this.storeMousePositionEvent.bind(this),50).bindTo(document)}show(t,e,i,n,s){this.record={table:t,uid:e};let o="";void 0!==t&&(o+="table="+encodeURIComponent(t)),void 0!==e&&(o+=(o.length>0?"&":"")+"uid="+e),void 0!==i&&(o+=(o.length>0?"&":"")+"context="+i),void 0!==n&&(o+=(o.length>0?"&":"")+"enDisItems="+n),void 0!==s&&(o+=(o.length>0?"&":"")+"addParams="+s),this.fetch(o)}fetch(t){let e=TYPO3.settings.ajaxUrls.contextmenu;new n(e).withQueryArguments(t).get().then(async t=>{const e=await t.resolve();void 0!==t&&Object.keys(t).length>0&&this.populateData(e,0)})}populateData(e,n){l.initializeContextMenuContainer();const o=i.default("#contentMenu"+n);if(o.length&&(0===n||i.default("#contentMenu"+(n-1)).is(":visible"))){const l=this.drawMenu(e,n);o.html('<div class="list-group">'+l+"</div>"),i.default("a.list-group-item",o).on("click",e=>{e.preventDefault();const o=i.default(e.currentTarget);if(o.hasClass("list-group-item-submenu"))return void this.openSubmenu(n,o);const l=o.data("callback-action"),a=o.data("callback-module");o.data("callback-module")?t([a],t=>{t[l].bind(o)(this.record.table,this.record.uid)}):s&&"function"==typeof s[l]?s[l].bind(o)(this.record.table,this.record.uid):console.log("action: "+l+" not found"),this.hideAll()}),o.css(this.getPosition(o)).show()}}openSubmenu(t,e){const n=i.default("#contentMenu"+(t+1)).html("");e.next().find(".list-group").clone(!0).appendTo(n),n.css(this.getPosition(n)).show()}getPosition(t){let e=this.mousePos.X,n=this.mousePos.Y;const s=i.default(window).width()-20,o=i.default(window).height(),l=t.width(),a=t.height(),u=this.mousePos.X-i.default(document).scrollLeft(),c=this.mousePos.Y-i.default(document).scrollTop();return o-a<c&&(c>a?n-=a-10:n+=o-a-c),s-l<u&&(u>l?e-=l-10:s-l-u<i.default(document).scrollLeft()?e=i.default(document).scrollLeft():e+=s-l-u),{left:e+"px",top:n+"px"}}drawMenu(t,e){let i="";for(let n of Object.values(t))if("item"===n.type)i+=l.drawActionItem(n);else if("divider"===n.type)i+='<a class="list-group-item list-group-item-divider"></a>';else if("submenu"===n.type||n.childItems){i+='<a class="list-group-item list-group-item-submenu"><span class="list-group-item-icon">'+n.icon+"</span> "+n.label+'&nbsp;&nbsp;<span class="fa fa-caret-right"></span></a>';i+='<div class="context-menu contentMenu'+(e+1)+'" style="display:none;"><div class="list-group">'+this.drawMenu(n.childItems,1)+"</div></div>"}return i}mouseOutFromMenu(t){const e=i.default(t);e.length>0&&e.is(":visible")&&!l.within(e,this.mousePos.X,this.mousePos.Y)?this.hide(t):e.length>0&&e.is(":visible")&&(this.delayContextMenuHide=!0)}hide(t){this.delayContextMenuHide=!1,window.setTimeout(()=>{this.delayContextMenuHide||i.default(t).hide()},500)}hideAll(){this.hide("#contentMenu0"),this.hide("#contentMenu1")}}return new l}));
\ No newline at end of file
var __importDefault=this&&this.__importDefault||function(t){return t&&t.__esModule?t:{default:t}};define(["require","exports","jquery","TYPO3/CMS/Core/Ajax/AjaxRequest","./ContextMenuActions","TYPO3/CMS/Core/Event/ThrottleEvent"],(function(t,e,s,i,n,o){"use strict";s=__importDefault(s);class a{constructor(){this.mousePos={X:null,Y:null},this.delayContextMenuHide=!1,this.record={uid:null,table:null},this.eventSources=[],this.storeMousePositionEvent=t=>{this.mousePos={X:t.pageX,Y:t.pageY},this.mouseOutFromMenu("#contentMenu0"),this.mouseOutFromMenu("#contentMenu1")},this.initializeEvents()}static drawActionItem(t){const e=t.additionalAttributes||{};let s="";for(const t of Object.entries(e)){const[e,i]=t;s+=" "+e+'="'+i+'"'}return'<li role="menuitem" class="list-group-item" tabindex="-1" data-callback-action="'+t.callbackAction+'"'+s+'><span class="list-group-item-icon">'+t.icon+"</span> "+t.label+"</li>"}static within(t,e,s){const i=t.offset();return s>=i.top&&s<i.top+t.height()&&e>=i.left&&e<i.left+t.width()}static initializeContextMenuContainer(){if(0===s.default("#contentMenu0").length){const t='<div id="contentMenu0" class="context-menu"></div><div id="contentMenu1" class="context-menu" style="display: block;"></div>';s.default("body").append(t)}}initializeEvents(){s.default(document).on("click contextmenu",".t3js-contextmenutrigger",t=>{const e=s.default(t.currentTarget);e.prop("onclick")&&"click"===t.type||(t.preventDefault(),this.show(e.data("table"),e.data("uid"),e.data("context"),e.data("iteminfo"),e.data("parameters"),t.target))}),new o("mousemove",this.storeMousePositionEvent.bind(this),50).bindTo(document)}show(t,e,s,i,n,o=null){this.record={table:t,uid:e};const a=o.matches("a, button, [tabindex]")?o:o.closest("a, button, [tabindex]");this.eventSources.push(a);let l="";void 0!==t&&(l+="table="+encodeURIComponent(t)),void 0!==e&&(l+=(l.length>0?"&":"")+"uid="+e),void 0!==s&&(l+=(l.length>0?"&":"")+"context="+s),void 0!==i&&(l+=(l.length>0?"&":"")+"enDisItems="+i),void 0!==n&&(l+=(l.length>0?"&":"")+"addParams="+n),this.fetch(l)}fetch(t){const e=TYPO3.settings.ajaxUrls.contextmenu;new i(e).withQueryArguments(t).get().then(async t=>{const e=await t.resolve();void 0!==t&&Object.keys(t).length>0&&this.populateData(e,0)})}populateData(e,i){a.initializeContextMenuContainer();const o=s.default("#contentMenu"+i);if(o.length&&(0===i||s.default("#contentMenu"+(i-1)).is(":visible"))){const a=this.drawMenu(e,i);o.html('<ul class="list-group">'+a+"</ul>"),s.default("li.list-group-item",o).on("click",e=>{e.preventDefault();const o=s.default(e.currentTarget);if(o.hasClass("list-group-item-submenu"))return void this.openSubmenu(i,o);const a=o.data("callback-action"),l=o.data("callback-module");o.data("callback-module")?t([l],t=>{t[a].bind(o)(this.record.table,this.record.uid)}):n&&"function"==typeof n[a]?n[a].bind(o)(this.record.table,this.record.uid):console.log("action: "+a+" not found"),this.hideAll()}),s.default("li.list-group-item",o).on("keydown",t=>{const e=s.default(t.currentTarget);switch(t.key){case"Down":case"ArrowDown":this.setFocusToNextItem(e.get(0));break;case"Up":case"ArrowUp":this.setFocusToPreviousItem(e.get(0));break;case"Right":case"ArrowRight":if(!e.hasClass("list-group-item-submenu"))return;this.openSubmenu(i,e);break;case"Home":this.setFocusToFirstItem(e.get(0));break;case"End":this.setFocusToLastItem(e.get(0));break;case"Enter":case"Space":e.click();break;case"Esc":case"Escape":case"Left":case"ArrowLeft":this.hide("#"+e.parents(".context-menu").first().attr("id"));break;case"Tab":this.hideAll();break;default:return}t.preventDefault()}),o.css(this.getPosition(o)).show(),s.default("li.list-group-item[tabindex=-1]",o).first().focus()}}setFocusToPreviousItem(t){let e=this.getItemBackward(t.previousElementSibling);e||(e=this.getLastItem(t)),e.focus()}setFocusToNextItem(t){let e=this.getItemForward(t.nextElementSibling);e||(e=this.getFirstItem(t)),e.focus()}setFocusToFirstItem(t){let e=this.getFirstItem(t);e&&e.focus()}setFocusToLastItem(t){let e=this.getLastItem(t);e&&e.focus()}getItemBackward(t){for(;t&&(!t.classList.contains("list-group-item")||"-1"!==t.getAttribute("tabindex"));)t=t.previousElementSibling;return t}getItemForward(t){for(;t&&(!t.classList.contains("list-group-item")||"-1"!==t.getAttribute("tabindex"));)t=t.nextElementSibling;return t}getFirstItem(t){return this.getItemForward(t.parentElement.firstElementChild)}getLastItem(t){return this.getItemBackward(t.parentElement.lastElementChild)}openSubmenu(t,e){this.eventSources.push(e[0]);const i=s.default("#contentMenu"+(t+1)).html("");e.next().find(".list-group").clone(!0).appendTo(i),i.css(this.getPosition(i)).show(),s.default(".list-group-item[tabindex=-1]",i).first().focus()}getPosition(t){let e=0,i=0,n=this.eventSources[this.eventSources.length-1];if(n){const t=n.getBoundingClientRect();e=t.right,i=t.top}else e=this.mousePos.X,i=this.mousePos.Y;const o=s.default(window).width()-20,a=s.default(window).height(),l=t.width(),u=t.height(),r=e-s.default(document).scrollLeft(),c=i-s.default(document).scrollTop();return a-u<c&&(c>u?i-=u-10:i+=a-u-c),o-l<r&&(r>l?e-=l-10:o-l-r<s.default(document).scrollLeft()?e=s.default(document).scrollLeft():e+=o-l-r),{left:e+"px",top:i+"px"}}drawMenu(t,e){let s="";for(const i of Object.values(t))if("item"===i.type)s+=a.drawActionItem(i);else if("divider"===i.type)s+='<li role="separator" class="list-group-item list-group-item-divider"></li>';else if("submenu"===i.type||i.childItems){s+='<li role="menuitem" aria-haspopup="true" class="list-group-item list-group-item-submenu" tabindex="-1"><span class="list-group-item-icon">'+i.icon+"</span> "+i.label+'&nbsp;&nbsp;<span class="fa fa-caret-right"></span></li>';s+='<div class="context-menu contentMenu'+(e+1)+'" style="display:none;"><ul role="menu" class="list-group">'+this.drawMenu(i.childItems,1)+"</ul></div>"}return s}mouseOutFromMenu(t){const e=s.default(t);e.length>0&&e.is(":visible")&&!a.within(e,this.mousePos.X,this.mousePos.Y)?this.hide(t):e.length>0&&e.is(":visible")&&(this.delayContextMenuHide=!0)}hide(t){this.delayContextMenuHide=!1,window.setTimeout(()=>{if(!this.delayContextMenuHide){s.default(t).hide();const e=this.eventSources.pop();e&&s.default(e).focus()}},500)}hideAll(){this.hide("#contentMenu0"),this.hide("#contentMenu1")}}return new a}));
\ No newline at end of file
......@@ -252,7 +252,8 @@ define(['jquery',
this.identifier,
$node.data('context'),
$node.data('iteminfo'),
$node.data('parameters')
$node.data('parameters'),
node
);
}
};
......@@ -266,7 +267,8 @@ define(['jquery',
this.identifier,
$node.data('context'),
$node.data('iteminfo'),
$node.data('parameters')
$node.data('parameters'),
node
);
}
};
......
.. include:: ../../Includes.txt
======================================================
Feature: #89496: Make context menu usable via keyboard
======================================================
See :issue:`89496`
Description
===========
The context menus are now usable via keyboard. Pressing Shift+F10
will open the context menu. Now it is also possible to use arrows, home and end keys
in order to navigate through the menu. Besides that, using enter and
space keys will active items or open submenus.
This change follows the best practices as described in WAI-ARIA Authoring Practices 1.1,
see the `W3 document`_ for further reading.
.. _W3 document: https://www.w3.org/TR/wai-aria-practices-1.1/#keyboard-interaction-12
Impact
======
Added :html:`tabindex`, :html:`role`, and :html:`aria-*` attributes to context menus
as advised in WAI-ARIA Authoring Practices 1.1. Screen readers are now
able to recognize the context menu properly.
.. index:: Backend, JavaScript, ext:backend
......@@ -115,7 +115,7 @@ class PageTreeFilterCest
$inlineMnGroupIcon = '#identifier-0_92 > g.node-icon-container';
$I->click($inlineMnGroupIcon);
$I->canSeeElement('#contentMenu0');
$I->click('Delete', '#contentMenu0');
$I->click('[data-callback-action="deleteRecord"]', '#contentMenu0');
// don't use $modalDialog->clickButtonInDialog due to too low timeout
$modalDialog->canSeeDialog();
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment