From 4d8d688afb76a9e4d656c8e80cbf7a02db57ae0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Poizat?= Date: Thu, 11 Dec 2025 13:55:30 +0100 Subject: [PATCH] Fix AI scribe menu going off screen to the top If it was opened at the top of the composer, it could go off screen to the top. modalHeight was missing so the calculateModalPosition method could not protect the AI scribe menu to go off screen from the top. --- .../ai/presentation/widgets/ai_scribe.dart | 15 +++++ .../presentation/widgets/ai_scribe_test.dart | 61 ++++++++++++++++++- 2 files changed, 75 insertions(+), 1 deletion(-) diff --git a/scribe/lib/scribe/ai/presentation/widgets/ai_scribe.dart b/scribe/lib/scribe/ai/presentation/widgets/ai_scribe.dart index 31c6ba778..48effa947 100644 --- a/scribe/lib/scribe/ai/presentation/widgets/ai_scribe.dart +++ b/scribe/lib/scribe/ai/presentation/widgets/ai_scribe.dart @@ -67,10 +67,16 @@ Future showAIScribeDialog({ ); if (buttonPosition != null) { + final categories = availableCategories ?? AIScribeMenuCategory.values; + final modalHeight = hasContent + ? (categories.length * AIScribeSizes.menuItemHeight) + AIScribeSizes.fieldSpacing + AIScribeSizes.barHeight + : AIScribeSizes.barHeight; + final position = calculateModalPosition( context: context, buttonPosition: buttonPosition, modalWidth: AIScribeSizes.barWidth, + modalHeight: modalHeight, ); return PointerInterceptor( @@ -158,6 +164,15 @@ Future showAIScribeDialog({ ); } +double calculateMenuDialogHeight({ + required bool hasContent, + required int categoryCount, +}) { + return hasContent + ? (categoryCount * AIScribeSizes.menuItemHeight) + AIScribeSizes.fieldSpacing + AIScribeSizes.barHeight + : AIScribeSizes.barHeight; +} + ({double left, double bottom}) calculateModalPosition({ required BuildContext context, required Offset buttonPosition, diff --git a/scribe/test/scribe/ai/presentation/widgets/ai_scribe_test.dart b/scribe/test/scribe/ai/presentation/widgets/ai_scribe_test.dart index 1a2cf7acb..4089af2b5 100644 --- a/scribe/test/scribe/ai/presentation/widgets/ai_scribe_test.dart +++ b/scribe/test/scribe/ai/presentation/widgets/ai_scribe_test.dart @@ -4,7 +4,66 @@ import 'package:scribe/scribe/ai/presentation/styles/ai_scribe_styles.dart'; import 'package:scribe/scribe/ai/presentation/widgets/ai_scribe.dart'; void main() { - group('calculateModalPosition::', () { + group('calculateMenuDialogHeight::', () { + test('should calculate correct height with content and default 4 categories', () { + // Arrange + const hasContent = true; + const categoryCount = 4; + + // Act + final height = calculateMenuDialogHeight( + hasContent: hasContent, + categoryCount: categoryCount, + ); + + // Assert + // 4 categories × 40px + 8px spacing + 48px bar = 216px + const expectedHeight = (4 * AIScribeSizes.menuItemHeight) + + AIScribeSizes.fieldSpacing + + AIScribeSizes.barHeight; + expect(height, expectedHeight); + expect(height, 216.0); + }); + + test('should calculate correct height with content and custom categories', () { + // Arrange + const hasContent = true; + const categoryCount = 2; + + // Act + final height = calculateMenuDialogHeight( + hasContent: hasContent, + categoryCount: categoryCount, + ); + + // Assert + // 2 categories × 40px + 8px spacing + 48px bar = 136px + const expectedHeight = (2 * AIScribeSizes.menuItemHeight) + + AIScribeSizes.fieldSpacing + + AIScribeSizes.barHeight; + expect(height, expectedHeight); + expect(height, 136.0); + }); + + test('should calculate correct height without content (bar only)', () { + // Arrange + const hasContent = false; + const categoryCount = 4; // Should be ignored when hasContent is false + + // Act + final height = calculateMenuDialogHeight( + hasContent: hasContent, + categoryCount: categoryCount, + ); + + // Assert + // Only the bar height + expect(height, AIScribeSizes.barHeight); + expect(height, 48.0); + }); + }); + + group('calculateModalPosition::', () { testWidgets('should adjust left position when modal would go off-screen to the right', (tester) async { // Arrange const screenSize = Size(800, 600);