fix: simple table issues on mobile (#7115)

* fix: header row/column tap areas are too small on mobile

* test: header row/column tap areas are too small on mobile

* feat: enable auto scroll after inserting column or row

* fix: enter after emoji will create a softbreak on mobile

* fix: header row/column tap areas are too small on mobile

* fix: simple table alignment not work for item that wraps

* test: simple table alignment not work for item that wraps
This commit is contained in:
Lucas 2024-12-31 16:01:45 +08:00 committed by GitHub
parent 7dedb84504
commit 8f7cb50dd4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 239 additions and 40 deletions

View File

@ -2,9 +2,10 @@ import 'dart:async';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/_simple_table_bottom_sheet_actions.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
@ -244,6 +245,53 @@ void main() {
expect(table.isHeaderColumnEnabled, isTrue);
expect(table.isHeaderRowEnabled, isTrue);
// disable header column
{
// focus on the first cell
unawaited(
editorState.updateSelectionWithReason(
Selection.collapsed(Position(path: firstParagraphPath)),
reason: SelectionUpdateReason.uiEvent,
),
);
await tester.pumpAndSettle();
// click the row menu button
await tester.clickColumnMenuButton(0);
final toggleButton = find.descendant(
of: find.byType(SimpleTableHeaderActionButton),
matching: find.byType(CupertinoSwitch),
);
await tester.tapButton(toggleButton);
}
// enable header row
{
// focus on the first cell
unawaited(
editorState.updateSelectionWithReason(
Selection.collapsed(Position(path: firstParagraphPath)),
reason: SelectionUpdateReason.uiEvent,
),
);
await tester.pumpAndSettle();
// click the row menu button
await tester.clickRowMenuButton(0);
// enable header column
final toggleButton = find.descendant(
of: find.byType(SimpleTableHeaderActionButton),
matching: find.byType(CupertinoSwitch),
);
await tester.tapButton(toggleButton);
}
// check the table is updated
expect(table.isHeaderColumnEnabled, isFalse);
expect(table.isHeaderRowEnabled, isFalse);
// set to page width
{
final table = editorState.getNodeAtPath([0])!;

View File

@ -86,6 +86,9 @@ class SimpleTableContext {
/// This value is available on mobile only
final ValueNotifier<int?> isReorderingHitIndex = ValueNotifier(null);
/// Scroll controller for the table
ScrollController? horizontalScrollController;
void _onHoveringOnColumnsAndRowsChanged() {
if (!_enableTableDebugLog) {
return;

View File

@ -117,6 +117,8 @@ extension TableOptionOperation on EditorState {
required Node tableCellNode,
required TableAlign align,
}) async {
await clearColumnTextAlign(tableCellNode: tableCellNode);
final columnIndex = tableCellNode.columnIndex;
await _updateTableAttributes(
tableCellNode: tableCellNode,
@ -144,6 +146,8 @@ extension TableOptionOperation on EditorState {
required Node tableCellNode,
required TableAlign align,
}) async {
await clearRowTextAlign(tableCellNode: tableCellNode);
final rowIndex = tableCellNode.rowIndex;
await _updateTableAttributes(
tableCellNode: tableCellNode,
@ -385,4 +389,67 @@ extension TableOptionOperation on EditorState {
transaction.updateNode(parentTableNode, attributes);
await apply(transaction);
}
/// Clear the text align of the column at the index where the table cell node is located.
Future<void> clearColumnTextAlign({
required Node tableCellNode,
}) async {
final parentTableNode = tableCellNode.parentTableNode;
if (parentTableNode == null) {
Log.warn('parent table node is null');
return;
}
final columnIndex = tableCellNode.columnIndex;
final transaction = this.transaction;
for (var i = 0; i < parentTableNode.rowLength; i++) {
final cell = parentTableNode.getTableCellNode(
rowIndex: i,
columnIndex: columnIndex,
);
if (cell == null) {
continue;
}
for (final child in cell.children) {
transaction.updateNode(child, {
blockComponentAlign: null,
});
}
}
if (transaction.operations.isNotEmpty) {
await apply(transaction);
}
}
/// Clear the text align of the row at the index where the table cell node is located.
Future<void> clearRowTextAlign({
required Node tableCellNode,
}) async {
final parentTableNode = tableCellNode.parentTableNode;
if (parentTableNode == null) {
Log.warn('parent table node is null');
return;
}
final rowIndex = tableCellNode.rowIndex;
final transaction = this.transaction;
for (var i = 0; i < parentTableNode.columnLength; i++) {
final cell = parentTableNode.getTableCellNode(
rowIndex: rowIndex,
columnIndex: i,
);
if (cell == null) {
continue;
}
for (final child in cell.children) {
transaction.updateNode(
child,
{
blockComponentAlign: null,
},
);
}
}
if (transaction.operations.isNotEmpty) {
await apply(transaction);
}
}
}

View File

@ -47,8 +47,16 @@ class _DesktopSimpleTableWidgetState extends State<DesktopSimpleTableWidget> {
final scrollController = ScrollController();
late final editorState = context.read<EditorState>();
@override
void initState() {
super.initState();
simpleTableContext.horizontalScrollController = scrollController;
}
@override
void dispose() {
simpleTableContext.horizontalScrollController = null;
scrollController.dispose();
super.dispose();

View File

@ -47,8 +47,16 @@ class _MobileSimpleTableWidgetState extends State<MobileSimpleTableWidget> {
final scrollController = ScrollController();
late final editorState = context.read<EditorState>();
@override
void initState() {
super.initState();
simpleTableContext.horizontalScrollController = scrollController;
}
@override
void dispose() {
simpleTableContext.horizontalScrollController = null;
scrollController.dispose();
super.dispose();

View File

@ -239,18 +239,20 @@ class SimpleTableInsertActions extends ISimpleTableBottomSheetActions {
SimpleTableInsertAction(
type: SimpleTableMoreAction.insertAbove,
enableLeftBorder: true,
onTap: () => _onActionTap(
onTap: (increaseCounter) async => _onActionTap(
context,
SimpleTableMoreAction.insertAbove,
type: SimpleTableMoreAction.insertAbove,
increaseCounter: increaseCounter,
),
),
const HSpace(2),
SimpleTableInsertAction(
type: SimpleTableMoreAction.insertBelow,
enableRightBorder: true,
onTap: () => _onActionTap(
onTap: (increaseCounter) async => _onActionTap(
context,
SimpleTableMoreAction.insertBelow,
type: SimpleTableMoreAction.insertBelow,
increaseCounter: increaseCounter,
),
),
],
@ -260,18 +262,20 @@ class SimpleTableInsertActions extends ISimpleTableBottomSheetActions {
SimpleTableInsertAction(
type: SimpleTableMoreAction.insertLeft,
enableLeftBorder: true,
onTap: () => _onActionTap(
onTap: (increaseCounter) async => _onActionTap(
context,
SimpleTableMoreAction.insertLeft,
type: SimpleTableMoreAction.insertLeft,
increaseCounter: increaseCounter,
),
),
const HSpace(2),
SimpleTableInsertAction(
type: SimpleTableMoreAction.insertRight,
enableRightBorder: true,
onTap: () => _onActionTap(
onTap: (increaseCounter) async => _onActionTap(
context,
SimpleTableMoreAction.insertRight,
type: SimpleTableMoreAction.insertRight,
increaseCounter: increaseCounter,
),
),
],
@ -279,7 +283,11 @@ class SimpleTableInsertActions extends ISimpleTableBottomSheetActions {
};
}
void _onActionTap(BuildContext context, SimpleTableMoreAction type) {
Future<void> _onActionTap(
BuildContext context, {
required SimpleTableMoreAction type,
required int increaseCounter,
}) async {
final simpleTableContext = context.read<SimpleTableContext>();
final tableNode = cellNode.parentTableNode;
if (tableNode == null) {
@ -291,34 +299,48 @@ class SimpleTableInsertActions extends ISimpleTableBottomSheetActions {
case SimpleTableMoreAction.insertAbove:
// update the highlight status for the selecting row
simpleTableContext.selectingRow.value = cellNode.rowIndex + 1;
editorState.insertRowInTable(
await editorState.insertRowInTable(
tableNode,
cellNode.rowIndex,
);
case SimpleTableMoreAction.insertBelow:
editorState.insertRowInTable(
await editorState.insertRowInTable(
tableNode,
cellNode.rowIndex + 1,
);
// scroll to the next cell position
editorState.scrollService?.scrollTo(
SimpleTableConstants.defaultRowHeight,
duration: Durations.short3,
);
case SimpleTableMoreAction.insertLeft:
// update the highlight status for the selecting column
simpleTableContext.selectingColumn.value = cellNode.columnIndex + 1;
editorState.insertColumnInTable(
await editorState.insertColumnInTable(
tableNode,
cellNode.columnIndex,
);
case SimpleTableMoreAction.insertRight:
editorState.insertColumnInTable(
await editorState.insertColumnInTable(
tableNode,
cellNode.columnIndex + 1,
);
final horizontalScrollController =
simpleTableContext.horizontalScrollController;
if (horizontalScrollController != null) {
final previousWidth = horizontalScrollController.offset;
horizontalScrollController.jumpTo(
previousWidth + SimpleTableConstants.defaultColumnWidth,
);
}
default:
assert(false, 'Unsupported action: $type');
}
}
}
class SimpleTableInsertAction extends StatelessWidget {
class SimpleTableInsertAction extends StatefulWidget {
const SimpleTableInsertAction({
super.key,
required this.type,
@ -330,7 +352,16 @@ class SimpleTableInsertAction extends StatelessWidget {
final SimpleTableMoreAction type;
final bool enableLeftBorder;
final bool enableRightBorder;
final void Function() onTap;
final ValueChanged<int> onTap;
@override
State<SimpleTableInsertAction> createState() =>
_SimpleTableInsertActionState();
}
class _SimpleTableInsertActionState extends State<SimpleTableInsertAction> {
// used to count how many times the action is tapped
int increaseCounter = 0;
@override
Widget build(BuildContext context) {
@ -341,19 +372,19 @@ class SimpleTableInsertAction extends StatelessWidget {
shape: _buildBorder(),
),
child: AnimatedGestureDetector(
onTapUp: onTap,
onTapUp: () => widget.onTap(increaseCounter++),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.all(1),
child: FlowySvg(
type.leftIconSvg,
widget.type.leftIconSvg,
size: const Size.square(22),
),
),
FlowyText(
type.name,
widget.type.name,
fontSize: 12,
figmaLineHeight: 16,
),
@ -370,10 +401,10 @@ class SimpleTableInsertAction extends StatelessWidget {
);
return RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: enableLeftBorder ? radius : Radius.zero,
bottomLeft: enableLeftBorder ? radius : Radius.zero,
topRight: enableRightBorder ? radius : Radius.zero,
bottomRight: enableRightBorder ? radius : Radius.zero,
topLeft: widget.enableLeftBorder ? radius : Radius.zero,
bottomLeft: widget.enableLeftBorder ? radius : Radius.zero,
topRight: widget.enableRightBorder ? radius : Radius.zero,
bottomRight: widget.enableRightBorder ? radius : Radius.zero,
),
);
}
@ -592,7 +623,7 @@ class _SimpleTableHeaderActionButtonState
child: CupertinoSwitch(
value: value,
activeColor: Theme.of(context).colorScheme.primary,
onChanged: (_) {},
onChanged: (_) => _toggle(),
),
),
);
@ -1198,19 +1229,12 @@ class SimpleTableQuickActions extends StatelessWidget {
SimpleTableMoreAction.copy,
),
),
FutureBuilder(
future: getIt<ClipboardService>().getData(),
builder: (context, snapshot) {
final hasContent = snapshot.data?.tableJson != null;
return SimpleTableQuickAction(
type: SimpleTableMoreAction.paste,
isEnabled: hasContent,
onTap: () => _onActionTap(
context,
SimpleTableMoreAction.paste,
),
);
},
SimpleTableQuickAction(
type: SimpleTableMoreAction.paste,
onTap: () => _onActionTap(
context,
SimpleTableMoreAction.paste,
),
),
SimpleTableQuickAction(
type: SimpleTableMoreAction.delete,

View File

@ -61,8 +61,8 @@ packages:
dependency: "direct main"
description:
path: "."
ref: cfb8b1b
resolved-ref: cfb8b1b6eb06f73a4fb297b6fd1d54b0ccec2922
ref: "9f6a299"
resolved-ref: "9f6a29968ecbb61678b8e0e8c9d90bcba44a24e3"
url: "https://github.com/AppFlowy-IO/appflowy-editor.git"
source: git
version: "4.0.0"

View File

@ -174,7 +174,7 @@ dependency_overrides:
appflowy_editor:
git:
url: https://github.com/AppFlowy-IO/appflowy-editor.git
ref: "cfb8b1b"
ref: "9f6a299"
appflowy_editor_plugins:
git:

View File

@ -1,5 +1,6 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter_test/flutter_test.dart';
import 'simple_table_test_helper.dart';
@ -193,5 +194,45 @@ void main() {
expect(tableNode.tableAlign, align);
}
});
test('clear the existing align of the column before updating', () async {
final (editorState, tableNode) = createEditorStateAndTable(
rowCount: 2,
columnCount: 3,
);
final firstCellNode = tableNode.getTableCellNode(
rowIndex: 0,
columnIndex: 0,
);
Node firstParagraphNode = firstCellNode!.children.first;
// format the first paragraph to center align
final transaction = editorState.transaction;
transaction.updateNode(
firstParagraphNode,
{
blockComponentAlign: TableAlign.right.key,
},
);
await editorState.apply(transaction);
firstParagraphNode = editorState.getNodeAtPath([0, 0, 0, 0])!;
expect(
firstParagraphNode.attributes[blockComponentAlign],
TableAlign.right.key,
);
await editorState.updateColumnAlign(
tableCellNode: firstCellNode,
align: TableAlign.center,
);
expect(
firstParagraphNode.attributes[blockComponentAlign],
null,
);
});
});
}