Search code examples
htmlcsssvelte

Why button using 'focus-within' is not working on iOS


I need a hidden delete button to appear and work when a input is focused using markup and CSS in Svelte.

I got it all working in browsers for OS X and Raspberry Pi OS (Chrome, Chromium, Safari and Firefox). Click here to see it.

The problem is that the button appears but is not working in any of my iOS browsers (Safari or Firefox). Nothing is happening when the delete button is clicked.

I've tried following:

Here is the markup...

<form>
    {#if $todos}
        {#each $todos as { data }, i}
            <div id="todo">
                <button on:click|preventDefault={remove(i + 1)}>🗑</button>
                <input
                    bind:value={data.name}
                    on:change={update(i + 1)}
                    size={data.name.length}
                    maxlength="35"
                />
            </div>
        {/each}
    {/if}
</form>

...and here is the styling...

<style>
    form,
    div {
        display: flex;
        flex-wrap: wrap;
    }

    input {
        border-style: none;
        font-size: 2vh;
    }

    input:focus {
        border-style: solid;
    }

    button {
        visibility: hidden;
        font-size: 2vh;
    }

    #todo:focus-within button {
        visibility: visible;
    }
</style>

form,
div {
  display: flex;
}

form {
  width: 100vw;
}

input {
  border-style: none;
}

input:focus {
  border-style: solid;
}

button {
  visibility: hidden;
}

#todo1:focus-within button {
  visibility: visible;
}

#todo2:focus-within button {
  visibility: visible;
}

#todo3:focus-within button {
  visibility: visible;
}
<html lang="en">

<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <title>Svelte + Node.js API</title>
</head>

<body>
  <h1>To Do</h1>
  <form>
    <div id="todo1">
      <button onclick="alert('Input deleted')">🗑</button>
      <input value="Try it out">
    </div>
    <div id="todo2">
      <button onclick="alert('Input deleted')">🗑</button>
      <input value="Fix the bug">
    </div>
    <div id="todo3">
      <button onclick="alert('Input deleted')">🗑</button>
      <input value="Celebrate">
    </div>
  </form>
</body>

</html>


Solution

  • With a little bit of debugging, it turns out, that by attempting to click on a button, you actually click on a <div> element itself:

    function setLogs(element) {
      element.addEventListener("focus", () => {
        console.log("focus", element);
      });
    
      element.addEventListener("blur", () => {
        console.log("blur", element);
      });
    
      for (const child of element.children)
        setLogs(child);
    }
    
    setLogs(document.body);
    button {
      visibility: hidden;
    }
    
    #todo:focus-within button {
      visibility: visible;
    }
    <div class="content">
      <h1>To Do</h1>
      <form onsubmit="return false">
        <div id="todo">
          <button onclick="console.log('Input deleted')">🗑</button>
          <input value="Try it out">
        </div>
      </form>
    </div>

    The reason for that is that focusing on one element after another one is a two-part event: first, you remove focus from the first element, then you set focus to the second one. By clicking on the area with the button, you remove the focus from the <input> element. Removed focus leads to :focus-within stop being applicable, and the button becomes hidden again. So, when the second part of the clicking event happens, there's no button there anymore, and the click is applied to the next element in the stack – the <div> element itself.