Search code examples
javascripthtmldomsafariclient-side-validation

Detect if reportValidity() is currently showing the validityMessage on a DOM element?


Calling reportValidity() on in invalid field causes the validation message to show in a browser-dependent way.

However, calling reportValidity() again causes the validation message to disappear on Safari but not Firefox. Repeatedly calling it causes it to toggle, making cross-browser validation difficult.

In my specific case, I want to show the validation message on form submit and on input focus. When you submit the form and it is invalid, the event triggers reportValidity() on the form. However, this will focus the input that is invalid, triggering the reportValidity() again from my focus event, hiding the message that was just shown.

Is there a way to reliably show the message when you don't know if it's already showing? Does the browser provide an interface for detecting if the validity message is showing?

Example:

const input = document.getElementById("invalid")
input.addEventListener("focus", () => input.reportValidity())
const form = document.getElementById("form")
form.addEventListener("submit", (e) => { e.preventDefault(); form.reportValidity() })
<form action="#" id="form">
  <input type="number" max="5" value="10" name="invalid" id="invalid" autocomplete="off">
  <input type="submit" value="validate">
</form>

On Safari, this example will cause the validation message to hide when submitting the form. This is not the desired behavior.


Solution

  • You should file a bug at https://bugs.webkit.org/.

    The issue seems to be caused by the fact the browser will itself focus the <input> from the call to reportValidity(), causing that toggling. So one way to handle it is to listen to the invalid event and prevent it when it's already been called in the same task. Luckily, that event is fired synchronously and is cancellable, so it's relatively easy to handle it.

    const input = document.getElementById("invalid")
    input.addEventListener("focus", () => input.reportValidity())
    const form = document.getElementById("form")
    form.addEventListener("submit", (e) => { e.preventDefault(); form.reportValidity() })
    let reporting = false;
    input.addEventListener("invalid", (e) => {
      // debounce the reportValidity call
      if (reporting) { e.preventDefault(); }
      reporting = true;
      setTimeout(() => reporting = false);
    });
    input.addEventListener("focusin", (e) => {
      input.reportValidity();
    });
    <form action="#" id="form">
      <input type="number" max="5" value="10" name="invalid" id="invalid" autocomplete="off">
      <input type="submit" value="validate">
    </form>