-
Notifications
You must be signed in to change notification settings - Fork 211
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
fix(number-field): update IME change detection #4672
Changes from all commits
202a83d
ee6e38b
c45ff1e
061a97e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -63,8 +63,17 @@ export const remapMultiByteCharacters: Record<string, string> = { | |
'%': '%', | ||
'+': '+', | ||
ー: '-', | ||
一: '1', | ||
二: '2', | ||
三: '3', | ||
四: '4', | ||
五: '5', | ||
六: '6', | ||
七: '7', | ||
八: '8', | ||
九: '9', | ||
零: '0', | ||
}; | ||
|
||
const chevronIcon: Record<string, (dir: 'Down' | 'Up') => TemplateResult> = { | ||
s: (dir) => html` | ||
<sp-icon-chevron50 | ||
|
@@ -175,20 +184,25 @@ export class NumberField extends TextfieldBase { | |
private _trackingValue = ''; | ||
private lastCommitedValue?: number; | ||
|
||
private setValue(value: number = this.value): void { | ||
this.value = value; | ||
private setValue(newValue: number = this.value): void { | ||
// Capture previous value for accurate IME change detection | ||
const previousValue = this.lastCommitedValue; | ||
|
||
this.value = newValue; | ||
|
||
if ( | ||
typeof this.lastCommitedValue === 'undefined' || | ||
this.lastCommitedValue === this.value | ||
typeof previousValue === 'undefined' || | ||
previousValue === this.value | ||
) { | ||
// Do not announce when the value is unchanged. | ||
return; | ||
} | ||
|
||
this.lastCommitedValue = this.value; | ||
|
||
this.dispatchEvent( | ||
new Event('change', { bubbles: true, composed: true }) | ||
); | ||
this.lastCommitedValue = this.value; | ||
} | ||
|
||
/** | ||
|
@@ -214,7 +228,13 @@ export class NumberField extends TextfieldBase { | |
private valueBeforeFocus: string = ''; | ||
private isIntentDecimal: boolean = false; | ||
|
||
private convertValueToNumber(value: string): number { | ||
private convertValueToNumber(inputValue: string): number { | ||
// Normalize full-width characters to their ASCII equivalents | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Without normalizing this value (remapping the multi-byte characters) before the input manipulation, we would get |
||
let normalizedValue = inputValue | ||
.split('') | ||
.map((char) => remapMultiByteCharacters[char] || char) | ||
.join(''); | ||
|
||
const separators = this.valueBeforeFocus | ||
.split('') | ||
.filter((char) => this.decimalsChars.has(char)); | ||
|
@@ -223,7 +243,7 @@ export class NumberField extends TextfieldBase { | |
if ( | ||
isIPhone() && | ||
this.inputElement.inputMode === 'decimal' && | ||
value !== this.valueBeforeFocus | ||
normalizedValue !== this.valueBeforeFocus | ||
) { | ||
const parts = this.numberFormatter.formatToParts(1000.1); | ||
|
||
|
@@ -234,12 +254,15 @@ export class NumberField extends TextfieldBase { | |
for (const separator of uniqueSeparators) { | ||
const isDecimalSeparator = separator === replacementDecimal; | ||
if (!isDecimalSeparator && !this.isIntentDecimal) { | ||
value = value.replace(new RegExp(separator, 'g'), ''); | ||
normalizedValue = normalizedValue.replace( | ||
new RegExp(separator, 'g'), | ||
'' | ||
); | ||
} | ||
} | ||
|
||
let hasReplacedDecimal = false; | ||
const valueChars = value.split(''); | ||
const valueChars = normalizedValue.split(''); | ||
for (let index = valueChars.length - 1; index >= 0; index--) { | ||
const char = valueChars[index]; | ||
if (this.decimalsChars.has(char)) { | ||
|
@@ -249,11 +272,10 @@ export class NumberField extends TextfieldBase { | |
} else valueChars[index] = ''; | ||
} | ||
} | ||
value = valueChars.join(''); | ||
normalizedValue = valueChars.join(''); | ||
} | ||
return this.numberParser.parse(value); | ||
return this.numberParser.parse(normalizedValue); | ||
} | ||
|
||
private get _step(): number { | ||
if (typeof this.step !== 'undefined') { | ||
return this.step; | ||
|
@@ -492,6 +514,7 @@ export class NumberField extends TextfieldBase { | |
.split('') | ||
.map((char) => remapMultiByteCharacters[char] || char) | ||
.join(''); | ||
|
||
if (this.numberParser.isValidPartialNumber(value)) { | ||
// Use starting value as this.value is the `input` value. | ||
this.lastCommitedValue = this.lastCommitedValue ?? this.value; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -502,6 +502,18 @@ describe('NumberField', () => { | |
el.value = 52; | ||
expect(changeSpy.callCount).to.equal(0); | ||
}); | ||
it('handles IME input correctly and dispatches change event', async () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This test covers both the |
||
el.focus(); | ||
el.dispatchEvent(new CompositionEvent('compositionstart')); | ||
// input multibyte characters | ||
await sendKeys({ type: '123' }); | ||
await elementUpdated(el); | ||
el.dispatchEvent(new CompositionEvent('compositionend')); | ||
await elementUpdated(el); | ||
await sendKeys({ press: 'Enter' }); | ||
expect(el.value).to.equal(50123); | ||
expect(changeSpy.callCount).to.equal(1); | ||
}); | ||
it('via scroll', async () => { | ||
el.focus(); | ||
await elementUpdated(el); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For IME input in Safari, the value conversion process would cause
this.value
andthis.lastCommitedValue
to be identical at the time of the comparison, even though a change had happened. So I addedpreviousValue
to hold the value ofthis.lastCommitedValue
before the update tothis.value
, which fixed it! :)