Improve element.focus method (with options) or provide a better new focus method


#1

Managing focus and moving focus from one element to another is quite important for creating accessible webpages.

At the same time the provided focus function by the browser comes with annoying shortcomings making this task a pain in the ass.

There are in essence two major problems.

1. Implicit call to scrollIntoView

If an element gains focus, the :focus style is attached and after that it is checked, wether this element is currently in view. If not, it will be scrolled into view. This might sound as a nice built-in feature, it becomes annoying as soon as you are using animations. A demo of what happens can be seen here: https://jsfiddle.net/trixta/ab226ap9/.

As a side effect this behavior also always causes layout thrashing.

2. focus doesn’t work with visibility transitions

If an element is transitioned from visibility: hidden to visibility: visible. It is considered as visibility: hidden until the transition stops. Until then it can’t be focused. While I don’t understand why, I assume this can’t be changed for backcompat reasons anymore. However an element that is already in fact visible to the user and is transitioning to visible should be focusable. A demo that shows this problem can be seen here: https://jsfiddle.net/trixta/6racogpn/


#2

Why is visibility transitioned in your demos? The animation seems to render fine without it.

I’ve created a demo that used the "transitionend" event to set the focus on the input: https://jsfiddle.net/1f13pnwb/1/, and I’m not experiencing the scroll-into-view issue anymore.


#3

It would be an a11y fail, if it wouldn’t be visibility: hidden. For two simple reasons.

  1. If you tab through the page you can reach the input (and all other focusable content in this area) without seeing it. (And you might re-add content jumps.)
  2. You have introduced a control that shows/hides another content area, but for screenreader users this area is not hidden and therefore the control wouldn’t make sense.

You need visibility: hidden to hide it.

Yes, this is a workaround, but it doesn’t really help in real world situations. In those situations you have a generalized JS code and some CSS code. The JS should not know whether there is a CSS transition or what transition is used. The CSS dev might also want to use multiple effects and so on. Even your example doesn’t work properly, it focuses the input on every transitionend not only on showing. If another transition is in place you get multiple events, which transitionend is the right to use? What happens if no CSS transition is used and instead another JS script hooks into a triggered event to provide a JS animation and so on.

And here is where the pain starts. You need to write a quite amount of code, that re-checks the visual state of the element over and over again to decide, when it is save to focus it.

If you look into this project: https://github.com/medialize/ally.js. You will see some code that does just that, checking as soon as an element can be focused without creating a scroll problem and so on. It’s quite amount of code to realize this simple a11y focus management task.

At the end of this you will realize, that you can’t focus an input async on most mobile browser, you need to focus it inside of a trusted click or touch* event.


#4
  1. Implicit call to scrollIntoView

whatwg/html#94 is trying to get that behavior specified. whatwg/html#834 Is discussing adding and argument to Element#focus() to control the style of scrolling.

It’s quite amount of code to realize this simple a11y focus management task.

As far as I see, IntersectionObserver will help with reducing the footprint.

At the end of this you will realize, that you can’t focus an input async on most mobile browser, you need to focus it inside of a trusted click or touch* event.

do you have links to research on this?


#5

Thanks very helpful informations (as always).

About the mobile problem. I have not so much, but you can google those problems (https://www.google.de/search?client=opera&q=input+focus+iphone+keyboard&sourceid=opera&ie=UTF-8&oe=UTF-8).

Basically the restrictions with focus on mobile are similar to requestFullscreen. The reasoning is that it would be a usability problem if the software keyboard is shown automatically on page load in mobile browsers.

I’m trying to solve the problem different. Instead of waiting until the element is in viewport. I collect the scroll positions of all ancestors, focus the element and then restore the scroll positions.


#6

The reasoning is that it would be a usability problem if the software keyboard is shown automatically on page load in mobile browsers.

That’s the opposite of my experience. Focusing an input element would make the OSK (On Screen Keyboard) show up, which is why I generally prefer to focus the container element, rather than the first input element.

But what you’re saying is that the OSK doesn’t show up unless there is immediate user intent (expressed through a click/tap). This does not mean that the element doesn’t receive focus, though. It just means that the secondary action - showing the OSK when an element receives focus - is not as controllable as you want it to be.

I’m trying to solve the problem different. Instead of waiting until the element is in viewport. I collect the scroll positions of all ancestors, focus the element and then restore the scroll positions.

I’ve not pursued this idea in ally.js because of the following reasons:

  • I don’t know if any other component is listening to scroll events, which might get confused by this moot double-scroll action.
  • Asynchronous operations happening on focus that require to know the position of the element receiving focus might execute at a bad time and not recover the position because there’s no scroll event after the focused element became visible in the viewport.
  • I don’t know how things would behave in while the off-screen element has focus and the user does something. Granted, this is only the case for a few hundred milliseconds, but it’s still an unknown.

That said, I imagine this approach to be far more efficient than what ally.js does at the moment. Which is calculate the visible area of an element on every animation frame.


#7

What about the visibility: visible restriction?

I mean: Despite the fact that an element is visible (for the sighted user), because it is transitioning from hidden to visible, it is not focusable (for the visually impaired user).

As I can re-call correctly, earlier drafts including earlier implementations of the visibility transition forced us to write code like this:

.box {
    visibility: hidden;
    opacity: 0;
    transition: visibility 0 400ms, opacity 400ms;
}

.box.show {
    transition-delay: 0;
    opacity: 1;
    visibility: visible;
}

This was changed, so that developers can write visibility transitions more intuitive. But this change was only solved for sighted users and if developers want to write accessible components they still need to write CSS like this or do jumps in their JS focus implementations.

As a result the developer community were given an improved API to write inaccessible code more easily.


#8

I’m afraid I don’t understand what the problem is, other than developers potentially not properly understanding how transitions work.

To what change are you referring? The given CSS example is pretty much the way I’ve solved this problem in my apps today.


#9

The old specification said:

visibility: interpolated via a discrete step. The interpolation happens in real number space between 0 and 1, where 1 is “visible” and all other values are “hidden”.

This made the code above necessary to create a fadeOut effect.

The new spec says:

visibility: if one of the values is ‘visible’, interpolated as a discrete step where values of the timing function between 0 and 1 map to ‘visible’ and other values of the timing function (which occur only at the start/end of the transition or as a result of ‘cubic-bezier()’ functions with Y values outside of [0, 1]) map to the closer endpoint; if neither value is ‘visible’ then not interpolable.

This makes animating visibility in conjunction with other properties much simpler. You can write it this way:

.box {
    visibility: hidden;
    opacity: 0;
    transition: visibility 400ms, opacity 400ms;
}

.box.show {
    opacity: 1;
    visibility: visible;
}

But: If you get the computed visibility style during such a transition, you will see that the browser actually sets the element to visibility: hidden and does not allow focus at this time.

If you look around, you will see that 99% of all developers are using the later technique and not yours. And the change was introduced to make developer lives easier, but also introduced this a11y issue at the same time.


#10

Thanks for the pointer, I completely missed that…

At least Firefox, Safari, Chrome, IE10+ disagree with your statement, if this test is accurate: the element is visible throughout the transition. How did you come to your conclusion that this was broken?


#11

I have a lot to do, so I can’t look deeper into this. But here is my testcase saying it is hidden: https://jsfiddle.net/trixta/f8owaL10/


#12

I’ve changed my test-case to log the first few animation frames. The problem is the value isn’t transitioned right away, but 1 frame (Chrome) or 2 frames (Firefox) later. As far as I understand Starting of transitions there’s no mandate for the first animation frame to occur synchronously, so the browser’s behavior seems fine from a specification perspective. The transition-delay test-case shows immediate change. I have no idea if both notations should behave identically.


#13

I just spoke to @tabatkins and got confirmation that this is a “quality-of-implementation issue”. Both notations should behave the same way, i.e change the value of the visibility property synchronously. He suggest we start filing bugs against the various browsers.


#14

I’m pretty sure it’s not a quality of implementation issue. The jsbin you posted uses a transition duration of zero for visibility. No transition is generated when the duration is zero (see https://drafts.csswg.org/css-transitions/#starting) so you should not expect the same result as when actually running a transition.

When transitions are generated, as you note, transitions do not start immediately. That’s deliberate since it allows the browser to layerize content as needed before the animation starts so there is no jump at the start. As a result, the animation sits at progress 0 for one or two frames which, if you’re transitioning from ‘hidden’ to ‘visible’, produces visibility: hidden.


#15

(For my own reference, to clarify the comment about zero-duration transitions, I’m referring to this jsbin. The reason this one goes to ‘visible’ right away when hiding (unlike the other jsbin) is that it uses the transition timing defined by the destination style, which has no delay, hence a zero combined duration. You can see the difference in Firefox at least by opening the DevTools Inspector and using the Animations panel although you’ll need to select the second jsbin runner iframe as the target to debug. You’ll notice that no transition is generated for the hide case.)


#16

So if it is not an implementation issue of the CSS transitions. What can we do to make focusing in those cases less painful?

Redefine the focusing method, if an element is transitioned to visible await a focusable state and focus then?