Skip to content

Commit 4d3bbf3

Browse files
authored
Make the focus node on SelectableRegion optional. (flutter#158994)
## Description This makes the `focusNode` for `SelectableRegion` optional so that: - Users of the widget are no longer required to use `SelectableRegion` from within a `StatefulWidget` - They aren't likely to forget to dispose of a node they didn't supply. - Simpler to use, and the node is not used very often anyhow. Also made the `SelectableRegion` sample actually use `SelectableRegion`. ## Tests - Modified all the `SelectableRegion` tests to remove 3 identical lines of boilerplate from each (except 2, which actually used their focus nodes).
1 parent 917b48d commit 4d3bbf3

File tree

5 files changed

+28
-313
lines changed

5 files changed

+28
-313
lines changed

examples/api/lib/material/selectable_region/selectable_region.0.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ class SelectableRegionExampleApp extends StatelessWidget {
1515
@override
1616
Widget build(BuildContext context) {
1717
return MaterialApp(
18-
home: SelectionArea(
18+
home: SelectableRegion(
19+
selectionControls: materialTextSelectionControls,
1920
child: Scaffold(
2021
appBar: AppBar(title: const Text('SelectableRegion Sample')),
2122
body: const Center(

packages/flutter/lib/src/material/selection_area.dart

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -109,18 +109,10 @@ class SelectionArea extends StatefulWidget {
109109

110110
/// State for a [SelectionArea].
111111
class SelectionAreaState extends State<SelectionArea> {
112-
FocusNode get _effectiveFocusNode => widget.focusNode ?? (_internalNode ??= FocusNode());
113-
FocusNode? _internalNode;
114112
final GlobalKey<SelectableRegionState> _selectableRegionKey = GlobalKey<SelectableRegionState>();
115113
/// The [State] of the [SelectableRegion] for which this [SelectionArea] wraps.
116114
SelectableRegionState get selectableRegion => _selectableRegionKey.currentState!;
117115

118-
@override
119-
void dispose() {
120-
_internalNode?.dispose();
121-
super.dispose();
122-
}
123-
124116
@override
125117
Widget build(BuildContext context) {
126118
assert(debugCheckHasMaterialLocalizations(context));
@@ -133,7 +125,7 @@ class SelectionAreaState extends State<SelectionArea> {
133125
return SelectableRegion(
134126
key: _selectableRegionKey,
135127
selectionControls: controls,
136-
focusNode: _effectiveFocusNode,
128+
focusNode: widget.focusNode,
137129
contextMenuBuilder: widget.contextMenuBuilder,
138130
magnifierConfiguration: widget.magnifierConfiguration ?? TextMagnifier.adaptiveMagnifierConfiguration,
139131
onSelectionChanged: widget.onSelectionChanged,

packages/flutter/lib/src/widgets/selectable_region.dart

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ import 'text_selection.dart';
3737
import 'text_selection_toolbar_anchors.dart';
3838

3939
// Examples can assume:
40-
// FocusNode _focusNode = FocusNode();
4140
// late GlobalKey key;
4241

4342
const Set<PointerDeviceKind> _kLongPressSelectionDevices = <PointerDeviceKind>{
@@ -105,7 +104,6 @@ const double _kSelectableVerticalComparingThreshold = 3.0;
105104
/// MaterialApp(
106105
/// home: SelectableRegion(
107106
/// selectionControls: materialTextSelectionControls,
108-
/// focusNode: _focusNode, // initialized to FocusNode()
109107
/// child: Scaffold(
110108
/// appBar: AppBar(title: const Text('Flutter Code Sample')),
111109
/// body: ListView(
@@ -218,7 +216,7 @@ class SelectableRegion extends StatefulWidget {
218216
const SelectableRegion({
219217
super.key,
220218
this.contextMenuBuilder,
221-
required this.focusNode,
219+
this.focusNode,
222220
required this.selectionControls,
223221
required this.child,
224222
this.magnifierConfiguration = TextMagnifierConfiguration.disabled,
@@ -235,7 +233,7 @@ class SelectableRegion extends StatefulWidget {
235233
final TextMagnifierConfiguration magnifierConfiguration;
236234

237235
/// {@macro flutter.widgets.Focus.focusNode}
238-
final FocusNode focusNode;
236+
final FocusNode? focusNode;
239237

240238
/// The child widget this selection area applies to.
241239
///
@@ -373,10 +371,14 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
373371
/// The list of native text processing actions provided by the engine.
374372
final List<ProcessTextAction> _processTextActions = <ProcessTextAction>[];
375373

374+
// The focus node to use if the widget didn't supply one.
375+
FocusNode? _localFocusNode;
376+
FocusNode get _focusNode => widget.focusNode ?? (_localFocusNode ??= FocusNode(debugLabel: 'SelectableRegion'));
377+
376378
@override
377379
void initState() {
378380
super.initState();
379-
widget.focusNode.addListener(_handleFocusChanged);
381+
_focusNode.addListener(_handleFocusChanged);
380382
_initMouseGestureRecognizer();
381383
_initTouchGestureRecognizer();
382384
// Right clicks.
@@ -426,9 +428,15 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
426428
void didUpdateWidget(SelectableRegion oldWidget) {
427429
super.didUpdateWidget(oldWidget);
428430
if (widget.focusNode != oldWidget.focusNode) {
429-
oldWidget.focusNode.removeListener(_handleFocusChanged);
430-
widget.focusNode.addListener(_handleFocusChanged);
431-
if (widget.focusNode.hasFocus != oldWidget.focusNode.hasFocus) {
431+
if (oldWidget.focusNode == null && widget.focusNode != null) {
432+
_localFocusNode?.removeListener(_handleFocusChanged);
433+
_localFocusNode?.dispose();
434+
_localFocusNode = null;
435+
} else if (widget.focusNode == null && oldWidget.focusNode != null) {
436+
oldWidget.focusNode!.removeListener(_handleFocusChanged);
437+
}
438+
_focusNode.addListener(_handleFocusChanged);
439+
if (_focusNode.hasFocus != oldWidget.focusNode?.hasFocus) {
432440
_handleFocusChanged();
433441
}
434442
}
@@ -439,7 +447,7 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
439447
}
440448

441449
void _handleFocusChanged() {
442-
if (!widget.focusNode.hasFocus) {
450+
if (!_focusNode.hasFocus) {
443451
if (kIsWeb) {
444452
PlatformSelectableRegionContextMenu.detach(_selectionDelegate);
445453
}
@@ -628,7 +636,7 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
628636
_lastPointerDeviceKind = details.kind;
629637
switch (_getEffectiveConsecutiveTapCount(details.consecutiveTapCount)) {
630638
case 1:
631-
widget.focusNode.requestFocus();
639+
_focusNode.requestFocus();
632640
switch (defaultTargetPlatform) {
633641
case TargetPlatform.android:
634642
case TargetPlatform.fuchsia:
@@ -843,7 +851,7 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
843851

844852
void _handleTouchLongPressStart(LongPressStartDetails details) {
845853
HapticFeedback.selectionClick();
846-
widget.focusNode.requestFocus();
854+
_focusNode.requestFocus();
847855
_selectWordAt(offset: details.globalPosition);
848856
// Platforms besides Android will show the text selection handles when
849857
// the long press is initiated. Android shows the text selection handles when
@@ -883,7 +891,7 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
883891
final Offset? previousSecondaryTapDownPosition = _lastSecondaryTapDownPosition;
884892
final bool toolbarIsVisible = _selectionOverlay?.toolbarIsVisible ?? false;
885893
_lastSecondaryTapDownPosition = details.globalPosition;
886-
widget.focusNode.requestFocus();
894+
_focusNode.requestFocus();
887895
switch (defaultTargetPlatform) {
888896
case TargetPlatform.android:
889897
case TargetPlatform.fuchsia:
@@ -1706,6 +1714,9 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
17061714
_selectionOverlay?.hideMagnifier();
17071715
_selectionOverlay?.dispose();
17081716
_selectionOverlay = null;
1717+
widget.focusNode?.removeListener(_handleFocusChanged);
1718+
_localFocusNode?.removeListener(_handleFocusChanged);
1719+
_localFocusNode?.dispose();
17091720
super.dispose();
17101721
}
17111722

@@ -1730,9 +1741,9 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
17301741
excludeFromSemantics: true,
17311742
child: Actions(
17321743
actions: _actions,
1733-
child: Focus(
1744+
child: Focus.withExternalFocusNode(
17341745
includeSemantics: false,
1735-
focusNode: widget.focusNode,
1746+
focusNode: _focusNode,
17361747
child: result,
17371748
),
17381749
),

packages/flutter/test/widgets/default_colors_test.dart

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,8 @@ void main() {
6969

7070
testWidgets('Default text selection color', (WidgetTester tester) async {
7171
final GlobalKey key = GlobalKey();
72-
final FocusNode focusNode = FocusNode();
73-
addTearDown(focusNode.dispose);
7472
final OverlayEntry overlayEntry = OverlayEntry(
7573
builder: (BuildContext context) => SelectableRegion(
76-
focusNode: focusNode,
7774
selectionControls: emptyTextSelectionControls,
7875
child: Align(
7976
key: key,

0 commit comments

Comments
 (0)