Support full-width numerals and minus sign in input[type=number]
This patch adds normalization support for full-width digits, the minus
sign (U+FF0D, U+30FC), and the full-width period (U+FF0E) when handling
typed input in number fields. These characters are commonly produced by
Japanese IMEs when entering numeric values.
Bug: 383232110
Change-Id: I6d5764444e94f55aae2a01981e2e3fe985e7922b
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6609680
Reviewed-by: Joey Arhar <jarhar@chromium.org>
Commit-Queue: Kent Tamura <tkent@chromium.org>
Reviewed-by: Kent Tamura <tkent@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1474647}
diff --git a/AUTHORS b/AUTHORS
index 6975fdc..b3f918a 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -861,6 +861,7 @@
Kwangho Shin <k_h.shin@samsung.com>
Kyle Nahrgang <kpn24@drexel.edu>
Kyle Plumadore <kyle.plumadore@amd.com>
+Kyouhei Horizumi <kyouhei.horizumi@gmail.com>
Kyounga Ra <kyounga.ra@gmail.com>
Kyoungdeok Kwon <kkd927@gmail.com>
Kyung Yeol Kim <chitacan@gmail.com>
diff --git a/third_party/blink/renderer/core/html/build.gni b/third_party/blink/renderer/core/html/build.gni
index c6abaea..0a09930e 100644
--- a/third_party/blink/renderer/core/html/build.gni
+++ b/third_party/blink/renderer/core/html/build.gni
@@ -685,6 +685,7 @@
"forms/html_option_element_test.cc",
"forms/html_text_area_element_test.cc",
"forms/internal_popup_menu_test.cc",
+ "forms/number_input_type_test.cc",
"forms/option_list_test.cc",
"forms/step_range_test.cc",
"forms/text_control_element_test.cc",
diff --git a/third_party/blink/renderer/core/html/forms/number_input_type.cc b/third_party/blink/renderer/core/html/forms/number_input_type.cc
index fb0c8f6..6fb97d3 100644
--- a/third_party/blink/renderer/core/html/forms/number_input_type.cc
+++ b/third_party/blink/renderer/core/html/forms/number_input_type.cc
@@ -46,6 +46,7 @@
#include "third_party/blink/renderer/core/layout/layout_object.h"
#include "third_party/blink/renderer/core/layout/layout_object_inlines.h"
#include "third_party/blink/renderer/platform/bindings/exception_state.h"
+#include "third_party/blink/renderer/platform/runtime_enabled_features.h"
#include "third_party/blink/renderer/platform/text/platform_locale.h"
#include "third_party/blink/renderer/platform/wtf/math_extras.h"
@@ -134,6 +135,45 @@
return false;
}
+String NumberInputType::NormalizeFullWidthNumberChars(const String& input) {
+ StringBuilder result;
+ const wtf_size_t len = input.length();
+ result.ReserveCapacity(len);
+ for (wtf_size_t i = 0; i < len; ++i) {
+ UChar c = input[i];
+ if (c >= kFullwidthDigitZero && c <= kFullwidthDigitNine) {
+ // Convert full-width digits (0-9, U+FF10-U+FF19) to ASCII digits (0-9)
+ result.Append(c - kFullwidthDigitZero + kDigitZeroCharacter);
+ } else if (c == kKatakanaHiraganaProlongedSoundMark ||
+ c == kFullwidthHyphenMinus) {
+ // Convert full-width minus signs and the Japanese IME long sound symbol
+ // ("ー", U+30FC) to ASCII '-'.
+ // Note: On Japanese IMEs, typing a minus sign in full-width mode can
+ // produce either 'ー' (U+30FC) or '-' (U+FF0D), depending on the input
+ // mode.
+ //
+ // There are two common full-width input modes:
+ // - Full-width alphanumeric mode: typing '-' usually results in '-'.
+ // - Full-width Japanese kana mode: typing '-' may yield 'ー'.
+ //
+ // Especially, when **only the symbol is typed**, IMEs tend to insert 'ー'
+ // as a long sound mark. If digits follow, the symbol remains unchanged.
+ // For example, entering "ー2" instead of "-2" is a typical case.
+ //
+ // Since users generally intend to input negative numbers in such cases,
+ // we normalize both 'ー' and '-' to ASCII minus '-'.
+ result.Append(kHyphenMinusCharacter);
+ } else if (c == kFullwidthFullStop) {
+ // Convert full-width period (., U+FF0E) to ASCII dot (.)
+ result.Append(kFullstopCharacter);
+ } else {
+ // Preserve other characters
+ result.Append(c);
+ }
+ }
+ return result.ReleaseString();
+}
+
StepRange NumberInputType::CreateStepRange(
AnyStepHandling any_step_handling) const {
DEFINE_STATIC_LOCAL(
@@ -192,10 +232,16 @@
BeforeTextInsertedEvent& event) {
Locale& locale = GetLocale();
+ String normalized_input = event.GetText();
+ if (RuntimeEnabledFeatures::NumberInputFullWidthCharsEnabled()) {
+ // Normalize full-width digits and minus sign to ASCII
+ normalized_input = NormalizeFullWidthNumberChars(normalized_input);
+ }
+
// If the cleaned up text doesn't match input text, don't insert partial input
// since it could be an incorrect paste.
String updated_event_text =
- locale.StripInvalidNumberCharacters(event.GetText(), "0123456789.Ee-+");
+ locale.StripInvalidNumberCharacters(normalized_input, "0123456789.Ee-+");
// Check if locale supports more cleanup rules
if (!locale.UsesSingleCharNumberFiltering()) {
@@ -265,7 +311,7 @@
left_half = left_half + c;
final_event_text.Append(c);
}
- event.SetText(final_event_text.ToString());
+ event.SetText(final_event_text.ReleaseString());
}
Decimal NumberInputType::ParseToNumber(const String& src,
diff --git a/third_party/blink/renderer/core/html/forms/number_input_type.h b/third_party/blink/renderer/core/html/forms/number_input_type.h
index 60bc259d..36365a8 100644
--- a/third_party/blink/renderer/core/html/forms/number_input_type.h
+++ b/third_party/blink/renderer/core/html/forms/number_input_type.h
@@ -42,6 +42,7 @@
explicit NumberInputType(HTMLInputElement& element)
: TextFieldInputType(Type::kNumber, element) {}
bool TypeMismatchFor(const String&) const;
+ CORE_EXPORT static String NormalizeFullWidthNumberChars(const String& input);
private:
void CountUsage() override;
diff --git a/third_party/blink/renderer/core/html/forms/number_input_type_test.cc b/third_party/blink/renderer/core/html/forms/number_input_type_test.cc
new file mode 100644
index 0000000..8066c33
--- /dev/null
+++ b/third_party/blink/renderer/core/html/forms/number_input_type_test.cc
@@ -0,0 +1,41 @@
+// Copyright 2025 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/html/forms/number_input_type.h"
+
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/renderer/platform/wtf/text/wtf_string.h"
+
+namespace blink {
+
+TEST(NumberInputTypeTest, NormalizeFullWidthNumberChars) {
+ const auto& normalize = NumberInputType::NormalizeFullWidthNumberChars;
+
+ // Returns empty string unchanged
+ EXPECT_EQ(normalize(""), "");
+
+ // Converts long sound mark (ー, U+30FC) alone to ASCII minus
+ EXPECT_EQ(normalize(u"ー"), "-");
+
+ // Converts full-width minus sign (-, U+FF0D) to ASCII minus
+ EXPECT_EQ(normalize(u"-"), "-");
+
+ // Converts full-width period (., U+FF0E) to ASCII dot
+ EXPECT_EQ(normalize(u"."), ".");
+
+ // Converts full-width minus, digits, and period in sequence
+ EXPECT_EQ(normalize(u"ー-01234.56789"), "--01234.56789");
+
+ // Keeps non-numeric ASCII characters unchanged
+ EXPECT_EQ(normalize("abc"), "abc");
+ EXPECT_EQ(normalize("33-4"), "33-4");
+
+ // Keeps unrelated full-width punctuation (/, U+FF0F) unchanged
+ EXPECT_EQ(normalize(u"/"), u"/");
+
+ // Keeps unrelated full-width punctuation (:, U+FF1A) unchanged
+ EXPECT_EQ(normalize(u":"), u":");
+}
+
+} // namespace blink
diff --git a/third_party/blink/renderer/platform/runtime_enabled_features.json5 b/third_party/blink/renderer/platform/runtime_enabled_features.json5
index c0cc7295..908800ab 100644
--- a/third_party/blink/renderer/platform/runtime_enabled_features.json5
+++ b/third_party/blink/renderer/platform/runtime_enabled_features.json5
@@ -3315,6 +3315,10 @@
base_feature: "none",
},
{
+ name: "NumberInputFullWidthChars",
+ status: "stable",
+ },
+ {
name: "ObservableAPI",
status: "stable",
public: true,
diff --git a/third_party/blink/renderer/platform/wtf/text/character_names.h b/third_party/blink/renderer/platform/wtf/text/character_names.h
index a76f36a..73a29ad 100644
--- a/third_party/blink/renderer/platform/wtf/text/character_names.h
+++ b/third_party/blink/renderer/platform/wtf/text/character_names.h
@@ -166,6 +166,7 @@
const UChar kHiraganaLetterSmallACharacter = 0x3041;
const UChar kHiraganaLetterACharacter = 0x3042;
const UChar kKatakanaMiddleDot = 0x30FB;
+const UChar kKatakanaHiraganaProlongedSoundMark = 0x30FC;
// U+6***
const UChar kCjkWaterCharacter = 0x6C34;
@@ -183,7 +184,10 @@
const UChar kZeroWidthNoBreakSpaceCharacter = 0xFEFF;
const UChar kFullwidthExclamationMark = 0xFF01;
const UChar kFullwidthComma = 0xFF0C;
+const UChar kFullwidthHyphenMinus = 0xFF0D;
const UChar kFullwidthFullStop = 0xFF0E;
+const UChar kFullwidthDigitZero = 0xFF10;
+const UChar kFullwidthDigitNine = 0xFF19;
const UChar kFullwidthColon = 0xFF1A;
const UChar kFullwidthSemicolon = 0xFF1B;
const UChar kObjectReplacementCharacter = 0xFFFC;
@@ -278,8 +282,11 @@
using WTF::unicode::kFullstopCharacter;
using WTF::unicode::kFullwidthColon;
using WTF::unicode::kFullwidthComma;
+using WTF::unicode::kFullwidthDigitNine;
+using WTF::unicode::kFullwidthDigitZero;
using WTF::unicode::kFullwidthExclamationMark;
using WTF::unicode::kFullwidthFullStop;
+using WTF::unicode::kFullwidthHyphenMinus;
using WTF::unicode::kFullwidthSemicolon;
using WTF::unicode::kGreekCapitalReversedDottedLunateSigmaSymbol;
using WTF::unicode::kGreekKappaSymbol;
@@ -309,6 +316,7 @@
using WTF::unicode::kIdeographicSpaceCharacter;
using WTF::unicode::kInhibitArabicFormShapingCharacter;
using WTF::unicode::kInhibitSymmetricSwappingCharacter;
+using WTF::unicode::kKatakanaHiraganaProlongedSoundMark;
using WTF::unicode::kKatakanaMiddleDot;
using WTF::unicode::kLatinCapitalLetterIWithDotAbove;
using WTF::unicode::kLatinSmallLetterDotlessI;