I'm developing an app with Angular and Semantic-UI. The app should be accessible, this means it should be compliant with WCAG 2.0.
To reach this purpose the modals should keep focus within the dialog and prevents users from going outside or move with "tabs" between elements of the page that lays under the modal.
I have found some working examples, like the following:
JQuery dialog: https://jqueryui.com/dialog/#modal-confirmation
dialog HTML 5.1 element: https://demo.agektmr.com/dialog
ARIA modal dialog example:
http://w3c.github.io/aria-practices/examples/dialog-modal/dialog.html
(that I have reproduced on Plunker)
Here is my try to create an accessible modal with Semantic-UI: https://plnkr.co/edit/HjhkZg
As you can see I used the following attributes:
role="dialog"
aria-labelledby="modal-title"
aria-modal="true"
But they don't solve my issue. Do you know any way to make my modal keeping focus and lose it only when user click on cancel/confirm buttons?
There is currently no easy way to achieve this. The inert attribute was proposed to try to solve this problem by making any element with the attribute and all of it's children inaccessible. However, adoption has been slow and only recently did it land in Chrome Canary behind a flag.
Another proposed solution is making a native API that would keep track of the modal stack, essentially making everything not currently the top of the stack inert. I'm not sure the status of the proposal, but it doesn't look like it will be implemented any time soon.
So where does that leave us?
Unfortunately without a good solution. One solution that is popular is to create a query selector of all known focusable elements and then trap focus to the modal by adding a keydown event to the last and first elements in the modal. However, with the rise of web components and shadow DOM, this solution can no longer find all focusable elements.
If you always control all the elements within the dialog (and you're not creating a generic dialog library), then probably the easiest way to go is to add an event listener for keydown on the first and last focusable elements, check if tab or shift tab was used, and then focus the first or last element to trap focus.
If you're creating a generic dialog library, the only thing I have found that works reasonably well is to either use the inert polyfill or make everything outside of the modal have a tabindex=-1.
var nonModalNodes;
function openDialog() {
var modalNodes = Array.from( document.querySelectorAll('dialog *') );
// by only finding elements that do not have tabindex="-1" we ensure we don't
// corrupt the previous state of the element if a modal was already open
nonModalNodes = document.querySelectorAll('body *:not(dialog):not([tabindex="-1"])');
for (var i = 0; i < nonModalNodes.length; i++) {
var node = nonModalNodes[i];
if (!modalNodes.includes(node)) {
// save the previous tabindex state so we can restore it on close
node._prevTabindex = node.getAttribute('tabindex');
node.setAttribute('tabindex', -1);
// tabindex=-1 does not prevent the mouse from focusing the node (which
// would show a focus outline around the element). prevent this by disabling
// outline styles while the modal is open
// #see https://www.sitepoint.com/when-do-elements-take-the-focus/
node.style.outline = 'none';
}
}
}
function closeDialog() {
// close the modal and restore tabindex
if (this.type === 'modal') {
document.body.style.overflow = null;
// restore or remove tabindex from nodes
for (var i = 0; i < nonModalNodes.length; i++) {
var node = nonModalNodes[i];
if (node._prevTabindex) {
node.setAttribute('tabindex', node._prevTabindex);
node._prevTabindex = null;
}
else {
node.removeAttribute('tabindex');
}
node.style.outline = null;
}
}
}
The different "working examples" do not work as expected with a screenreader.
They do not trap the screenreader visual focus inside the modal.
For this to work, you have to :
Set the aria-hidden attribute on any other nodes
disable keyboard focusable elements inside those trees (links using tabindex=-1, controls using disabled, ...)
The jQuery :focusable pseudo selector can be useful to find focusable elements.
add a transparent layer over the page to disable mouse selection.
or you can use the css pointer-events: none property when the browser handles it with non SVG elements, not in IE
This focus-trap plugin is excellent at making sure that focus stays trapped inside of dialogue elements.
It sounds like your problem can be broken down into 2 categories:
focus on dialog box
Add a tabindex of -1 to the main container which is the DOM element that has role="dialog". Set the focus to the container.
wrapping the tab key
I found no other way of doing this except by getting the tabbable elements within the dialog box and listening it on keydown. When I know the element in focus (document.activeElement) is the last one on the list, I make it wrap
"focus" events can be intercepted in the capture phase, so you can listen for them at the document.body level, squelch them before they reach the target element, and redirect focus back to a control in your modal dialog. This example assumes a modal dialog with an input element gets displayed and assigned to the variable currDialog:
document.body.addEventListener("focus", (event) => {
if (currDialog && !currDialog.contains(event.target)) {
event.preventDefault();
event.stopPropagation();
currDialog.querySelector("input").focus();
}
}, {capture: true});
You may also want to contain such a dialog in a fixed-position, clear (or low-opacity) backdrop element that takes up the full screen in order to capture and suppress mouse/pointer events, so that no browser feedback (hover, etc.) occurs that could give the user the impression that the background is active.
Don't use any solution requiring you to look up "tabbable" elements. Instead, use keydown and either click events or a backdrop in an effective manor.
(Angular1)
See Asheesh Kumar's answer at https://stackoverflow.com/a/31292097/1754995 for something similar to what I am going for below.
(Angular2-x, I haven't done Angular1 in a while)
Say you have 3 components: BackdropComponent, ModalComponent (has an input), and AppComponent (has an input, the BackdropComponent, and the ModalComponent). You display BackdropComponent and ModalComponent with the correct z-index, both are currently displayed/visible.
What you need to do is have a general window.keydown event with preventDefault() to stop all tabbing when the backdrop/modal component is displayed. I recommend you put that on a BackdropComponent. Then you need a keydown.tab event with stopPropagation() to handle tabbing for the ModalComponent. Both the window.keydown and keydown.tab could probably be in the ModalComponent but there is purpose in a BackdropComponent further than just modals.
This should prevent clicking and tabbing to the AppComponent input and only click or tab to the ModalComponent input [and browser stuffs] when the modal is shown.
If you don't want to use a backdrop to prevent clicking, you can use use click events similarly to the keydown events described above.
Backdrop Component:
#Component({
selector: 'my-backdrop',
host: {
'tabindex': '-1',
'(window:keydown)': 'preventTabbing($event)'
},
...
})
export class BackdropComponent {
...
private preventTabbing(event: KeyboardEvent) {
if (event.keyCode === 9) { // && backdrop shown?
event.preventDefault();
}
}
...
}
Modal Component:
#Component({
selector: 'my-modal',
host: {
'tabindex': '-1',
'(keydown.tab)': 'onTab($event)'
},
...
})
export class ModalComponent {
...
private onTab(event: KeyboardEvent) {
event.stopPropagation();
}
...
}
Here's my solution. It traps Tab or Shift+Tab as necessary on first/last element of modal dialog (in my case found with role="dialog"). Eligible elements being checked are all visible input controls whose HTML may be input,select,textarea,button.
$(document).on('keydown', function(e) {
var target = e.target;
var shiftPressed = e.shiftKey;
// If TAB key pressed
if (e.keyCode == 9) {
// If inside a Modal dialog (determined by attribute role="dialog")
if ($(target).parents('[role=dialog]').length) {
// Find first or last input element in the dialog parent (depending on whether Shift was pressed).
// Input elements must be visible, and can be Input/Select/Button/Textarea.
var borderElem = shiftPressed ?
$(target).closest('[role=dialog]').find('input:visible,select:visible,button:visible,textarea:visible').first()
:
$(target).closest('[role=dialog]').find('input:visible,select:visible,button:visible,textarea:visible').last();
if ($(borderElem).length) {
if ($(target).is($(borderElem))) {
return false;
} else {
return true;
}
}
}
}
return true;
});
we can use the focus trap npm package.
npm i focus-trap
This might help someone who is looking for solution in Angular.
Step 1: Add keydown event on dialog component
#HostListener('document:keydown', ['$event'])
handleTabKeyWInModel(event: any) {
this.sharedService.handleTabKeyWInModel(event, '#modal_id', this.elementRef.nativeElement, 'input,button,select,textarea,a,[tabindex]:not([tabindex="-1"])');
}
This will filters the elements which are preseneted in the Modal dialog.
Step 2: Add common method to handle focus in shared service (or you can add it in your component as well)
handleTabKeyWInModel(e, modelId: string, nativeElement, tagsList: string) {
if (e.keyCode === 9) {
const focusable = nativeElement.querySelector(modelId).querySelectorAll(tagsList);
if (focusable.length) {
const first = focusable[0];
const last = focusable[focusable.length - 1];
const shift = e.shiftKey;
if (shift) {
if (e.target === first) { // shift-tab pressed on first input in dialog
last.focus();
e.preventDefault();
}
} else {
if (e.target === last) { // tab pressed on last input in dialog
first.focus();
e.preventDefault();
}
}
}
}
}
Now this method will take the modal dialog native element and start evaluate on every tab key. Finally we will filter the event on first and last so that we can focus on appropriate elements (on first after last element tab click and on last shift+tab event on first element).
Happy Coding.. :)
I used one of the methods suggested by Steven Lambert, namely, listening to keydown events and intercepting "tab" and "shift+tab" keys. Here's my sample code (Angular 5):
import { Directive, ElementRef, Attribute, HostListener, OnInit } from '#angular/core';
/**
* This directive allows to override default tab order for page controls.
* Particularly useful for working around the modal dialog TAB issue
* (when tab key allows to move focus outside of dialog).
*
* Usage: add "custom-taborder" and "tab-next='next_control'"/"tab-prev='prev_control'" attributes
* to the first and last controls of the dialog.
*
* For example, the first control is <input type="text" name="ctlName">
* and the last one is <button type="submit" name="btnOk">
*
* You should modify the above declarations as follows:
* <input type="text" name="ctlName" custom-taborder tab-prev="btnOk">
* <button type="submit" name="btnOk" custom-taborder tab-next="ctlName">
*/
#Directive({
selector: '[custom-taborder]'
})
export class CustomTabOrderDirective {
private elem: HTMLInputElement;
private nextElemName: string;
private prevElemName: string;
private nextElem: HTMLElement;
private prevElem: HTMLElement;
constructor(
private elemRef: ElementRef
, #Attribute('tab-next') public tabNext: string
, #Attribute('tab-prev') public tabPrev: string
) {
this.elem = this.elemRef.nativeElement;
this.nextElemName = tabNext;
this.prevElemName = tabPrev;
}
ngOnInit() {
if (this.nextElemName) {
var elems = document.getElementsByName(this.nextElemName);
if (elems && elems.length && elems.length > 0)
this.nextElem = elems[0];
}
if (this.prevElemName) {
var elems = document.getElementsByName(this.prevElemName);
if (elems && elems.length && elems.length > 0)
this.prevElem = elems[0];
}
}
#HostListener('keydown', ['$event'])
onKeyDown(event: KeyboardEvent) {
if (event.key !== "Tab")
return;
if (!event.shiftKey && this.nextElem) {
this.nextElem.focus();
event.preventDefault();
}
if (event.shiftKey && this.prevElem) {
this.prevElem.focus();
event.preventDefault();
}
}
}
To use this directive, just import it to your module and add to Declarations section.
I've been successful using Angular Material's A11yModule.
Using your favorite package manager install these to packages into your Angular app.
**"#angular/material": "^10.1.2"**
**"#angular/cdk": "^10.1.2"**
In your Angular module where you import the Angular Material modules add this:
**import {A11yModule} from '#angular/cdk/a11y';**
In your component HTML apply the cdkTrapFocus directive to any parent element, example: div, form, etc.
Run the app, tabbing will now be contained within the decorated parent element.
For jquery users:
Assign role="dialog" to your modal
Find first and last interactive element inside the dialog modal.
Check if current target is one of them(depending on shift key is
pressed or not).
If target element is one of first or last interactive element of the
dialog, return false
Working code sample:
//on keydown inside dialog
$('.modal[role=dialog]').on('keydown', e => {
let target = e.target;
let shiftPressed = e.shiftKey;
// If TAB is pressed
if (e.keyCode === 9) {
// Find first and last element in the ,modal-dialog parent.
// Elements must be interactive i.e. visible, and can be Input/Select/Button/Textarea.
let first = $(target).closest('[role=dialog]').find('input:visible,select:visible,button:visible,textarea:visible').first();
let last = $(target).closest('[role=dialog]').find('input:visible,select:visible,button:visible,textarea:visible').last();
let borderElem = shiftPressed ? first : last //border element on the basis of shift key pressed
if ($(borderElem).length) {
return !$(target).is($(borderElem)); //if target is border element , return false
}
}
return true;
});
I read through most of the answers, while the package focus-trap seems like a good option. #BenVida shared a very simple VanillaJS solution here in another Stack Overflow post.
Here is the code:
const container=document.querySelector("_selector_for_the_container_")
//optional: needed only if the container element is not focusable already
container.setAttribute("tabindex","0")
container.addEventListener("focusout", (ev)=>{
if (!container.contains(ev.relatedTarget)) container.focus()
})
So I have a cell table with click event selection model working fine.
I later found out you can press UP and DOWN arrows to get the highlighted row to change, but the awful thing is you have to press Space for it to actually call the SelectionChangeEvent.
I am trying to cheat my way a little, by catching the UP and DOWN events and firing the SPACE event. Sadly it doesn't work :(
Here is my code any help would be appreciated!
table.setKeyboardSelectionPolicy(KeyboardSelectionPolicy.ENABLED);
table.sinkEvents(Event.KEYUP);
table.sinkEvents(Event.KEYDOWN);
table.sinkEvents(32);
table.addHandler(new KeyUpHandler(){
#Override
public void onKeyUp(KeyUpEvent event)
{
System.out.println(event.getNativeKeyCode());
if(event.getNativeEvent().getKeyCode() == 40)
{
// down is pressed
int i = rows.getFilterList().indexOf(selectionModel.getLastSelectedObject())+1;
if(i >= 0 && i < rows.getFilterList().size())
{
// selectionModel.setSelected(selectionModel.getLastSelectedObject(), false);
// selectionModel.setSelected(rows.getFilterList().get(i), true);
// SelectionChangeEvent.fire(selectionModel);
System.out.println("firing native event space");
DomEvent.fireNativeEvent(Document.get().createKeyUpEvent(false, false, false, false, 32), table);
}
}
else if(event.getNativeEvent().getKeyCode() == 38)
{
// up is pressed
int i = rows.getFilterList().indexOf(selectionModel.getLastSelectedObject())-1;
if(i >= 0 && i < rows.getFilterList().size())
{
// selectionModel.setSelected(selectionModel.getLastSelectedObject(), false);
// selectionModel.setSelected(rows.getFilterList().get(i), true);
// SelectionChangeEvent.fire(selectionModel);
System.out.println("firing native event space");
DomEvent.fireNativeEvent(Document.get().createKeyUpEvent(false, false, false, false, 32), table);
}
}
}
}, KeyUpEvent.getType());
32 is assumingly the NativeEvent for space, my console prints something like:
40
firing native event space
32
so assumingly the event type 32 is being called for the object table.
I check if the object is selected, because on the right hand side of the screen I have additional information being pulled out from a list, since the cell table doesn't show all the information. I want it so when I press UP and DOWN the RHS information changes and I dont have to press SPACE to prompt the info change
Ideally you would poke into the selection internals. Specifically the DefaultKeyboardSelectionHandler is the default implementation of keyboard navigation and the DefaultSelectionEventManager is the default implementation of selection actions using spacebar/clicks (they are both CellPreviewEvent.Handlers).
Anyway, you can force the keyboard selection to be bound to the underlying SelectionModel by using setKeyboardSelectionPolicy(KeyboardSelectionPolicy.BOUND_TO_SELECTION). It should be fine for your use case. Much like what is done for the CellList showcase sample (the selection API is the same across cell widgets).
Is there a way to add an Event.ContextClick to a Gui.Window in a Unity Editor script?
The following is my context menu method that I've tried calling from both OnGUI() and my window's WindowFunction (call sites denoted below as "site: no luck"). I have not been able to get the "Success" message to show up unless I'm right clicking directly in the main editor window. If I right click in any of the Gui.Windows I have created, the ContextClick event doesn't show up.
void OnStateContextMenu(){
Event evt = Event.current;
// Ignore anything but contextclicks
if(evt.type != EventType.ContextClick)return;
Debug.Log("Success");
// Add generic menu at context point
GenericMenu menu = new GenericMenu();
menu.AddItem (new GUIContent ("AddState"),false,AddState,evt.mousePosition);
menu.ShowAsContext ();
evt.Use();
}
And the call site(s):
void doWindow(int id){
// OnStateContextMenu(); //site1: no luck
GUI.DragWindow();
}
void OnGUI(){
OnStateContextMenu(); //site2: no luck here either
BeginWindows();
wndRect = GUI.Window(0,wndRect,doWindow,"StateWnd");
EndWindows();
}
Update
For reference, green area responds to right-click, red area does not. But I want it to. The right-click menu I've created has specific actions I only want visible if the mouse cursor right clicks inside one of my windows, the 'Hello' in the image. Note: Ignore the button, right click doesn't work anywhere inside that window.
This might not directly answer your question but should be able to help
You are trying to achieve a rightclick function inside your red box( as shown in picute )
I had a sort alike question a while back but it was not for a rightclick but for a mouseover
so i figured this might be able to help you
string mouseover; // first of i created a new string
if (GUI.Button (new Rect (100,100,200,200),new GUIContent("Load game", "MouseOverOnButton0") ,menutexture ))
{
//added a mousoveronbutton command to my GUIcontent
executestuff();
}
buttoncheck();
}
void buttoncheck()
{
mouseover = GUI.tooltip;
if(mouseover == "MouseOverOnButton0")
{
GUI.Box(new Rect(380,45,235,25),"Not a implemented function as of yet ");
}
}
this code made a new gui box the moment the mouse hitted the box.
If you created the hello in a seperate box you could use this
if(mouseover == hello)
{
if(rightclick == true)
{
execute the stuff you want
}
}
or something like that. Hope this helps a bit atleast
UPDATE
To obtain the rightclick event you will have to use the
if(Event.current.button == 1 && Event.current.isMouse)
You have to place this in the OnGUI to work properly
this way you first trigger the in box part, then check for a right click and execute the stuff you want.
I'm trying to toggle two functions. When user clicks the pause button, the input fields are disabled, the label is text is changed to grey and the button changes to a different image. I thought I could use .toggle(), but I can't get the two functions to work either -- only the first one function runs (pauseEmailChannel();), not both on toggle click. I found the even/odd clicks detection script here on SO, but that is not "toggling" these two functions on the click event. My code may be ugly code, but I'm still learning and wanted to show how I am thinking -- right or wrong. At any rate, can someone give me a solution to how to do this? I didn't think it would be too difficult but I'm stuck. Thanks.
HTML
jQuery
$(".btn_pause").click(function(){
var count = 0;
count++;
//even odd click detect
var isEven = function(num) {
return (num % 2 === 0) ? true : false;
};
// on odd clicks do this
if (isEven(count) === false) {
pauseEmailChannel();
}
// on even clicks do this
else if (isEven(count) === true) {
restoreEmailChannel();
}
});
// when user clicks pause button - gray out/disable
function pauseEmailChannel(){
$("#channel-email").css("color", "#b1b1b1");
$("#notify-via-email").attr("disabled", true);
$("#pause-email").removeClass("btn_pause").addClass("btn_disable-pause");
}
// when user clicks cancel button - restore default
function restoreEmailChannel(){
$("#channel-email").css("color", "#000000");
$("#notify-email").attr("disabled", false);
$("#pause-email").removeClass("disable-pause").addClass("btn_pause");
$("input[value='email']").removeClass("btn_disable-remove").addClass("btn_remove");
}
try this code. It should work fine, except that I could make a mistake when it is even and when odd, but that should be easy to fix.
$(".btn_pause").click(function(){
var oddClick = $(this).data("oddClick");
$(this).data("oddClick", !oddClick);
if(oddClick) {
pauseEmailChannel();
}
else {
restoreEmailChannel();
}
});
The count variable is initialized and set to 0 every time .btn_pause is clicked. You need to move the variable to a higher scope.
For example,
$(".btn_pause").each(function(){
var count = 0;
$(this).click(function(){
count++;
...
});
});
In this way count is initialized only once and it is accessible in the click event handler.
As an alternative way you can also use:
$(".btn_pause").each(function(){
var count = 0;
$(this).click(function(){
[restoreEmailChannel, pauseEmailChannel][count = 1 - count]();
});
});
If the previous construct was too abstract, a more verbose one will look like this:
$(".btn_pause").each(function(){
/* Current element in the array to be executed */
var count = 0;
/* An array with references to Functions */
var fn = [pauseEmailChannel, restoreEmailChannel];
$(this).click(function(){
/* Get Function from the array and execute it */
fn[count]();
/* Calculate next array element to be executed.
* Notice this expression will make "count" loop between the values 0 and 1.
*/
count = 1 - count;
});
});
I don't see an event in the Bing map API v7 that will surface a double click event. Is there a way to do this? Assuming there isn't native support that I missed, I think I will have to write my own double click handler with a timer.
I had also a problem with the click-events. In facts, the normal click-event also fires during a double-click event. That is why I had to implement my own double-click handler. My approach can be translated to the rigth click, because I am only using the single-click event, which is also available for the right mouse button.
//Set up my Handler (of course every object can be the target)
Microsoft.Maps.Events.addHandler(map, 'click', Click);
//count variable, that counts the amount of clicks that belong together
myClick=0;
//A click fires this function
function click (e)
{
//If it is the first click of a "series", than start the timeout after which the clicks are handled
if (myClick == 0)
{
//Target have to be buffered
target= e;
//accumulate the clicks for 200ms and react afterwards
setTimeout("reaction(target)", 200);
}
//count the clicks
myClick = myClick+1;
}
//At the end of timeout check how often a click has been performed and react
function reaction(e)
{
if (myClick==1)
{
alert("Single Click!");
}
else (myClick==2)
{
alert("Double click!");
}
else (myClick==3)
{
alert("Tripple click");
}
//reset ClickCount to zero for the next clicks
myClick = 0;
}
Moreover it might be interesting to remove the standart double-click behaviour of Bing-Maps, to zoom in. This can be realized by the following code:
Microsoft.Maps.Events.addHandler(map, 'dblclick', function(e){
e.handled = true;
});
If you only use double click event
Microsoft.Maps.Events.addHandler(this.map, 'dblclick', functionHandler)
should solve the problem