On Firefox and the disabled input saga
The HTML spec has always been incomplete and leaves to the browsers to decide a lot of critical details on how particular elements should be rendered or function. In the not-so-distant past, this experimental nature led to a lot of developers having headaches and panic attacks when they needed to provide a similar user experience on different browsers. It also left a blazing trail of hacky code snippets, sprinkled with comments like “No idea why, but IE needs this”. But then the main browsers settled their wars, W3C improved the specs and bit by bit the state of web UX development became less of a nightmare. It’s 2020 now and surely browsers are working consistently with the main HTML elements, right?
The problem
Of course not! The particular UX inconsistency we hit stems from the decision of Mozilla developers to follow the spec much more closely than other browsers and deny users the ability to select text inside a disabled input. It can be argued that what Mozilla did is the right implementation and other browsers got it wrong, or that the spec was ill-conceived but all that is beyond the scope of this article. It has since been fixed (or broken, depending on your stance on the spec) in Firefox 76 – it only took about 18 years of discussions and rethinking to fix a bugreport. Even though it is now fixed, the approach we used might still be useful in similar cases, so please read on.
But there is a workaround
You might be eager to say that there is a simple workaround for this – don’t disable the input but make it readonly. This behaves consistently among browsers and is awesome and works just like disabled should… But it is not the same, and it especially is not in an Angular app:
- There is no attribute binding for readonly with FormControl or FormGroup for reactive forms
- You cannot make a select or button readonly
- Most important of all – readonly does not trickle down from form to fieldset to inputs and controls inside, while disabled does
Solution
The native differences between readonly and disabled were not a problem for us, but the issues with reactive form elements and the not inherited disabling of parent elements forced us to patch the problem. It’s not the prettiest of workarounds but the way can be outlined as:
- Make a component that handles input logic. In our case, we also needed a styling element with its own selector, so we implemented the solution there. However, you can also select all inputs and selects if you want it implicitly handled throughout your app. Whatever you decide, make sure to make the element a ControlValueAccessor as we want it to function as a regular input.
- Override the disabled setter – it is the one called when the disabled attribute is set on the native DOM element. The most important part is to remove the disabled attribute (as we don’t want the default browser behavior) and set the readonly native attribute accordingly. It’s also nice to set our own attribute (let’s say isDisabled) to keep the state of the input.
- Implement the setDisabledState method to trigger the disabled setter, so we don’t have to repeat code. This is the method called when the form API sets the disabled state.
- Set a CSS class based on our isDisabled attribute so we can style it closer to a disabled input and users can understand why the input is not functioning as expected.
If the outlined workaround is still vague, please follow along in this example until you find enlightenment.
Conclusion
Browsers and frameworks are nowadays doing an excellent job of giving developers and users a consistent experience. However, there will always be cases where a particular element is not functioning according to spec, or the project requirements really need to override the default behavior. Being able to easily do so allows the developer much more flexibility and time to focus on the more important parts of the project.