Added : customize the color and background color of selected text (#1601)

* Added Emoji Support

* Added Color Picker for font color and background color

* chore: revert code

* feat: re-implement the color picker

* test: add test case for adding color

* test: update appflowy_editor test flag

Co-authored-by: Muhammad Rizwan <haris.arshad.2010@gmail.com>
Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
This commit is contained in:
Muhammad Rizwan 2022-12-23 16:57:17 +05:00 committed by GitHub
parent f9cc05319b
commit e4b07e69fa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 788 additions and 6 deletions

View File

@ -44,7 +44,7 @@ jobs:
- uses: codecov/codecov-action@v3
with:
name: appflowy_editor
flags: appflowy editor
flags: appflowy_editor
env_vars: ${{ matrix.os }}
fail_ci_if_error: true
verbose: true

View File

@ -96,7 +96,8 @@
"inlineCode": "Inline Code",
"quote": "Quote Block",
"header": "Header",
"highlight": "Highlight"
"highlight": "Highlight",
"color": "Color"
},
"tooltip": {
"lightMode": "Switch to Light mode",

View File

@ -90,7 +90,8 @@
"inlineCode": "Código embebido",
"quote": "Cita",
"header": "Título",
"highlight": "Resaltado"
"highlight": "Resaltado",
"color": "Color"
},
"tooltip": {
"lightMode": "Cambiar a modo Claro",

View File

@ -0,0 +1,3 @@
<svg width="10" height="8" viewBox="0 0 10 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 5.2L2.84615 7L9 1" stroke="#00BCF0" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 198 B

View File

@ -16,6 +16,8 @@
"@heading3": {},
"highlight": "Highlight",
"@highlight": {},
"color": "Color",
"@color": {},
"image": "Image",
"@image": {},
"italic": "Italic",
@ -31,5 +33,45 @@
"text": "Text",
"@text": {},
"underline": "Underline",
"@underline": {}
"@underline": {},
"fontColorDefault": "Default",
"@fontColorDefault": {},
"fontColorGray": "Gray",
"@fontColorGray": {},
"fontColorBrown": "Brown",
"@fontColorBrown": {},
"fontColorOrange": "Orange",
"@fontColorOrange": {},
"fontColorYellow": "Yellow",
"@fontColorYellow": {},
"fontColorGreen": "Green",
"@fontColorGreen": {},
"fontColorBlue": "Blue",
"@fontColorBlue": {},
"fontColorPurple": "Purple",
"@fontColorPurple": {},
"fontColorPink": "Pink",
"@fontColorPink": {},
"fontColorRed": "Red",
"@fontColorRed": {},
"backgroundColorDefault": "Default background",
"@backgroundColorDefault": {},
"backgroundColorGray": "Gray background",
"@backgroundColorGray": {},
"backgroundColorBrown": "Brown background",
"@backgroundColorBrown": {},
"backgroundColorOrange": "Orange background",
"@backgroundColorOrange": {},
"backgroundColorYellow": "Yellow background",
"@backgroundColorYellow": {},
"backgroundColorGreen": "Green background",
"@backgroundColorGreen": {},
"backgroundColorBlue": "Blue background",
"@backgroundColorBlue": {},
"backgroundColorPurple": "Purple background",
"@backgroundColorPurple": {},
"backgroundColorPink": "Pink background",
"@backgroundColorPink": {},
"backgroundColorRed": "Red background",
"@backgroundColorRed": {}
}

View File

@ -45,6 +45,7 @@ class BuiltInAttributeKey {
BuiltInAttributeKey.underline,
BuiltInAttributeKey.strikethrough,
BuiltInAttributeKey.backgroundColor,
BuiltInAttributeKey.color,
BuiltInAttributeKey.href,
BuiltInAttributeKey.code,
];

View File

@ -34,6 +34,17 @@ extension TextNodeExtension on TextNode {
return value != null;
});
bool allSatisfyFontColorInSelection(Selection selection) =>
allSatisfyInSelection(selection, BuiltInAttributeKey.color, (value) {
return value != null;
});
bool allSatisfyBackgroundColorInSelection(Selection selection) =>
allSatisfyInSelection(selection, BuiltInAttributeKey.backgroundColor,
(value) {
return value != null;
});
bool allSatisfyBoldInSelection(Selection selection) =>
allSatisfyInSelection(selection, BuiltInAttributeKey.bold, (value) {
return value == true;

View File

@ -22,10 +22,41 @@ class MessageLookup extends MessageLookupByLibrary {
final messages = _notInlinedMessages(_notInlinedMessages);
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
"backgroundColorBlue":
MessageLookupByLibrary.simpleMessage("Blue background"),
"backgroundColorBrown":
MessageLookupByLibrary.simpleMessage("Brown background"),
"backgroundColorDefault":
MessageLookupByLibrary.simpleMessage("Default background"),
"backgroundColorGray":
MessageLookupByLibrary.simpleMessage("Gray background"),
"backgroundColorGreen":
MessageLookupByLibrary.simpleMessage("Green background"),
"backgroundColorOrange":
MessageLookupByLibrary.simpleMessage("Orange background"),
"backgroundColorPink":
MessageLookupByLibrary.simpleMessage("Pink background"),
"backgroundColorPurple":
MessageLookupByLibrary.simpleMessage("Purple background"),
"backgroundColorRed":
MessageLookupByLibrary.simpleMessage("Red background"),
"backgroundColorYellow":
MessageLookupByLibrary.simpleMessage("Yellow background"),
"bold": MessageLookupByLibrary.simpleMessage("Bold"),
"bulletedList": MessageLookupByLibrary.simpleMessage("Bulleted List"),
"checkbox": MessageLookupByLibrary.simpleMessage("Checkbox"),
"color": MessageLookupByLibrary.simpleMessage("Color"),
"embedCode": MessageLookupByLibrary.simpleMessage("Embed Code"),
"fontColorBlue": MessageLookupByLibrary.simpleMessage("Blue"),
"fontColorBrown": MessageLookupByLibrary.simpleMessage("Brown"),
"fontColorDefault": MessageLookupByLibrary.simpleMessage("Default"),
"fontColorGray": MessageLookupByLibrary.simpleMessage("Gray"),
"fontColorGreen": MessageLookupByLibrary.simpleMessage("Green"),
"fontColorOrange": MessageLookupByLibrary.simpleMessage("Orange"),
"fontColorPink": MessageLookupByLibrary.simpleMessage("Pink"),
"fontColorPurple": MessageLookupByLibrary.simpleMessage("Purple"),
"fontColorRed": MessageLookupByLibrary.simpleMessage("Red"),
"fontColorYellow": MessageLookupByLibrary.simpleMessage("Yellow"),
"heading1": MessageLookupByLibrary.simpleMessage("H1"),
"heading2": MessageLookupByLibrary.simpleMessage("H2"),
"heading3": MessageLookupByLibrary.simpleMessage("H3"),

View File

@ -131,6 +131,16 @@ class AppFlowyEditorLocalizations {
);
}
/// `Color`
String get color {
return Intl.message(
'Color',
name: 'color',
desc: '',
args: [],
);
}
/// `Image`
String get image {
return Intl.message(
@ -210,6 +220,206 @@ class AppFlowyEditorLocalizations {
args: [],
);
}
/// `Default`
String get fontColorDefault {
return Intl.message(
'Default',
name: 'fontColorDefault',
desc: '',
args: [],
);
}
/// `Gray`
String get fontColorGray {
return Intl.message(
'Gray',
name: 'fontColorGray',
desc: '',
args: [],
);
}
/// `Brown`
String get fontColorBrown {
return Intl.message(
'Brown',
name: 'fontColorBrown',
desc: '',
args: [],
);
}
/// `Orange`
String get fontColorOrange {
return Intl.message(
'Orange',
name: 'fontColorOrange',
desc: '',
args: [],
);
}
/// `Yellow`
String get fontColorYellow {
return Intl.message(
'Yellow',
name: 'fontColorYellow',
desc: '',
args: [],
);
}
/// `Green`
String get fontColorGreen {
return Intl.message(
'Green',
name: 'fontColorGreen',
desc: '',
args: [],
);
}
/// `Blue`
String get fontColorBlue {
return Intl.message(
'Blue',
name: 'fontColorBlue',
desc: '',
args: [],
);
}
/// `Purple`
String get fontColorPurple {
return Intl.message(
'Purple',
name: 'fontColorPurple',
desc: '',
args: [],
);
}
/// `Pink`
String get fontColorPink {
return Intl.message(
'Pink',
name: 'fontColorPink',
desc: '',
args: [],
);
}
/// `Red`
String get fontColorRed {
return Intl.message(
'Red',
name: 'fontColorRed',
desc: '',
args: [],
);
}
/// `Default background`
String get backgroundColorDefault {
return Intl.message(
'Default background',
name: 'backgroundColorDefault',
desc: '',
args: [],
);
}
/// `Gray background`
String get backgroundColorGray {
return Intl.message(
'Gray background',
name: 'backgroundColorGray',
desc: '',
args: [],
);
}
/// `Brown background`
String get backgroundColorBrown {
return Intl.message(
'Brown background',
name: 'backgroundColorBrown',
desc: '',
args: [],
);
}
/// `Orange background`
String get backgroundColorOrange {
return Intl.message(
'Orange background',
name: 'backgroundColorOrange',
desc: '',
args: [],
);
}
/// `Yellow background`
String get backgroundColorYellow {
return Intl.message(
'Yellow background',
name: 'backgroundColorYellow',
desc: '',
args: [],
);
}
/// `Green background`
String get backgroundColorGreen {
return Intl.message(
'Green background',
name: 'backgroundColorGreen',
desc: '',
args: [],
);
}
/// `Blue background`
String get backgroundColorBlue {
return Intl.message(
'Blue background',
name: 'backgroundColorBlue',
desc: '',
args: [],
);
}
/// `Purple background`
String get backgroundColorPurple {
return Intl.message(
'Purple background',
name: 'backgroundColorPurple',
desc: '',
args: [],
);
}
/// `Pink background`
String get backgroundColorPink {
return Intl.message(
'Pink background',
name: 'backgroundColorPink',
desc: '',
args: [],
);
}
/// `Red background`
String get backgroundColorRed {
return Intl.message(
'Red background',
name: 'backgroundColorRed',
desc: '',
args: [],
);
}
}
class AppLocalizationDelegate

View File

@ -0,0 +1,168 @@
import 'package:appflowy_editor/src/infra/flowy_svg.dart';
import 'package:flutter/material.dart';
class ColorOption {
const ColorOption({
required this.colorHex,
required this.name,
});
final String colorHex;
final String name;
}
enum _ColorType {
font,
background,
}
class ColorPicker extends StatefulWidget {
const ColorPicker({
super.key,
this.selectedFontColorHex,
this.selectedBackgroundColorHex,
required this.pickerBackgroundColor,
required this.fontColorOptions,
required this.backgroundColorOptions,
required this.pickerItemHoverColor,
required this.pickerItemTextColor,
required this.onSubmittedbackgroundColorHex,
required this.onSubmittedFontColorHex,
});
final String? selectedFontColorHex;
final String? selectedBackgroundColorHex;
final Color pickerBackgroundColor;
final Color pickerItemHoverColor;
final Color pickerItemTextColor;
final void Function(String color) onSubmittedbackgroundColorHex;
final void Function(String color) onSubmittedFontColorHex;
final List<ColorOption> fontColorOptions;
final List<ColorOption> backgroundColorOptions;
@override
State<ColorPicker> createState() => _ColorPickerState();
}
class _ColorPickerState extends State<ColorPicker> {
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: widget.pickerBackgroundColor,
boxShadow: [
BoxShadow(
blurRadius: 5,
spreadRadius: 1,
color: Colors.black.withOpacity(0.1),
),
],
borderRadius: BorderRadius.circular(6.0),
),
height: 250,
width: 220,
padding: const EdgeInsets.fromLTRB(10, 6, 10, 6),
child: ScrollConfiguration(
behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
// font color
_buildHeader('font color'),
// padding
const SizedBox(height: 6),
_buildColorItems(
_ColorType.font,
widget.fontColorOptions,
widget.selectedFontColorHex,
),
// background color
const SizedBox(height: 6),
_buildHeader('background color'),
const SizedBox(height: 6),
_buildColorItems(
_ColorType.background,
widget.backgroundColorOptions,
widget.selectedBackgroundColorHex,
),
],
),
),
),
);
}
Widget _buildHeader(String text) {
return Text(
text,
style: const TextStyle(
color: Colors.grey,
fontWeight: FontWeight.bold,
),
);
}
Widget _buildColorItems(
_ColorType type, List<ColorOption> options, String? selectedColor) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: options
.map((e) => _buildColorItem(type, e, e.colorHex == selectedColor))
.toList(),
);
}
Widget _buildColorItem(_ColorType type, ColorOption option, bool isChecked) {
return SizedBox(
height: 36,
child: InkWell(
customBorder: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(6),
),
hoverColor: widget.pickerItemHoverColor,
onTap: () {
if (type == _ColorType.font) {
widget.onSubmittedFontColorHex(option.colorHex);
} else if (type == _ColorType.background) {
widget.onSubmittedbackgroundColorHex(option.colorHex);
}
},
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
// padding
const SizedBox(width: 6),
// icon
SizedBox.square(
dimension: 12,
child: Container(
decoration: BoxDecoration(
color: Color(int.tryParse(option.colorHex) ?? 0xFFFFFFFF),
shape: BoxShape.circle,
),
),
),
// padding
const SizedBox(width: 10),
// text
Expanded(
child: Text(
option.name,
style:
TextStyle(fontSize: 12, color: widget.pickerItemTextColor),
),
),
// checkbox
if (isChecked) const FlowySvg(name: 'checkmark'),
const SizedBox(width: 6),
],
),
),
);
}
}

View File

@ -255,6 +255,11 @@ class _FlowyRichTextState extends State<FlowyRichText> with SelectableMixin {
TextStyle(backgroundColor: attributes.backgroundColor),
);
}
if (attributes.color != null) {
textStyle = textStyle.combine(
TextStyle(color: attributes.color),
);
}
}
offset += textInsert.length;
textSpans.add(

View File

@ -4,6 +4,7 @@ import 'package:appflowy_editor/src/extensions/url_launcher_extension.dart';
import 'package:appflowy_editor/src/flutter/overlay.dart';
import 'package:appflowy_editor/src/infra/clipboard.dart';
import 'package:appflowy_editor/src/infra/flowy_svg.dart';
import 'package:appflowy_editor/src/render/color_menu/color_picker.dart';
import 'package:appflowy_editor/src/render/link_menu/link_menu.dart';
import 'package:appflowy_editor/src/extensions/text_node_extensions.dart';
import 'package:appflowy_editor/src/extensions/editor_state_extensions.dart';
@ -264,6 +265,37 @@ List<ToolbarItem> defaultToolbarItems = [
editorState.editorStyle.highlightColorHex!,
),
),
ToolbarItem(
id: 'appflowy.toolbar.color',
type: 4,
tooltipsMessage: AppFlowyEditorLocalizations.current.color,
iconBuilder: (isHighlight) => Icon(
Icons.color_lens_outlined,
size: 14,
color: isHighlight ? Colors.lightBlue : Colors.white,
),
validator: _showInBuiltInTextSelection,
highlightCallback: (editorState) =>
_allSatisfy(
editorState,
BuiltInAttributeKey.color,
(value) =>
value != null &&
value != _generateFontColorOptions(editorState).first.colorHex,
) ||
_allSatisfy(
editorState,
BuiltInAttributeKey.backgroundColor,
(value) =>
value != null &&
value !=
_generateBackgroundColorOptions(editorState).first.colorHex,
),
handler: (editorState, context) => showColorMenu(
context,
editorState,
),
),
];
ToolbarItemValidator _onlyShowInSingleTextSelection = (editorState) {
@ -301,6 +333,8 @@ bool _allSatisfy(
}
OverlayEntry? _linkMenuOverlay;
OverlayEntry? _colorMenuOverlay;
EditorState? _editorState;
bool _changeSelectionInner = false;
void showLinkMenu(
@ -343,6 +377,7 @@ void showLinkMenu(
BuiltInAttributeKey.href,
);
}
_linkMenuOverlay = OverlayEntry(builder: (context) {
return Positioned(
top: matchRect.bottom + 5.0,
@ -360,6 +395,7 @@ void showLinkMenu(
text,
textNode: textNode,
);
_dismissLinkMenu();
},
onCopyLink: () {
@ -419,3 +455,211 @@ void _dismissLinkMenu() {
.removeListener(_dismissLinkMenu);
_editorState = null;
}
void _dismissColorMenu() {
// workaround: SelectionService has been released after hot reload.
final isSelectionDisposed =
_editorState?.service.selectionServiceKey.currentState == null;
if (isSelectionDisposed) {
return;
}
if (_editorState?.service.selectionService.currentSelection.value == null) {
return;
}
if (_changeSelectionInner) {
_changeSelectionInner = false;
return;
}
_colorMenuOverlay?.remove();
_colorMenuOverlay = null;
_editorState?.service.scrollService?.enable();
_editorState?.service.keyboardService?.enable();
_editorState?.service.selectionService.currentSelection
.removeListener(_dismissColorMenu);
_editorState = null;
}
void showColorMenu(
BuildContext context,
EditorState editorState, {
Selection? customSelection,
}) {
final rects = editorState.service.selectionService.selectionRects;
var maxBottom = 0.0;
late Rect matchRect;
for (final rect in rects) {
if (rect.bottom > maxBottom) {
maxBottom = rect.bottom;
matchRect = rect;
}
}
final baseOffset =
editorState.renderBox?.localToGlobal(Offset.zero) ?? Offset.zero;
matchRect = matchRect.shift(-baseOffset);
_dismissColorMenu();
_editorState = editorState;
// Since the link menu will only show in single text selection,
// We get the text node directly instead of judging details again.
final selection = customSelection ??
editorState.service.selectionService.currentSelection.value;
final node = editorState.service.selectionService.currentSelectedNodes;
if (selection == null || node.isEmpty || node.first is! TextNode) {
return;
}
final textNode = node.first as TextNode;
String? backgroundColorHex;
if (textNode.allSatisfyBackgroundColorInSelection(selection)) {
backgroundColorHex = textNode.getAttributeInSelection<String>(
selection,
BuiltInAttributeKey.backgroundColor,
);
}
String? fontColorHex;
if (textNode.allSatisfyFontColorInSelection(selection)) {
fontColorHex = textNode.getAttributeInSelection<String>(
selection,
BuiltInAttributeKey.color,
);
} else {
fontColorHex = editorState.editorStyle.textStyle?.color?.toHex();
}
final style = editorState.editorStyle;
_colorMenuOverlay = OverlayEntry(builder: (context) {
return Positioned(
top: matchRect.bottom + 5.0,
left: matchRect.left + 10,
child: Material(
color: Colors.transparent,
child: ColorPicker(
pickerBackgroundColor:
style.selectionMenuBackgroundColor ?? Colors.white,
pickerItemHoverColor: style.selectionMenuItemSelectedColor ??
Colors.blue.withOpacity(0.3),
pickerItemTextColor: style.selectionMenuItemTextColor ?? Colors.black,
selectedFontColorHex: fontColorHex,
selectedBackgroundColorHex: backgroundColorHex,
fontColorOptions: _generateFontColorOptions(editorState),
backgroundColorOptions: _generateBackgroundColorOptions(editorState),
onSubmittedbackgroundColorHex: (color) {
formatHighlightColor(
editorState,
color,
);
_dismissColorMenu();
},
onSubmittedFontColorHex: (color) {
formatFontColor(
editorState,
color,
);
_dismissColorMenu();
},
),
),
);
});
Overlay.of(context)?.insert(_colorMenuOverlay!);
editorState.service.scrollService?.disable();
editorState.service.keyboardService?.disable();
editorState.service.selectionService.currentSelection
.addListener(_dismissColorMenu);
}
List<ColorOption> _generateFontColorOptions(EditorState editorState) {
final defaultColor =
editorState.editorStyle.textStyle?.color ?? Colors.black; // black
return [
ColorOption(
colorHex: defaultColor.toHex(),
name: AppFlowyEditorLocalizations.current.fontColorDefault,
),
ColorOption(
colorHex: Colors.grey.toHex(),
name: AppFlowyEditorLocalizations.current.fontColorGray,
),
ColorOption(
colorHex: Colors.brown.toHex(),
name: AppFlowyEditorLocalizations.current.fontColorBrown,
),
ColorOption(
colorHex: Colors.yellow.toHex(),
name: AppFlowyEditorLocalizations.current.fontColorYellow,
),
ColorOption(
colorHex: Colors.green.toHex(),
name: AppFlowyEditorLocalizations.current.fontColorGreen,
),
ColorOption(
colorHex: Colors.blue.toHex(),
name: AppFlowyEditorLocalizations.current.fontColorBlue,
),
ColorOption(
colorHex: Colors.purple.toHex(),
name: AppFlowyEditorLocalizations.current.fontColorPurple,
),
ColorOption(
colorHex: Colors.pink.toHex(),
name: AppFlowyEditorLocalizations.current.fontColorPink,
),
ColorOption(
colorHex: Colors.red.toHex(),
name: AppFlowyEditorLocalizations.current.fontColorRed,
),
];
}
List<ColorOption> _generateBackgroundColorOptions(EditorState editorState) {
final defaultBackgroundColorHex =
editorState.editorStyle.highlightColorHex ?? '0x6000BCF0';
return [
ColorOption(
colorHex: defaultBackgroundColorHex,
name: AppFlowyEditorLocalizations.current.backgroundColorDefault,
),
ColorOption(
colorHex: Colors.grey.withOpacity(0.3).toHex(),
name: AppFlowyEditorLocalizations.current.backgroundColorGray,
),
ColorOption(
colorHex: Colors.brown.withOpacity(0.3).toHex(),
name: AppFlowyEditorLocalizations.current.backgroundColorBrown,
),
ColorOption(
colorHex: Colors.yellow.withOpacity(0.3).toHex(),
name: AppFlowyEditorLocalizations.current.backgroundColorYellow,
),
ColorOption(
colorHex: Colors.green.withOpacity(0.3).toHex(),
name: AppFlowyEditorLocalizations.current.backgroundColorGreen,
),
ColorOption(
colorHex: Colors.blue.withOpacity(0.3).toHex(),
name: AppFlowyEditorLocalizations.current.backgroundColorBlue,
),
ColorOption(
colorHex: Colors.purple.withOpacity(0.3).toHex(),
name: AppFlowyEditorLocalizations.current.backgroundColorPurple,
),
ColorOption(
colorHex: Colors.pink.withOpacity(0.3).toHex(),
name: AppFlowyEditorLocalizations.current.backgroundColorPink,
),
ColorOption(
colorHex: Colors.red.withOpacity(0.3).toHex(),
name: AppFlowyEditorLocalizations.current.backgroundColorRed,
),
];
}
extension on Color {
String toHex() {
return '0x${value.toRadixString(16)}';
}
}

View File

@ -173,6 +173,22 @@ bool formatHighlight(EditorState editorState, String colorHex) {
);
}
bool formatHighlightColor(EditorState editorState, String colorHex) {
return formatRichTextPartialStyle(
editorState,
BuiltInAttributeKey.backgroundColor,
customValue: colorHex,
);
}
bool formatFontColor(EditorState editorState, String colorHex) {
return formatRichTextPartialStyle(
editorState,
BuiltInAttributeKey.color,
customValue: colorHex,
);
}
bool formatRichTextPartialStyle(EditorState editorState, String styleKey,
{Object? customValue}) {
Attributes attributes = {

View File

@ -50,14 +50,15 @@ void main() async {
null,
delta: Delta()
..insert(
'appflowy.io',
link,
attributes: {
BuiltInAttributeKey.href: link,
},
),
);
await editor.startTesting();
final finder = find.byType(RichText);
await tester.pumpAndSettle();
final finder = find.text(link, findRichText: true);
expect(finder, findsOneWidget);
// tap the link

View File

@ -2,6 +2,7 @@ import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/src/extensions/text_node_extensions.dart';
import 'package:appflowy_editor/src/render/toolbar/toolbar_item_widget.dart';
import 'package:appflowy_editor/src/render/toolbar/toolbar_widget.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import '../../infra/test_editor.dart';
@ -327,4 +328,51 @@ void main() async {
);
});
}));
group('toolbar, color picker', (() {
testWidgets(
'Select Text, Click Toolbar and set color for the selected text',
(tester) async {
final editor = tester.editor..insertTextNode(singleLineText);
await editor.startTesting();
final node = editor.nodeAtPath([0]) as TextNode;
final selection = Selection(
start: Position(path: [0], offset: 0),
end: Position(path: [0], offset: singleLineText.length),
);
await editor.updateSelection(selection);
expect(find.byType(ToolbarWidget), findsOneWidget);
final colorButton = find.byWidgetPredicate((widget) {
if (widget is ToolbarItemWidget) {
return widget.item.id == 'appflowy.toolbar.color';
}
return false;
});
expect(colorButton, findsOneWidget);
await tester.tap(colorButton);
await tester.pumpAndSettle();
// select a yellow color
final yellowButton = find.text('Yellow');
await tester.tap(yellowButton);
await tester.pumpAndSettle();
expect(
node.allSatisfyInSelection(
selection,
BuiltInAttributeKey.color,
(value) {
return value == Colors.yellow.toHex();
},
),
true,
);
});
}));
}
extension on Color {
String toHex() {
return '0x${value.toRadixString(16)}';
}
}