From 896c19a292659faddd2d36a1d61785395a0a89d8 Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Tue, 6 Oct 2020 17:02:04 -0700 Subject: [PATCH] Characters docs (#67361) --- .../lib/demo/material/tabs_demo.dart | 2 +- .../flutter/lib/src/cupertino/text_field.dart | 2 ++ packages/flutter/lib/src/material/search.dart | 6 ++++++ .../flutter/lib/src/material/text_field.dart | 10 +++++++++- .../flutter/lib/src/services/text_formatter.dart | 13 +++++++++++-- .../flutter/lib/src/widgets/editable_text.dart | 16 ++++++++++++++++ packages/flutter/lib/src/widgets/navigator.dart | 10 ++++++++++ 7 files changed, 55 insertions(+), 4 deletions(-) diff --git a/dev/integration_tests/flutter_gallery/lib/demo/material/tabs_demo.dart b/dev/integration_tests/flutter_gallery/lib/demo/material/tabs_demo.dart index 9cef2c057e..9f94906c9a 100644 --- a/dev/integration_tests/flutter_gallery/lib/demo/material/tabs_demo.dart +++ b/dev/integration_tests/flutter_gallery/lib/demo/material/tabs_demo.dart @@ -14,7 +14,7 @@ const String _kGalleryAssetsPackage = 'flutter_gallery_assets'; class _Page { _Page({ this.label }); final String label; - String get id => label[0]; + String get id => label.characters.first; @override String toString() => '$runtimeType("$label")'; } diff --git a/packages/flutter/lib/src/cupertino/text_field.dart b/packages/flutter/lib/src/cupertino/text_field.dart index 3c5f527108..bdf1def3cf 100644 --- a/packages/flutter/lib/src/cupertino/text_field.dart +++ b/packages/flutter/lib/src/cupertino/text_field.dart @@ -131,6 +131,8 @@ class _CupertinoTextFieldSelectionGestureDetectorBuilder extends TextSelectionGe /// field (e.g., by pressing a button on the soft keyboard), the text field /// calls the [onSubmitted] callback. /// +/// {@macro flutter.widgets.editableText.complexCharacters} +/// /// To control the text that is displayed in the text field, use the /// [controller]. For example, to set the initial value of the text field, use /// a [controller] that already contains some text such as: diff --git a/packages/flutter/lib/src/material/search.dart b/packages/flutter/lib/src/material/search.dart index 5d9b4a8b82..c3bfa79649 100644 --- a/packages/flutter/lib/src/material/search.dart +++ b/packages/flutter/lib/src/material/search.dart @@ -43,6 +43,9 @@ import 'theme.dart'; /// the [AppBar.leading] position e.g. from the hamburger menu to the back arrow /// used to exit the search page. /// +/// ## Handling emojis and other complex characters +/// {@macro flutter.widgets.editableText.complexCharacters} +/// /// See also: /// /// * [SearchDelegate] to define the content of the search page. @@ -85,6 +88,9 @@ Future showSearch({ /// A given [SearchDelegate] can only be associated with one active [showSearch] /// call. Call [SearchDelegate.close] before re-using the same delegate instance /// for another [showSearch] call. +/// +/// ## Handling emojis and other complex characters +/// {@macro flutter.widgets.editableText.complexCharacters} abstract class SearchDelegate { /// Constructor to be called by subclasses which may specify [searchFieldLabel], [keyboardType] and/or diff --git a/packages/flutter/lib/src/material/text_field.dart b/packages/flutter/lib/src/material/text_field.dart index 0668f8738f..4f76dcc5af 100644 --- a/packages/flutter/lib/src/material/text_field.dart +++ b/packages/flutter/lib/src/material/text_field.dart @@ -227,7 +227,7 @@ class _TextFieldSelectionGestureDetectorBuilder extends TextSelectionGestureDete /// builder: (BuildContext context) { /// return AlertDialog( /// title: const Text('Thanks!'), -/// content: Text ('You typed "$value".'), +/// content: Text ('You typed "$value", which has length ${value.characters.length}.'), /// actions: [ /// TextButton( /// onPressed: () { Navigator.pop(context); }, @@ -256,6 +256,14 @@ class _TextFieldSelectionGestureDetectorBuilder extends TextSelectionGestureDete /// /// Keep in mind you can also always read the current string from a TextField's /// [TextEditingController] using [TextEditingController.text]. +/// + /// ## Handling emojis and other complex characters +/// {@macro flutter.widgets.editableText.complexCharacters} +/// +/// In the live Dartpad example above, try typing the emoji 👨‍👩‍👦 +/// into the field and submitting. Because the example code measures the length +/// with `value.characters.length`, the emoji is correctly counted as a single +/// character. /// /// See also: /// diff --git a/packages/flutter/lib/src/services/text_formatter.dart b/packages/flutter/lib/src/services/text_formatter.dart index 03a0078a07..6eadddbc1f 100644 --- a/packages/flutter/lib/src/services/text_formatter.dart +++ b/packages/flutter/lib/src/services/text_formatter.dart @@ -25,6 +25,9 @@ import 'text_input.dart'; /// To create custom formatters, extend the [TextInputFormatter] class and /// implement the [formatEditUpdate] method. /// +/// ## Handling emojis and other complex characters +/// {@macro flutter.widgets.editableText.complexCharacters} +/// /// See also: /// /// * [EditableText] on which the formatting apply. @@ -300,13 +303,19 @@ class WhitelistingTextInputFormatter extends FilteringTextInputFormatter { } /// A [TextInputFormatter] that prevents the insertion of more characters -/// (currently defined as Unicode scalar values) than allowed. +/// than allowed. /// /// Since this formatter only prevents new characters from being added to the /// text, it preserves the existing [TextEditingValue.selection]. /// +/// Characters are counted as user-perceived characters using the +/// [characters](https://pub.dev/packages/characters) package, so even complex +/// characters like extended grapheme clusters and surrogate pairs are counted +/// as single characters. +/// +/// See also: /// * [maxLength], which discusses the precise meaning of "number of -/// characters" and how it may differ from the intuitive meaning. +/// characters". class LengthLimitingTextInputFormatter extends TextInputFormatter { /// Creates a formatter that prevents the insertion of more characters than a /// limit. diff --git a/packages/flutter/lib/src/widgets/editable_text.dart b/packages/flutter/lib/src/widgets/editable_text.dart index 1ba70495bd..6608428a5a 100644 --- a/packages/flutter/lib/src/widgets/editable_text.dart +++ b/packages/flutter/lib/src/widgets/editable_text.dart @@ -946,6 +946,22 @@ class EditableText extends StatefulWidget { /// {@end-tool} /// {@endtemplate} /// + /// ## Handling emojis and other complex characters + /// {@template flutter.widgets.editableText.complexCharacters} + /// It's important to always use + /// [characters](https://pub.dev/packages/characters) when dealing with user + /// input text that may contain complex characters. This will ensure that + /// extended grapheme clusters and surrogate pairs are treated as single + /// characters, as they appear to the user. + /// + /// For example, when finding the length of some user input, use + /// `string.characters.length`. Do NOT use `string.length` or even + /// `string.runes.length`. For the complex character "👨‍👩‍👦", this + /// appears to the user as a single character, and `string.characters.length` + /// intuitively returns 1. On the other hand, `string.length` returns 8, and + /// `string.runes.length` returns 5! + /// {@endtemplate} + /// /// See also: /// /// * [inputFormatters], which are called before [onChanged] diff --git a/packages/flutter/lib/src/widgets/navigator.dart b/packages/flutter/lib/src/widgets/navigator.dart index 512336f901..8e278486af 100644 --- a/packages/flutter/lib/src/widgets/navigator.dart +++ b/packages/flutter/lib/src/widgets/navigator.dart @@ -1571,6 +1571,16 @@ class Navigator extends StatefulWidget { /// The [NavigatorState] and [initialRoute] will be passed to the callback. /// The callback must return a list of [Route] objects with which the history /// will be primed. + /// + /// When parsing the initialRoute, if there's any chance that the it may + /// contain complex characters, it's best to use the + /// [characters](https://pub.dev/packages/characters) API. This will ensure + /// that extended grapheme clusters and surrogate pairs are treated as single + /// characters by the code, the same way that they appear to the user. For + /// example, the string "👨‍👩‍👦" appears to the user as a single + /// character and `string.characters.length` intuitively returns 1. On the + /// other hand, `string.length` returns 8, and `string.runes.length` returns + /// 5! final RouteListFactory onGenerateInitialRoutes; /// Whether this navigator should report route update message back to the