1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'package:flutter/foundation.dart';
6import 'package:flutter/gestures.dart';
7import 'package:flutter/rendering.dart';
8
9import 'editable_text.dart';
10import 'framework.dart';
11
12// Enable if you want verbose logging about tap region changes.
13const bool _kDebugTapRegion = false;
14
15bool _tapRegionDebug(String message, [Iterable<String>? details]) {
16 if (_kDebugTapRegion) {
17 debugPrint('TAP REGION: $message');
18 if (details != null && details.isNotEmpty) {
19 for (final String detail in details) {
20 debugPrint(' $detail');
21 }
22 }
23 }
24 // Return true so that it can be easily used inside of an assert.
25 return true;
26}
27
28/// The type of callback that [TapRegion.onTapOutside] and
29/// [TapRegion.onTapInside] take.
30///
31/// The event is the pointer event that caused the callback to be called.
32typedef TapRegionCallback = void Function(PointerDownEvent event);
33
34/// An interface for registering and unregistering a [RenderTapRegion]
35/// (typically created with a [TapRegion] widget) with a
36/// [RenderTapRegionSurface] (typically created with a [TapRegionSurface]
37/// widget).
38abstract class TapRegionRegistry {
39 /// Register the given [RenderTapRegion] with the registry.
40 void registerTapRegion(RenderTapRegion region);
41
42 /// Unregister the given [RenderTapRegion] with the registry.
43 void unregisterTapRegion(RenderTapRegion region);
44
45 /// Allows finding of the nearest [TapRegionRegistry], such as a
46 /// [RenderTapRegionSurface].
47 ///
48 /// Will throw if a [TapRegionRegistry] isn't found.
49 static TapRegionRegistry of(BuildContext context) {
50 final TapRegionRegistry? registry = maybeOf(context);
51 assert(() {
52 if (registry == null) {
53 throw FlutterError(
54 'TapRegionRegistry.of() was called with a context that does not contain a TapRegionSurface widget.\n'
55 'No TapRegionSurface widget ancestor could be found starting from the context that was passed to '
56 'TapRegionRegistry.of().\n'
57 'The context used was:\n'
58 ' $context',
59 );
60 }
61 return true;
62 }());
63 return registry!;
64 }
65
66 /// Allows finding of the nearest [TapRegionRegistry], such as a
67 /// [RenderTapRegionSurface].
68 static TapRegionRegistry? maybeOf(BuildContext context) {
69 return context.findAncestorRenderObjectOfType<RenderTapRegionSurface>();
70 }
71}
72
73/// A widget that provides notification of a tap inside or outside of a set of
74/// registered regions, without participating in the [gesture
75/// disambiguation](https://flutter.dev/gestures/#gesture-disambiguation)
76/// system.
77///
78/// The regions are defined by adding [TapRegion] widgets to the widget tree
79/// around the regions of interest, and they will register with this
80/// [TapRegionSurface]. Each of the tap regions can optionally belong to a group
81/// by assigning a [TapRegion.groupId], where all the regions with the same
82/// groupId act as if they were all one region.
83///
84/// When a tap outside of a registered region or region group is detected, its
85/// [TapRegion.onTapOutside] callback is called. If the tap is outside one
86/// member of a group, but inside another, no notification is made.
87///
88/// When a tap inside of a registered region or region group is detected, its
89/// [TapRegion.onTapInside] callback is called. If the tap is inside one member
90/// of a group, all members are notified.
91///
92/// The [TapRegionSurface] should be defined at the highest level needed to
93/// encompass the entire area where taps should be monitored. This is typically
94/// around the entire app. If the entire app isn't covered, then taps outside of
95/// the [TapRegionSurface] will be ignored and no [TapRegion.onTapOutside] calls
96/// will be made for those events. The [WidgetsApp], [MaterialApp] and
97/// [CupertinoApp] automatically include a [TapRegionSurface] around their
98/// entire app.
99///
100/// [TapRegionSurface] does not participate in the [gesture
101/// disambiguation](https://flutter.dev/gestures/#gesture-disambiguation)
102/// system, so if multiple [TapRegionSurface]s are active at the same time, they
103/// will all fire, and so will any other gestures recognized by a
104/// [GestureDetector] or other pointer event handlers.
105///
106/// [TapRegion]s register only with the nearest ancestor [TapRegionSurface].
107///
108/// See also:
109///
110/// * [RenderTapRegionSurface], the render object that is inserted into the
111/// render tree by this widget.
112/// * <https://flutter.dev/gestures/#gesture-disambiguation> for more
113/// information about the gesture system and how it disambiguates inputs.
114class TapRegionSurface extends SingleChildRenderObjectWidget {
115 /// Creates a const [RenderTapRegionSurface].
116 ///
117 /// The [child] attribute is required.
118 const TapRegionSurface({
119 super.key,
120 required Widget super.child,
121 });
122
123 @override
124 RenderObject createRenderObject(BuildContext context) {
125 return RenderTapRegionSurface();
126 }
127
128 @override
129 void updateRenderObject(
130 BuildContext context,
131 RenderProxyBoxWithHitTestBehavior renderObject,
132 ) {}
133}
134
135/// A render object that provides notification of a tap inside or outside of a
136/// set of registered regions, without participating in the [gesture
137/// disambiguation](https://flutter.dev/gestures/#gesture-disambiguation) system
138/// (other than to consume tap down events if [TapRegion.consumeOutsideTaps] is
139/// true).
140///
141/// The regions are defined by adding [RenderTapRegion] render objects in the
142/// render tree around the regions of interest, and they will register with this
143/// [RenderTapRegionSurface]. Each of the tap regions can optionally belong to a
144/// group by assigning a [RenderTapRegion.groupId], where all the regions with
145/// the same groupId act as if they were all one region.
146///
147/// When a tap outside of a registered region or region group is detected, its
148/// [TapRegion.onTapOutside] callback is called. If the tap is outside one
149/// member of a group, but inside another, no notification is made.
150///
151/// When a tap inside of a registered region or region group is detected, its
152/// [TapRegion.onTapInside] callback is called. If the tap is inside one member
153/// of a group, all members are notified.
154///
155/// The [RenderTapRegionSurface] should be defined at the highest level needed
156/// to encompass the entire area where taps should be monitored. This is
157/// typically around the entire app. If the entire app isn't covered, then taps
158/// outside of the [RenderTapRegionSurface] will be ignored and no
159/// [RenderTapRegion.onTapOutside] calls will be made for those events. The
160/// [WidgetsApp], [MaterialApp] and [CupertinoApp] automatically include a
161/// [RenderTapRegionSurface] around the entire app.
162///
163/// [RenderTapRegionSurface] does not participate in the [gesture
164/// disambiguation](https://flutter.dev/gestures/#gesture-disambiguation)
165/// system, so if multiple [RenderTapRegionSurface]s are active at the same
166/// time, they will all fire, and so will any other gestures recognized by a
167/// [GestureDetector] or other pointer event handlers.
168///
169/// [RenderTapRegion]s register only with the nearest ancestor
170/// [RenderTapRegionSurface].
171///
172/// See also:
173///
174/// * [TapRegionSurface], a widget that inserts a [RenderTapRegionSurface] into
175/// the render tree.
176/// * [TapRegionRegistry.of], which can find the nearest ancestor
177/// [RenderTapRegionSurface], which is a [TapRegionRegistry].
178class RenderTapRegionSurface extends RenderProxyBoxWithHitTestBehavior implements TapRegionRegistry {
179 final Expando<BoxHitTestResult> _cachedResults = Expando<BoxHitTestResult>();
180 final Set<RenderTapRegion> _registeredRegions = <RenderTapRegion>{};
181 final Map<Object?, Set<RenderTapRegion>> _groupIdToRegions = <Object?, Set<RenderTapRegion>>{};
182
183 @override
184 void registerTapRegion(RenderTapRegion region) {
185 assert(_tapRegionDebug('Region $region registered.'));
186 assert(!_registeredRegions.contains(region));
187 _registeredRegions.add(region);
188 if (region.groupId != null) {
189 _groupIdToRegions[region.groupId] ??= <RenderTapRegion>{};
190 _groupIdToRegions[region.groupId]!.add(region);
191 }
192 }
193
194 @override
195 void unregisterTapRegion(RenderTapRegion region) {
196 assert(_tapRegionDebug('Region $region unregistered.'));
197 assert(_registeredRegions.contains(region));
198 _registeredRegions.remove(region);
199 if (region.groupId != null) {
200 assert(_groupIdToRegions.containsKey(region.groupId));
201 _groupIdToRegions[region.groupId]!.remove(region);
202 if (_groupIdToRegions[region.groupId]!.isEmpty) {
203 _groupIdToRegions.remove(region.groupId);
204 }
205 }
206 }
207
208 @override
209 bool hitTest(BoxHitTestResult result, {required Offset position}) {
210 if (!size.contains(position)) {
211 return false;
212 }
213
214 final bool hitTarget = hitTestChildren(result, position: position) || hitTestSelf(position);
215
216 if (hitTarget) {
217 final BoxHitTestEntry entry = BoxHitTestEntry(this, position);
218 _cachedResults[entry] = result;
219 result.add(entry);
220 }
221
222 return hitTarget;
223 }
224
225 @override
226 void handleEvent(PointerEvent event, HitTestEntry entry) {
227 assert(debugHandleEvent(event, entry));
228 assert(() {
229 for (final RenderTapRegion region in _registeredRegions) {
230 if (!region.enabled) {
231 return false;
232 }
233 }
234 return true;
235 }(), 'A RenderTapRegion was registered when it was disabled.');
236
237 if (event is! PointerDownEvent) {
238 return;
239 }
240
241 if (_registeredRegions.isEmpty) {
242 assert(_tapRegionDebug('Ignored tap event because no regions are registered.'));
243 return;
244 }
245
246 final BoxHitTestResult? result = _cachedResults[entry];
247
248 if (result == null) {
249 assert(_tapRegionDebug('Ignored tap event because no surface descendants were hit.'));
250 return;
251 }
252
253 // A child was hit, so we need to call onTapOutside for those regions or
254 // groups of regions that were not hit.
255 final Set<RenderTapRegion> hitRegions =
256 _getRegionsHit(_registeredRegions, result.path).cast<RenderTapRegion>().toSet();
257 final Set<RenderTapRegion> insideRegions = <RenderTapRegion>{};
258 assert(_tapRegionDebug('Tap event hit ${hitRegions.length} descendants.'));
259
260 for (final RenderTapRegion region in hitRegions) {
261 if (region.groupId == null) {
262 insideRegions.add(region);
263 continue;
264 }
265 // Add all grouped regions to the insideRegions so that groups act as a
266 // single region.
267 insideRegions.addAll(_groupIdToRegions[region.groupId]!);
268 }
269 // If they're not inside, then they're outside.
270 final Set<RenderTapRegion> outsideRegions = _registeredRegions.difference(insideRegions);
271
272 bool consumeOutsideTaps = false;
273 for (final RenderTapRegion region in outsideRegions) {
274 assert(_tapRegionDebug('Calling onTapOutside for $region'));
275 if (region.consumeOutsideTaps) {
276 assert(_tapRegionDebug('Stopping tap propagation for $region (and all of ${region.groupId})'));
277 consumeOutsideTaps = true;
278 }
279 region.onTapOutside?.call(event);
280 }
281 for (final RenderTapRegion region in insideRegions) {
282 assert(_tapRegionDebug('Calling onTapInside for $region'));
283 region.onTapInside?.call(event);
284 }
285
286 // If any of the "outside" regions have consumeOutsideTaps set, then stop
287 // the propagation of the event through the gesture recognizer by adding it
288 // to the recognizer and immediately resolving it.
289 if (consumeOutsideTaps) {
290 GestureBinding.instance.gestureArena.add(event.pointer, _DummyTapRecognizer()).resolve(GestureDisposition.accepted);
291 }
292 }
293
294 // Returns the registered regions that are in the hit path.
295 Iterable<HitTestTarget> _getRegionsHit(Set<RenderTapRegion> detectors, Iterable<HitTestEntry> hitTestPath) {
296 final Set<HitTestTarget> hitRegions = <HitTestTarget>{};
297 for (final HitTestEntry<HitTestTarget> entry in hitTestPath) {
298 final HitTestTarget target = entry.target;
299 if (_registeredRegions.contains(target)) {
300 hitRegions.add(target);
301 }
302 }
303 return hitRegions;
304 }
305}
306
307// A dummy tap recognizer so that we don't have to deal with the lifecycle of
308// TapGestureRecognizer, since we're just going to immediately resolve it
309// anyhow.
310class _DummyTapRecognizer extends GestureArenaMember {
311 @override
312 void acceptGesture(int pointer) { }
313
314 @override
315 void rejectGesture(int pointer) { }
316}
317
318/// A widget that defines a region that can detect taps inside or outside of
319/// itself and any group of regions it belongs to, without participating in the
320/// [gesture
321/// disambiguation](https://flutter.dev/gestures/#gesture-disambiguation) system
322/// (other than to consume tap down events if [consumeOutsideTaps] is true).
323///
324/// This widget indicates to the nearest ancestor [TapRegionSurface] that the
325/// region occupied by its child will participate in the tap detection for that
326/// surface.
327///
328/// If this region belongs to a group (by virtue of its [groupId]), all the
329/// regions in the group will act as one.
330///
331/// If there is no [TapRegionSurface] ancestor, [TapRegion] will do nothing.
332class TapRegion extends SingleChildRenderObjectWidget {
333 /// Creates a const [TapRegion].
334 ///
335 /// The [child] argument is required.
336 const TapRegion({
337 super.key,
338 required super.child,
339 this.enabled = true,
340 this.behavior = HitTestBehavior.deferToChild,
341 this.onTapOutside,
342 this.onTapInside,
343 this.groupId,
344 this.consumeOutsideTaps = false,
345 String? debugLabel,
346 }) : debugLabel = kReleaseMode ? null : debugLabel;
347
348 /// Whether or not this [TapRegion] is enabled as part of the composite region.
349 final bool enabled;
350
351 /// How to behave during hit testing when deciding how the hit test propagates
352 /// to children and whether to consider targets behind this [TapRegion].
353 ///
354 /// Defaults to [HitTestBehavior.deferToChild].
355 ///
356 /// See [HitTestBehavior] for the allowed values and their meanings.
357 final HitTestBehavior behavior;
358
359 /// A callback to be invoked when a tap is detected outside of this
360 /// [TapRegion] and any other region with the same [groupId], if any.
361 ///
362 /// The [PointerDownEvent] passed to the function is the event that caused the
363 /// notification. If this region is part of a group (i.e. [groupId] is set),
364 /// then it's possible that the event may be outside of this immediate region,
365 /// although it will be within the region of one of the group members.
366 final TapRegionCallback? onTapOutside;
367
368 /// A callback to be invoked when a tap is detected inside of this
369 /// [TapRegion], or any other tap region with the same [groupId], if any.
370 ///
371 /// The [PointerDownEvent] passed to the function is the event that caused the
372 /// notification. If this region is part of a group (i.e. [groupId] is set),
373 /// then it's possible that the event may be outside of this immediate region,
374 /// although it will be within the region of one of the group members.
375 final TapRegionCallback? onTapInside;
376
377 /// An optional group ID that groups [TapRegion]s together so that they
378 /// operate as one region. If any member of a group is hit by a particular
379 /// tap, then the [onTapOutside] will not be called for any members of the
380 /// group. If any member of the group is hit, then all members will have their
381 /// [onTapInside] called.
382 ///
383 /// If the group id is null, then only this region is hit tested.
384 final Object? groupId;
385
386 /// If true, then the group that this region belongs to will stop the
387 /// propagation of the tap down event in the gesture arena.
388 ///
389 /// This is useful if you want to block the tap down from being given to a
390 /// [GestureDetector] when [onTapOutside] is called.
391 ///
392 /// If other [TapRegion]s with the same [groupId] have [consumeOutsideTaps]
393 /// set to false, but this one is true, then this one will take precedence,
394 /// and the event will be consumed.
395 ///
396 /// Defaults to false.
397 final bool consumeOutsideTaps;
398
399 /// An optional debug label to help with debugging in debug mode.
400 ///
401 /// Will be null in release mode.
402 final String? debugLabel;
403
404 @override
405 RenderObject createRenderObject(BuildContext context) {
406 return RenderTapRegion(
407 registry: TapRegionRegistry.maybeOf(context),
408 enabled: enabled,
409 consumeOutsideTaps: consumeOutsideTaps,
410 behavior: behavior,
411 onTapOutside: onTapOutside,
412 onTapInside: onTapInside,
413 groupId: groupId,
414 debugLabel: debugLabel,
415 );
416 }
417
418 @override
419 void updateRenderObject(BuildContext context, covariant RenderTapRegion renderObject) {
420 renderObject
421 ..registry = TapRegionRegistry.maybeOf(context)
422 ..enabled = enabled
423 ..behavior = behavior
424 ..groupId = groupId
425 ..onTapOutside = onTapOutside
426 ..onTapInside = onTapInside;
427 if (!kReleaseMode) {
428 renderObject.debugLabel = debugLabel;
429 }
430 }
431
432 @override
433 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
434 super.debugFillProperties(properties);
435 properties.add(FlagProperty('enabled', value: enabled, ifFalse: 'DISABLED', defaultValue: true));
436 properties.add(DiagnosticsProperty<HitTestBehavior>('behavior', behavior, defaultValue: HitTestBehavior.deferToChild));
437 properties.add(DiagnosticsProperty<Object?>('debugLabel', debugLabel, defaultValue: null));
438 properties.add(DiagnosticsProperty<Object?>('groupId', groupId, defaultValue: null));
439 }
440}
441
442/// A render object that defines a region that can detect taps inside or outside
443/// of itself and any group of regions it belongs to, without participating in
444/// the [gesture
445/// disambiguation](https://flutter.dev/gestures/#gesture-disambiguation)
446/// system.
447///
448/// This render object indicates to the nearest ancestor [TapRegionSurface] that
449/// the region occupied by its child (or itself if [behavior] is
450/// [HitTestBehavior.opaque]) will participate in the tap detection for that
451/// surface.
452///
453/// If this region belongs to a group (by virtue of its [groupId]), all the
454/// regions in the group will act as one.
455///
456/// If there is no [RenderTapRegionSurface] ancestor in the render tree,
457/// [RenderTapRegion] will do nothing.
458///
459/// The [behavior] attribute describes how to behave during hit testing when
460/// deciding how the hit test propagates to children and whether to consider
461/// targets behind the tap region. Defaults to [HitTestBehavior.deferToChild].
462/// See [HitTestBehavior] for the allowed values and their meanings.
463///
464/// See also:
465///
466/// * [TapRegion], a widget that inserts a [RenderTapRegion] into the render
467/// tree.
468class RenderTapRegion extends RenderProxyBoxWithHitTestBehavior {
469 /// Creates a [RenderTapRegion].
470 RenderTapRegion({
471 TapRegionRegistry? registry,
472 bool enabled = true,
473 bool consumeOutsideTaps = false,
474 this.onTapOutside,
475 this.onTapInside,
476 super.behavior = HitTestBehavior.deferToChild,
477 Object? groupId,
478 String? debugLabel,
479 }) : _registry = registry,
480 _enabled = enabled,
481 _consumeOutsideTaps = consumeOutsideTaps,
482 _groupId = groupId,
483 debugLabel = kReleaseMode ? null : debugLabel;
484
485 bool _isRegistered = false;
486
487 /// A callback to be invoked when a tap is detected outside of this
488 /// [RenderTapRegion] and any other region with the same [groupId], if any.
489 ///
490 /// The [PointerDownEvent] passed to the function is the event that caused the
491 /// notification. If this region is part of a group (i.e. [groupId] is set),
492 /// then it's possible that the event may be outside of this immediate region,
493 /// although it will be within the region of one of the group members.
494 TapRegionCallback? onTapOutside;
495
496 /// A callback to be invoked when a tap is detected inside of this
497 /// [RenderTapRegion], or any other tap region with the same [groupId], if any.
498 ///
499 /// The [PointerDownEvent] passed to the function is the event that caused the
500 /// notification. If this region is part of a group (i.e. [groupId] is set),
501 /// then it's possible that the event may be outside of this immediate region,
502 /// although it will be within the region of one of the group members.
503 TapRegionCallback? onTapInside;
504
505 /// A label used in debug builds. Will be null in release builds.
506 String? debugLabel;
507
508 /// Whether or not this region should participate in the composite region.
509 bool get enabled => _enabled;
510 bool _enabled;
511 set enabled(bool value) {
512 if (_enabled != value) {
513 _enabled = value;
514 markNeedsLayout();
515 }
516 }
517
518 /// Whether or not the tap down even that triggers a call to [onTapOutside]
519 /// will continue on to participate in the gesture arena.
520 ///
521 /// If any [RenderTapRegion] in the same group has [consumeOutsideTaps] set to
522 /// true, then the tap down event will be consumed before other gesture
523 /// recognizers can process them.
524 bool get consumeOutsideTaps => _consumeOutsideTaps;
525 bool _consumeOutsideTaps;
526 set consumeOutsideTaps(bool value) {
527 if (_consumeOutsideTaps != value) {
528 _consumeOutsideTaps = value;
529 markNeedsLayout();
530 }
531 }
532
533 /// An optional group ID that groups [RenderTapRegion]s together so that they
534 /// operate as one region. If any member of a group is hit by a particular
535 /// tap, then the [onTapOutside] will not be called for any members of the
536 /// group. If any member of the group is hit, then all members will have their
537 /// [onTapInside] called.
538 ///
539 /// If the group id is null, then only this region is hit tested.
540 Object? get groupId => _groupId;
541 Object? _groupId;
542 set groupId(Object? value) {
543 if (_groupId != value) {
544 // If the group changes, we need to unregister and re-register under the
545 // new group. The re-registration happens automatically in layout().
546 if (_isRegistered) {
547 _registry!.unregisterTapRegion(this);
548 _isRegistered = false;
549 }
550 _groupId = value;
551 markNeedsLayout();
552 }
553 }
554
555 /// The registry that this [RenderTapRegion] should register with.
556 ///
557 /// If the [registry] is null, then this region will not be registered
558 /// anywhere, and will not do any tap detection.
559 ///
560 /// A [RenderTapRegionSurface] is a [TapRegionRegistry].
561 TapRegionRegistry? get registry => _registry;
562 TapRegionRegistry? _registry;
563 set registry(TapRegionRegistry? value) {
564 if (_registry != value) {
565 if (_isRegistered) {
566 _registry!.unregisterTapRegion(this);
567 _isRegistered = false;
568 }
569 _registry = value;
570 markNeedsLayout();
571 }
572 }
573
574 @override
575 void layout(Constraints constraints, {bool parentUsesSize = false}) {
576 super.layout(constraints, parentUsesSize: parentUsesSize);
577 if (_registry == null) {
578 return;
579 }
580 if (_isRegistered) {
581 _registry!.unregisterTapRegion(this);
582 }
583 final bool shouldBeRegistered = _enabled && _registry != null;
584 if (shouldBeRegistered) {
585 _registry!.registerTapRegion(this);
586 }
587 _isRegistered = shouldBeRegistered;
588 }
589
590 @override
591 void dispose() {
592 if (_isRegistered) {
593 _registry!.unregisterTapRegion(this);
594 }
595 super.dispose();
596 }
597
598 @override
599 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
600 super.debugFillProperties(properties);
601 properties.add(DiagnosticsProperty<String?>('debugLabel', debugLabel, defaultValue: null));
602 properties.add(DiagnosticsProperty<Object?>('groupId', groupId, defaultValue: null));
603 properties.add(FlagProperty('enabled', value: enabled, ifFalse: 'DISABLED', defaultValue: true));
604 }
605}
606
607/// A [TapRegion] that adds its children to the tap region group for widgets
608/// based on the [EditableText] text editing widget, such as [TextField] and
609/// [CupertinoTextField].
610///
611/// Widgets that are wrapped with a [TextFieldTapRegion] are considered to be
612/// part of a text field for purposes of unfocus behavior. So, when the user
613/// taps on them, the currently focused text field won't be unfocused by
614/// default. This allows controls like spinners, copy buttons, and formatting
615/// buttons to be associated with a text field without causing the text field to
616/// lose focus when they are interacted with.
617///
618/// {@tool dartpad}
619/// This example shows how to use a [TextFieldTapRegion] to wrap a set of
620/// "spinner" buttons that increment and decrement a value in the text field
621/// without causing the text field to lose keyboard focus.
622///
623/// This example includes a generic `SpinnerField<T>` class that you can copy/paste
624/// into your own project and customize.
625///
626/// ** See code in examples/api/lib/widgets/tap_region/text_field_tap_region.0.dart **
627/// {@end-tool}
628///
629/// See also:
630///
631/// * [TapRegion], the widget that this widget uses to add widgets to the group
632/// of text fields.
633class TextFieldTapRegion extends TapRegion {
634 /// Creates a const [TextFieldTapRegion].
635 ///
636 /// The [child] field is required.
637 const TextFieldTapRegion({
638 super.key,
639 required super.child,
640 super.enabled,
641 super.onTapOutside,
642 super.onTapInside,
643 super.consumeOutsideTaps,
644 super.debugLabel,
645 }) : super(groupId: EditableText);
646}
647