Skip to the code
cf.

Show password toggle

EasyDOM

The page has a sign-in form with a password field and a button for showing the password. The button starts hidden, because it can't work without JavaScript:

<form id="sign-in">
  <label for="password">Password</label>
  <input id="password" name="password" type="password" autocomplete="current-password">
  <button id="show-password" type="button" aria-controls="password" hidden>Show password</button>
  <button>Sign in</button>
</form>

Write setupPasswordToggle(input, button). It shows the button by removing its hidden.

Clicking the button shows the password: it sets the input's type to "text" and the button's aria-pressed to "true". Clicking it again hides the password: type goes back to "password", and aria-pressed to "false".

On setup, set aria-pressed to match the input: "true" if its type is already "text", and "false" otherwise.

Keep the button's text as it is. aria-pressed makes the button a toggle button, and screen readers read its name followed by its state, pressed or not pressed. A name that also changed, say to Hide password, would contradict the state.

If the input is in a form, hide the password again when that form is submitted, without stopping the submission. Browsers can save what's typed in a text field as an autocomplete suggestion, and a password must never become one. An input outside any form works the same, with no form to listen to.

setupPasswordToggle(document.getElementById("password"), document.getElementById("show-password"));
// the button appears, with aria-pressed="false"
// click it: the password shows as text, and aria-pressed is "true"
// submit the form: the field is a password field again before the form is sent

What the tests check

  1. shows the button on setup
    const button = document.getElementById("show-password");
    setupPasswordToggle(document.getElementById("password"), button);
    expect(button.hidden).toBe(false);
    
  2. starts with aria-pressed false while the password is hidden
    const button = document.getElementById("show-password");
    setupPasswordToggle(document.getElementById("password"), button);
    expect(button.getAttribute("aria-pressed")).toBe("false");
    
  3. starts pressed when the input already shows its text
    const input = document.getElementById("password");
    const button = document.getElementById("show-password");
    input.type = "text";
    setupPasswordToggle(input, button);
    expect(button.getAttribute("aria-pressed")).toBe("true");
    fire(button, "click");
    expect(input.type).toBe("password");
    expect(button.getAttribute("aria-pressed")).toBe("false");
    
  4. clicking shows the password and presses the button
    const input = document.getElementById("password");
    const button = document.getElementById("show-password");
    setupPasswordToggle(input, button);
    fire(button, "click");
    expect(input.type).toBe("text");
    expect(button.getAttribute("aria-pressed")).toBe("true");
    
  5. clicking again hides the password and unpresses the button
    const input = document.getElementById("password");
    const button = document.getElementById("show-password");
    setupPasswordToggle(input, button);
    fire(button, "click");
    fire(button, "click");
    expect(input.type).toBe("password");
    expect(button.getAttribute("aria-pressed")).toBe("false");
    fire(button, "click");
    expect(input.type).toBe("text");
    
  6. keeps the button's text the same
    const button = document.getElementById("show-password");
    setupPasswordToggle(document.getElementById("password"), button);
    fire(button, "click");
    expect(button.textContent).toBe("Show password");
    fire(button, "click");
    expect(button.textContent).toBe("Show password");
    
  7. hides the password again when the form is submitted, without stopping it
    const input = document.getElementById("password");
    const button = document.getElementById("show-password");
    setupPasswordToggle(input, button);
    fire(button, "click");
    const event = fire(document.getElementById("sign-in"), "submit");
    expect(input.type).toBe("password");
    expect(button.getAttribute("aria-pressed")).toBe("false");
    expect(event.defaultPrevented).toBe(false);
    
  8. works for an input outside any form
    const input = document.getElementById("password");
    const button = document.getElementById("show-password");
    document.body.append(input, button);
    setupPasswordToggle(input, button);
    fire(button, "click");
    expect(input.type).toBe("text");
    expect(button.getAttribute("aria-pressed")).toBe("true");
    

Learn more

solution.jsSaved in this browser

Run tests runs every test on your code, here in your browser. The problem is solved when they all pass.

Ctrl + Enter runs the tests (⌘ + Enter on a Mac).