Make the focus node on SelectableRegion optional. (#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).
This commit is contained in:
Greg Spencer 2024-11-15 14:39:41 -08:00 committed by GitHub
parent 917b48d942
commit 4d3bbf30c8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 28 additions and 313 deletions

View File

@ -15,7 +15,8 @@ class SelectableRegionExampleApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: SelectionArea(
home: SelectableRegion(
selectionControls: materialTextSelectionControls,
child: Scaffold(
appBar: AppBar(title: const Text('SelectableRegion Sample')),
body: const Center(

View File

@ -109,18 +109,10 @@ class SelectionArea extends StatefulWidget {
/// State for a [SelectionArea].
class SelectionAreaState extends State<SelectionArea> {
FocusNode get _effectiveFocusNode => widget.focusNode ?? (_internalNode ??= FocusNode());
FocusNode? _internalNode;
final GlobalKey<SelectableRegionState> _selectableRegionKey = GlobalKey<SelectableRegionState>();
/// The [State] of the [SelectableRegion] for which this [SelectionArea] wraps.
SelectableRegionState get selectableRegion => _selectableRegionKey.currentState!;
@override
void dispose() {
_internalNode?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
assert(debugCheckHasMaterialLocalizations(context));
@ -133,7 +125,7 @@ class SelectionAreaState extends State<SelectionArea> {
return SelectableRegion(
key: _selectableRegionKey,
selectionControls: controls,
focusNode: _effectiveFocusNode,
focusNode: widget.focusNode,
contextMenuBuilder: widget.contextMenuBuilder,
magnifierConfiguration: widget.magnifierConfiguration ?? TextMagnifier.adaptiveMagnifierConfiguration,
onSelectionChanged: widget.onSelectionChanged,

View File

@ -37,7 +37,6 @@ import 'text_selection.dart';
import 'text_selection_toolbar_anchors.dart';
// Examples can assume:
// FocusNode _focusNode = FocusNode();
// late GlobalKey key;
const Set<PointerDeviceKind> _kLongPressSelectionDevices = <PointerDeviceKind>{
@ -105,7 +104,6 @@ const double _kSelectableVerticalComparingThreshold = 3.0;
/// MaterialApp(
/// home: SelectableRegion(
/// selectionControls: materialTextSelectionControls,
/// focusNode: _focusNode, // initialized to FocusNode()
/// child: Scaffold(
/// appBar: AppBar(title: const Text('Flutter Code Sample')),
/// body: ListView(
@ -218,7 +216,7 @@ class SelectableRegion extends StatefulWidget {
const SelectableRegion({
super.key,
this.contextMenuBuilder,
required this.focusNode,
this.focusNode,
required this.selectionControls,
required this.child,
this.magnifierConfiguration = TextMagnifierConfiguration.disabled,
@ -235,7 +233,7 @@ class SelectableRegion extends StatefulWidget {
final TextMagnifierConfiguration magnifierConfiguration;
/// {@macro flutter.widgets.Focus.focusNode}
final FocusNode focusNode;
final FocusNode? focusNode;
/// The child widget this selection area applies to.
///
@ -373,10 +371,14 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
/// The list of native text processing actions provided by the engine.
final List<ProcessTextAction> _processTextActions = <ProcessTextAction>[];
// The focus node to use if the widget didn't supply one.
FocusNode? _localFocusNode;
FocusNode get _focusNode => widget.focusNode ?? (_localFocusNode ??= FocusNode(debugLabel: 'SelectableRegion'));
@override
void initState() {
super.initState();
widget.focusNode.addListener(_handleFocusChanged);
_focusNode.addListener(_handleFocusChanged);
_initMouseGestureRecognizer();
_initTouchGestureRecognizer();
// Right clicks.
@ -426,9 +428,15 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
void didUpdateWidget(SelectableRegion oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.focusNode != oldWidget.focusNode) {
oldWidget.focusNode.removeListener(_handleFocusChanged);
widget.focusNode.addListener(_handleFocusChanged);
if (widget.focusNode.hasFocus != oldWidget.focusNode.hasFocus) {
if (oldWidget.focusNode == null && widget.focusNode != null) {
_localFocusNode?.removeListener(_handleFocusChanged);
_localFocusNode?.dispose();
_localFocusNode = null;
} else if (widget.focusNode == null && oldWidget.focusNode != null) {
oldWidget.focusNode!.removeListener(_handleFocusChanged);
}
_focusNode.addListener(_handleFocusChanged);
if (_focusNode.hasFocus != oldWidget.focusNode?.hasFocus) {
_handleFocusChanged();
}
}
@ -439,7 +447,7 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
}
void _handleFocusChanged() {
if (!widget.focusNode.hasFocus) {
if (!_focusNode.hasFocus) {
if (kIsWeb) {
PlatformSelectableRegionContextMenu.detach(_selectionDelegate);
}
@ -628,7 +636,7 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
_lastPointerDeviceKind = details.kind;
switch (_getEffectiveConsecutiveTapCount(details.consecutiveTapCount)) {
case 1:
widget.focusNode.requestFocus();
_focusNode.requestFocus();
switch (defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
@ -843,7 +851,7 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
void _handleTouchLongPressStart(LongPressStartDetails details) {
HapticFeedback.selectionClick();
widget.focusNode.requestFocus();
_focusNode.requestFocus();
_selectWordAt(offset: details.globalPosition);
// Platforms besides Android will show the text selection handles when
// the long press is initiated. Android shows the text selection handles when
@ -883,7 +891,7 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
final Offset? previousSecondaryTapDownPosition = _lastSecondaryTapDownPosition;
final bool toolbarIsVisible = _selectionOverlay?.toolbarIsVisible ?? false;
_lastSecondaryTapDownPosition = details.globalPosition;
widget.focusNode.requestFocus();
_focusNode.requestFocus();
switch (defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
@ -1706,6 +1714,9 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
_selectionOverlay?.hideMagnifier();
_selectionOverlay?.dispose();
_selectionOverlay = null;
widget.focusNode?.removeListener(_handleFocusChanged);
_localFocusNode?.removeListener(_handleFocusChanged);
_localFocusNode?.dispose();
super.dispose();
}
@ -1730,9 +1741,9 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
excludeFromSemantics: true,
child: Actions(
actions: _actions,
child: Focus(
child: Focus.withExternalFocusNode(
includeSemantics: false,
focusNode: widget.focusNode,
focusNode: _focusNode,
child: result,
),
),

View File

@ -69,11 +69,8 @@ void main() {
testWidgets('Default text selection color', (WidgetTester tester) async {
final GlobalKey key = GlobalKey();
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
final OverlayEntry overlayEntry = OverlayEntry(
builder: (BuildContext context) => SelectableRegion(
focusNode: focusNode,
selectionControls: emptyTextSelectionControls,
child: Align(
key: key,

File diff suppressed because it is too large Load Diff