checkbox onChange in <legend> inside disabled <fieldset> not firing in Firefox with React

Viewed 307

The following works in Firefox 80.0 and Chromium 84.0.4147.89.

const fieldset = document.getElementById("fieldset");
const toggle = document.getElementById("toggle");

toggle.addEventListener("change", () => {
  if (fieldset.hasAttribute("disabled")) {
    fieldset.removeAttribute("disabled");
  } else {
    fieldset.setAttribute("disabled", true);
  }
});
<form action="#">
  <fieldset id="fieldset">
    <legend>
      <label>toggle <input id="toggle" type="checkbox" /></label>
    </legend>
    <input />
  </fieldset>
</form>

However, when I try and do something similar in React, it doesn't work in Firefox. The onChange event appears not to fire once the fieldset is disabled.

function App() {
  const [disabled, setDisabled] = React.useState(false);
  const toggleDisabled = React.useCallback(() => {
    setDisabled((disabled) => !disabled);
  }, []);

  return (
    <form action="#">
      <fieldset disabled={disabled}>
        <legend>
          <label>
            toggle <input onChange={toggleDisabled} type="checkbox" />
          </label>
        </legend>
        <input />
      </fieldset>
    </form>
  );
}

ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.0/umd/react-dom.production.min.js"></script>

<div id="root"></div>

The MDN fieldset article says:

[when the fieldset element is disabled] Note that form elements inside the <legend> element won't be disabled.

W3C (Example B) and WHATWG also mention that the contents of the <legend> should not be disabled.

So I believe that the two pieces of code should behave in the same way: I should be able to toggle the disabled attribute using the checkbox.

How can I achieve the same effect in React on Firefox 80.0+?

1 Answers

This behavior seems expected by the browser according to 2 lines earlier in that paragraph.

disabled If this Boolean attribute is set, all form controls that are descendants of the <fieldset>, are disabled, meaning they are not editable and won't be submitted along with the . They won't receive any browsing events, like mouse clicks or focus-related events. By default browsers display such controls grayed out. Note that form elements inside the element won't be disabled. - MDN

It seems that Firefox does exactly what it describes that it should do.
A solution here would be to just place the <input> outside of the fieldset so that it won't be affected by the disabled property.


Edit

Your comment about listening for the events higher up in the DOM got me to thinking. How about if you bypass that by binding your own event listener with good ol' addEventListener, in combination with the useRef and useEffect hooks. Create a reference to the checkbox and to listen for the change event after the first render. Now the event is listening to the input itself.
This "workaround" does seem to work in FF.

function App() {
  const [disabled, setDisabled] = React.useState(false);
  const toggleElement = React.useRef(null)
  const toggleDisabled = React.useCallback(() => {
    setDisabled(disabled => !disabled);
  }, []);
  
  React.useEffect(() => {
    toggleElement.current.addEventListener('change', toggleDisabled);
  }, []);

  return (
    <form action="#">
      <fieldset disabled={disabled}>
        <legend>
          <label>
          toggle <input ref={toggleElement} type="checkbox" />
      </label>
        </legend>
        <input />
      </fieldset>
    </form>
  );
}

ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.0/umd/react-dom.production.min.js"></script>

<div id="root"></div>

Related