Fix various "first press" bugs on single select dropdowns

On OS X, pressing the command key while a Choices select is focused
will add the text "meta". While looking into the details, I found
quite a few more errors surrounding the "first keypress" behavior.
When a closed select is focused, the following things generate
incorrect behavior:

- Any key while shift was previously held: the first character
  is incorrectly capitalized/lowercased
- If the keypress begins a "compose" input (e.g. option+` on mac)
  the value will be "dead"
- Various other non-printable keys also emit output, such as
  the arrow keys, media keys, etc.

Fix the entire category of behavior by better evaluating what
counts as a "printable" character.
This commit is contained in:
Kris Reeves 2023-02-06 15:12:51 -08:00
parent 5dbea2825a
commit f73a16f383
No known key found for this signature in database
GPG key ID: 3221D8D625551098
5 changed files with 67 additions and 16 deletions

View file

@ -1,4 +1,4 @@
/*! choices.js v10.2.0 | © 2022 Josh Johnson | https://github.com/jshjohnson/Choices#readme */ /*! choices.js v10.2.0 | © 2023 Josh Johnson | https://github.com/jshjohnson/Choices#readme */
(function webpackUniversalModuleDefinition(root, factory) { (function webpackUniversalModuleDefinition(root, factory) {
if(typeof exports === 'object' && typeof module === 'object') if(typeof exports === 'object' && typeof module === 'object')
module.exports = factory(); module.exports = factory();
@ -1263,9 +1263,30 @@ var Choices = /** @class */function () {
var hasFocusedInput = this.input.isFocussed; var hasFocusedInput = this.input.isFocussed;
var hasActiveDropdown = this.dropdown.isActive; var hasActiveDropdown = this.dropdown.isActive;
var hasItems = this.itemList.hasChildren(); var hasItems = this.itemList.hasChildren();
var keyString = String.fromCharCode(keyCode); /*
// eslint-disable-next-line no-control-regex See:
var wasPrintableChar = /[^\x00-\x1F]/.test(keyString); https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key
https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values
https://en.wikipedia.org/wiki/UTF-16#Code_points_from_U+010000_to_U+10FFFF - UTF-16 surrogate pairs
https://stackoverflow.com/a/70866532 - "Unidentified" for mobile
http://www.unicode.org/versions/Unicode5.2.0/ch16.pdf#G19635 - U+FFFF is reserved (Section 16.7)
Logic: when a key event is sent, `event.key` represents its printable value _or_ one
of a large list of special values indicating meta keys/functionality. In addition,
key events for compose functionality contain a value of `Dead` when mid-composition.
I can't quite verify it, but non-English IMEs may also be able to generate key codes
for code points in the surrogate-pair range, which could potentially be seen as having
key.length > 1. Since `Fn` is one of the special keys, we can't distinguish by that
alone.
Here, key.length === 1 means we know for sure the input was printable and not a special
`key` value. When the length is greater than 1, it could be either a printable surrogate
pair or a special `key` value. We can tell the difference by checking if the _character
code_ value (not code point!) is in the "surrogate pair" range or not.
We don't use .codePointAt because an invalid code point would return 65535, which wouldn't
pass the >= 0x10000 check we would otherwise use.
> ...The Unicode Standard sets aside 66 noncharacter code points. The last two code points
> of each plane are noncharacters: U+FFFE and U+FFFF on the BMP...
*/
var wasPrintableChar = event.key.length === 1 || event.key.length === 2 && event.key.charCodeAt(0) >= 0xD800 || event.key === 'Unidentified';
var BACK_KEY = constants_1.KEY_CODES.BACK_KEY, var BACK_KEY = constants_1.KEY_CODES.BACK_KEY,
DELETE_KEY = constants_1.KEY_CODES.DELETE_KEY, DELETE_KEY = constants_1.KEY_CODES.DELETE_KEY,
ENTER_KEY = constants_1.KEY_CODES.ENTER_KEY, ENTER_KEY = constants_1.KEY_CODES.ENTER_KEY,
@ -1275,15 +1296,15 @@ var Choices = /** @class */function () {
DOWN_KEY = constants_1.KEY_CODES.DOWN_KEY, DOWN_KEY = constants_1.KEY_CODES.DOWN_KEY,
PAGE_UP_KEY = constants_1.KEY_CODES.PAGE_UP_KEY, PAGE_UP_KEY = constants_1.KEY_CODES.PAGE_UP_KEY,
PAGE_DOWN_KEY = constants_1.KEY_CODES.PAGE_DOWN_KEY; PAGE_DOWN_KEY = constants_1.KEY_CODES.PAGE_DOWN_KEY;
if (!this._isTextElement && !hasActiveDropdown && wasPrintableChar) { if (!this._isTextElement && !hasActiveDropdown) {
this.showDropdown(); this.showDropdown();
if (!this.input.isFocussed) { if (!this.input.isFocussed && wasPrintableChar) {
/* /*
We update the input value with the pressed key as We update the input value with the pressed key as
the input was not focussed at the time of key press the input was not focussed at the time of key press
therefore does not have the value of the key. therefore does not have the value of the key.
*/ */
this.input.value += event.key.toLowerCase(); this.input.value += event.key;
} }
} }
switch (keyCode) { switch (keyCode) {

File diff suppressed because one or more lines are too long

View file

@ -1 +1 @@
/*! choices.js v10.2.0 | © 2022 Josh Johnson | https://github.com/jshjohnson/Choices#readme */ /*! choices.js v10.2.0 | © 2023 Josh Johnson | https://github.com/jshjohnson/Choices#readme */

File diff suppressed because one or more lines are too long

View file

@ -1440,9 +1440,39 @@ class Choices implements Choices {
const hasFocusedInput = this.input.isFocussed; const hasFocusedInput = this.input.isFocussed;
const hasActiveDropdown = this.dropdown.isActive; const hasActiveDropdown = this.dropdown.isActive;
const hasItems = this.itemList.hasChildren(); const hasItems = this.itemList.hasChildren();
const keyString = String.fromCharCode(keyCode); /*
// eslint-disable-next-line no-control-regex See:
const wasPrintableChar = /[^\x00-\x1F]/.test(keyString); https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key
https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values
https://en.wikipedia.org/wiki/UTF-16#Code_points_from_U+010000_to_U+10FFFF - UTF-16 surrogate pairs
https://stackoverflow.com/a/70866532 - "Unidentified" for mobile
http://www.unicode.org/versions/Unicode5.2.0/ch16.pdf#G19635 - U+FFFF is reserved (Section 16.7)
Logic: when a key event is sent, `event.key` represents its printable value _or_ one
of a large list of special values indicating meta keys/functionality. In addition,
key events for compose functionality contain a value of `Dead` when mid-composition.
I can't quite verify it, but non-English IMEs may also be able to generate key codes
for code points in the surrogate-pair range, which could potentially be seen as having
key.length > 1. Since `Fn` is one of the special keys, we can't distinguish by that
alone.
Here, key.length === 1 means we know for sure the input was printable and not a special
`key` value. When the length is greater than 1, it could be either a printable surrogate
pair or a special `key` value. We can tell the difference by checking if the _character
code_ value (not code point!) is in the "surrogate pair" range or not.
We don't use .codePointAt because an invalid code point would return 65535, which wouldn't
pass the >= 0x10000 check we would otherwise use.
> ...The Unicode Standard sets aside 66 noncharacter code points. The last two code points
> of each plane are noncharacters: U+FFFE and U+FFFF on the BMP...
*/
const wasPrintableChar = (
event.key.length === 1
|| (event.key.length === 2 && event.key.charCodeAt(0) >= 0xD800)
|| event.key === 'Unidentified'
);
const { const {
BACK_KEY, BACK_KEY,
@ -1456,16 +1486,16 @@ class Choices implements Choices {
PAGE_DOWN_KEY, PAGE_DOWN_KEY,
} = KEY_CODES; } = KEY_CODES;
if (!this._isTextElement && !hasActiveDropdown && wasPrintableChar) { if (!this._isTextElement && !hasActiveDropdown) {
this.showDropdown(); this.showDropdown();
if (!this.input.isFocussed) { if (!this.input.isFocussed && wasPrintableChar) {
/* /*
We update the input value with the pressed key as We update the input value with the pressed key as
the input was not focussed at the time of key press the input was not focussed at the time of key press
therefore does not have the value of the key. therefore does not have the value of the key.
*/ */
this.input.value += event.key.toLowerCase(); this.input.value += event.key;
} }
} }