Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use the native beforeinput event if it's supported #11211

Open
ianstormtaylor opened this issue Oct 13, 2017 · 38 comments
Open

Use the native beforeinput event if it's supported #11211

ianstormtaylor opened this issue Oct 13, 2017 · 38 comments

Comments

@ianstormtaylor
Copy link

ianstormtaylor commented Oct 13, 2017

Do you want to request a feature or report a bug?

Improvement.

What is the current behavior?

Right now, the synthetic onBeforeInput event is being created based on two other events:

  • textInput when possible—which is in Webkit.
  • keypress as a fallback.

But these days in Chrome, Safari and Opera the spec'd beforeinput event is available and actually fires. And when it does, it includes other spec'd properties which can be extremely helpful:

  • inputType tells you whether the event is inserting text, replacing text, inserting a line break, etc.
  • getTargetRanges() tells you where the input is taking place in the DOM.

Right now this information isn't exposed, because even if the browser supports beforeinput, it's not being checked for.

What is the expected behavior?

Instead React should treat textInput as a slightly-preferred fallback for native beforeinput support, but add beforeinput as the true goal. So we'd end up with a fallback stack of:

  • beforeinput
  • textInput
  • keypress

Which guarantees that the nativeEvent will always be the most spec'd and have the most relevant information associated with it.


The beforeinput event's extra properties are critical in contenteditable situations, when you want to prevent the default browser behavior from firing but perform the logic on an internal model instead. (I'm looking to do this for Slate.)

Without that extra information you have to fallback to hackier behavior—allowing the event to occur, trying to parse the DOM for what the change was, then re-rendering to remove it, etc. I want to avoid this on the more modern browsers, because it results in reduced performance.


There is another situation that this fixes, which is that spellcheck right now doesn't trigger React's onBeforeInput handler, even though modern browsers fire the beforeinput event, because it's not being listened for right now.

@ianstormtaylor
Copy link
Author

Any word from any maintainers about this? Just curious what the opinion is. Thanks!

@aweary
Copy link
Contributor

aweary commented Jan 8, 2018

@ianstormtaylor do you know if there's any reliable way to detect if beforeinput is natively supported? It looks like logic we currently use for event support detection won't work in this case.

it includes other spec'd properties which can be extremely helpful

How hard do you think it would be to polyfill those spec'd properties for other browsers? We usually don't include event properties if we can't provide them consistently.

@ianstormtaylor
Copy link
Author

@aweary why is it that the current logic doesn't work? We use what seems like a similar detection method for it right now. (Although I have to be honest, I'm not sure if it's as reliable as needed by React with it's much broader browser targets.)

As for polyfilling the spec'd properties. I'm not sure, but I think it would be hard. I'd actually advise that instead of trying to polyfill the properties as top-level properties on the synthetic events, that users reach into the nativeEvent to retrieve them.

But I think there might be a larger issue that React's onBeforeInput is not currently the same as the browser spec for beforeinput. It seems like React's is only concerned with explicitly inserting characters of text, whereas the browser spec has things like formatBold and formatItalic, which don't insert text and would be the result of keys like cmd+b.

I'm not super familiar with React's events, but it seems like the existing onBeforeInput should really be named onTextInput, leaving open the option to add a full onBeforeInput in it's place.

@aweary
Copy link
Contributor

aweary commented Jan 9, 2018

@ianstormtaylor I tested isEventSupported and your detection method and both return false in the latest Chrome (63.0.3239.132). It seems like beforeinput events are being triggered though, so I'm not sure why it doesn't work.

https://jsfiddle.net/t4dsqLj9/

As for polyfilling the spec'd properties. I'm not sure, but I think it would be hard. I'd actually advise that instead of trying to polyfill the properties as top-level properties on the synthetic events, that users reach into the nativeEvent to retrieve them.

That seems fine to me until it has better browser support.

But I think there might be a larger issue that React's onBeforeInput is not currently the same as the browser spec for beforeinput. It seems like React's is only concerned with explicitly inserting characters of text, whereas the browser spec has things like formatBold and formatItalic, which don't insert text and would be the result of keys like cmd+b.

If it doesn't implement the spec'd behavior we could potentionally see if we could update the event plugin so it does? I don't think the inconsistencies are intentional.

@ianstormtaylor
Copy link
Author

@aweary ah gotcha, it might be because Chrome doesn't implement the spec fully yet? Not sure on that one.

Updating could be good! I don't know enough about the internals to be able to suggest things there I think. The issue is that right now it's text input only, whereas the newer specs result it in being used for events that aren't necessarily character-inserting.

@johan
Copy link

johan commented Jun 7, 2018

@aweary While the hack slate uses to test for beforeinput support now generates a false negative for chrome:

const testEl = window.document.createElement('div');
testEl.contentEditable = true;
'onbeforeinput' in testEl; // => false in chrome 66.0.3359.181, despite support

...might something that tests for the inputType property in an input event instead do the trick?

'inputType' in (new InputEvent('input'));
  • it's false in Firefox 59.0.2 (64-bit), which doesn't support it (but has Input Events level 0 support, where less meaty/informative input events fire, but beforeinput events don't exist)
  • it's true in chrome 66.0.3359.181, which support Input Events Level 1 and fires beforeinput
  • it's true in safari 11.1 (13605.1.33.1.4), which support Input Events Level 2 and fires beforeinput

It might be further splayed out to handle arbitrarily old browsers with something like this, too:

'inputType' in (window.InputEvent ? new InputEvent('input') : {});

@ianstormtaylor
Copy link
Author

@johan you're right. That's interesting. I did a little investigating, and it looks like this tests end up as:

const event = window.InputEvent ? new InputEvent('input') : {}
const hasLevel1 = 'inputType' in event
const element = document.createElement('div')
element.contentEditable = true
const hasLevel2 = 'onbeforeinput' in element

Depending on whether you're testing for Input Events Level 1 support (true in Safari, Chrome, Opera). Or for Input Events Level 2 support (true in Safari).

Seems like React doesn't need to distinguish for its purposes, so the Level 1 test is the way to go. (Also because the Level 2 test seems a bit fishy anyways.)

@ShannonLCapper
Copy link

We've been taking a look at this issue because we're experiencing a problem with cancelling textInput events in the flavor of Safari that runs in Outlook add-ins on Mac. We'd argue that React should only use the native beforeinput event if it has level 2 support, since there are consumers that currently rely on the synthetic onBeforeInput being cancellable.

I will probably be opening up a PR to React to implement this behavior, but React has such a huge net of supported browsers that I don't know how to go about determining that adding this behavior won't break anything...

@johan
Copy link

johan commented Mar 30, 2019

@johan you're right. That's interesting. I did a little investigating [...]

@ianstormtaylor Now that Firefox released a version 66 which tests positive for that hasLevel1 (=has inputType property on Input events), yet doesn't fire the super useful beforeinput event which would carry target ranges info, as Chrome and Safari do (though, counter-intuitively, only Safari tests positive for your hasLevel2 which lightly feature detects that handler's name), the landscape just got broken up into three levels of support as March 2019 draws to a close:

const ie: InputEvent = new (window as any).InputEvent('input');
const ce: HTMLElement = document.createElement('div');
ce.contentEditable = 'true';

/**
 * Do InputEvents have an `inputType` property?
 *
 * @note 2019-03: Chrome, Safari, and Firefox 66+
 */
export const hasInputEventsWithInputType = 'inputType' in ie;

/**
 * Do `beforeinput` events fire with targetRanges details?
 *
 * @note 2019-03: Chrome and Safari
 */
const hasInputEventsWithTargetRanges = 'getTargetRanges' in ie;

/**
 * Is the full InputEvents Level 2 spec supported?
 *
 * @note 2019-03: Safari
 */
const hasInputEventsLevel2 = 'onbeforeinput' in ce;

Be careful what you test for, and all that. 🙈

@masayuki-nakano
Copy link

FYI: When Firefox ship beforeinput event, probably, ship HTMLElement.onbeforeinput togetter. Spec bug is here and Chrome's bug is here.

Perhaps, Mozilla won't ship InputEvent.getTargetRanges() until shipping beforeinput event because it's not useful with input event.

I think that ideal feature detection of whether beforeinput event is supported by the browser is, check the editing host's onbeforeinput is undefined or not (with ===). Additionally, Chrome breaks the common rule of event implementation, so, you should check whether the browser engine is Blink when onbeforeinput is undefined.

Unfortunately, there is no way to detect whether the browser supports Input Events Level 1 or Level 2 until receiving Level 2 specific inputType value. On the other hand, Level 2 definition around IME is really unstable for me and the most important difference between Level 1 and Level 2 is around IME composition. So, I recommend to expect Level 2 behavior even though the browser is WebKit. Something current behavior would be changed.

@stale
Copy link

stale bot commented Jan 10, 2020

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contribution.

@stale stale bot added the Resolution: Stale Automatically closed due to inactivity label Jan 10, 2020
@ianstormtaylor
Copy link
Author

Not stale. This is still frustrating to have to work around.

@stale stale bot removed the Resolution: Stale Automatically closed due to inactivity label Jan 10, 2020
@johan
Copy link

johan commented Mar 3, 2020

Yeah, it would be delightful not to need to create a ref and useEffect block to register the native event yourself, but if you need to fire a preventDefault() on that beforeinput event to take control of a content-editable element, that seems to be the only way to go about it at the moment.

@stale
Copy link

stale bot commented Jun 2, 2020

This issue has been automatically marked as stale. If this issue is still affecting you, please leave any comment (for example, "bump"), and we'll keep it open. We are sorry that we haven't been able to prioritize it yet. If you have any new additional information, please include it with your comment!

@stale stale bot added the Resolution: Stale Automatically closed due to inactivity label Jun 2, 2020
@chutchi2
Copy link

chutchi2 commented Jun 5, 2020

This is still affecting draft-js

@trueadm
Copy link
Contributor

trueadm commented Aug 7, 2020

I started to look into this today and would like to get some thoughts on #19554. Specifically in regards to the detection mechanism. I tried detecting it via onbeforeinput on the HTMLElement, but that didn't prove reliable.

@gaearon
Copy link
Collaborator

gaearon commented Aug 7, 2020

To people who expressed frustration in this thread — I totally feel you but the most productive way to move this forward is to share your expertise in this and help us figure out a reliable detection mechanism. Sending a PR with that would have moved the issue further much earlier.

@johan
Copy link

johan commented Aug 8, 2020

I started to look into this today and would like to get some thoughts on #19554.

Thanks for working on this! I'll add some notes on the PR.

Specifically in regards to the detection mechanism. I tried detecting it via onbeforeinput on the HTMLElement, but that didn't prove reliable.

You can do it on a newly created div, on which you have set the contentEditable property to 'true', but that is a stronger test for Input Events Level 2, which only Safari supports at the moment (including high-def support for TouchBar events, on macs that have that hardware).

Your current check for 'getTargetRanges' in new InputEvent(...) is the best test for the existence of the event itself, backwards as it might seem.

@masayuki-nakano
Copy link

FYI: Firefox Nightly does now support beforeinput events by default. We really need feedback from web developers before shipping it in the Release channel. So, once you find something incompatible things on Firefox, let us know. Thank you.

@trueadm
Copy link
Contributor

trueadm commented Oct 31, 2020

@masayuki-nakano Thank you for the update and for all the work you've put into this. The support for beforeinput is very important to us and also for our tooling, such as Draft. What is the latest status for this? I tried Nightly out and the performance seems good, as does the additonal functionality it provides. I'm eager to know if there are plans to get this into the release channel soon :)

@birtles
Copy link

birtles commented Nov 1, 2020

@masayuki-nakano is away for the next couple of days so let me add a couple of details and he can provide more information when he gets back.

The current status is that beforeinput is enabled in Nightly and we would really like to be confident that it will not cause compatibility problems before turning it on in release, since it is harder to change after that. Unfortunately, many web sites use UA string matching and do not use beforeinput events when the UA is Firefox. (This is partly because Chrome ships beforeinput support in a manner that is not feature-detectable.)

If React could start using Firefox's native beforeinput when available (e.g. using the same feature detection mechanism as CK editor, i.e. getTargetRanges()) that would really help build confidence that we can ship it. I believe Q1 2021 might be a possible target for shipping to release.

@masayuki-nakano
Copy link

Thank you @birtles for the explanation. Yes, we'd love to ship it as far as possible, but after shipping it, the behavior change becomes too risky because such things may require UA string check if they are caused by a bug of Firefox or just undefined incompatible things with the others. So, for reducing such risk, we'd love you to feedback with testing with Firefox Nightly. Especially we'd love to know what are unacceptable differences from the other browsers. I guess that such things mainly exist at the result of getTargetRanges().

@trueadm
Copy link
Contributor

trueadm commented Dec 4, 2020

@masayuki-nakano I tried out the latest nightly of FF and it's awesome. For context, we're looking to start using native beforeinput on facebook.com, so this is a highly desirable feature, especially as we plan to block input via preventDefault and use our own rendering of text for editors/inputs. The changes in FF fix so many issues on Facebook when it comes to text input, so it's really important to us that we can try and get this in where possible as we have many open bug reports regarding text input.

On a side note, I noticed that FF Nightly copies Chromium in that insertFromComposition and deleteByComposition do not fire when entering text via composition. It would be great if FF could implement these like Safari does, as this fixes many edge cases around IME text input (Safari leads the way here).

@masayuki-nakano
Copy link

I noticed that FF Nightly copies Chromium in that insertFromComposition and deleteByComposition do not fire when entering text via composition. It would be great if FF could implement these like Safari does, as this fixes many edge cases around IME text input (Safari leads the way here).

Well, they are only in Level 2, but the Level 2's definition around composition may break a lot of existing web apps due to the incompatible things. That's the reason why I considered that Gecko follows Blink (i.e., meaning supports only Level 1 by default). If you just want beforeinput event at committing IME composition to be cancelable, it might become into Level 1.

FYI1: Firefox has experimental implementation about Input Events Level 2 behind a pref, dom.input_events.conform_to_level_1.

FYI2: If Facebook use getTargetRanges, and give us feedback around a lot of small differences from the other browsers whether all of them are acceptable for web app vendors or some of them are not, we could ship beforeinput event in the release channel too. (Surprisingly, there is no definition about what getTargetRanges return...)

@trueadm
Copy link
Contributor

trueadm commented Dec 5, 2020

@masayuki-nakano Let me give Level 2 a try (it's a bit confusing why the flag says level 1 though).

What we want to do is allow native composition – i.e. not cancelling it, as you can't really cancel it on Blink. Instead we want to know about the composed text. Only WebKit currently fires insertFromComposition and deleteByComposition, but I guess this might work with the Level 2 flag like you mentioned. If Level 2's definition is broken, then I'd expect WebKit to also show breakages, but from extensive testing, WebKit actually has the least edge-cases when it comes to text input.

What differences in getTargetRanges are there in FF compared to other browsers that you're aware of? We do try and leverage it is available, although I did notice a few issues using it with Blink – notably the offsets are wrong when entering auto suggested text on Mac using the on-screen accessibility keyboard. It also seems that this keyboard trips up Firefox too, although in different ways (FF is the only browser that reports the keyboard text entry as composition).

@masayuki-nakano
Copy link

masayuki-nakano commented Dec 7, 2020

@masayuki-nakano Let me give Level 2 a try (it's a bit confusing why the flag says level 1 though).

The pref means that limits the behavior for conforming to Level 1's spec. Therefore, it sounds odd.

What we want to do is allow native composition – i.e. not cancelling it, as you can't really cancel it on Blink. Instead we want to know about the composed text. Only WebKit currently fires insertFromComposition and deleteByComposition, but I guess this might work with the Level 2 flag like you mentioned.

Yes.

If Level 2's definition is broken, then I'd expect WebKit to also show breakages, but from extensive testing, WebKit actually has the least edge-cases when it comes to text input.

WebKit supports only inputType values of Level 2, but does not conform to Level 2 spec's definition around composition events. So, learning the events from the spec, WebKit must make developers confused.

And what you want is exactly the spec issue. I'll ping to the guys.

What differences in getTargetRanges are there in FF compared to other browsers that you're aware of? We do try and leverage it is available, although I did notice a few issues using it with Blink – notably the offsets are wrong when entering auto suggested text on Mac using the on-screen accessibility keyboard. It also seems that this keyboard trips up Firefox too, although in different ways (FF is the only browser that reports the keyboard text entry as composition).

Thank you for the information. I've not tested it, I'll check it.

The main differences of getTargetRanges are, Blink and WebKit returns start position of a parent block element when you type Backspace from start of a block element like <p>.
https://wpt.fyi/results/input-events/input-events-get-target-ranges-backspace.tentative.html?label=experimental&label=master&aligned
In this case, Gecko returns start of first leaf node instead.

Similarly, there is same difference when you type Delete at end of a block element.
https://wpt.fyi/results/input-events/input-events-get-target-ranges-forwarddelete.tentative.html?label=experimental&label=master&aligned

Finally, deleting an atomic element like <img> with non-collapsed selection range may return different range.
https://wpt.fyi/results/input-events/input-events-get-target-ranges-non-collapsed-selection.tentative.html%3FBackspace?label=experimental&label=master&aligned
Gecko returns extended range if it's surrounded by text node(s). But Blink/WebKit do not.

@devongovett
Copy link
Contributor

Ran into this issue today as well. Unfortunately, React's onBeforeInput event is really more like onBeforeInsert. It doesn't fire for deletions, formatting, etc. See here for a full list of inputType values.

It's likely that changing the behavior to match the spec would break a bunch of existing React components that rely on onBeforeInput only being for insertions. Perhaps it makes sense to rename the existing event to something else in the next major version, and make onBeforeInput map to the native event? This way at least existing code could simply run a codemod to change to the legacy event name without also needing to handle when e.data is null. Alternatively, the old event could be dropped completely, but perhaps it's still useful given browser support for the native event is not perfect yet.

@trueadm
Copy link
Contributor

trueadm commented Jan 22, 2021

@devongovett It's something I thought long and hard about, especially as I'm currently working on a text editor at Facebook and obviously want to leverage the benefits of beforeinput.

It's more likely that we'd add support to React for beforeinput when FF ships stable support for beforeinput, as it's the last major browser that doesn't have support for it (although, hopefully it will soon thanks to the work done by @masayuki-nakano!). It would also have to be under a different alias, like you mentioned, such as: unstable_onBeforeInput. We'd switch the names around in React 19, giving folks plenty of time to plan for the change.

@trueadm
Copy link
Contributor

trueadm commented Jan 29, 2021

@masayuki-nakano Have there been any updates or updates regarding this feature shipping? We're actively looking to adopt beforeinput internally at Facebook to power a new text editor that is under development and we're also keen to see how React can adopt this too.

We noticed that getTargetRanges works as expected in FF Nightly in many of our test cases. In fact, we actually found some bugs in Chromium (see https://bugs.chromium.org/p/chromium/issues/detail?id=1043564). We'll be reaching out to get this bug fixed, but it's reassuring to see that FF gets this right (great work!).

We're looking to ship this new feature by the end of this half, so any estimations would be great.

@masayuki-nakano
Copy link

@trueadm Hi, we've decided last week that it's shipped in 87 unless we'd get some bug reports which can block the release.
https://groups.google.com/g/mozilla.dev.platform/c/C_92-abaiuw

And thank you for the feedback about the getTargetRanges!

@masayuki-nakano
Copy link

Oops, 87 will be shipped March, 23.
https://wiki.mozilla.org/Release_Management/Calendar

@JanJakes
Copy link

JanJakes commented Feb 1, 2021

We're actively looking to adopt beforeinput internally at Facebook to power a new text editor that is under development and we're also keen to see how React can adopt this too.

@trueadm Just curious, if that's no secret, are you working on something other than Draft.js that would be open-sourced at some point or is it purely an internal project?

@trueadm
Copy link
Contributor

trueadm commented Feb 1, 2021

@JanJakes Yes, I'm working on something other than Draft.js. I expect the project to be eventually open-sourced at some point too, although no guarantees or dates as it's too early :)

@diegohaz
Copy link

@masayuki-nakano Let me give Level 2 a try (it's a bit confusing why the flag says level 1 though).

The pref means that limits the behavior for conforming to Level 1's spec. Therefore, it sounds odd.

@masayuki-nakano Does it mean if I set dom.input_events.conform_to_level_1 to false I'll get Firefox working with Input Events level 2? I'm trying to do this, but input types such as insertFromComposition are still not fired. In fact, I didn't see any difference.

@karlcow
Copy link

karlcow commented Jan 6, 2022

This creates webcompat issues for Firefox.
https://bugzilla.mozilla.org/show_bug.cgi?id=1739489

@HarryPretel
Copy link

HarryPretel commented Jan 25, 2023

Still having this issue; anyone else?

I need to access the selectionStart and selectionEnd of an <input type="text" /> when deleting characters. Since the SyntheticEvent isn't triggered on a "deleteContentBackward", my current workaround is to find the difference between the current and previous values.

Anyone else willing to share their workarounds?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests