TextField FocusNode attach location change

Summary

EditableText.focusNode is now attached to a dedicated Focus widget below EditableText.

Context

A text input field widget (TextField, for example) typically owns a FocusNode. When that FocusNode is the primary focus of the app, events (such as key presses) are sent to the BuildContext to which the FocusNode is attached.

The FocusNode also plays a roll in shortcut handling: The Shortcuts widget translates key sequences into an Intent, and tries to find the first suitable handler for that Intent starting from the BuildContext to which the FocusNode is attached, to the root of the widget tree. This means an Actions widget (that provides handlers for different Intents) won’t be able to handle any shortcut Intents when the BuildContext that has the primary focus is above it in the tree.

Previously for EditableText, the FocusNode was attached to the BuildContext of EditableTextState. Any Actions widgets defined in EditableTextState (which will be inflated below the BuildContext of the EditableTextState) couldn’t handle shortcuts even when that EditableText was focused, for the reason stated above.

Description of change

EditableTextState now creates a dedicated Focus widget to host EditableText.focusNode. This allows EditableTextStates to define handlers for shortcut Intents. For instance, EditableText now has a handler that handles the “deleteCharacter” intent when the DEL key is pressed.

This change does not involve any public API changes but breaks codebases relying on that particular implementation detail to tell if a FocusNode is associated with a text input field.

This change does not break any builds but can introduce runtime issues, or cause existing tests to fail.

Migration guide

The EditableText widget takes a FocusNode as a parameter, which was previously attached to its EditableText’s BuildContext. If you are relying on runtime typecheck to find out if a FocusNode is attached to a text input field or a selectable text field like so:

  • focusNode.context.widget is EditableText
  • (focusNode.context as StatefulElement).state as EditableTextState

Then please read on and consider following the migration steps to avoid breakages.

If you’re not sure whether a codebase needs migration, search for is EditableText, as EditableText, is EditableTextState, and as EditableTextState and verify if any of the search results are doing a typecheck or typecast on a FocusNode.context. If so, then migration is needed.

To avoid performing a typecheck, or downcasting the BuildContext associated with the FocusNode of interest, and depending on the actual capabilities the codebase is trying to invoke from the given FocusNode, fire an Intent from that BuildContext. For instance, if you wish to update the text of the currently focused TextField to a specific value, see the following example:

Code before migration:

final Widget? focusedWidget = primaryFocus?.context?.widget;
if (focusedWidget is EditableText) {
  widget.controller.text = 'Updated Text';
}

Code after migration:

final BuildContext? focusedContext = primaryFocus?.context;
if (focusedContext != null) {
  Actions.maybeInvoke(focusedContext, ReplaceTextIntent('UpdatedText'));
}

For a comprehensive list of Intents supported by the EditableText widget, refer to the documentation of the EditableText widget.

Timeline

Landed in version: 2.6.0-12.0.pre
In stable release: 2.10.0

References

API documentation:

Relevant PR: