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;