mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2025-07-30 04:21:45 +00:00
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:
parent
7dedb84504
commit
8f7cb50dd4
@ -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])!;
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
|
@ -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,
|
||||
|
@ -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"
|
||||
|
@ -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:
|
||||
|
@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user