diff --git a/ui/flutter/lib/features/tasks/presentation/pages/create_task_window_page.dart b/ui/flutter/lib/features/tasks/presentation/pages/create_task_window_page.dart index abc96c13..5ffb71a2 100644 --- a/ui/flutter/lib/features/tasks/presentation/pages/create_task_window_page.dart +++ b/ui/flutter/lib/features/tasks/presentation/pages/create_task_window_page.dart @@ -41,6 +41,7 @@ import '../../application/pending_download.dart'; import '../../application/task_list_navigation.dart'; import '../widgets/history_search_field.dart'; import '../widgets/create_task_action_buttons.dart'; +import '../widgets/create_task_advanced_toggle.dart'; import '../widgets/resolve_download_dialog.dart'; class CreateTaskWindowPage extends ConsumerStatefulWidget { @@ -560,20 +561,11 @@ class _CreateTaskWindowPageState extends ConsumerState { ), const SizedBox(height: 20), Align( - alignment: Alignment.centerLeft, - child: GestureDetector( - onTap: _toggleAdvanced, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - context.l10n.advanced, - style: TextStyle(color: palette.textPrimary, fontSize: 14, fontWeight: FontWeight.w600), - ), - const SizedBox(width: 8), - _TripleChevronIcon(expanded: _showAdvanced, color: palette.textSecondary), - ], - ), + alignment: AlignmentDirectional.centerStart, + child: CreateTaskAdvancedToggle( + key: const ValueKey('create-task-advanced-toggle'), + expanded: _showAdvanced, + onPressed: _toggleAdvanced, ), ), AnimatedCrossFade( @@ -1933,30 +1925,6 @@ class _MiniInputAction extends StatelessWidget { } } -class _TripleChevronIcon extends StatelessWidget { - const _TripleChevronIcon({required this.expanded, required this.color}); - - final bool expanded; - final Color color; - - @override - Widget build(BuildContext context) { - final icon = expanded ? Icons.expand_less : Icons.expand_more; - return SizedBox( - width: 14, - height: 18, - child: Stack( - clipBehavior: Clip.none, - children: [ - Positioned(top: 0, left: 0, child: Icon(icon, size: 14, color: color)), - Positioned(top: 4, left: 0, child: Icon(icon, size: 14, color: color)), - Positioned(top: 8, left: 0, child: Icon(icon, size: 14, color: color)), - ], - ), - ); - } -} - class _SegmentButton extends StatelessWidget { const _SegmentButton({required this.label, required this.active, required this.onTap}); diff --git a/ui/flutter/lib/features/tasks/presentation/widgets/create_task_advanced_toggle.dart b/ui/flutter/lib/features/tasks/presentation/widgets/create_task_advanced_toggle.dart new file mode 100644 index 00000000..aa8e6112 --- /dev/null +++ b/ui/flutter/lib/features/tasks/presentation/widgets/create_task_advanced_toggle.dart @@ -0,0 +1,49 @@ +import 'package:shadcn_flutter/shadcn_flutter.dart'; + +import '../../../../l10n/l10n.dart'; +import '../../../../shared/theme/app_design_tokens.dart'; +import '../../../../shared/theme/app_palette.dart'; + +/// A quiet disclosure control with the same keyboard behavior as other buttons. +class CreateTaskAdvancedToggle extends StatelessWidget { + const CreateTaskAdvancedToggle({super.key, required this.expanded, required this.onPressed}); + + final bool expanded; + final VoidCallback onPressed; + + @override + Widget build(BuildContext context) { + final palette = AppPalette.of(context); + final reduceMotion = MediaQuery.disableAnimationsOf(context); + return Semantics( + expanded: expanded, + child: ButtonStyleOverride.inherit( + padding: (context, states, value) => const EdgeInsets.symmetric(vertical: AppDesignTokens.space12), + child: GhostButton( + onPressed: onPressed, + child: ConstrainedBox( + constraints: const BoxConstraints(minHeight: 20), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: Text( + context.l10n.advanced, + style: TextStyle(color: palette.textPrimary, fontSize: 14, fontWeight: FontWeight.w600), + ), + ), + const SizedBox(width: AppDesignTokens.space8), + AnimatedRotation( + turns: expanded ? 0.5 : 0, + duration: reduceMotion ? Duration.zero : const Duration(milliseconds: 180), + curve: Curves.easeOutCubic, + child: Icon(Icons.keyboard_arrow_down, size: 16, color: palette.textSecondary), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/ui/flutter/test/features/tasks/presentation/create_task_advanced_toggle_test.dart b/ui/flutter/test/features/tasks/presentation/create_task_advanced_toggle_test.dart new file mode 100644 index 00000000..34a5a5ee --- /dev/null +++ b/ui/flutter/test/features/tasks/presentation/create_task_advanced_toggle_test.dart @@ -0,0 +1,59 @@ +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:gopeed/features/tasks/presentation/widgets/create_task_advanced_toggle.dart'; +import 'package:gopeed/shared/theme/app_component_themes.dart'; +import 'package:gopeed/shared/theme/app_theme.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart' as shad; + +void main() { + for (final width in [360.0, 768.0, 1024.0]) { + testWidgets('advanced disclosure supports keyboard and touch at width $width', (tester) async { + tester.view.physicalSize = Size(width, 768); + tester.view.devicePixelRatio = 1; + addTearDown(tester.view.resetPhysicalSize); + addTearDown(tester.view.resetDevicePixelRatio); + var expanded = false; + await tester.pumpWidget( + shad.ShadcnApp( + theme: AppTheme.light(), + materialTheme: AppTheme.materialLight(), + home: AppComponentThemes( + child: StatefulBuilder( + builder: (context, setState) => Center( + child: Padding( + padding: const EdgeInsets.all(16), + child: CreateTaskAdvancedToggle( + expanded: expanded, + onPressed: () => setState(() => expanded = !expanded), + ), + ), + ), + ), + ), + ), + ); + await tester.pumpAndSettle(); + expect(tester.getSize(find.byType(shad.GhostButton)).height, greaterThanOrEqualTo(44)); + expect( + find.byWidgetPredicate((widget) => widget is Semantics && widget.properties.expanded == false), + findsOneWidget, + ); + await tester.sendKeyEvent(LogicalKeyboardKey.tab); + await tester.pump(); + await tester.sendKeyEvent(LogicalKeyboardKey.enter); + await tester.pumpAndSettle(); + expect(expanded, isTrue); + expect( + find.byWidgetPredicate((widget) => widget is Semantics && widget.properties.expanded == true), + findsOneWidget, + ); + await tester.tap(find.text('Advanced')); + await tester.pumpAndSettle(); + expect(expanded, isFalse); + expect(tester.takeException(), isNull); + await tester.pumpWidget(const SizedBox.shrink()); + await tester.pumpAndSettle(); + }); + } +}