feat: improve keyboard behavior in mobile toolbar (#287)

This commit is contained in:
Yijing Huang 2023-07-12 09:14:45 -05:00 • committed by GitHub
commit 87fd7ed82d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 169 additions and 10 deletions

View file

@ -16,8 +16,12 @@ concurrency:
env:
FLUTTER_VERSION: "3.10.1"
# 3 jobs are configured.
# The first one runs tests on desktop OSs.
# The second runs tests on iOS simulator.
# The third runs tests on Android emulator.
jobs:
tests:
desktop:
strategy:
matrix:
os: [macos-13, macos-12, macos-11, ubuntu-22.04, ubuntu-20.04, windows-2022, windows-2019]
@ -50,3 +54,82 @@ jobs:
os: ${{ matrix.os }}
attempt_limit: 10
attempt_delay: 10000
ios:
strategy:
matrix:
device:
# The available simulators are listed by the "xcrun xctrace list devices" command
- "iPhone 11 Simulator (15.2)"
fail-fast: true
runs-on: macos-11
steps:
- name: "List all simulators"
run: "xcrun xctrace list devices"
- name: "Start Simulator"
# the command "xcrun simctl boot" expects a device identifier
# the assignment of the UDID variable consists of retrieving the ID of the simulator
# by extracting it from the command "xcrun xctrace list devices"
run: |
UDID=$(xcrun xctrace list devices | grep "^${{ matrix.device }}" | awk '{gsub(/[()]/,""); print $NF}')
echo $UDID
xcrun simctl boot "${UDID:?No Simulator with this name found}"
# Similar steps as the desktop job
- uses: actions/checkout@v3
- uses: subosito/flutter-action@v2
with:
channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true
- name: Run all the tests under test/mobile
run: |
flutter pub get
flutter analyze .
dart format --set-exit-if-changed .
flutter test test/mobile --coverage
- name: Upload coverage to Codecov
uses: Wandalen/wretry.action@v1.0.36
with:
action: codecov/codecov-action@v3
with: |
fail_ci_if_error: true
verbose: true
os: ${{ matrix.device }}
attempt_limit: 10
attempt_delay: 10000
android:
strategy:
matrix:
api-level:
- 30
fail-fast: true
runs-on: macos-11
steps:
# Similar steps as the desktop job
- uses: actions/checkout@v3
- uses: subosito/flutter-action@v2
with:
channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true
- name: Run all the tests under test/mobile
run: |
flutter pub get
flutter analyze .
dart format --set-exit-if-changed .
flutter test test/mobile --coverage
- name: Upload coverage to Codecov
uses: Wandalen/wretry.action@v1.0.36
with:
action: codecov/codecov-action@v3
with: |
fail_ci_if_error: true
verbose: true
os: ${{ matrix.api-level }}
attempt_limit: 10
attempt_delay: 10000

View file

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.00586 2.92969C4.79686 2.92969 3.00586 4.72069 3.00586 6.92969V16.9297C3.00586 19.1387 4.79686 20.9297 7.00586 20.9297H17.0059C19.2149 20.9297 21.0059 19.1387 21.0059 16.9297V6.92969C21.0059 4.72069 19.2149 2.92969 17.0059 2.92969H7.00586ZM9.00586 7.92969C9.26186 7.92969 9.52885 8.01569 9.72485 8.21069L12.0059 10.4917L14.2869 8.21069C14.4819 8.01569 14.7499 7.92969 15.0059 7.92969C15.2619 7.92969 15.5289 8.01569 15.7249 8.21069C16.1149 8.60169 16.1149 9.25768 15.7249 9.64868L13.4438 11.9297L15.7249 14.2107C16.1149 14.6017 16.1149 15.2577 15.7249 15.6487C15.3339 16.0387 14.6779 16.0387 14.2869 15.6487L12.0059 13.3677L9.72485 15.6487C9.33385 16.0387 8.67787 16.0387 8.28687 15.6487C7.89687 15.2577 7.89687 14.6017 8.28687 14.2107L10.5679 11.9297L8.28687 9.64868C7.89687 9.25768 7.89687 8.60169 8.28687 8.21069C8.48187 8.01569 8.74986 7.92969 9.00586 7.92969Z" fill="#2F3030"/>
</svg>

After

Width:  |  Height:  |  Size: 997 B

View file

@ -1,3 +1,4 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
/// [AppFlowyKeyboardService] is responsible for processing shortcut keys,
@ -34,4 +35,14 @@ abstract class AppFlowyKeyboardService {
bool showCursor = false,
UnfocusDisposition disposition = UnfocusDisposition.scope,
});
/// Closes the keyboard
///
/// Used in mobile
void closeKeyboard();
/// Enable the keyboard in mobile
///
/// Used in mobile
void enableKeyBoard(Selection selection);
}

View file

@ -106,6 +106,18 @@ class KeyboardServiceWidgetState extends State<KeyboardServiceWidget>
@override
void enable() => focusNode.requestFocus();
@override
void closeKeyboard() {
assert(PlatformExtension.isMobile, 'only support mobile platform');
textInputService.close();
}
@override
void enableKeyBoard(Selection selection) {
assert(PlatformExtension.isMobile, 'only support mobile platform');
_attachTextInputService(selection);
}
@override
KeyEventResult onKey(RawKeyEvent event) => throw UnimplementedError();

View file

@ -118,21 +118,50 @@ class _MobileToolbarWidgetState extends State<MobileToolbarWidget> {
editorState: widget.editorState,
selection: widget.selection,
toolbarItems: widget.toolbarItems,
itemOnPressed: (selectedItemIndex) {
closeMenu: () {
if (_showItemMenu) {
setState(() {
_showItemMenu = false;
});
}
},
itemWithMenuOnPressed: (selectedItemIndex) {
setState(() {
// If last selected item is selected again, toggle item menu
if (_selectedToolbarItemIndex == selectedItemIndex) {
_showItemMenu = !_showItemMenu;
} else {
_selectedToolbarItemIndex = selectedItemIndex;
// If not, show item menu
_showItemMenu = true;
// close keyboard when menu pop up
widget.editorState.service.keyboardService
?.closeKeyboard();
}
_selectedToolbarItemIndex = selectedItemIndex;
});
},
),
),
_CloseKeyboardBtn(widget.editorState),
const Padding(
padding: EdgeInsets.symmetric(vertical: 8),
child: VerticalDivider(),
),
_showItemMenu
? IconButton(
padding: EdgeInsets.zero,
alignment: Alignment.centerLeft,
onPressed: () {
setState(() {
_showItemMenu = false;
widget.editorState.service.keyboardService!
.enableKeyBoard(widget.selection);
});
},
icon: const AFMobileIcon(
afMobileIcons: AFMobileIcons.close,
),
)
: _QuitEditingBtn(widget.editorState),
],
),
),
@ -153,14 +182,16 @@ class _MobileToolbarWidgetState extends State<MobileToolbarWidget> {
}
}
class _CloseKeyboardBtn extends StatelessWidget {
const _CloseKeyboardBtn(this.editorState);
class _QuitEditingBtn extends StatelessWidget {
const _QuitEditingBtn(this.editorState);
final EditorState editorState;
@override
Widget build(BuildContext context) {
return IconButton(
padding: EdgeInsets.zero,
alignment: Alignment.centerLeft,
onPressed: () {
// clear selection to close keyboard and toolbar
editorState.selectionService.updateSelection(null);
@ -173,13 +204,15 @@ class _CloseKeyboardBtn extends StatelessWidget {
class _ToolbarItemListView extends StatelessWidget {
const _ToolbarItemListView({
Key? key,
required this.itemOnPressed,
required this.itemWithMenuOnPressed,
required this.toolbarItems,
required this.editorState,
required this.selection,
required this.closeMenu,
}) : super(key: key);
final Function(int index) itemOnPressed;
final Function(int index) itemWithMenuOnPressed;
final Function() closeMenu;
final List<MobileToolbarItem> toolbarItems;
final EditorState editorState;
final Selection selection;
@ -194,8 +227,10 @@ class _ToolbarItemListView extends StatelessWidget {
onPressed: () {
if (toolbarItem.hasMenu) {
// open /close current item menu through its parent widget(MobileToolbarWidget)
itemOnPressed.call(index);
itemWithMenuOnPressed.call(index);
} else {
// close menu if other item's menu is still on the screen
closeMenu.call();
toolbarItems[index].actionHandler?.call(
editorState,
selection,

View file

@ -75,6 +75,8 @@ class _BackgroundColorOptionsWidgetsState
e.colorHex,
);
});
} else {
// TODO(yijing): handle when no text is selected
}
},
isSelected: isSelected,

View file

@ -73,6 +73,8 @@ class _TextColorOptionsWidgetsState extends State<TextColorOptionsWidgets> {
e.colorHex,
);
});
} else {
// TODO(yijing): handle when no text is selected
}
},
isSelected: isSelected,

View file

@ -19,7 +19,8 @@ enum AFMobileIcons {
numberedList('toolbar_icons/numbered_list'),
checkbox('toolbar_icons/checkbox'),
quote('toolbar_icons/quote'),
divider('toolbar_icons/divider');
divider('toolbar_icons/divider'),
close('toolbar_icons/close');
final String iconPath;
const AFMobileIcons(this.iconPath);

View file

@ -1,3 +1,5 @@
@TestOn('android || ios')
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:appflowy_editor/appflowy_editor.dart';

View file

@ -1,3 +1,5 @@
@TestOn('android || ios')
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:appflowy_editor/appflowy_editor.dart';

View file

@ -1,3 +1,5 @@
@TestOn('android || ios')
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:appflowy_editor/appflowy_editor.dart';

View file

@ -1,3 +1,5 @@
@TestOn('android || ios')
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:appflowy_editor/appflowy_editor.dart';

View file

@ -1,3 +1,5 @@
@TestOn('android || ios')
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:appflowy_editor/appflowy_editor.dart';