Ngx-bootstrap: fix(modal): focus coming out of Modal after pressing Tab key

Created on 27 Mar 2017  Â·  59Comments  Â·  Source: valor-software/ngx-bootstrap

Hi,

I am using ng2 bootstrap,version 1.3.3 and your modal component in one of our projects.

Our requirement is to trap the focus inside the modal however the focus is moving outside of the modal on tabbing. It then stays outside the modal until it traverses all the tabs on the page.

Is there a way you could keep the focus trapped inside the modal until the modal is closed? Or in other words it should continue looping with focus being within the modal.

_Expected behavior – Focus should be trapped inside the modal window and on closing it focus should move to the element which triggers the modal window._

This is critical interaction for AX users. Appreciate any support you can provide with regards to this.

7.0.0 BREAKING CHANGE WIP comp(modal) hard (weeks) feat-request accessibility

Most helpful comment

@31053 focus trap, will be added soon

All 59 comments

Have you tried using tabIndex as per bootstrap docs? (http://getbootstrap.com/javascript/#live-demo)

We are already using tabIndex as per bootstrap docs however it does not help.

The modal library needs to be updated to prevent the focus going out of modal until it is closed.

Pl refer to attached screenshot for your static modal code. In the example the focus comes out of the modal.

image

Hi

any update on this?

@GuskiS @valorkin any update on this ?

@yogsadafal we are writing new modal component loader, to resolve all modal issue existing at the moment :)

@valorkin thanks for the update, when can we expect the new modal component ?

This or next week, we will be testing to cover/close existing modal issue.
Without introducing breaking changes, it could be tricky :)

@valorkin please let us know if the new component is released or is there any progress on the issue.

Work in progress :)
It works already, polishing and testing at the moment

thanks for the update :+1:

Hi, any update on this?

@valorkin hey is there any progress on this issue? this was closed & open again. when can we expect it to be available for use?

@valorkin @GuskiS any update on this?

@yogsadafal there are will be one more release focused on datepicker next week
and we have a PR with side nav component and focus trap directive
but they are for ng v4+
so in couple of weeks it should be released

@valorkin : is the solution available in 1.9.1 version?
Also , can you please check with once #2526 for item no.1. Thank You.

Hi Any updates on the solution?

As I said before, this week we will have last v1 releases
Next week we will work on v2 release
Which will include focus trap feature

Thank You Valorkin. Appreciate your response. Awaiting for the trap feature within the modal.

Thanks again!

Hi Valorkin, Any updates? thanks.

Hi Valorkin, Any updates? Thanks in advance.

Hello Valorkin!

Would you be able to share a target closure date for this issue?

Regards
Ajay

Hey having the same issue, any updates?

Thanks!

Hi @valorkin

May I know when this fix will be available.

Thanks.

@valorkin Hi, any updates on this? Keep up the good work btw! :)

Hey, as you can see we closed more than 100 issues last week. We are hardening beta.8 to make release and start adding new features

@valorkin Hey, thanks for the reply. Good to hear you guys are working hard on beta.8. Thanks for the update anyways and thanks for the awesome work so far. :)

Hi guys any fix for this issue?

Hello, have you guys found a solution?

This is the example of bootstrap's modal.
https://getbootstrap.com/docs/4.0/components/modal/
It works fine.
How does it jail focus inside modal?

@31053 focus trap, will be added soon

Hi all, it appears there's quite a bit of discussion around this issue. We used https://github.com/GoogleChrome/inert-polyfill in a project as a workaround which has been fine so far. If the limitations don't work in your particular use case, there's also https://github.com/WICG/inert.

From the README section of the GoogleChrome/inert-polyfill#limitations section:

If these limitations do not work for your project, there is also a WICG polyfill, which uses MutationObserver to recursively walk HTML trees to clear tabIndex (clearing or setting to -1). This is more correct, but will incur a performance hit when inert is enabled or disabled. The GoogleChrome hosted polyfill simply overloads focus and related events to prevent focus.

@valorkin May I know the planned release date for "focus trap" issue?

@valorkin Any new updates? This issue has been open for over a year, and always coming _soon_

Hello, any update for this issue? Could you guys give me the solution for this :(

Hello, any update for this issue?

Hello, I've made my own directive to trap focus inside the modal window, it works fine for my circumstances, I know it's not the best code you've seen but it will do the trick... feel free to give me feed back

modal-focus.directive.ts.txt

@iMushi How do you use it?

@iMushi How do you use it?
Here is a working example

https://stackblitz.com/edit/ngx-bootstrap-u1ow9s

Would highly appreciate this feature as well, as it is crucial for barrier assessments (i.e. for motor handycapped users that cannot use the mouse).

@iMushi : it's good, I used focus-trap too, but it's got the bug like when you focus on the URL bar on your browser and press Tab button, the tab will escape from the modal too

no one please help me with this issues :(

@vn425282 just made an adjustment... let me know

Note: it only works with one modal at the time, like I said, I made this for my circumstances...

+1. We need this focusTrap ability as well.

@valorkin any update on this issue? We're considering moving to ng-bootstrap modal because of this issue but if a fix is coming soon we could wait a bit.

@valorkin we are also planning to move to ng-bootstrap, this issue was posted almost 2years ago & we are waiting for fix since then :( do we have any ETA??

@iMushi How do you use it?
Here is a working example

https://stackblitz.com/edit/ngx-bootstrap-u1ow9s

iMushi's directive seems to work well!

@valorkin Is there an update on this issue?

@silvl added to a list of priority tasks

@Domainv is there a rough timeline for the resolution of this issue? Unclear whether "priority tasks" means we should be on the lookout in the next few weeks or if it could be a while.

This is a bit of an ugly hack, but it works for me right now. This will only work for a single modal, so it will not work for multiple or stacked modals!

When a modal opens, it will attempt to set the aria-labledby attribute to the ID of the modal-title element, it it exists and if that element has an ID. It will do the same for the aria-describedby attribute and a .modal-subtitle element.

it will then set focus inside the modal, and they trap keyboard focus to be inside the modal.

When the modal is closed, it will undo everything.

export class AppComponent implements OnInit {
    constructor(@Inject('Window') private readonly window: Window,
                private readonly modalService: BsModalService) { }

    ngOnInit(): void {
        this.makeModalsAccessible();
    }

    private makeModalsAccessible(): void {
        let modalEl: null | HTMLElement = null;
        let keyboardEventFn: (e: KeyboardEvent) => void | undefined = undefined;

        this.modalService.onShown.subscribe(() => {
            modalEl = this.window.document.querySelector<HTMLElement>('body > .modal:last-of-type');
            if (modalEl) {
                const modalTitle = modalEl.querySelector('.modal-title');
                if (modalTitle && modalTitle.id) {
                    modalEl.setAttribute('aria-labeledby', modalTitle.id);
                }

                const modalSubtitle = modalEl.querySelector('.modal-subtitle');
                if (modalSubtitle && modalSubtitle.id) {
                    modalEl.setAttribute('aria-describedby', modalSubtitle.id);
                }

                //small delay is needed for some reason - needed when using with a real screenreader! 
                setTimeout(() => {
                    if (modalEl) {
                        modalEl.focus();
                        keyboardEventFn = this.trapKeyboardFocus(modalEl);
                    }
                }, 10);
            }
        });

        this.modalService.onHide.subscribe(() => {
            if (modalEl) {
                modalEl.blur();
                modalEl.removeEventListener('keydown', keyboardEventFn);
            }
        });
    }

    private trapKeyboardFocus(modalEl: HTMLElement): (e: KeyboardEvent) => void {
        const fn = (e: KeyboardEvent): void => {
            if (e.key === 'Tab') {
                //We re-select on every tab press so we can adapt to any added/removed focusable elements
                // tslint:disable-next-line: max-line-length
                const focusEls = modalEl.querySelectorAll<HTMLElement>('a[href]:not([disabled]), button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex="0"]');

                const firstFocusEl = focusEls[0];
                const lastFocusEl = focusEls[focusEls.length - 1];

                if (e.shiftKey) { //shift + tab
                    if (this.window.document.activeElement === firstFocusEl) {
                        lastFocusEl.focus();
                        e.preventDefault();
                    }
                } else { //Tab
                    if (this.window.document.activeElement === lastFocusEl) {
                        firstFocusEl.focus();
                        e.preventDefault();
                    }
                }
            }
        };

        modalEl.addEventListener('keydown', fn);
        return fn;
    }
}

Again, it's not perfect, and it's a very non-angular way of doing things... but it seems as if there is no other way to do this right now since the modal component of ngx-bootsrap feels a bit limited in this capacity. i hope the issue can be addressed so that we can just have a focus:true option just like the vanilla bootstrap framework that this is supposed to have feature parity with.

@valorkin @shailpatels Is there a timeline or any updates on a resolution for this issue? It has been open for a long time and impacting many people.

any news on this one? Not a fan of this hacky ways of solving this. I mean, we can always make directive that would solve this kind of issues but still.... would be great to have it out-of-the-box

@jgood044 Unfortunately we haven't found a good solution for our project either. The best way seems to be to manually intercept the tab event and force it to stay within the modal. The other method
is to prevent any element outside of the modal to be tab-able when the modal gets opened and then re-add everything when the modal gets closed but this is more of a hack although it is faster to implement.

To deal with this issue I just put tab traps to keep them between the first and last selectable elements in the modal's content.

<button type="button" name='first-button' (click)="firstAction()" attr-label='first button action' (keydown)="preventTabBack($event)">First button</button>
<other-stuff/>
<button type="button" name='last-button' (click)="lastButtonAction()" attr-label='last button action' (keydown)="preventTab($event)">Last button</button>

Then in my component:

preventTabBack(event, condition?) {
    UtilitiesHelper.preventTabBack(event, condition);
}
preventTab(event, condition?) {
    UtilitiesHelper.preventTab(event, condition);
}

And in my Util library:

// prevent SHIFT+TAB so we can't go back from FIRST tab-able element outside a modal dialog
// add this to the first tab-able element in template:  (keydown)="preventTabBack($event)"
// condition is optional for cases where a button may not be last focusable element, for instance an invalid form making submit button disabled
static preventTabBack(event, condition?) {
    if (condition == undefined || condition) {
        if (event.shiftKey && event.keyCode == 9) {
            //shift was down when tab was pressed
            event.preventDefault();
        }
    }
}
// prevent TAB so we can't go beyond LAST tab-able element outside a modal dialog
// add this to the last tab-able element in template:  (keydown)="preventTab($event)"
// condition is optional for cases where a button may not be last focusable element, for instance an invalid form making submit button disabled
static preventTab(event, condition?) {
    if (condition == undefined || condition) {
        if (!event.shiftKey && event.keyCode == 9) {
            //shift was NOT down when tab was pressed
            event.preventDefault();
        }
    }
}

Is this moving along? I see this was linked to a PR in February?

My company just identified this as an accessibility issue and I would like to avoid hacks/work-arounds

Any update on this issue fix?

We are facing this issue for a long time and expecting a proper fix for this rather than a hack or a work around for trapping the focus in the modal alone.

Also wanted to know if there was an update here, we're running into the same issue with complaints from clients. Is the final consensus to use one of the methods in the comments above?

Okay, I have an info about that
PR for that already exists, but, requires a lot of changes, due to Ivy, and also, even after that, it will be a huge breaking change for all users of ngx-bootstrap, so, it will be postponed to 7.0.0 major release.
When 7.0.0 will be released? I can't tell right now any timeline

Was this page helpful?
0 / 5 - 0 ratings

Related issues

phmello picture phmello  Â·  3Comments

hugonne picture hugonne  Â·  3Comments

tuoitrexuquang picture tuoitrexuquang  Â·  3Comments

ctrl-brk picture ctrl-brk  Â·  3Comments

tpiros picture tpiros  Â·  3Comments