feat: support RTL in toolbar (#496)
This commit is contained in:
parent
d1b9084ffb
commit
2b58d4f9ca
7 changed files with 103 additions and 22 deletions
|
|
@ -66,6 +66,7 @@ class _DesktopEditorState extends State<DesktopEditor> {
|
||||||
...alignmentItems,
|
...alignmentItems,
|
||||||
],
|
],
|
||||||
editorState: editorState,
|
editorState: editorState,
|
||||||
|
textDirection: widget.textDirection,
|
||||||
editorScrollController: editorScrollController,
|
editorScrollController: editorScrollController,
|
||||||
child: Directionality(
|
child: Directionality(
|
||||||
textDirection: widget.textDirection,
|
textDirection: widget.textDirection,
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ class FloatingToolbar extends StatefulWidget {
|
||||||
required this.items,
|
required this.items,
|
||||||
required this.editorState,
|
required this.editorState,
|
||||||
required this.editorScrollController,
|
required this.editorScrollController,
|
||||||
|
required this.textDirection,
|
||||||
required this.child,
|
required this.child,
|
||||||
this.style = const FloatingToolbarStyle(),
|
this.style = const FloatingToolbarStyle(),
|
||||||
});
|
});
|
||||||
|
|
@ -29,6 +30,7 @@ class FloatingToolbar extends StatefulWidget {
|
||||||
final List<ToolbarItem> items;
|
final List<ToolbarItem> items;
|
||||||
final EditorState editorState;
|
final EditorState editorState;
|
||||||
final EditorScrollController editorScrollController;
|
final EditorScrollController editorScrollController;
|
||||||
|
final TextDirection? textDirection;
|
||||||
final Widget child;
|
final Widget child;
|
||||||
final FloatingToolbarStyle style;
|
final FloatingToolbarStyle style;
|
||||||
|
|
||||||
|
|
@ -172,6 +174,7 @@ class _FloatingToolbarState extends State<FloatingToolbar>
|
||||||
editorState: editorState,
|
editorState: editorState,
|
||||||
backgroundColor: widget.style.backgroundColor,
|
backgroundColor: widget.style.backgroundColor,
|
||||||
toolbarActiveColor: widget.style.toolbarActiveColor,
|
toolbarActiveColor: widget.style.toolbarActiveColor,
|
||||||
|
textDirection: widget.textDirection ?? Directionality.of(context),
|
||||||
);
|
);
|
||||||
return _toolbarWidget!;
|
return _toolbarWidget!;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,12 @@ import 'package:flutter/material.dart';
|
||||||
|
|
||||||
const floatingToolbarHeight = 32.0;
|
const floatingToolbarHeight = 32.0;
|
||||||
|
|
||||||
|
@visibleForTesting
|
||||||
|
const floatingToolbarContainerKey =
|
||||||
|
Key('appflowy_editor_floating_toolbar_container');
|
||||||
|
@visibleForTesting
|
||||||
|
const floatingToolbarItemPrefixKey = 'appflowy_editor_floating_toolbar_item';
|
||||||
|
|
||||||
class FloatingToolbarWidget extends StatefulWidget {
|
class FloatingToolbarWidget extends StatefulWidget {
|
||||||
const FloatingToolbarWidget({
|
const FloatingToolbarWidget({
|
||||||
super.key,
|
super.key,
|
||||||
|
|
@ -11,12 +17,14 @@ class FloatingToolbarWidget extends StatefulWidget {
|
||||||
required this.toolbarActiveColor,
|
required this.toolbarActiveColor,
|
||||||
required this.items,
|
required this.items,
|
||||||
required this.editorState,
|
required this.editorState,
|
||||||
|
required this.textDirection,
|
||||||
});
|
});
|
||||||
|
|
||||||
final List<ToolbarItem> items;
|
final List<ToolbarItem> items;
|
||||||
final Color backgroundColor;
|
final Color backgroundColor;
|
||||||
final Color toolbarActiveColor;
|
final Color toolbarActiveColor;
|
||||||
final EditorState editorState;
|
final EditorState editorState;
|
||||||
|
final TextDirection textDirection;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<FloatingToolbarWidget> createState() => _FloatingToolbarWidgetState();
|
State<FloatingToolbarWidget> createState() => _FloatingToolbarWidgetState();
|
||||||
|
|
@ -37,18 +45,24 @@ class _FloatingToolbarWidgetState extends State<FloatingToolbarWidget> {
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
height: floatingToolbarHeight,
|
height: floatingToolbarHeight,
|
||||||
child: Row(
|
child: Row(
|
||||||
|
key: floatingToolbarContainerKey,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: activeItems.map((item) {
|
textDirection: widget.textDirection,
|
||||||
final builder = item.builder;
|
children: activeItems
|
||||||
return Center(
|
.mapIndexed(
|
||||||
child: builder!(
|
(index, item) => Center(
|
||||||
context,
|
key: Key(
|
||||||
widget.editorState,
|
'${floatingToolbarItemPrefixKey}_${item.id}_$index',
|
||||||
widget.toolbarActiveColor,
|
),
|
||||||
),
|
child: item.builder!(
|
||||||
);
|
context,
|
||||||
}).toList(growable: false),
|
widget.editorState,
|
||||||
|
widget.toolbarActiveColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(growable: false),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -62,8 +76,10 @@ class _FloatingToolbarWidgetState extends State<FloatingToolbarWidget> {
|
||||||
if (activeItems.isEmpty) {
|
if (activeItems.isEmpty) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// sort by group.
|
// sort by group.
|
||||||
activeItems.sort((a, b) => a.group.compareTo(b.group));
|
activeItems.sort((a, b) => a.group.compareTo(b.group));
|
||||||
|
|
||||||
// insert the divider.
|
// insert the divider.
|
||||||
return activeItems
|
return activeItems
|
||||||
.splitBetween((first, second) => first.group != second.group)
|
.splitBetween((first, second) => first.group != second.group)
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,7 @@ class CustomToolbarItemColor extends StatelessWidget {
|
||||||
border: Border.all(color: Colors.blue),
|
border: Border.all(color: Colors.blue),
|
||||||
),
|
),
|
||||||
child: FloatingToolbar(
|
child: FloatingToolbar(
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
items: [bulletedListItem],
|
items: [bulletedListItem],
|
||||||
style: const FloatingToolbarStyle(
|
style: const FloatingToolbarStyle(
|
||||||
backgroundColor: Colors.red,
|
backgroundColor: Colors.red,
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,18 @@ import 'package:flutter_test/flutter_test.dart';
|
||||||
import '../find_replace_menu/find_replace_menu_utils.dart';
|
import '../find_replace_menu/find_replace_menu_utils.dart';
|
||||||
import '../util/util.dart';
|
import '../util/util.dart';
|
||||||
|
|
||||||
|
final floatingToolbarItems = [
|
||||||
|
paragraphItem,
|
||||||
|
...headingItems,
|
||||||
|
...markdownFormatItems,
|
||||||
|
quoteItem,
|
||||||
|
bulletedListItem,
|
||||||
|
numberedListItem,
|
||||||
|
linkItem,
|
||||||
|
buildTextColorItem(),
|
||||||
|
buildHighlightColorItem(),
|
||||||
|
];
|
||||||
|
|
||||||
class TestableEditor {
|
class TestableEditor {
|
||||||
TestableEditor({
|
TestableEditor({
|
||||||
required this.tester,
|
required this.tester,
|
||||||
|
|
@ -42,6 +54,7 @@ class TestableEditor {
|
||||||
Widget Function(Widget child)? wrapper,
|
Widget Function(Widget child)? wrapper,
|
||||||
TargetPlatform? platform,
|
TargetPlatform? platform,
|
||||||
String? defaultTextDirection,
|
String? defaultTextDirection,
|
||||||
|
TextDirection textDirection = TextDirection.ltr,
|
||||||
}) async {
|
}) async {
|
||||||
await AppFlowyEditorLocalizations.load(locale);
|
await AppFlowyEditorLocalizations.load(locale);
|
||||||
|
|
||||||
|
|
@ -106,23 +119,20 @@ class TestableEditor {
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
editor = FloatingToolbar(
|
editor = FloatingToolbar(
|
||||||
items: [
|
items: floatingToolbarItems,
|
||||||
paragraphItem,
|
|
||||||
...headingItems,
|
|
||||||
...markdownFormatItems,
|
|
||||||
quoteItem,
|
|
||||||
bulletedListItem,
|
|
||||||
numberedListItem,
|
|
||||||
linkItem,
|
|
||||||
buildTextColorItem(),
|
|
||||||
buildHighlightColorItem(),
|
|
||||||
],
|
|
||||||
editorState: editorState,
|
editorState: editorState,
|
||||||
|
textDirection: textDirection,
|
||||||
editorScrollController: editorScrollController,
|
editorScrollController: editorScrollController,
|
||||||
child: editor,
|
child: editor,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
editor = Directionality(
|
||||||
|
textDirection: textDirection,
|
||||||
|
child: editor,
|
||||||
|
);
|
||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
MaterialApp(
|
MaterialApp(
|
||||||
theme: ThemeData(platform: platform),
|
theme: ThemeData(platform: platform),
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@ Future<void> _testLinkMenuInSingleTextSelection(WidgetTester tester) async {
|
||||||
buildHighlightColorItem(),
|
buildHighlightColorItem(),
|
||||||
],
|
],
|
||||||
editorState: editor.editorState,
|
editorState: editor.editorState,
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
editorScrollController: EditorScrollController(
|
editorScrollController: EditorScrollController(
|
||||||
editorState: editor.editorState,
|
editorState: editor.editorState,
|
||||||
scrollController: scrollController,
|
scrollController: scrollController,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
import '../../infra/testable_editor.dart';
|
import '../../infra/testable_editor.dart';
|
||||||
|
|
@ -21,11 +22,59 @@ void main() async {
|
||||||
endOffset: text.length,
|
endOffset: text.length,
|
||||||
);
|
);
|
||||||
await editor.updateSelection(selection);
|
await editor.updateSelection(selection);
|
||||||
await tester.pumpAndSettle();
|
|
||||||
|
|
||||||
final floatingToolbar = find.byType(FloatingToolbarWidget);
|
final floatingToolbar = find.byType(FloatingToolbarWidget);
|
||||||
expect(floatingToolbar, findsOneWidget);
|
expect(floatingToolbar, findsOneWidget);
|
||||||
expect(tester.getTopLeft(floatingToolbar).dy >= 0, true);
|
expect(tester.getTopLeft(floatingToolbar).dy >= 0, true);
|
||||||
|
|
||||||
|
await editor.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'select the first line of the document, the toolbar layout should be right to left in RTL mode',
|
||||||
|
(tester) async {
|
||||||
|
final editor = tester.editor..addParagraphs(3, initialText: text);
|
||||||
|
await editor.startTesting(
|
||||||
|
withFloatingToolbar: true,
|
||||||
|
textDirection: TextDirection.rtl,
|
||||||
|
);
|
||||||
|
|
||||||
|
final selection = Selection.single(
|
||||||
|
path: [0],
|
||||||
|
startOffset: 0,
|
||||||
|
endOffset: text.length,
|
||||||
|
);
|
||||||
|
await editor.updateSelection(selection);
|
||||||
|
|
||||||
|
final floatingToolbar = find.byType(FloatingToolbarWidget);
|
||||||
|
expect(floatingToolbar, findsOneWidget);
|
||||||
|
final floatingToolbarContainer = tester.widget<Row>(
|
||||||
|
find.byKey(floatingToolbarContainerKey),
|
||||||
|
);
|
||||||
|
expect(floatingToolbarContainer.textDirection, TextDirection.rtl);
|
||||||
|
final List<Widget> toolbarItemWidgets = floatingToolbarContainer.children;
|
||||||
|
expect(
|
||||||
|
toolbarItemWidgets.length,
|
||||||
|
// the floating toolbar items will add the divider between the groups,
|
||||||
|
// so the length of the toolbar items will be the sum of the (floatingToolbarItems.length +
|
||||||
|
// the number of the groups) - 1.
|
||||||
|
floatingToolbarItems.length +
|
||||||
|
floatingToolbarItems.map((e) => e.group).toSet().length -
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
|
||||||
|
final expectedIds = floatingToolbarItems.map((e) => e.id).toList();
|
||||||
|
var j = 0;
|
||||||
|
for (int i = 0; i < toolbarItemWidgets.length; i++) {
|
||||||
|
final id = '${floatingToolbarItemPrefixKey}_${expectedIds[j]}_$i';
|
||||||
|
final key = toolbarItemWidgets[i].key as ValueKey;
|
||||||
|
if (key.value.contains(placeholderItem.id)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
expect(key, Key(id));
|
||||||
|
j++;
|
||||||
|
}
|
||||||
|
|
||||||
await editor.dispose();
|
await editor.dispose();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue