|
5 | 5 | import 'dart:collection'; |
6 | 6 | import 'dart:ui'; |
7 | 7 |
|
| 8 | +import 'package:flutter/foundation.dart'; |
8 | 9 | import 'package:flutter/material.dart'; |
9 | 10 | import 'package:flutter/services.dart'; |
10 | 11 | import 'package:flutter_test/flutter_test.dart'; |
@@ -1976,6 +1977,143 @@ void main() { |
1976 | 1977 | await tester.restoreFrom(restorationData); |
1977 | 1978 | expect(find.byType(AlertDialog), findsOneWidget); |
1978 | 1979 | }, skip: isBrowser); // https://github.com/flutter/flutter/issues/33615 |
| 1980 | + |
| 1981 | + testWidgets('FocusTrap moves focus to given focus scope when triggered', (WidgetTester tester) async { |
| 1982 | + final FocusScopeNode focusScope = FocusScopeNode(); |
| 1983 | + final FocusNode focusNode = FocusNode(debugLabel: 'Test'); |
| 1984 | + await tester.pumpWidget( |
| 1985 | + Directionality( |
| 1986 | + textDirection: TextDirection.ltr, |
| 1987 | + child: FocusScope( |
| 1988 | + node: focusScope, |
| 1989 | + child: FocusTrap( |
| 1990 | + focusScopeNode: focusScope, |
| 1991 | + child: Column( |
| 1992 | + children: <Widget>[ |
| 1993 | + const Text('Other Widget'), |
| 1994 | + FocusTrapTestWidget('Focusable', focusNode: focusNode, onTap: () { |
| 1995 | + focusNode.requestFocus(); |
| 1996 | + }), |
| 1997 | + ], |
| 1998 | + ), |
| 1999 | + ), |
| 2000 | + ), |
| 2001 | + ), |
| 2002 | + ); |
| 2003 | + |
| 2004 | + await tester.pump(); |
| 2005 | + |
| 2006 | + Future<void> click(Finder finder) async { |
| 2007 | + final TestGesture gesture = await tester.startGesture( |
| 2008 | + tester.getCenter(finder), |
| 2009 | + kind: PointerDeviceKind.mouse, |
| 2010 | + ); |
| 2011 | + await gesture.up(); |
| 2012 | + await gesture.removePointer(); |
| 2013 | + } |
| 2014 | + |
| 2015 | + expect(focusScope.hasFocus, isFalse); |
| 2016 | + expect(focusNode.hasFocus, isFalse); |
| 2017 | + |
| 2018 | + await click(find.text('Focusable')); |
| 2019 | + await tester.pump(const Duration(seconds: 1)); |
| 2020 | + |
| 2021 | + expect(focusScope.hasFocus, isTrue); |
| 2022 | + expect(focusNode.hasPrimaryFocus, isTrue); |
| 2023 | + |
| 2024 | + await click(find.text('Other Widget')); |
| 2025 | + // Have to wait out the double click timer. |
| 2026 | + await tester.pump(const Duration(seconds: 1)); |
| 2027 | + |
| 2028 | + switch (defaultTargetPlatform) { |
| 2029 | + case TargetPlatform.iOS: |
| 2030 | + case TargetPlatform.android: |
| 2031 | + if (kIsWeb) { |
| 2032 | + // Web is a desktop platform. |
| 2033 | + expect(focusScope.hasPrimaryFocus, isTrue); |
| 2034 | + expect(focusNode.hasFocus, isFalse); |
| 2035 | + } else { |
| 2036 | + expect(focusScope.hasFocus, isTrue); |
| 2037 | + expect(focusNode.hasPrimaryFocus, isTrue); |
| 2038 | + } |
| 2039 | + break; |
| 2040 | + case TargetPlatform.fuchsia: |
| 2041 | + case TargetPlatform.linux: |
| 2042 | + case TargetPlatform.macOS: |
| 2043 | + case TargetPlatform.windows: |
| 2044 | + expect(focusScope.hasPrimaryFocus, isTrue); |
| 2045 | + expect(focusNode.hasFocus, isFalse); |
| 2046 | + break; |
| 2047 | + } |
| 2048 | + }, variant: TargetPlatformVariant.all()); |
| 2049 | + |
| 2050 | + testWidgets("FocusTrap doesn't unfocus if focus was set to something else before the frame ends", (WidgetTester tester) async { |
| 2051 | + final FocusScopeNode focusScope = FocusScopeNode(); |
| 2052 | + final FocusNode focusNode = FocusNode(debugLabel: 'Test'); |
| 2053 | + final FocusNode otherFocusNode = FocusNode(debugLabel: 'Other'); |
| 2054 | + FocusNode? previousFocus; |
| 2055 | + await tester.pumpWidget( |
| 2056 | + Directionality( |
| 2057 | + textDirection: TextDirection.ltr, |
| 2058 | + child: FocusScope( |
| 2059 | + node: focusScope, |
| 2060 | + child: FocusTrap( |
| 2061 | + focusScopeNode: focusScope, |
| 2062 | + child: Column( |
| 2063 | + children: <Widget>[ |
| 2064 | + FocusTrapTestWidget( |
| 2065 | + 'Other Widget', |
| 2066 | + focusNode: otherFocusNode, |
| 2067 | + onTap: () { |
| 2068 | + previousFocus = FocusManager.instance.primaryFocus; |
| 2069 | + otherFocusNode.requestFocus(); |
| 2070 | + }, |
| 2071 | + ), |
| 2072 | + FocusTrapTestWidget( |
| 2073 | + 'Focusable', |
| 2074 | + focusNode: focusNode, |
| 2075 | + onTap: () { |
| 2076 | + focusNode.requestFocus(); |
| 2077 | + }, |
| 2078 | + ), |
| 2079 | + ], |
| 2080 | + ), |
| 2081 | + ), |
| 2082 | + ), |
| 2083 | + ), |
| 2084 | + ); |
| 2085 | + |
| 2086 | + Future<void> click(Finder finder) async { |
| 2087 | + final TestGesture gesture = await tester.startGesture( |
| 2088 | + tester.getCenter(finder), |
| 2089 | + kind: PointerDeviceKind.mouse, |
| 2090 | + ); |
| 2091 | + await gesture.up(); |
| 2092 | + await gesture.removePointer(); |
| 2093 | + } |
| 2094 | + |
| 2095 | + await tester.pump(); |
| 2096 | + expect(focusScope.hasFocus, isFalse); |
| 2097 | + expect(focusNode.hasPrimaryFocus, isFalse); |
| 2098 | + |
| 2099 | + await click(find.text('Focusable')); |
| 2100 | + |
| 2101 | + expect(focusScope.hasFocus, isTrue); |
| 2102 | + expect(focusNode.hasPrimaryFocus, isTrue); |
| 2103 | + |
| 2104 | + await click(find.text('Other Widget')); |
| 2105 | + await tester.pump(const Duration(seconds: 1)); |
| 2106 | + |
| 2107 | + // The previous focus as collected by the "Other Widget" should be the |
| 2108 | + // previous focus, not be unfocused to the scope, since the primary focus |
| 2109 | + // was set by something other than the FocusTrap (the "Other Widget") during |
| 2110 | + // the frame. |
| 2111 | + expect(previousFocus, equals(focusNode)); |
| 2112 | + |
| 2113 | + expect(focusScope.hasFocus, isTrue); |
| 2114 | + expect(focusNode.hasPrimaryFocus, isFalse); |
| 2115 | + expect(otherFocusNode.hasPrimaryFocus, isTrue); |
| 2116 | + }, variant: TargetPlatformVariant.all()); |
1979 | 2117 | } |
1980 | 2118 |
|
1981 | 2119 | double _getOpacity(GlobalKey key, WidgetTester tester) { |
@@ -2232,3 +2370,68 @@ class _RestorableDialogTestWidget extends StatelessWidget { |
2232 | 2370 | ); |
2233 | 2371 | } |
2234 | 2372 | } |
| 2373 | + |
| 2374 | +class FocusTrapTestWidget extends StatefulWidget { |
| 2375 | + const FocusTrapTestWidget( |
| 2376 | + this.label, { |
| 2377 | + super.key, |
| 2378 | + required this.focusNode, |
| 2379 | + this.onTap, |
| 2380 | + this.autofocus = false, |
| 2381 | + }); |
| 2382 | + |
| 2383 | + final String label; |
| 2384 | + final FocusNode focusNode; |
| 2385 | + final VoidCallback? onTap; |
| 2386 | + final bool autofocus; |
| 2387 | + |
| 2388 | + @override |
| 2389 | + State<FocusTrapTestWidget> createState() => _FocusTrapTestWidgetState(); |
| 2390 | +} |
| 2391 | + |
| 2392 | +class _FocusTrapTestWidgetState extends State<FocusTrapTestWidget> { |
| 2393 | + Color color = Colors.white; |
| 2394 | + |
| 2395 | + @override |
| 2396 | + void initState() { |
| 2397 | + super.initState(); |
| 2398 | + widget.focusNode.addListener(_handleFocusChange); |
| 2399 | + } |
| 2400 | + |
| 2401 | + void _handleFocusChange() { |
| 2402 | + if (widget.focusNode.hasPrimaryFocus) { |
| 2403 | + setState(() { |
| 2404 | + color = Colors.grey.shade500; |
| 2405 | + }); |
| 2406 | + } else { |
| 2407 | + setState(() { |
| 2408 | + color = Colors.white; |
| 2409 | + }); |
| 2410 | + } |
| 2411 | + } |
| 2412 | + |
| 2413 | + @override |
| 2414 | + void dispose() { |
| 2415 | + widget.focusNode.removeListener(_handleFocusChange); |
| 2416 | + widget.focusNode.dispose(); |
| 2417 | + super.dispose(); |
| 2418 | + } |
| 2419 | + |
| 2420 | + @override |
| 2421 | + Widget build(BuildContext context) { |
| 2422 | + return Focus( |
| 2423 | + autofocus: widget.autofocus, |
| 2424 | + focusNode: widget.focusNode, |
| 2425 | + child: GestureDetector( |
| 2426 | + onTap: () { |
| 2427 | + widget.onTap?.call(); |
| 2428 | + }, |
| 2429 | + child: Container( |
| 2430 | + color: color, |
| 2431 | + alignment: Alignment.center, |
| 2432 | + child: Text(widget.label, style: const TextStyle(color: Colors.black)), |
| 2433 | + ), |
| 2434 | + ), |
| 2435 | + ); |
| 2436 | + } |
| 2437 | +} |
0 commit comments