Latest news about Bitcoin and all cryptocurrencies. Your daily crypto news habit.
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
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.
“Design”
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?
- Cover the entire page with a shadow.
- Place a dialog(window, lightbox or task) above 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.
Best practices
MDN has a perfect article about building accesseble dialog boxes. It explain WHAT YOU have to do, but not explaining — HOW.
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?
- http://www.material-ui.com/#/components/dialog? No focus management.
- https://semantic-ui.com/modules/modal.html — Nope.
- https://react.semantic-ui.com/modules/modal — Nope
- https://atlaskit.atlassian.com/components/modal-dialog — Nope
- https://rambler-digital-solutions.github.io/rambler-ui/#/components/Popup?_k=vs348d — Nope.
- https://ant.design/components/modal/ — 🤷♂
- http://blueprintjs.com/docs/#core/components/dialog — 🤷♂
- ….this list is infinite….
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.
Focus trap
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 is an Trap. I found only emulation.
The worse thing you can do.
How react-focus-trap works.
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?
How to lock a Focus
There is 3.5 good ways to do it.
The best way
- Move all elements from body into div with tabIndex=-1.
- By tabindex is not working for chilren, so you might set negative tabIndex to all the elements with a script, or just set inert, a new html property to disable inteactions with a whole DOM-tree… which is not supported anywhere, yet (but you can use polifills).
- By the time you can set pointer-events: none, and disable user-select on top node. Just to be sure.
- Move modal out of that div and gave a positive tabIndex.
- And least but not least — you should set aria-hidden to the Div, to not let the screen reader to read anything outside the modal
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
The smart way
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
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.
The Focus Lock
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
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.
TabIndex and the Prisoner of Azkaban
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: 1. find common parent of modal and document.activeElement 2. get all tabbable element inside common parent. 3. 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:
- Focus outside Modal
- Focus inside Modal
- Focus outside Modal
- Yet again inside..
Neat :) Most of inert realizations does ignore tabIndex at all :)
Conclusion
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, our it will steal ANY focus)
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 :)
PS!
You still have to hide “everything else” with aria-hidden. Trap-lock cant do it for you.
You also have to wrap focus trap with element with role=’dialog’, and yet again you have to do it by yourself.
PPS:
So, you have 2 solutions
- block everything except the Modal (using a inert).
- Or to apply a reverse force and “move” everything into the Modal (using a trap).
In both cases do not forget about aria-hidden.
A good article you should read about building accessible dialogs from Google IO:
Building better accessibility primitives
So this is React-Aria, the modals that “just works”.
This is React-focus-lock, a jail for your focus.
This is Component with Single Responsibility, and you can use it to “fix” ANY modal you are using.
And this one is for Vue.js (10 lines of code, sweet!)
PS: And dont forget about pure vanilla dom-focus-lock. Which will use focusIn event :)
It’s a (focus) Trap! was originally published in Hacker Noon on Medium, where people are continuing the conversation by highlighting and responding to this story.
Disclaimer
The views and opinions expressed in this article are solely those of the authors and do not reflect the views of Bitcoin Insider. Every investment and trading move involves risk - this is especially true for cryptocurrencies given their volatility. We strongly advise our readers to conduct their own research when making a decision.