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 {
child = null;
/// {@macro flutter.cupertino.CupertinoTextSelectionToolbarButton.onPressed}
final VoidCallback onPressed;
final VoidCallback? onPressed;
/// {@macro flutter.cupertino.CupertinoTextSelectionToolbarButton.child}
final Widget? child;
......
......@@ -66,7 +66,7 @@ class CupertinoSpellCheckSuggestionsToolbar extends StatelessWidget {
CupertinoLocalizations.of(editableTextState.context);
return <ContextMenuButtonItem>[
ContextMenuButtonItem(
onPressed: () {},
onPressed: null,
label: localizations.noSpellCheckReplacementsLabel,
)
];
......
......@@ -51,7 +51,7 @@ class DesktopTextSelectionToolbarButton extends StatelessWidget {
);
/// {@macro flutter.material.TextSelectionToolbarTextButton.onPressed}
final VoidCallback onPressed;
final VoidCallback? onPressed;
/// {@macro flutter.material.TextSelectionToolbarTextButton.child}
final Widget child;
......
......@@ -47,7 +47,7 @@ class ContextMenuButtonItem {
});
/// The callback to be called when the button is pressed.
final VoidCallback onPressed;
final VoidCallback? onPressed;
/// The type of button this represents.
final ContextMenuButtonType type;
......
......@@ -14,10 +14,10 @@ void main() {
CupertinoApp(
home: Center(
child: CupertinoDesktopTextSelectionToolbarButton(
child: const Text('Tap me'),
onPressed: () {
pressed = true;
},
child: const Text('Tap me'),
),
),
),
......@@ -34,8 +34,8 @@ void main() {
CupertinoApp(
home: Center(
child: CupertinoDesktopTextSelectionToolbarButton(
child: const Text('Tap me'),
onPressed: () { },
child: const Text('Tap me'),
),
),
),
......@@ -71,4 +71,21 @@ void main() {
));
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() {
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(
CupertinoApp(
home: _FakeEditableText(),
......@@ -73,8 +73,9 @@ void main() {
CupertinoSpellCheckSuggestionsToolbar.buildButtonItems(editableTextState);
expect(buttonItems, isNotNull);
expect(buttonItems!.length, 1);
expect(buttonItems.first.label, 'No Replacements Found');
expect(buttonItems, hasLength(1));
expect(buttonItems!.first.label, 'No Replacements Found');
expect(buttonItems.first.onPressed, isNull);
});
}
......
......@@ -71,4 +71,20 @@ void main() {
));
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() {
MaterialApp(
home: Center(
child: DesktopTextSelectionToolbarButton(
child: const Text('Tap me'),
onPressed: () {
pressed = true;
},
child: const Text('Tap me'),
),
),
),
......@@ -28,4 +28,21 @@ void main() {
await tester.tap(find.byType(DesktopTextSelectionToolbarButton));
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() {
final ContextMenuButtonItem cutButton = items!.first;
expect(cutButton.type, ContextMenuButtonType.cut);
cutButton.onPressed();
cutButton.onPressed?.call();
await tester.pump();
expect(controller.text, isEmpty);
......@@ -2492,7 +2492,7 @@ void main() {
final ContextMenuButtonItem copyButton = items!.first;
expect(copyButton.type, ContextMenuButtonType.copy);
copyButton.onPressed();
copyButton.onPressed?.call();
await tester.pump();
expect(controller.text, equals(text));
......@@ -2560,7 +2560,7 @@ void main() {
// Setting data which will be pasted into the clipboard.
await Clipboard.setData(const ClipboardData(text: text));
pasteButton.onPressed();
pasteButton.onPressed?.call();
await tester.pump();
expect(controller.text, equals(text + text));
......@@ -2619,7 +2619,7 @@ void main() {
final ContextMenuButtonItem selectAllButton = items!.first;
expect(selectAllButton.type, ContextMenuButtonType.selectAll);
selectAllButton.onPressed();
selectAllButton.onPressed?.call();
await tester.pump();
expect(controller.text, equals(text));
......@@ -15169,6 +15169,68 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async
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 {
tester.binding.platformDispatcher.nativeSpellCheckServiceDefinedTestValue =
true;
......
......@@ -1754,7 +1754,7 @@ void main() {
expect(buttonItems[0].type, ContextMenuButtonType.copy);
// Press `Copy` item
buttonItems[0].onPressed.call();
buttonItems[0].onPressed?.call();
final SelectableRegionState regionState = tester.state<SelectableRegionState>(find.byType(SelectableRegion));
......@@ -1808,7 +1808,7 @@ void main() {
expect(buttonItems[1].type, ContextMenuButtonType.selectAll);
// Press `Select All` item
buttonItems[1].onPressed.call();
buttonItems[1].onPressed?.call();
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