Skip to content

Commit bdb74e1

Browse files
Fix Action.overridable example (#110824)
1 parent 3c5a074 commit bdb74e1

File tree

3 files changed

+104
-126
lines changed

3 files changed

+104
-126
lines changed

examples/api/lib/widgets/actions/action.action_overridable.0.dart

Lines changed: 43 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -11,150 +11,73 @@ void main() {
1111
runApp(
1212
const MaterialApp(
1313
home: Scaffold(
14-
body: Center(child: SimpleUSPhoneNumberEntry()),
14+
body: Center(child: VerificationCodeGenerator()),
1515
),
1616
),
1717
);
1818
}
1919

20-
// This implements a custom phone number input field that handles the
21-
// [DeleteCharacterIntent] intent.
22-
class DigitInput extends StatefulWidget {
23-
const DigitInput({
24-
super.key,
25-
required this.controller,
26-
required this.focusNode,
27-
this.maxLength,
28-
this.textInputAction = TextInputAction.next,
29-
});
20+
const CopyTextIntent copyTextIntent = CopyTextIntent._();
21+
class CopyTextIntent extends Intent {
22+
const CopyTextIntent._();
23+
}
3024

31-
final int? maxLength;
32-
final TextEditingController controller;
33-
final TextInputAction textInputAction;
34-
final FocusNode focusNode;
25+
class CopyableText extends StatelessWidget {
26+
const CopyableText({ super.key, required this.text });
3527

36-
@override
37-
DigitInputState createState() => DigitInputState();
38-
}
28+
final String text;
3929

40-
class DigitInputState extends State<DigitInput> {
41-
late final Action<DeleteCharacterIntent> _deleteTextAction =
42-
CallbackAction<DeleteCharacterIntent>(
43-
onInvoke: (DeleteCharacterIntent intent) {
44-
// For simplicity we delete everything in the section.
45-
widget.controller.clear();
46-
return null;
47-
},
48-
);
30+
void _copy(CopyTextIntent intent) => Clipboard.setData(ClipboardData(text: text));
4931

5032
@override
5133
Widget build(BuildContext context) {
52-
return Actions(
53-
actions: <Type, Action<Intent>>{
54-
// Make the default `DeleteCharacterIntent` handler overridable.
55-
DeleteCharacterIntent: Action<DeleteCharacterIntent>.overridable(
56-
defaultAction: _deleteTextAction, context: context),
57-
},
58-
child: TextField(
59-
controller: widget.controller,
60-
textInputAction: TextInputAction.next,
61-
keyboardType: TextInputType.phone,
62-
focusNode: widget.focusNode,
63-
decoration: const InputDecoration(
64-
border: OutlineInputBorder(),
34+
final Action<CopyTextIntent> defaultCopyAction = CallbackAction<CopyTextIntent>(onInvoke: _copy);
35+
return Shortcuts(
36+
shortcuts: const <ShortcutActivator, Intent> { SingleActivator(LogicalKeyboardKey.keyC, control: true) : copyTextIntent },
37+
child: Actions(
38+
actions: <Type, Action<Intent>> {
39+
/// The Action is made overridable so the VerificationCodeGenerator
40+
/// widget can override how copying is handled.
41+
CopyTextIntent: Action<CopyTextIntent>.overridable(defaultAction: defaultCopyAction, context: context),
42+
},
43+
child: Focus(
44+
autofocus: true,
45+
child: DefaultTextStyle.merge(
46+
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
47+
child: Text(text),
48+
),
6549
),
66-
inputFormatters: <TextInputFormatter>[
67-
FilteringTextInputFormatter.digitsOnly,
68-
LengthLimitingTextInputFormatter(widget.maxLength),
69-
],
7050
),
7151
);
7252
}
7353
}
7454

75-
class SimpleUSPhoneNumberEntry extends StatefulWidget {
76-
const SimpleUSPhoneNumberEntry({super.key});
77-
78-
@override
79-
State<SimpleUSPhoneNumberEntry> createState() =>
80-
_SimpleUSPhoneNumberEntryState();
81-
}
82-
83-
class _DeleteDigit extends Action<DeleteCharacterIntent> {
84-
_DeleteDigit(this.state);
55+
class VerificationCodeGenerator extends StatelessWidget {
56+
const VerificationCodeGenerator({ super.key });
8557

86-
final _SimpleUSPhoneNumberEntryState state;
87-
@override
88-
void invoke(DeleteCharacterIntent intent) {
89-
assert(callingAction != null);
90-
callingAction?.invoke(intent);
91-
92-
if (state.lineNumberController.text.isEmpty &&
93-
state.lineNumberFocusNode.hasFocus) {
94-
state.prefixFocusNode.requestFocus();
95-
}
96-
97-
if (state.prefixController.text.isEmpty && state.prefixFocusNode.hasFocus) {
98-
state.areaCodeFocusNode.requestFocus();
99-
}
58+
void _copy(CopyTextIntent intent) {
59+
debugPrint('Content copied');
60+
Clipboard.setData(const ClipboardData(text: '111222333'));
10061
}
10162

102-
// This action is only enabled when the `callingAction` exists and is
103-
// enabled.
104-
@override
105-
bool get isActionEnabled => callingAction?.isActionEnabled ?? false;
106-
}
107-
108-
class _SimpleUSPhoneNumberEntryState extends State<SimpleUSPhoneNumberEntry> {
109-
final FocusNode areaCodeFocusNode = FocusNode();
110-
final TextEditingController areaCodeController = TextEditingController();
111-
final FocusNode prefixFocusNode = FocusNode();
112-
final TextEditingController prefixController = TextEditingController();
113-
final FocusNode lineNumberFocusNode = FocusNode();
114-
final TextEditingController lineNumberController = TextEditingController();
115-
11663
@override
11764
Widget build(BuildContext context) {
11865
return Actions(
119-
actions: <Type, Action<Intent>>{
120-
DeleteCharacterIntent: _DeleteDigit(this),
121-
},
122-
child: Row(
123-
mainAxisAlignment: MainAxisAlignment.spaceBetween,
66+
actions: <Type, Action<Intent>> { CopyTextIntent: CallbackAction<CopyTextIntent>(onInvoke: _copy) },
67+
child: Column(
68+
mainAxisAlignment: MainAxisAlignment.center,
12469
children: <Widget>[
125-
const Expanded(
126-
child: Text('(', textAlign: TextAlign.center),
127-
),
128-
Expanded(
129-
flex: 3,
130-
child: DigitInput(
131-
focusNode: areaCodeFocusNode,
132-
controller: areaCodeController,
133-
maxLength: 3,
134-
),
135-
),
136-
const Expanded(
137-
child: Text(')', textAlign: TextAlign.center),
138-
),
139-
Expanded(
140-
flex: 3,
141-
child: DigitInput(
142-
focusNode: prefixFocusNode,
143-
controller: prefixController,
144-
maxLength: 3,
145-
),
146-
),
147-
const Expanded(
148-
child: Text('-', textAlign: TextAlign.center),
149-
),
150-
Expanded(
151-
flex: 4,
152-
child: DigitInput(
153-
focusNode: lineNumberFocusNode,
154-
controller: lineNumberController,
155-
textInputAction: TextInputAction.done,
156-
maxLength: 4,
157-
),
70+
const Text('Press Ctrl-C to Copy'),
71+
const SizedBox(height: 10),
72+
Row(
73+
mainAxisAlignment: MainAxisAlignment.center,
74+
children: const <Widget>[
75+
CopyableText(text: '111'),
76+
SizedBox(width: 5,),
77+
CopyableText(text: '222'),
78+
SizedBox(width: 5,),
79+
CopyableText(text: '333'),
80+
],
15881
),
15982
],
16083
),
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:flutter/material.dart';
6+
import 'package:flutter/services.dart';
7+
import 'package:flutter_api_samples/widgets/actions/action.action_overridable.0.dart' as example;
8+
import 'package:flutter_test/flutter_test.dart';
9+
10+
void main() {
11+
TestWidgetsFlutterBinding.ensureInitialized();
12+
final _MockClipboard mockClipboard = _MockClipboard();
13+
14+
testWidgets('Copies text on Ctrl-C', (WidgetTester tester) async {
15+
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, mockClipboard.handleMethodCall);
16+
await tester.pumpWidget(const MaterialApp(
17+
home: Scaffold(
18+
body: Center(child: example.VerificationCodeGenerator()),
19+
),
20+
),
21+
);
22+
23+
expect(primaryFocus, isNotNull);
24+
expect(mockClipboard.clipboardData, isNull);
25+
26+
await tester.sendKeyDownEvent(LogicalKeyboardKey.control);
27+
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC);
28+
await tester.sendKeyUpEvent(LogicalKeyboardKey.control);
29+
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC);
30+
31+
expect(mockClipboard.clipboardData?['text'], '111222333');
32+
});
33+
}
34+
35+
class _MockClipboard {
36+
_MockClipboard();
37+
38+
Map<String, dynamic>? clipboardData;
39+
40+
Future<Object?> handleMethodCall(MethodCall methodCall) async {
41+
switch (methodCall.method) {
42+
case 'Clipboard.setData':
43+
clipboardData = methodCall.arguments as Map<String, dynamic>;
44+
return null;
45+
}
46+
if (methodCall.method.startsWith('Clipboard')) {
47+
throw StateError('unrecognized method call: ${methodCall.method}');
48+
}
49+
return null;
50+
}
51+
}

packages/flutter/lib/src/widgets/actions.dart

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -145,12 +145,16 @@ abstract class Action<T extends Intent> with Diagnosticable {
145145
/// parent widgets that also support this [Intent].
146146
///
147147
/// {@tool dartpad}
148-
/// This sample implements a custom text input field that handles the
149-
/// [DeleteCharacterIntent] intent, as well as a US telephone number input
150-
/// widget that consists of multiple text fields for area code, prefix and line
151-
/// number. When the backspace key is pressed, the phone number input widget
152-
/// sends the focus to the preceding text field when the currently focused
153-
/// field becomes empty.
148+
/// This sample shows how to implement a rudimentary `CopyableText` widget
149+
/// that responds to Ctrl-C by copying its own content to the clipboard.
150+
///
151+
/// if `CopyableText` is to be provided in a package, developers using the
152+
/// widget may want to change how copying is handled. As the author of the
153+
/// package, you can enable that by making the corresponding [Action]
154+
/// overridable. In the second part of the code sample, three `CopyableText`
155+
/// widgets are used to build a verification code widget which overrides the
156+
/// "copy" action by copying the combined numbers from all three `CopyableText`
157+
/// widgets.
154158
///
155159
/// ** See code in examples/api/lib/widgets/actions/action.action_overridable.0.dart **
156160
/// {@end-tool}

0 commit comments

Comments
 (0)