From f450a89fb8afc8931c0235029ea23cb9d7b2b453 Mon Sep 17 00:00:00 2001 From: Angelo Silvestre Date: Mon, 12 Aug 2024 20:31:57 -0300 Subject: [PATCH] Display panels in an OverlayPortal --- .../demo_panel_behind_keyboard.dart | 238 ++++++++++-------- .../keyboard_panel_scaffold.dart | 78 ++++-- .../keyboard_panel_scaffold_test.dart | 46 +++- 3 files changed, 237 insertions(+), 125 deletions(-) diff --git a/super_editor/example/lib/demos/experiments/demo_panel_behind_keyboard.dart b/super_editor/example/lib/demos/experiments/demo_panel_behind_keyboard.dart index 28cc1159b..0d938c746 100644 --- a/super_editor/example/lib/demos/experiments/demo_panel_behind_keyboard.dart +++ b/super_editor/example/lib/demos/experiments/demo_panel_behind_keyboard.dart @@ -159,11 +159,18 @@ class _PanelBehindKeyboardDemoState extends State { Widget build(BuildContext context) { return Scaffold( resizeToAvoidBottomInset: false, - body: KeyboardPanelScaffold( - controller: _keyboardPanelController, - contentBuilder: _buildSuperEditor, - aboveKeyboardBuilder: _buildTopPanel, - keyboardPanelBuilder: _buildKeyboardPanel, + body: SafeArea( + bottom: false, + left: false, + right: false, + child: Column( + children: [ + _buildTopPanelToggle(context), + Expanded( + child: _buildSuperEditor(context, _isKeyboardPanelVisible), + ), + ], + ), ), ); } @@ -186,6 +193,20 @@ class _PanelBehindKeyboardDemoState extends State { ); } + Widget _buildTopPanelToggle(BuildContext context) { + return KeyboardPanelScaffold( + controller: _keyboardPanelController, + aboveKeyboardBuilder: _buildTopPanel, + keyboardPanelBuilder: _buildKeyboardPanel, + contentBuilder: (context, wantsToShowKeyboardPanel) { + return ElevatedButton( + onPressed: _keyboardPanelController.toggleAboveKeyboardPanel, + child: Text('Toggle above-keyboard panel'), + ); + }, + ); + } + Widget _buildTopPanel(BuildContext context, bool isKeyboardPanelVisible) { return Container( width: double.infinity, @@ -210,121 +231,124 @@ class _PanelBehindKeyboardDemoState extends State { } Widget _buildKeyboardPanel(BuildContext context) { - return SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Alignment', - style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - ), - Row( - children: [ - Expanded( - child: _buildKeyboardPanelButton( - onPressed: () {}, - child: Icon(Icons.format_align_left), + return ColoredBox( + color: Theme.of(context).scaffoldBackgroundColor, + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Alignment', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + Row( + children: [ + Expanded( + child: _buildKeyboardPanelButton( + onPressed: () {}, + child: Icon(Icons.format_align_left), + ), ), - ), - SizedBox(width: 8), - Expanded( - child: _buildKeyboardPanelButton( - onPressed: () {}, - child: Icon(Icons.format_align_center), + SizedBox(width: 8), + Expanded( + child: _buildKeyboardPanelButton( + onPressed: () {}, + child: Icon(Icons.format_align_center), + ), ), - ), - SizedBox(width: 8), - Expanded( - child: _buildKeyboardPanelButton( - onPressed: () {}, - child: Icon(Icons.format_align_right), + SizedBox(width: 8), + Expanded( + child: _buildKeyboardPanelButton( + onPressed: () {}, + child: Icon(Icons.format_align_right), + ), ), - ), - SizedBox(width: 8), - Expanded( - child: _buildKeyboardPanelButton( - onPressed: () {}, - child: Icon(Icons.format_align_justify), + SizedBox(width: 8), + Expanded( + child: _buildKeyboardPanelButton( + onPressed: () {}, + child: Icon(Icons.format_align_justify), + ), ), - ), - ], - ), - SizedBox(height: 8), - Text( - 'Text Style', - style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - ), - Row( - children: [ - Expanded( - child: _buildKeyboardPanelButton( - onPressed: () {}, - child: Icon(Icons.format_bold), + ], + ), + SizedBox(height: 8), + Text( + 'Text Style', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + Row( + children: [ + Expanded( + child: _buildKeyboardPanelButton( + onPressed: () {}, + child: Icon(Icons.format_bold), + ), ), - ), - SizedBox(width: 8), - Expanded( - child: _buildKeyboardPanelButton( - onPressed: () {}, - child: Icon(Icons.format_italic), + SizedBox(width: 8), + Expanded( + child: _buildKeyboardPanelButton( + onPressed: () {}, + child: Icon(Icons.format_italic), + ), ), - ), - SizedBox(width: 8), - Expanded( - child: _buildKeyboardPanelButton( - onPressed: () {}, - child: Icon(Icons.format_strikethrough), + SizedBox(width: 8), + Expanded( + child: _buildKeyboardPanelButton( + onPressed: () {}, + child: Icon(Icons.format_strikethrough), + ), ), + ], + ), + SizedBox(height: 8), + Text( + 'Convertions', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + SizedBox( + width: double.infinity, + child: _buildKeyboardPanelButton( + onPressed: () {}, + child: Text('Header 1'), ), - ], - ), - SizedBox(height: 8), - Text( - 'Convertions', - style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - ), - SizedBox( - width: double.infinity, - child: _buildKeyboardPanelButton( - onPressed: () {}, - child: Text('Header 1'), ), - ), - SizedBox(height: 8), - SizedBox( - width: double.infinity, - child: _buildKeyboardPanelButton( - onPressed: () {}, - child: Text('Header 2'), + SizedBox(height: 8), + SizedBox( + width: double.infinity, + child: _buildKeyboardPanelButton( + onPressed: () {}, + child: Text('Header 2'), + ), ), - ), - SizedBox(height: 8), - SizedBox( - width: double.infinity, - child: _buildKeyboardPanelButton( - onPressed: () {}, - child: Text('Blockquote'), + SizedBox(height: 8), + SizedBox( + width: double.infinity, + child: _buildKeyboardPanelButton( + onPressed: () {}, + child: Text('Blockquote'), + ), ), - ), - SizedBox(height: 8), - SizedBox( - width: double.infinity, - child: _buildKeyboardPanelButton( - onPressed: () {}, - child: Text('Ordered List'), + SizedBox(height: 8), + SizedBox( + width: double.infinity, + child: _buildKeyboardPanelButton( + onPressed: () {}, + child: Text('Ordered List'), + ), ), - ), - SizedBox(height: 8), - SizedBox( - width: double.infinity, - child: _buildKeyboardPanelButton( - onPressed: () {}, - child: Text('Unordered List'), + SizedBox(height: 8), + SizedBox( + width: double.infinity, + child: _buildKeyboardPanelButton( + onPressed: () {}, + child: Text('Unordered List'), + ), ), - ), - ], + ], + ), ), ), ); diff --git a/super_editor/lib/src/infrastructure/keyboard_panel_scaffold.dart b/super_editor/lib/src/infrastructure/keyboard_panel_scaffold.dart index b1fec94ce..e9de1a6e7 100644 --- a/super_editor/lib/src/infrastructure/keyboard_panel_scaffold.dart +++ b/super_editor/lib/src/infrastructure/keyboard_panel_scaffold.dart @@ -79,6 +79,9 @@ class _KeyboardPanelScaffoldState extends State with Sing /// the keyboard panel, we need to animated it ourselves. late final AnimationController _panelExitAnimation; + /// Shows/hides the [OverlayPortal] containing the keyboard panel and above-keyboard panel. + final OverlayPortalController _overlayPortalController = OverlayPortalController(); + @override void initState() { super.initState(); @@ -127,6 +130,17 @@ class _KeyboardPanelScaffoldState extends State with Sing } void _onKeyboardPanelChanged() { + if (!widget.controller.wantsToShowAboveKeyboardPanel && !widget.controller.wantsToShowKeyboardPanel) { + _overlayPortalController.hide(); + return; + } + + if (!_overlayPortalController.isShowing) { + // The user wants to show the above-keyboard panel or the keyboard panel, but the overlay + // isn't visible. Show it. + _overlayPortalController.show(); + } + if (!widget.controller.wantsToShowKeyboardPanel && !widget.controller.wantsToShowSoftwareKeyboard && _latestViewInsets.bottom == 0.0) { @@ -185,23 +199,34 @@ class _KeyboardPanelScaffoldState extends State with Sing // and the keyboard panel was previously visible. Otherwise, there will be an empty // region between the top of the software keyboard and the bottom of the above-keyboard panel. (widget.controller.wantsToShowSoftwareKeyboard && _latestViewInsets.bottom < _keyboardHeight); - return Column( - children: [ - Expanded( - child: widget.contentBuilder( - context, - widget.controller.wantsToShowKeyboardPanel, + + return OverlayPortal( + controller: _overlayPortalController, + overlayChildBuilder: (context) { + return Positioned( + bottom: 0, + left: 0, + right: 0, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (widget.controller.wantsToShowAboveKeyboardPanel) + widget.aboveKeyboardBuilder( + context, + widget.controller.wantsToShowKeyboardPanel, + ), + SizedBox( + height: _keyboardHeight, + child: wantsToShowKeyboardPanel ? widget.keyboardPanelBuilder(context) : null, + ), + ], ), - ), - widget.aboveKeyboardBuilder( - context, - widget.controller.wantsToShowKeyboardPanel, - ), - SizedBox( - height: _keyboardHeight, - child: wantsToShowKeyboardPanel ? widget.keyboardPanelBuilder(context) : null, - ), - ], + ); + }, + child: widget.contentBuilder( + context, + widget.controller.wantsToShowKeyboardPanel, + ), ); } } @@ -220,6 +245,9 @@ class KeyboardPanelController with ChangeNotifier { bool get wantsToShowSoftwareKeyboard => _wantsToShowSoftwareKeyboard; bool _wantsToShowSoftwareKeyboard = false; + bool get wantsToShowAboveKeyboardPanel => _wantsToShowAboveKeyboardPanel; + bool _wantsToShowAboveKeyboardPanel = false; + void showKeyboardPanel() { _wantsToShowKeyboardPanel = true; _wantsToShowSoftwareKeyboard = false; @@ -249,4 +277,22 @@ class KeyboardPanelController with ChangeNotifier { softwareKeyboardController.close(); notifyListeners(); } + + void showAboveKeyboardPanel() { + _wantsToShowAboveKeyboardPanel = true; + notifyListeners(); + } + + void hideAboveKeyboardPanel() { + _wantsToShowAboveKeyboardPanel = false; + notifyListeners(); + } + + void toggleAboveKeyboardPanel() { + if (_wantsToShowAboveKeyboardPanel) { + hideAboveKeyboardPanel(); + } else { + showAboveKeyboardPanel(); + } + } } diff --git a/super_editor/test/infrastructure/keyboard_panel_scaffold_test.dart b/super_editor/test/infrastructure/keyboard_panel_scaffold_test.dart index 212cedd83..b6ce6bb58 100644 --- a/super_editor/test/infrastructure/keyboard_panel_scaffold_test.dart +++ b/super_editor/test/infrastructure/keyboard_panel_scaffold_test.dart @@ -17,7 +17,18 @@ void main() { }); testWidgetsOnMobile('shows above-keyboard panel at the bottom when there is no keyboard', (tester) async { - await _pumpTestApp(tester); + final softwareKeyboardController = SoftwareKeyboardController(); + final controller = KeyboardPanelController(softwareKeyboardController: softwareKeyboardController); + + await _pumpTestApp( + tester, + controller: controller, + softwareKeyboardController: softwareKeyboardController, + ); + + // Request to show the above-keyboard panel. + controller.showAboveKeyboardPanel(); + await tester.pump(); // Ensure the above-keyboard panel sits at the bottom of the screen. expect( @@ -37,7 +48,18 @@ void main() { }); testWidgetsOnMobile('shows above-keyboard panel above the keyboard', (tester) async { - await _pumpTestApp(tester); + final softwareKeyboardController = SoftwareKeyboardController(); + final controller = KeyboardPanelController(softwareKeyboardController: softwareKeyboardController); + + await _pumpTestApp( + tester, + controller: controller, + softwareKeyboardController: softwareKeyboardController, + ); + + // Request to show the above-keyboard panel. + controller.showAboveKeyboardPanel(); + await tester.pump(); // Place the caret at the beginning of the document to show the software keyboard. await tester.placeCaretInParagraph('1', 0); @@ -60,6 +82,10 @@ void main() { softwareKeyboardController: softwareKeyboardController, ); + // Request to show the above-keyboard panel. + controller.showAboveKeyboardPanel(); + await tester.pump(); + // Place the caret at the beginning of the document to show the software keyboard. await tester.placeCaretInParagraph('1', 0); @@ -112,6 +138,10 @@ void main() { softwareKeyboardController: softwareKeyboardController, ); + // Request to show the above-keyboard panel. + controller.showAboveKeyboardPanel(); + await tester.pump(); + // Place the caret at the beginning of the document to show the software keyboard. await tester.placeCaretInParagraph('1', 0); @@ -142,6 +172,10 @@ void main() { softwareKeyboardController: softwareKeyboardController, ); + // Request to show the above-keyboard panel. + controller.showAboveKeyboardPanel(); + await tester.pump(); + // Place the caret at the beginning of the document to show the software keyboard. await tester.placeCaretInParagraph('1', 0); @@ -176,6 +210,10 @@ void main() { softwareKeyboardController: softwareKeyboardController, ); + // Request to show the above-keyboard panel. + controller.showAboveKeyboardPanel(); + await tester.pump(); + // Place the caret at the beginning of the document to show the software keyboard. await tester.placeCaretInParagraph('1', 0); @@ -210,6 +248,10 @@ void main() { softwareKeyboardController: softwareKeyboardController, ); + // Request to show the above-keyboard panel. + controller.showAboveKeyboardPanel(); + await tester.pump(); + // Place the caret at the beginning of the document to show the software keyboard. await tester.placeCaretInParagraph('1', 0);