feat: improve keyboard behavior in mobile toolbar (#287)
This commit is contained in:
parent
d87d353535
commit
87fd7ed82d
13 changed files with 169 additions and 10 deletions
85
.github/workflows/test.yml
vendored
85
.github/workflows/test.yml
vendored
|
|
@ -16,8 +16,12 @@ concurrency:
|
||||||
env:
|
env:
|
||||||
FLUTTER_VERSION: "3.10.1"
|
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:
|
jobs:
|
||||||
tests:
|
desktop:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [macos-13, macos-12, macos-11, ubuntu-22.04, ubuntu-20.04, windows-2022, windows-2019]
|
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 }}
|
os: ${{ matrix.os }}
|
||||||
attempt_limit: 10
|
attempt_limit: 10
|
||||||
attempt_delay: 10000
|
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
|
||||||
3
assets/mobile/toolbar_icons/close.svg
Normal file
3
assets/mobile/toolbar_icons/close.svg
Normal 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 |
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
/// [AppFlowyKeyboardService] is responsible for processing shortcut keys,
|
/// [AppFlowyKeyboardService] is responsible for processing shortcut keys,
|
||||||
|
|
@ -34,4 +35,14 @@ abstract class AppFlowyKeyboardService {
|
||||||
bool showCursor = false,
|
bool showCursor = false,
|
||||||
UnfocusDisposition disposition = UnfocusDisposition.scope,
|
UnfocusDisposition disposition = UnfocusDisposition.scope,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// Closes the keyboard
|
||||||
|
///
|
||||||
|
/// Used in mobile
|
||||||
|
void closeKeyboard();
|
||||||
|
|
||||||
|
/// Enable the keyboard in mobile
|
||||||
|
///
|
||||||
|
/// Used in mobile
|
||||||
|
void enableKeyBoard(Selection selection);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -106,6 +106,18 @@ class KeyboardServiceWidgetState extends State<KeyboardServiceWidget>
|
||||||
@override
|
@override
|
||||||
void enable() => focusNode.requestFocus();
|
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
|
@override
|
||||||
KeyEventResult onKey(RawKeyEvent event) => throw UnimplementedError();
|
KeyEventResult onKey(RawKeyEvent event) => throw UnimplementedError();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -118,21 +118,50 @@ class _MobileToolbarWidgetState extends State<MobileToolbarWidget> {
|
||||||
editorState: widget.editorState,
|
editorState: widget.editorState,
|
||||||
selection: widget.selection,
|
selection: widget.selection,
|
||||||
toolbarItems: widget.toolbarItems,
|
toolbarItems: widget.toolbarItems,
|
||||||
itemOnPressed: (selectedItemIndex) {
|
closeMenu: () {
|
||||||
|
if (_showItemMenu) {
|
||||||
|
setState(() {
|
||||||
|
_showItemMenu = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
itemWithMenuOnPressed: (selectedItemIndex) {
|
||||||
setState(() {
|
setState(() {
|
||||||
// If last selected item is selected again, toggle item menu
|
// If last selected item is selected again, toggle item menu
|
||||||
if (_selectedToolbarItemIndex == selectedItemIndex) {
|
if (_selectedToolbarItemIndex == selectedItemIndex) {
|
||||||
_showItemMenu = !_showItemMenu;
|
_showItemMenu = !_showItemMenu;
|
||||||
} else {
|
} else {
|
||||||
|
_selectedToolbarItemIndex = selectedItemIndex;
|
||||||
// If not, show item menu
|
// If not, show item menu
|
||||||
_showItemMenu = true;
|
_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 {
|
class _QuitEditingBtn extends StatelessWidget {
|
||||||
const _CloseKeyboardBtn(this.editorState);
|
const _QuitEditingBtn(this.editorState);
|
||||||
|
|
||||||
final EditorState editorState;
|
final EditorState editorState;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return IconButton(
|
return IconButton(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
// clear selection to close keyboard and toolbar
|
// clear selection to close keyboard and toolbar
|
||||||
editorState.selectionService.updateSelection(null);
|
editorState.selectionService.updateSelection(null);
|
||||||
|
|
@ -173,13 +204,15 @@ class _CloseKeyboardBtn extends StatelessWidget {
|
||||||
class _ToolbarItemListView extends StatelessWidget {
|
class _ToolbarItemListView extends StatelessWidget {
|
||||||
const _ToolbarItemListView({
|
const _ToolbarItemListView({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.itemOnPressed,
|
required this.itemWithMenuOnPressed,
|
||||||
required this.toolbarItems,
|
required this.toolbarItems,
|
||||||
required this.editorState,
|
required this.editorState,
|
||||||
required this.selection,
|
required this.selection,
|
||||||
|
required this.closeMenu,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
final Function(int index) itemOnPressed;
|
final Function(int index) itemWithMenuOnPressed;
|
||||||
|
final Function() closeMenu;
|
||||||
final List<MobileToolbarItem> toolbarItems;
|
final List<MobileToolbarItem> toolbarItems;
|
||||||
final EditorState editorState;
|
final EditorState editorState;
|
||||||
final Selection selection;
|
final Selection selection;
|
||||||
|
|
@ -194,8 +227,10 @@ class _ToolbarItemListView extends StatelessWidget {
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
if (toolbarItem.hasMenu) {
|
if (toolbarItem.hasMenu) {
|
||||||
// open /close current item menu through its parent widget(MobileToolbarWidget)
|
// open /close current item menu through its parent widget(MobileToolbarWidget)
|
||||||
itemOnPressed.call(index);
|
itemWithMenuOnPressed.call(index);
|
||||||
} else {
|
} else {
|
||||||
|
// close menu if other item's menu is still on the screen
|
||||||
|
closeMenu.call();
|
||||||
toolbarItems[index].actionHandler?.call(
|
toolbarItems[index].actionHandler?.call(
|
||||||
editorState,
|
editorState,
|
||||||
selection,
|
selection,
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,8 @@ class _BackgroundColorOptionsWidgetsState
|
||||||
e.colorHex,
|
e.colorHex,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
// TODO(yijing): handle when no text is selected
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
isSelected: isSelected,
|
isSelected: isSelected,
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,8 @@ class _TextColorOptionsWidgetsState extends State<TextColorOptionsWidgets> {
|
||||||
e.colorHex,
|
e.colorHex,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
// TODO(yijing): handle when no text is selected
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
isSelected: isSelected,
|
isSelected: isSelected,
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,8 @@ enum AFMobileIcons {
|
||||||
numberedList('toolbar_icons/numbered_list'),
|
numberedList('toolbar_icons/numbered_list'),
|
||||||
checkbox('toolbar_icons/checkbox'),
|
checkbox('toolbar_icons/checkbox'),
|
||||||
quote('toolbar_icons/quote'),
|
quote('toolbar_icons/quote'),
|
||||||
divider('toolbar_icons/divider');
|
divider('toolbar_icons/divider'),
|
||||||
|
close('toolbar_icons/close');
|
||||||
|
|
||||||
final String iconPath;
|
final String iconPath;
|
||||||
const AFMobileIcons(this.iconPath);
|
const AFMobileIcons(this.iconPath);
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
@TestOn('android || ios')
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
@TestOn('android || ios')
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
@TestOn('android || ios')
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
@TestOn('android || ios')
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
@TestOn('android || ios')
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue