DEV Community

Tommy Brunn
Tommy Brunn

Posted on • Originally published at Medium on

(Sort of) fixing autofocus in iOS Safari

This article was originally posted on Medium in 2016. It may be out of date at this point.

(Sort of) Fixing autofocus in iOS Safari

One of my colleagues is transitioning to the front-end team that I used to be a part of. To prepare him mentally for his journey into front-end development, I’ve been sending him a newsletter I call Front-End Hack of the Day. I’m posting them to Medium now for the world to enjoy

Imagine that you are building a form where you would like to help the user out by automatically focusing on the first input field.

<form>
    <input type="email" name="email" placeholder="foo@example.com" autofocus />
    <input type="password" name="password" />
</form>

You fire it up and try it out, and it works great. Ship it!

Sometime later, someone comes to you and says that it’s not working in iOS Safari. Off you go to caniuse.com and see that it is indeed not supported in that browser. Oh well, no big deal, we can fix that with a little bit of Javascript.

document.addEventListener('DOMContentLoaded', () => {
    Array.prototype.slice.call(document.querySelectorAll('input'))
        .filter((el) => el.hasAttribute('autofocus'))[0]
        .focus()
})

To your great surprise, you discover that this is not working either!

Turns out, Apple really doesn’t want you to focus input fields that the user hasn’t tapped on. Not only is the autofocus attribute not supported, but you have in fact made the situation worse!

What I feel like doing any time I’m developing for Safari

See, even manually calling focus on the element won’t work until the user has interacted with the page. If the input is inside an iframe and you try to call focus before the user has interacted, the keyboard opens, the input does not get focus, and typing on the keyboard does absolutely nothing. As an added bonus, if the viewport scrolled at all, the useless, blinking cursor will be displayed somewhere outside the input.

I haven’t been able to find any official resource explaining this decision, but I have to assume that it’s because focusing a field pops up the keyboard, which can be annoying if you didn’t have any intention of filling out the field.

Faking focus

We can’t fully emulate the autofocus behavior, but we can get pretty close.

Focusing a field does three things:

  1. Set focus styles
  2. Scroll the page so the field is somewhere in the middle of the viewport
  3. Open the keyboard

3 is the only thing that Apple has something against, but the other two can be implemented rather easily. I’m going to show you a very specific example, but for your own sanity, I suggest you come up with ways to abstract over this so that you don’t need to worry about whether you’re really focusing the field or if you’re just faking it.

Check out @klarna/ui on Github to see how we are doing it (programmaticFocus.js).

The first part is simple, to set the focus styles, just add a class with the same styling:

input:focus,
input.has-focus {
    border: green;
    color: black:
}

Scrolling the input into view is surprisingly simple, thanks to Element.scrollIntoView.

If we put it all together, we get something like:

const isIos = () => !!window.navigator.userAgent.match(/iPad|iPhone/i)

const hasInteracted = (() => {
    let interacted = false

    const onTouchStart = {
        interacted = true
        document.removeEventListener(onTouchStart)
    }
    document.addEventListener('touchstart', 'onTouchStart')

    return () => interacted
})()

const FOCUS_TYPES = {
    REAL: 'real',
    FAKE: 'fake'
}

const getFocusType = () => (hasInteracted() || !isIos())
    ? FOCUS_TYPES.REAL
    : FOCUS_TYPES.FAKE

const focus = (input) => {
    switch getFocusType() {
        case FOCUS_TYPES.REAL:
            return input.focus()
        case FOCUS_TYPES.FAKE:
            input.classList.add('has-focus')
            const onBlur = (input) => {
                input.classList.remove('has-focus')
                document.removeEventListener(onBlur)
            }
            input.addEventListener('blur', onBlur)
            input.scrollIntoView()
    }
}

document.addEventListener('DOMContentLoaded', () => {
    const autofocusedInput = Array.prototype.slice.call(
            document.querySelectorAll('input')
        ).filter((el) => el.hasAttribute('autofocus'))[0]

    focus(autofocusedInput)
})

What we end up with is a field that looks like it has focus and that is centered in the viewport. The keyboard won’t pop up, but that’s as close as we can get.

Hopefully this has been useful to you. The intention of these posts is not to show you some groundbreaking new front-end technique, but just to share some of the hacks my colleagues and I have had to implement over the years to deal with various browser quirks.

This was written mostly from memory, with some input from Xavier Via, so there may be some inaccuracies. Please leave a comment if I missed something.

Top comments (1)

Collapse
 
dperrymorrow profile image
David Morrow

When converting a NodeList to an Array, like you did here

Array.prototype.slice.call(document.querySelectorAll('input'))
Enter fullscreen mode Exit fullscreen mode

I find it a little simpler to use Array.from

Array.from(document.querySelectorAll('input'))
Enter fullscreen mode Exit fullscreen mode

Personal preference, and the same thing, but thought I would share.