w3c / uievents

UI Events
https://w3c.github.io/uievents/
Other
148 stars 51 forks source link

Event order between "compositionend" and "input" #202

Open masayuki-nakano opened 6 years ago

masayuki-nakano commented 6 years ago

Currently, UI Events declares event order of "compositionend" and "input" as:

  1. beforeinput
  2. (compositionupdate)
  3. input
  4. compositionend
  5. (no input)

At https://w3c.github.io/uievents/#events-composition-input-events

However, Firefox and Edge's order is:

  1. (beforeinput not yet supported on them)
  2. (compositionupdate)
  3. (no input)
  4. compositionend
  5. input

I.e., "input" event is fired after "compositionend".

On the other hand, Chrome and Safari dispatches as the spec declared.

I tried to make Firefox take the same event order as the draft in https://bugzilla.mozilla.org/show_bug.cgi?id=1305387. However, after changing the order, I needed following change in some places even in Firefox's UI: https://hg.mozilla.org/try/rev/9dbba7ff1d6f7f72a33dccc93fdda60f04d9942e

Then, I feel that the event order of Chrome, Safari and the spec does not make sense for web application developers. The reason is, if all browsers use the event order of Firefox and Edge, isComposing value of "input" event becomes false. So, if a web application needs to do something at every input except during composition, web application needs to listen to only input event if it's run on Firefox or Edge. I.e., can be implemented like:

foo.addEventListener("input", (event) => {
  if (event.isComposing) {
    // Nothing to do during composition
    return;
  }
  // Do something what you need.
});

On the other hand, it's run on Chrome or Safari, needs to be implemented like:

function onInput() {
  // Do something what you need.
}
foo.addEventListener("input", (event) => {
  if (event.isComposing) {
    // Nothing to do during composition
    return;
  }
  onInput();
});
foo.addEventListener("compositionend", (event) => {
  // Need to run "input" event handler here because "input" event whose isComposing is false
  // won't be fired until user inputs something without IME later. 
  onInput();
});

I believe that the behavior of Chrome, Safari and UI Events may make web applications not aware of IME.

From point of view of browser developer, this behavior is easier to implement actually since browser does not check whether it's followed by compositionend event when it tries to fire "input" event at every composition change. However, usefulness for web application developers must be more important than easier to implement by browsers.

What do you think, @garykac ?

masayuki-nakano commented 6 years ago

So, I think that isComposing of "beforeinput" followed by "compositionend" should be true, but isComposing of "input" event caused by committing composition should be false.

Jessidhia commented 6 years ago

The problem with this is that you cannot detect anymore whether the input that caused compositionend to happen was itself done while composing.

birtles commented 6 years ago

The problem with this is that you cannot detect anymore whether the input that caused compositionend to happen was itself done while composing.

Is there a particular use case you have in mind where you need to make this distinction?

masayuki-nakano commented 6 years ago

Another possible solution is, UI Events would change the declaration of InputEvent.isComposing.

It's currently declared as "true if the input event occurs as part of a composition session, i.e., after a compositionstart event and before the corresponding compositionend event.", however, if it's declared as the value of input events which are fired for committing composition is set to false, web apps need to check only input events whose isComposing is false (like running on Firefox and Edge).

birtles commented 6 years ago

@Kovensky What do you think of the proposal that we match the order of WebKit/Blink (and now the spec) but make the last input event before the compositionend event, have isComposing be set to false?

I think that would allow making the distinction you mentioned and, like @masayuki-nakano mentions, would mean that for many apps that want to ignore input during composition, they can simply ignore input events when isComposing is true without having to track state using compositionstart and compositionend.

@garykac?

Jessidhia commented 6 years ago

I remember having issues with Japanese IME input because the Enter key is typically what is used to trigger compositionend, but if it comes with isComposing: false, I can no longer tell if that Enter key was used to exit composition or to actually write a new line.

This was a few months ago, though, I don't remember if/how that was dealt with.

birtles commented 6 years ago

That's a fair point. I hadn't thought about multi-line input so much.

So with the proposal to make isComposing false for the last input event before the (last?) compositionend the author:

Does that sound right?

(I'm actually a bit confused now about if this proposal works in light of multiple compositionend events. The IMEs I use on Linux and Windows don't allow committing part of a composition string so I never get more than one compositionend event.)

masayuki-nakano commented 6 years ago

Currently, DOM composition events do not support of committing a part of composition. In such case, Firefox commits existing composition first, then, restart composition with new range.

Brian, you can check this behavior even with MS-IME.

  1. Open preferences dialog
  2. Press key assign from "Microsoft IME" to "ATOK"
  3. Type something which may cause 2 or more clauses.
  4. Type Space bar to convert.
  5. Type ArrowDown key.

And also, new line key handling is not defined under UI Events unfortunately. In most cases, Enter key press during composition of Japanese IME just commits composition. However, for example, Enter key press during composition of Korean IME causes both committing composition and inserting a new line. If all browsers support beforeinput, the new line input can be represented with it which should be fired immediately after compositionend. However, now, Firefox dispatches a set of key events (i.e., keydown, keypress and keyup events) after compositionend on Windows, and dispatches only keypress event after compositionend on macOS. So, this issue should be out of scope of this.

birtles commented 6 years ago

Currently, DOM composition events do not support of committing a part of composition. In such case, Firefox commits existing composition first, then, restart composition with new range.

Oh, I see. The spec seems to suggest there should be no extra compositionstart:

Note that while every composition only has one compositionstart event, it may have several compositionend events.

But I notice neither Chrome nor Firefox do that.

For my reference I am testing using this pen: https://codepen.io/birtles/pen/pZwNPx

Following the stems from Masayuki for MS-IME:

  1. Open preferences dialog
  2. Press key assign from "Microsoft IME" to "ATOK"
  3. Type something which may cause 2 or more clauses.
  4. Type Space bar to convert.
  5. Type ArrowDown key.

I get in Firefox:

compositionstart (value: )
input (value: k, isComposing: true)
input (value: こ, isComposing: true)
input (value: こr, isComposing: true)
input (value: これ, isComposing: true)
input (value: これh, isComposing: true)
input (value: これは, isComposing: true)
input (value: これはt, isComposing: true)
input (value: これはて, isComposing: true)
input (value: これはてs, isComposing: true)
input (value: これはてす, isComposing: true)
input (value: これはてすt, isComposing: true)
input (value: これはてすと, isComposing: true)
input (value: これはテスト, isComposing: true)
compositionend (value: これは)
input (value: これは, isComposing: false)
compositionstart (value: これは)
input (value: これはテスト, isComposing: true)
compositionend (value: これはテスト)
input (value: これはテスト, isComposing: false)

In Chrome:

compositionstart (value: )
input (value: k, isComposing: true)
input (value: k, isComposing: true)
input (value: こ, isComposing: true)
input (value: こ, isComposing: true)
input (value: こr, isComposing: true)
input (value: こr, isComposing: true)
input (value: これ, isComposing: true)
input (value: これ, isComposing: true)
input (value: これ, isComposing: true)
input (value: これh, isComposing: true)
input (value: これh, isComposing: true)
input (value: これは, isComposing: true)
input (value: これは, isComposing: true)
input (value: これはt, isComposing: true)
input (value: これはて, isComposing: true)
input (value: これはて, isComposing: true)
input (value: これはてs, isComposing: true)
input (value: これはてs, isComposing: true)
input (value: これはてす, isComposing: true)
input (value: これはてす, isComposing: true)
input (value: これはてすt, isComposing: true)
input (value: これはてすt, isComposing: true)
input (value: これはてすと, isComposing: true)
input (value: これはてすと, isComposing: true)
input (value: これはテスト, isComposing: true)
input (value: これは, isComposing: true)
compositionend (value: これは)
compositionstart (value: これは)
input (value: これはテスト, isComposing: true)
input (value: これはテスト, isComposing: true)
compositionend (value: これはテスト)

Edge:

compositionstart (value: )
input (value: k, isComposing: undefined)
input (value: こ, isComposing: undefined)
input (value: こr, isComposing: undefined)
input (value: これ, isComposing: undefined)
input (value: これh, isComposing: undefined)
input (value: これは, isComposing: undefined)
input (value: これはt, isComposing: undefined)
input (value: これはて, isComposing: undefined)
input (value: これはてs, isComposing: undefined)
input (value: これはてす, isComposing: undefined)
input (value: これはてすt, isComposing: undefined)
input (value: これはてすと, isComposing: undefined)
input (value: これはテスト, isComposing: undefined)
compositionend (value: これはテスト)

If some Korean IMEs allow Enter to do double-duty as committing a composition and entering a newline then it sounds like distinguishing between the two actions is a separate matter as you say. If beforeinput lets authors recognize a genuine newline (I didn't quite understand this part but would love to know how this work) then that's even better.

So it sounds like the proposal to make the last input event before the compositionend event have isComposing be set to false could work.

garykac commented 6 years ago

Sorry for the delays responding on that issue. I've been finishing other things up and am just now starting to be able to spend more time on UIEvents.

To summarize my understanding:

Regarding that last concern, I just tested this on Chrome Win and Mac with Korean and Japanese IMEs and noticed the following events after pressing Enter to accept the composition:

So (from this limited testing), I don't think there's a problem with distinguishing the final Enter of a composition from the newline (esp. when inputType is set).

As for the proposal, other than it makes the spec description more complex, I don't see serious problems with it. But I also don't have a sense of how serious the original problem is.

The tradeoff we're making is between:

But it doesn't seem (to me) like it's very common to need to distinguish between input events that are inside or outside composition. The vast majority of devs won't care. The one case I can think of where someone might want to do this is for devs writing rich text editors, but I don't consider it a problem if they have to write a bit more code to get things done - they're probably handling compositionend already.

Unless there's a real dev need to fix this, I'd rather keep the definition for isComposing simple.

Note that while every composition only has one compositionstart event, it may have several compositionend events.

I was wondering where this came from (it's not in the UIEvents spec) and finally found it as a non-normative note in Input Events spec (in Step 5) (https://www.w3.org/TR/input-events-2/).

AIUI, that is wrong. compositionstart and compositionend are supposed to be matching pairs. I'm not aware of any situation where there should be multiple compositionend events in a single composition session.

The UIEvents spec says (https://www.w3.org/TR/uievents/#events-compositionevents):

Conceptually, a composition session consists of one compositionstart event, one or more compositionupdate events, and one compositionend event, with the value of the data attribute persisting between each stage of this event chain during each session.

birtles commented 6 years ago

Thanks for following up on this!

The tradeoff we're making is between:

  • Making the spec's description more complex: "isComposing is set when composing, EXCEPT when..." (ugh)
  • Making things slightly harder for devs who need to ignore input events during composition (since they have to add an event handler for compositionend).

That priority of constituencies here surely means we should choose the latter (i.e. devs > spec writers).

But it doesn't seem (to me) like it's very common to need to distinguish between input events that are inside or outside composition. The vast majority of devs won't care.

I disagree. @masayuki-nakano has already indicated that it is common to make this distinction in Firefox source. In my work on Web apps too, I frequently need to make this distinction. Some common examples include:

masayuki-nakano commented 6 years ago
  • Making the spec's description more complex: "isComposing is set when composing, EXCEPT when..." (ugh)

How about: "isComposing is true when the text is still composing by composition" or something similar? I'm trying to say, when isComposing is true, its data may be updated/replaced during current composition. So, the data hasn't been fixed yet.

  • Making things slightly harder for devs who need to ignore input events during composition (since they have to add an event handler for compositionend).

I have no idea what the use-case of ignoring all input caused by composition. I suggested current isComposing but I intended to make input event useful for suggesting use-case because I didn't realize the event order difference between browsers (input vs. compositionend).

garykac commented 6 years ago

The tradeoff we're making is between:

Making the spec's description more complex: "isComposing is set when composing, EXCEPT when..." (ugh) Making things slightly harder for devs who need to ignore input events during composition (since they have to add an event handler for compositionend).

That priority of constituencies here surely means we should choose the latter (i.e. devs > spec writers).

I phrased that poorly. My concern is not for the spec writer (who only has to write it once), but for anyone who has to read the spec. A more complex spec is more complex for anyone who has to read it or any follow-on documentation (like MDN) that are based on the spec.

But it doesn't seem (to me) like it's very common to need to distinguish between input events that are inside or outside composition. The vast majority of devs won't care.

I disagree. @masayuki-nakano has already indicated that it is common to make this distinction in Firefox source.

But needing to make this distinction in FF code isn't a good reason to expose this to the web platform.

In my work on Web apps too, I frequently need to make this distinction. Some common examples include:

  • If you are triggering a search as the user types, you don't normally want to do that search until the string has been committed since, in the case of Japanese, you'll be searching on the hiragana instead of the kanji so it will likely be wasted work and distracting to the user.
  • If you're doing validation on data as it is typed, it's quite common that the uncommitted string will be invalid while the committed string will be valid and you don't want to annoy the user with "data invalid" UI while they're still composing. (As a concrete example, consider an app that checks an input station name is valid. While the string is uncommitted it will likely be incomplete or in hiragana but once committed it will typically be valid.)
  • If you're doing auto-save based on user input, it would likely be wasted work to save before the string is committed.

These are compelling examples. Thanks for taking the time to write them up. These are good cases for motivating this change.

Would it be sufficient to phrase this as something like: "During composition, the isComposing attribute should be true for all input events except for the final one that updates the DOM."

AUIU, if the user cancels composition, the final input would be a delete to remove the previous candidate. I think this is fine. I can't think of any other cases that might be problematic.

Does the final beforeinput event need to have isComposing set in the same manner? My instinct is "yes, it should", but I'm open to arguments why is doesn't need to be.

birtles commented 6 years ago

Thanks for following up on this.

But it doesn't seem (to me) like it's very common to need to distinguish between input events that are inside or outside composition. The vast majority of devs won't care.

I disagree. @masayuki-nakano has already indicated that it is common to make this distinction in Firefox source.

But needing to make this distinction in FF code isn't a good reason to expose this to the web platform.

Yes, sorry, by FF code I meant Firefox front-end code, i.e. the JS used for the FF user interface. While it's not entirely representative of typical Web content, for the purposes of handling input events I think it's reasonably close.

Would it be sufficient to phrase this as something like: "During composition, the isComposing attribute should be true for all input events except for the final one that updates the DOM."

I'm not familiar with the correct terminology here but that seems to match the intention at least. @masayuki-nakano does this sound right? The idea behind this language is that it then covers the cancel case too.

Does the final beforeinput event need to have isComposing set in the same manner? My instinct is "yes, it should", but I'm open to arguments why is doesn't need to be.

@masayuki-nakano what do you think about this? ↑

masayuki-nakano commented 6 years ago

Yes, sorry, by FF code I meant Firefox front-end code, i.e. the JS used for the FF user interface. While it's not entirely representative of typical Web content, for the purposes of handling input events I think it's reasonably close.

Firefox's front-end is one of the biggest web-applications in the world even though it uses some special API to access sensitive area. So, I believe that if some spec changes make Firefox's front-end more complicated, that means that web apps in the world are also made complicated if they need to implement similar feature.

Does the final beforeinput event need to have isComposing set in the same manner? My instinct is "yes, it should", but I'm open to arguments why is doesn't need to be.

@masayuki-nakano what do you think about this? ↑

Honestly, I'm still not sure, I'm still thinking this...

From a point of view of web browser, browser has not been handled native commit event yet at dispatching beforeinput event. I.e., there is a composition in the editor. So, setting false to isComposing of beforeinput may make sense.

On the other hand, beforeinput is not useful event if there were no inputType attribute value since web apps cannot know what will happen without the attribute. So, in other words, I have no idea how web apps use only isComposing information of beforeinput.

So, most important thing is, which value can prevent bugs of web apps...

birtles commented 6 years ago

Today Masayuki and I discussed whether or not beforeinput should also set isComposing to false on the last event before updating the DOM. I'll summarize what I recall and Masayuki can correct me where I'm wrong.

Argument for making isComposing true:

Argument for making isComposing false:

masayuki-nakano commented 6 years ago

(I'm a bit vague about this concern but hopefully Masayuki can clarify this point.)

No, you understand my arguing points perfectly.

So, beforeinput is NOT useful only with the fact that it's fired since beforeinput is fired before the change affects the value of focused editor. So, inputType is really important for beforeinput event listeners. I.e., I have no idea how important that isComposing is referred by beforeinput listeners. If a event listener listens both input and beforeinput events, isComposing of beforeinput may be referred. However, I have no idea what such listeners need to do.

Jessidhia commented 6 years ago

I ran into this again, but this time not even with input or beforeinput events, but with keydown events which is supposed to fire even earlier than beforeinput (need to watch the state of modifier keys so input events are not low level enough).

In Safari, compositionend fires even before the keydown with the Enter that commits the IME input is fired, which causes isComposing to be wrong there too. The only way I have to tell if that Enter key was actually an input to the IME is to look at the deprecated which property and check if it's 229 or 13.

birtles commented 6 years ago

@Kovensky I'm not sure I understand what you're suggesting here. Are you endorsing the importance of this issue or suggesting a particular value of isComposing?

Jessidhia commented 6 years ago

It is the importance of this issue, although this kinda looks like a Safari bug instead. The value of isComposing is true, by definition, before compositionend; the problem here is that Safari is emitting compositionend way before when it's supposed to be emitted. It's not even just that it's before input, it's happening even before keydown.

johanneswilm commented 6 years ago

But it doesn't seem (to me) like it's very common to need to distinguish between input events that are inside or outside composition. The vast majority of devs won't care.

Most web apps don't do text editing at all, and those 99% won't care, I agree. But what we need to look at are the 1% (or less) of devs working on editing apps. Without such editing apps, there is no content on the web so even if the percentage is low, it does matter for the web overall.

Those web apps doing text editing need to be concerned with the difference between composing and non-composing as they are prohibited from changing the DOM around the composition while it takes place. So as long as that is clearly communicated in some way, I think everything is fine.

If Web apps are waiting for isComposing === false and then taking some action that results in the DOM being updated and the composition canceled then they might arrive at a state where the string being composed remains, and not the final string that should have been committed.

If isComposing is set to false, while it really should be true and it does cause IMEs to be canceled half-way, then the web apps will stop checking that attribute and will be looking for some other indication of whether the composition has finished.

That is not really ideal as we are then moving away from the idea that you can build an editor by listening to beforeinput events and go back to the kind of patchwork that editors need to go through today.

johanneswilm commented 6 years ago

Some web apps want to be able to ignore the intermediate input events generated during composition, but they want the final input event (effectively, they want to consider the composition as a single input action).

I believe when we originally wrote the level 2 input events spec, our information was that the last input event may not give us information about the contents and placement of the entire composition string. And also at that start, when a composition is initiated on a pre-existing string, we would not know where that string is. This is why we added the cancelable beforeinput events with inputtypes deleteByComposition (before composition) and insertFromComposition (after composition).

Without those events, as in level 1, the only important piece of information we can listen for is when the composition has finished. Once that has happened, we diff the DOM and find out what has changed, based on that make a guess on what the user wanted to enter/delete, and then act upon that.

So unless something has changed so that one can always know where the entire string was placed based on the information of the last input event during composition and what has changed, I don't know if this is of much help to webapps.

masayuki-nakano commented 5 years ago

Even if browser only supports Input Events Level 1 (like Firefox and Chrome), input events during composition has insertCompositionText as inputType value. Therefore, even if InputEvent.isComposing is set to false during composition, web apps can distinguish whether every change is caused by IME or not.

aaclayton commented 4 years ago

I came across this as a problem for my own use case where I discovered that Safari/Chrome were triggering compositionend before keydown including for the ENTER keypress which concluded composition - resulting in a KeyboardEvent where the code is "Enter" and isComposing is false - but that enter keypress was - in fact - used to conclude a composition.

I don't have the level of expertise to contribute towards a solution here, but it seems that there was some productive conversation in this thread which dried out and didn't end up leading anywhere. Is this viewed as a non-issue? What should developers be doing to avoid triggering incorrect events on keyboard events which are actually due to international character composition?

birtles commented 4 years ago

Yes, it would be good to reach consensus on this. I believe we were proposing to change the value of isComposing for input events (and perhaps beforeinput) but that will depend on how willing Chrome/Safari are to make that change.

For what it's worth I also often encounter this issue, e.g. this code which is precisely the sort of code Masayuki mentioned at this beginning of this issue.

dandclark commented 1 year ago

It seems like there are good arguments here for Chromium and Safari converging to Firefox's behavior and updating the spec accordingly.

@mustaqahmed, would you be the right owner for this for Chrome, or do you know who could make that call?

@annevk is this something you'd be able to comment on for Safari?

annevk commented 1 year ago

How often are these events used? I'd be a bit worried about web compatibility, but perhaps with Chromium and WebKit changing this around the same time that can be mitigated somewhat.

cc @whsieh

mustaqahmed commented 1 year ago

@garykac: do you know who could answer the Chrome question above?

birtles commented 1 year ago

How often are these events used?

Pretty often. Apart from the issues linked to this one, there are plenty of blogs of people stumbling due to this, e.g.:

Just trying to distinguish an Enter keypress to commit a selection from a regular Enter keypress (e.g. to close a modal) is really hard in Safari. (The most reliable way being to use the deprecated keyCode property.)