diff --git a/packages/flutter/lib/src/widgets/context_menu_controller.dart b/packages/flutter/lib/src/widgets/context_menu_controller.dart index df75571d46d4..6e29a2b0ad9b 100644 --- a/packages/flutter/lib/src/widgets/context_menu_controller.dart +++ b/packages/flutter/lib/src/widgets/context_menu_controller.dart @@ -81,6 +81,7 @@ class ContextMenuController { /// * [remove], which removes only the current instance. static void removeAny() { _menuOverlayEntry?.remove(); + _menuOverlayEntry?.dispose(); _menuOverlayEntry = null; if (_shownInstance != null) { _shownInstance!.onRemove?.call(); diff --git a/packages/flutter/lib/src/widgets/text_selection.dart b/packages/flutter/lib/src/widgets/text_selection.dart index 177c32f7c631..adcba3a6ddea 100644 --- a/packages/flutter/lib/src/widgets/text_selection.dart +++ b/packages/flutter/lib/src/widgets/text_selection.dart @@ -1371,7 +1371,9 @@ class SelectionOverlay { void hideHandles() { if (_handles != null) { _handles![0].remove(); + _handles![0].dispose(); _handles![1].remove(); + _handles![1].dispose(); _handles = null; } } @@ -1480,7 +1482,9 @@ class SelectionOverlay { _magnifierController.hide(); if (_handles != null) { _handles![0].remove(); + _handles![0].dispose(); _handles![1].remove(); + _handles![1].dispose(); _handles = null; } if (_toolbar != null || _contextMenuController.isShown || _spellCheckToolbarController.isShown) { @@ -1500,6 +1504,7 @@ class SelectionOverlay { return; } _toolbar?.remove(); + _toolbar?.dispose(); _toolbar = null; } @@ -1508,6 +1513,7 @@ class SelectionOverlay { /// {@endtemplate} void dispose() { hide(); + _magnifierInfo.dispose(); } Widget _buildStartHandle(BuildContext context) { diff --git a/packages/flutter/test/material/text_form_field_test.dart b/packages/flutter/test/material/text_form_field_test.dart index 97323bf4e73f..d830020fde29 100644 --- a/packages/flutter/test/material/text_form_field_test.dart +++ b/packages/flutter/test/material/text_form_field_test.dart @@ -251,6 +251,40 @@ void main() { leakTrackingTestConfig: const LeakTrackingTestConfig(allowAllNotDisposed: true, allowAllNotGCed: true), ); + testWidgetsWithLeakTracking( + '$SelectionOverlay is not leaking', + (WidgetTester tester) async { + final TextEditingController controller = TextEditingController( + text: 'blah1 blah2', + ); + await tester.pumpWidget( + MaterialApp( + home: Material( + child: Center( + child: TextField( + controller: controller, + ), + ), + ), + ), + ); + + final Offset startBlah1 = textOffsetToPosition(tester, 0); + await tester.tapAt(startBlah1); + await tester.pump(const Duration(milliseconds: 100)); + await tester.tapAt(startBlah1); + await tester.pumpAndSettle(); + await tester.pump(); + controller.dispose(); + }, + skip: kIsWeb, // [intended] we don't supply the cut/copy/paste buttons on the web. + // TODO(polina-c): remove after fixing + // https://github.com/flutter/flutter/issues/132620 + leakTrackingTestConfig: const LeakTrackingTestConfig( + notDisposedAllowList: {'_InputBorderGap' : 1}, + ), + ); + testWidgets('the desktop cut/copy/paste buttons are disabled for read-only obscured form fields', (WidgetTester tester) async { final TextEditingController controller = TextEditingController( text: 'blah1 blah2',