From d5205b24647c660a4e5ba58126e95a653c012877 Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Tue, 16 Jun 2026 16:02:41 +0300 Subject: [PATCH] fix(motion): exclude focus and semantics on inactive QueryaCrossFadeStack children Prevents keyboard navigation focus leaks and screen reader accessibility events from targeting off-screen children inside QueryaCrossFadeStack. --- lib/core/motion/querya_cross_fade_stack.dart | 16 ++++-- .../motion/querya_cross_fade_stack_test.dart | 54 +++++++++++++++++++ 2 files changed, 65 insertions(+), 5 deletions(-) create mode 100644 test/core/motion/querya_cross_fade_stack_test.dart diff --git a/lib/core/motion/querya_cross_fade_stack.dart b/lib/core/motion/querya_cross_fade_stack.dart index 54ec1407..e3e01724 100644 --- a/lib/core/motion/querya_cross_fade_stack.dart +++ b/lib/core/motion/querya_cross_fade_stack.dart @@ -27,11 +27,17 @@ class QueryaCrossFadeStack extends StatelessWidget { Positioned.fill( child: IgnorePointer( ignoring: index != i, - child: AnimatedOpacity( - opacity: index == i ? 1 : 0, - duration: duration, - curve: curve, - child: children[i], + child: ExcludeFocus( + excluding: index != i, + child: ExcludeSemantics( + excluding: index != i, + child: AnimatedOpacity( + opacity: index == i ? 1 : 0, + duration: duration, + curve: curve, + child: children[i], + ), + ), ), ), ), diff --git a/test/core/motion/querya_cross_fade_stack_test.dart b/test/core/motion/querya_cross_fade_stack_test.dart new file mode 100644 index 00000000..0a829ad6 --- /dev/null +++ b/test/core/motion/querya_cross_fade_stack_test.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:querya_desktop/core/motion/querya_cross_fade_stack.dart'; + +void main() { + testWidgets('QueryaCrossFadeStack cross-fades children and excludes focus', (WidgetTester tester) async { + final focusNode1 = FocusNode(); + final focusNode2 = FocusNode(); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: QueryaCrossFadeStack( + index: 0, + children: [ + TextField( + key: const Key('input_active'), + focusNode: focusNode1, + ), + TextField( + key: const Key('input_hidden'), + focusNode: focusNode2, + ), + ], + ), + ), + ), + ); + + // 1. Verify index 0 is visible (opacity 1.0) and index 1 is invisible (opacity 0.0) + final animatedOpacityFinder = find.byType(AnimatedOpacity); + expect(animatedOpacityFinder, findsNWidgets(2)); + + final opacity1 = tester.widget(animatedOpacityFinder.at(0)).opacity; + final opacity2 = tester.widget(animatedOpacityFinder.at(1)).opacity; + expect(opacity1, 1.0); + expect(opacity2, 0.0); + + // 2. Focus the active TextField + focusNode1.requestFocus(); + await tester.pump(); + expect(focusNode1.hasFocus, isTrue); + + // 3. Attempt to focus the hidden TextField + focusNode2.requestFocus(); + await tester.pump(); + // It should NOT have focus because it is wrapped in ExcludeFocus(excluding: true) + expect(focusNode2.hasFocus, isFalse); + + // Cleanup + focusNode1.dispose(); + focusNode2.dispose(); + }); +}