Unverified Commit 3128df83 authored by Justin McCandless's avatar Justin McCandless Committed by GitHub

Disableable ContextMenuButtonItems (#124253)

Fixes https://github.com/flutter/flutter/issues/124247

| Native | Flutter before | Flutter after |
| --- | --- | --- |
| <img width="248" alt="Screenshot 2023-04-05 at 9 26 16 AM" src="https://user-images.githubusercontent.com/389558/230177116-154999e8-eef3-441d-9fe9-7063839a6b99.png"> | <img width="240" alt="Screenshot 2023-04-05 at 11 18 01 AM" src="https://user-images.githubusercontent.com/389558/230177125-1680e851-223e-4956-b5b6-1a24e11dc22a.png"> | <img width="226" alt="Screenshot 2023-04-05 at 11 17 36 AM" src="https://user-images.githubusercontent.com/389558/230177123-bde82134-67e1-4ce2-8eec-719eeb779bf4.png"> |

Also, it's now possible for anyone to create disabled buttons like this by setting ContextMenuButtonItem.onPressed to `null`.
parent bd2617ec
...@@ -69,7 +69,7 @@ class CupertinoDesktopTextSelectionToolbarButton extends StatefulWidget { ...@@ -69,7 +69,7 @@ class CupertinoDesktopTextSelectionToolbarButton extends StatefulWidget {
child = null; child = null;
/// {@macro flutter.cupertino.CupertinoTextSelectionToolbarButton.onPressed} /// {@macro flutter.cupertino.CupertinoTextSelectionToolbarButton.onPressed}
final VoidCallback onPressed; final VoidCallback? onPressed;
/// {@macro flutter.cupertino.CupertinoTextSelectionToolbarButton.child} /// {@macro flutter.cupertino.CupertinoTextSelectionToolbarButton.child}
final Widget? child; final Widget? child;
......
...@@ -66,7 +66,7 @@ class CupertinoSpellCheckSuggestionsToolbar extends StatelessWidget { ...@@ -66,7 +66,7 @@ class CupertinoSpellCheckSuggestionsToolbar extends StatelessWidget {
CupertinoLocalizations.of(editableTextState.context); CupertinoLocalizations.of(editableTextState.context);
return <ContextMenuButtonItem>[ return <ContextMenuButtonItem>[
ContextMenuButtonItem( ContextMenuButtonItem(
onPressed: () {}, onPressed: null,
label: localizations.noSpellCheckReplacementsLabel, label: localizations.noSpellCheckReplacementsLabel,
) )
]; ];
......
...@@ -51,7 +51,7 @@ class DesktopTextSelectionToolbarButton extends StatelessWidget { ...@@ -51,7 +51,7 @@ class DesktopTextSelectionToolbarButton extends StatelessWidget {
); );
/// {@macro flutter.material.TextSelectionToolbarTextButton.onPressed} /// {@macro flutter.material.TextSelectionToolbarTextButton.onPressed}
final VoidCallback onPressed; final VoidCallback? onPressed;
/// {@macro flutter.material.TextSelectionToolbarTextButton.child} /// {@macro flutter.material.TextSelectionToolbarTextButton.child}
final Widget child; final Widget child;
......
...@@ -47,7 +47,7 @@ class ContextMenuButtonItem { ...@@ -47,7 +47,7 @@ class ContextMenuButtonItem {
}); });
/// The callback to be called when the button is pressed. /// The callback to be called when the button is pressed.
final VoidCallback onPressed; final VoidCallback? onPressed;
/// The type of button this represents. /// The type of button this represents.
final ContextMenuButtonType type; final ContextMenuButtonType type;
......
...@@ -14,10 +14,10 @@ void main() { ...@@ -14,10 +14,10 @@ void main() {
CupertinoApp( CupertinoApp(
home: Center( home: Center(
child: CupertinoDesktopTextSelectionToolbarButton( child: CupertinoDesktopTextSelectionToolbarButton(
child: const Text('Tap me'),
onPressed: () { onPressed: () {
pressed = true; pressed = true;
}, },
child: const Text('Tap me'),
), ),
), ),
), ),
...@@ -34,8 +34,8 @@ void main() { ...@@ -34,8 +34,8 @@ void main() {
CupertinoApp( CupertinoApp(
home: Center( home: Center(
child: CupertinoDesktopTextSelectionToolbarButton( child: CupertinoDesktopTextSelectionToolbarButton(
child: const Text('Tap me'),
onPressed: () { }, onPressed: () { },
child: const Text('Tap me'),
), ),
), ),
), ),
...@@ -71,4 +71,21 @@ void main() { ...@@ -71,4 +71,21 @@ void main() {
)); ));
expect(opacity.opacity.value, 1.0); expect(opacity.opacity.value, 1.0);
}); });
testWidgets('passing null to onPressed disables the button', (WidgetTester tester) async {
await tester.pumpWidget(
const CupertinoApp(
home: Center(
child: CupertinoDesktopTextSelectionToolbarButton(
onPressed: null,
child: Text('Tap me'),
),
),
),
);
expect(find.byType(CupertinoButton), findsOneWidget);
final CupertinoButton button = tester.widget(find.byType(CupertinoButton));
expect(button.enabled, isFalse);
});
} }
...@@ -61,7 +61,7 @@ void main() { ...@@ -61,7 +61,7 @@ void main() {
expect(labels, isNot(contains('yeller'))); expect(labels, isNot(contains('yeller')));
}); });
testWidgets('buildButtonItems builds a "No Replacements Found" button when no suggestions', (WidgetTester tester) async { testWidgets('buildButtonItems builds a disabled "No Replacements Found" button when no suggestions', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
CupertinoApp( CupertinoApp(
home: _FakeEditableText(), home: _FakeEditableText(),
...@@ -73,8 +73,9 @@ void main() { ...@@ -73,8 +73,9 @@ void main() {
CupertinoSpellCheckSuggestionsToolbar.buildButtonItems(editableTextState); CupertinoSpellCheckSuggestionsToolbar.buildButtonItems(editableTextState);
expect(buttonItems, isNotNull); expect(buttonItems, isNotNull);
expect(buttonItems!.length, 1); expect(buttonItems, hasLength(1));
expect(buttonItems.first.label, 'No Replacements Found'); expect(buttonItems!.first.label, 'No Replacements Found');
expect(buttonItems.first.onPressed, isNull);
}); });
} }
......
...@@ -71,4 +71,20 @@ void main() { ...@@ -71,4 +71,20 @@ void main() {
)); ));
expect(opacity.opacity.value, 1.0); expect(opacity.opacity.value, 1.0);
}); });
testWidgets('passing null to onPressed disables the button', (WidgetTester tester) async {
await tester.pumpWidget(
const CupertinoApp(
home: Center(
child: CupertinoTextSelectionToolbarButton(
child: Text('Tap me'),
),
),
),
);
expect(find.byType(CupertinoButton), findsOneWidget);
final CupertinoButton button = tester.widget(find.byType(CupertinoButton));
expect(button.enabled, isFalse);
});
} }
...@@ -14,10 +14,10 @@ void main() { ...@@ -14,10 +14,10 @@ void main() {
MaterialApp( MaterialApp(
home: Center( home: Center(
child: DesktopTextSelectionToolbarButton( child: DesktopTextSelectionToolbarButton(
child: const Text('Tap me'),
onPressed: () { onPressed: () {
pressed = true; pressed = true;
}, },
child: const Text('Tap me'),
), ),
), ),
), ),
...@@ -28,4 +28,21 @@ void main() { ...@@ -28,4 +28,21 @@ void main() {
await tester.tap(find.byType(DesktopTextSelectionToolbarButton)); await tester.tap(find.byType(DesktopTextSelectionToolbarButton));
expect(pressed, true); expect(pressed, true);
}); });
testWidgets('passing null to onPressed disables the button', (WidgetTester tester) async {
await tester.pumpWidget(
const MaterialApp(
home: Center(
child: DesktopTextSelectionToolbarButton(
onPressed: null,
child: Text('Cannot tap me'),
),
),
),
);
expect(find.byType(TextButton), findsOneWidget);
final TextButton button = tester.widget(find.byType(TextButton));
expect(button.enabled, isFalse);
});
} }
...@@ -2424,7 +2424,7 @@ void main() { ...@@ -2424,7 +2424,7 @@ void main() {
final ContextMenuButtonItem cutButton = items!.first; final ContextMenuButtonItem cutButton = items!.first;
expect(cutButton.type, ContextMenuButtonType.cut); expect(cutButton.type, ContextMenuButtonType.cut);
cutButton.onPressed(); cutButton.onPressed?.call();
await tester.pump(); await tester.pump();
expect(controller.text, isEmpty); expect(controller.text, isEmpty);
...@@ -2492,7 +2492,7 @@ void main() { ...@@ -2492,7 +2492,7 @@ void main() {
final ContextMenuButtonItem copyButton = items!.first; final ContextMenuButtonItem copyButton = items!.first;
expect(copyButton.type, ContextMenuButtonType.copy); expect(copyButton.type, ContextMenuButtonType.copy);
copyButton.onPressed(); copyButton.onPressed?.call();
await tester.pump(); await tester.pump();
expect(controller.text, equals(text)); expect(controller.text, equals(text));
...@@ -2560,7 +2560,7 @@ void main() { ...@@ -2560,7 +2560,7 @@ void main() {
// Setting data which will be pasted into the clipboard. // Setting data which will be pasted into the clipboard.
await Clipboard.setData(const ClipboardData(text: text)); await Clipboard.setData(const ClipboardData(text: text));
pasteButton.onPressed(); pasteButton.onPressed?.call();
await tester.pump(); await tester.pump();
expect(controller.text, equals(text + text)); expect(controller.text, equals(text + text));
...@@ -2619,7 +2619,7 @@ void main() { ...@@ -2619,7 +2619,7 @@ void main() {
final ContextMenuButtonItem selectAllButton = items!.first; final ContextMenuButtonItem selectAllButton = items!.first;
expect(selectAllButton.type, ContextMenuButtonType.selectAll); expect(selectAllButton.type, ContextMenuButtonType.selectAll);
selectAllButton.onPressed(); selectAllButton.onPressed?.call();
await tester.pump(); await tester.pump();
expect(controller.text, equals(text)); expect(controller.text, equals(text));
...@@ -15169,6 +15169,68 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async ...@@ -15169,6 +15169,68 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async
expect(find.text('DELETE'), matcher); expect(find.text('DELETE'), matcher);
}); });
testWidgets('can show spell check suggestions toolbar when there are no spell check results on iOS', (WidgetTester tester) async {
tester.binding.platformDispatcher.nativeSpellCheckServiceDefinedTestValue =
true;
const TextEditingValue value = TextEditingValue(
text: 'tset test test',
selection: TextSelection(affinity: TextAffinity.upstream, baseOffset: 0, extentOffset: 4),
);
controller.value = value;
await tester.pumpWidget(
CupertinoApp(
home: EditableText(
backgroundCursorColor: Colors.grey,
controller: controller,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
selectionControls: materialTextSelectionControls,
spellCheckConfiguration:
const SpellCheckConfiguration(
misspelledTextStyle: CupertinoTextField.cupertinoMisspelledTextStyle,
spellCheckSuggestionsToolbarBuilder: CupertinoTextField.defaultSpellCheckSuggestionsToolbarBuilder,
),
),
),
);
final EditableTextState state =
tester.state<EditableTextState>(find.byType(EditableText));
// Can't show the toolbar when there's no focus.
expect(state.showSpellCheckSuggestionsToolbar(), false);
await tester.pumpAndSettle();
expect(find.byType(CupertinoTextSelectionToolbarButton), findsNothing);
// Can't show the toolbar when there are no spell check results.
expect(state.showSpellCheckSuggestionsToolbar(), false);
await tester.pumpAndSettle();
expect(find.byType(CupertinoTextSelectionToolbarButton), findsNothing);
// Shows 'No Replacements Found' when there are spell check results but no
// suggestions.
state.spellCheckResults = const SpellCheckResults('test tset test', <SuggestionSpan>[SuggestionSpan(TextRange(start: 0, end: 4), <String>[])]);
state.renderEditable.selectWordsInRange(
from: Offset.zero,
cause: SelectionChangedCause.tap,
);
await tester.pumpAndSettle();
// Toolbar will only show on non-web platforms.
expect(state.showSpellCheckSuggestionsToolbar(), isTrue);
await tester.pumpAndSettle();
expect(find.byType(CupertinoTextSelectionToolbarButton), findsOneWidget);
expect(find.byType(CupertinoButton), findsOneWidget);
expect(find.text('No Replacements Found'), findsOneWidget);
final CupertinoButton button = tester.widget(find.byType(CupertinoButton));
expect(button.enabled, isFalse);
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
skip: kIsWeb, // [intended]
);
testWidgets('cupertino spell check suggestions toolbar buttons correctly change the composing region', (WidgetTester tester) async { testWidgets('cupertino spell check suggestions toolbar buttons correctly change the composing region', (WidgetTester tester) async {
tester.binding.platformDispatcher.nativeSpellCheckServiceDefinedTestValue = tester.binding.platformDispatcher.nativeSpellCheckServiceDefinedTestValue =
true; true;
......
...@@ -1754,7 +1754,7 @@ void main() { ...@@ -1754,7 +1754,7 @@ void main() {
expect(buttonItems[0].type, ContextMenuButtonType.copy); expect(buttonItems[0].type, ContextMenuButtonType.copy);
// Press `Copy` item // Press `Copy` item
buttonItems[0].onPressed.call(); buttonItems[0].onPressed?.call();
final SelectableRegionState regionState = tester.state<SelectableRegionState>(find.byType(SelectableRegion)); final SelectableRegionState regionState = tester.state<SelectableRegionState>(find.byType(SelectableRegion));
...@@ -1808,7 +1808,7 @@ void main() { ...@@ -1808,7 +1808,7 @@ void main() {
expect(buttonItems[1].type, ContextMenuButtonType.selectAll); expect(buttonItems[1].type, ContextMenuButtonType.selectAll);
// Press `Select All` item // Press `Select All` item
buttonItems[1].onPressed.call(); buttonItems[1].onPressed?.call();
final SelectableRegionState regionState = tester.state<SelectableRegionState>(find.byType(SelectableRegion)); final SelectableRegionState regionState = tester.state<SelectableRegionState>(find.byType(SelectableRegion));
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment