It is a trap! We got your focus and will not let him out! These words you must hear every time you have opened a modal dialog. But you wont…
Modal dialog
, Lightbox
or Focused task
are an UI trick to let you do something “single” — upload a file, provide an URL, “Are you sure”, “Click a button to win $$$” and so on.
Usually, you shall not, and cannot not, do anything else — you are able to perform any actions only inside hightlited modal.
This is true to native modals, but not usually true for DOM implementations.
But, if you want to stylish your page, and your dialogs — you have to implement “modals” using the DOM API, HTML and CSS.
And to do it?
Simple and Usable. And here our story starts…
This is screen shot from react-aria-modal demo — a quite usable modal library, which will cover the one unusual thing.
Normally you cant click at elements “outside” — they are covered by shadow. But you can “Tab-out” from a modal.
In this case — modal is not very “modal”. React-aria-modal is fixing
it by using react-focus-trap
.
MDN has a perfect article about building accesseble dialog boxes. It explain WHAT YOU have to do, but not explaining — HOW.
Using the dialog role_The dialog role is used to mark up a DHTML based application dialog or window that separates content or UI from the…_developer.mozilla.org
In short: you have to manage keyboard focus. And next they describe how.
jQuery UI dialog — is a perfect solution. It passes all the tests.
But where to get a good component in React/Vue/Angular/non-jQuery world?
Ant is quite strange — it handles change focus by Tab, but allows to leave a Modal by Shift+Tab.
BluePrint.js works well, but will return focus only to autofocus element, or first element with tabIndex. Otherwise — will not.
They just are using improper way to manage a focus.
The is NO ideal solution. Hooray!
PS: Except jQuery. It just works.
Just to recall — I’v mention the react-aria-modal library. It can show a Modal. Correctly! And it uses react-focus-trap to manage focus.
react-focus-trap is a simple React component, which will not let your Focus
leave a boundaries of Modal
.
But then I checked the sources — I’v found that it is not a trap.
That’s A Trap.
I found only emulation. And emulation is the worse thing you can do.
It will attach global keyboard event listeners. On “Tab” it will collect all* tabbable elements and manually move focus from one to another.
By doing this it will control focus, and that is a goal.
But all* is not all. Tabbles does not include area elements, for example. And you should not emulate the browser bahevior.
How to do it in a better way?
There is 3.5 good ways to do it.
inert
, a new html property to disable inteactions with a whole DOM-tree… which is not supported anywhere, yet (but you can use polifills).Screen readers should also be trapped inside of the modal to prevent accidentally escaping. And aria-hidden on “everything” is the only way to achieve it.
This is native browser behavior, and anything except Modal will be untabbale in a real.
PS: Not everything — you still can tab-out from a page. May be you want it?
Keep tabbing within modal pane only_On my current project we have some modal panes that open up on certain actions. I am trying to get it so that when that…_stackoverflow.com
Just attach a handle to a last(and first) element, and handle Tab only on the “edges”
$(':tabbale:last').on('keydown', function (e) { if ($("this:focus") && (e.which == 9)) { e.preventDefault(); $(':input:first').focus(); }});
It will work, it will preserve browser behavior “between” edges… but will ignore tabIndex and element modifications.
This is better that “whole” emulation, but you should ignore this way.
The right way is not to emulate Tab, but to just not let him out.
Not let the focus out.
focus-trap-react is emulation, but react-focus-trap (a different one!) is not.
According to the sources react-focus-trap attaches a global listener to “focus” event.
Then something got focus it will check WHO. If focus is outside modal — it will be returned to the first element.
This solution is very close to the ideal, but it will always return a focus to the first element, and have no idea about tabIndex.
But anyway — this is the right way. Let the focus do anything inside modal, just lock it.
That means — if no good solution exists — it is time to create a new solution.
Lock and loaded…
First I planned to write an article about KISS principle, and explain the meaning of it by comparing react-focus-trap with react-focus-lock. But now I am unsure — it is will KISS-friendly or not..
But, to say the truth — this is not Focus Trap. And even not Focus-Lock. This is Focus Jail, or Focus Wall.
Focus is free inside, it just cant pass the Border Security (and Jon Snow)
React-focus-lock, the solution I’v build and I am going to talk about uses the last and not the best way to detect the focus change — onBlur/FocusOut event.
It is not as handy as FocusIn event, as long node will first lose focus, and then new component will get it — so you have to wait before check. But..
FocusOut is internal event, as long FocusIn is external.
If you can listen only on your own node — you should no it.
But the trickiest thing is to handle tabIndex _without_ emulation tabIndex.
I did not found any existing solution except focus-trap-react, which is not a solution, and yet again have to build my own. Algorithm is simple:
1. Remember the last focused item.2. On focusOut:
get all tabbable
elements inside the Modal3. Find the difference between last focused item and current.4. If diff(current-active)>1 -> return focus to the last node.5. If current < first node -> go to the last-by-order6. If current > last node -> go to the first-by-order7. If first < current < last -> move cursor to the nearest-in-dirrection
The most tricky one is #7. If you use tabIndex(dont do it!) you can have:
Neat :) Most of inert
realizations does ignore tabIndex at all :)
Anyway, I’v spent few hours on weekends and create a quite usable components, which can help you to lock the Pandora Box you might have.
Usage is simple:
<FocusLock disabled={disabled}> Something inside </FocusLock>
And you can use it for React(as react-focus-lock), or for Vue(as vue-focus-lock). And get a good experience in both cases.
Here is the demo: (lock is disabled by default, or it will steal ANY focus, forcing you to close browser tab :P)
Just wrap your modal with simple component, and you will become more WAI-ARIA compatible.
PS: And you will also get auto-focus out of the box :)
After a year of development *-Focus-lock could lock anything anywhere, supports “focus groups”, autodetects React 16 Portals, and provides some extra helper component for your service. It is still the “best” focus-locking library, based on the “right” principles.
You still have to hide “everything else” with aria-hidden. Lock can’t do it for you — it is not it’s job.
You also have to wrap focus trap with element with role=’dialog’
, and yet again you have to do it by yourself.
You have to make everything except you modal in-accessible. Un-reachable. Inert.
How to do it?
inert
attribute. Or React-Locky (or Dom-Locky) to just disable all iterations with the rest of the page.aria-hidden
to “hide it”.A good article you should read about building accessible dialogs from Google IO:
Building better accessibility primitives_As the sites we build increasingly become more app-like it's important that the platform keep up and give component…_robdodson.me
So this is React-Aria, the modals that “just works”.
react-aria-modal demo_A demo of react-aria-modal_davidtheclark.github.io
You just read an artile about React-focus-lock, a jail for your focus.
This is Component with Single Responsibility, to help you “fix” your modals
theKashey/react-focus-lock_react-focus-lock - It is a trap! A lock for a Focus._github.com
And this one is for Vue.js (10 lines of code, sweet!)
theKashey/vue-focus-lock_vue-focus-lock - It is a trap! A lock for a Focus._github.com
PS: And dont forget about pure vanilla dom-focus-lock. Which will use focusIn event :)
And don’t forget about another part of good modals — event isolation, and preventing body from scrolling, while modal is being opened.
How to train a your Scroll_So, JFYI, you just scrolled a page down to this place. You were able to do it. The problem is – sometimes you should…_hackernoon.com
How to fight the <body> scroll_First of all — WHY one have to fight the scroll?_medium.com