Unverified Commit cc2c902b authored by Taha Tesser's avatar Taha Tesser Committed by GitHub

Fix tappable area for `DropdownButtonFormField` & add `InkWell` to `DropdownButton` (#95906)

parent fc71ec55
...@@ -788,6 +788,9 @@ class DropdownButtonHideUnderline extends InheritedWidget { ...@@ -788,6 +788,9 @@ class DropdownButtonHideUnderline extends InheritedWidget {
/// shows the currently selected item as well as an arrow that opens a menu for /// shows the currently selected item as well as an arrow that opens a menu for
/// selecting another item. /// selecting another item.
/// ///
/// One ancestor must be a [Material] widget and typically this is
/// provided by the app's [Scaffold].
///
/// The type `T` is the type of the [value] that each dropdown item represents. /// The type `T` is the type of the [value] that each dropdown item represents.
/// All the entries in a given menu must represent values with consistent types. /// All the entries in a given menu must represent values with consistent types.
/// Typically, an enum is used. Each [DropdownMenuItem] in [items] must be /// Typically, an enum is used. Each [DropdownMenuItem] in [items] must be
...@@ -892,6 +895,61 @@ class DropdownButton<T> extends StatefulWidget { ...@@ -892,6 +895,61 @@ class DropdownButton<T> extends StatefulWidget {
assert(isExpanded != null), assert(isExpanded != null),
assert(autofocus != null), assert(autofocus != null),
assert(itemHeight == null || itemHeight >= kMinInteractiveDimension), assert(itemHeight == null || itemHeight >= kMinInteractiveDimension),
_inputDecoration = null,
_isEmpty = false,
_isFocused = false,
super(key: key);
DropdownButton._formField({
Key? key,
required this.items,
this.selectedItemBuilder,
this.value,
this.hint,
this.disabledHint,
required this.onChanged,
this.onTap,
this.elevation = 8,
this.style,
this.underline,
this.icon,
this.iconDisabledColor,
this.iconEnabledColor,
this.iconSize = 24.0,
this.isDense = false,
this.isExpanded = false,
this.itemHeight = kMinInteractiveDimension,
this.focusColor,
this.focusNode,
this.autofocus = false,
this.dropdownColor,
this.menuMaxHeight,
this.enableFeedback,
this.alignment = AlignmentDirectional.centerStart,
this.borderRadius,
required InputDecoration inputDecoration,
required bool isEmpty,
required bool isFocused,
}) : assert(items == null || items.isEmpty || value == null ||
items.where((DropdownMenuItem<T> item) {
return item.value == value;
}).length == 1,
"There should be exactly one item with [DropdownButtonFormField]'s value: "
'$value. \n'
'Either zero or 2 or more [DropdownMenuItem]s were detected '
'with the same value',
),
assert(elevation != null),
assert(iconSize != null),
assert(isDense != null),
assert(isExpanded != null),
assert(autofocus != null),
assert(itemHeight == null || itemHeight >= kMinInteractiveDimension),
assert(isEmpty != null),
assert(isFocused != null),
_inputDecoration = inputDecoration,
_isEmpty = isEmpty,
_isFocused = isFocused,
super(key: key); super(key: key);
/// The list of items the user can select. /// The list of items the user can select.
...@@ -1101,6 +1159,12 @@ class DropdownButton<T> extends StatefulWidget { ...@@ -1101,6 +1159,12 @@ class DropdownButton<T> extends StatefulWidget {
/// Defines the corner radii of the menu's rounded rectangle shape. /// Defines the corner radii of the menu's rounded rectangle shape.
final BorderRadius? borderRadius; final BorderRadius? borderRadius;
final InputDecoration? _inputDecoration;
final bool _isEmpty;
final bool _isFocused;
@override @override
State<DropdownButton<T>> createState() => _DropdownButtonState<T>(); State<DropdownButton<T>> createState() => _DropdownButtonState<T>();
} }
...@@ -1113,7 +1177,6 @@ class _DropdownButtonState<T> extends State<DropdownButton<T>> with WidgetsBindi ...@@ -1113,7 +1177,6 @@ class _DropdownButtonState<T> extends State<DropdownButton<T>> with WidgetsBindi
FocusNode? get focusNode => widget.focusNode ?? _internalNode; FocusNode? get focusNode => widget.focusNode ?? _internalNode;
bool _hasPrimaryFocus = false; bool _hasPrimaryFocus = false;
late Map<Type, Action<Intent>> _actionMap; late Map<Type, Action<Intent>> _actionMap;
late FocusHighlightMode _focusHighlightMode;
// Only used if needed to create _internalNode. // Only used if needed to create _internalNode.
FocusNode _createFocusNode() { FocusNode _createFocusNode() {
...@@ -1136,16 +1199,12 @@ class _DropdownButtonState<T> extends State<DropdownButton<T>> with WidgetsBindi ...@@ -1136,16 +1199,12 @@ class _DropdownButtonState<T> extends State<DropdownButton<T>> with WidgetsBindi
), ),
}; };
focusNode!.addListener(_handleFocusChanged); focusNode!.addListener(_handleFocusChanged);
final FocusManager focusManager = WidgetsBinding.instance!.focusManager;
_focusHighlightMode = focusManager.highlightMode;
focusManager.addHighlightModeListener(_handleFocusHighlightModeChange);
} }
@override @override
void dispose() { void dispose() {
WidgetsBinding.instance!.removeObserver(this); WidgetsBinding.instance!.removeObserver(this);
_removeDropdownRoute(); _removeDropdownRoute();
WidgetsBinding.instance!.focusManager.removeHighlightModeListener(_handleFocusHighlightModeChange);
focusNode!.removeListener(_handleFocusChanged); focusNode!.removeListener(_handleFocusChanged);
_internalNode?.dispose(); _internalNode?.dispose();
super.dispose(); super.dispose();
...@@ -1165,14 +1224,6 @@ class _DropdownButtonState<T> extends State<DropdownButton<T>> with WidgetsBindi ...@@ -1165,14 +1224,6 @@ class _DropdownButtonState<T> extends State<DropdownButton<T>> with WidgetsBindi
} }
} }
void _handleFocusHighlightModeChange(FocusHighlightMode mode) {
if (!mounted) {
return;
}
setState(() {
_focusHighlightMode = mode;
});
}
@override @override
void didUpdateWidget(DropdownButton<T> oldWidget) { void didUpdateWidget(DropdownButton<T> oldWidget) {
...@@ -1315,15 +1366,6 @@ class _DropdownButtonState<T> extends State<DropdownButton<T>> with WidgetsBindi ...@@ -1315,15 +1366,6 @@ class _DropdownButtonState<T> extends State<DropdownButton<T>> with WidgetsBindi
return result; return result;
} }
bool get _showHighlight {
switch (_focusHighlightMode) {
case FocusHighlightMode.touch:
return false;
case FocusHighlightMode.traditional:
return _hasPrimaryFocus;
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context)); assert(debugCheckHasMaterial(context));
...@@ -1386,12 +1428,6 @@ class _DropdownButtonState<T> extends State<DropdownButton<T>> with WidgetsBindi ...@@ -1386,12 +1428,6 @@ class _DropdownButtonState<T> extends State<DropdownButton<T>> with WidgetsBindi
Widget result = DefaultTextStyle( Widget result = DefaultTextStyle(
style: _enabled ? _textStyle! : _textStyle!.copyWith(color: Theme.of(context).disabledColor), style: _enabled ? _textStyle! : _textStyle!.copyWith(color: Theme.of(context).disabledColor),
child: Container( child: Container(
decoration: _showHighlight
? BoxDecoration(
color: widget.focusColor ?? Theme.of(context).focusColor,
borderRadius: const BorderRadius.all(Radius.circular(4.0)),
)
: null,
padding: padding.resolve(Directionality.of(context)), padding: padding.resolve(Directionality.of(context)),
height: widget.isDense ? _denseButtonHeight : null, height: widget.isDense ? _denseButtonHeight : null,
child: Row( child: Row(
...@@ -1446,24 +1482,30 @@ class _DropdownButtonState<T> extends State<DropdownButton<T>> with WidgetsBindi ...@@ -1446,24 +1482,30 @@ class _DropdownButtonState<T> extends State<DropdownButton<T>> with WidgetsBindi
}, },
); );
if (widget._inputDecoration != null) {
result = InputDecorator(
decoration: widget._inputDecoration!,
isEmpty: widget._isEmpty,
isFocused: widget._isFocused,
child: result,
);
}
return Semantics( return Semantics(
button: true, button: true,
child: Actions( child: Actions(
actions: _actionMap, actions: _actionMap,
child: Focus( child: InkWell(
mouseCursor: effectiveMouseCursor,
onTap: _enabled ? _handleTap : null,
canRequestFocus: _enabled, canRequestFocus: _enabled,
focusNode: focusNode, focusNode: focusNode,
autofocus: widget.autofocus, autofocus: widget.autofocus,
child: MouseRegion( focusColor: widget.focusColor ?? Theme.of(context).focusColor,
cursor: effectiveMouseCursor, enableFeedback: false,
child: GestureDetector(
onTap: _enabled ? _handleTap : null,
behavior: HitTestBehavior.opaque,
child: result, child: result,
), ),
), ),
),
),
); );
} }
} }
...@@ -1570,12 +1612,8 @@ class DropdownButtonFormField<T> extends FormField<T> { ...@@ -1570,12 +1612,8 @@ class DropdownButtonFormField<T> extends FormField<T> {
canRequestFocus: false, canRequestFocus: false,
skipTraversal: true, skipTraversal: true,
child: Builder(builder: (BuildContext context) { child: Builder(builder: (BuildContext context) {
return InputDecorator( return DropdownButtonHideUnderline(
decoration: effectiveDecoration.copyWith(errorText: field.errorText), child: DropdownButton<T>._formField(
isEmpty: isEmpty,
isFocused: Focus.of(context).hasFocus,
child: DropdownButtonHideUnderline(
child: DropdownButton<T>(
items: items, items: items,
selectedItemBuilder: selectedItemBuilder, selectedItemBuilder: selectedItemBuilder,
value: state.value, value: state.value,
...@@ -1600,7 +1638,9 @@ class DropdownButtonFormField<T> extends FormField<T> { ...@@ -1600,7 +1638,9 @@ class DropdownButtonFormField<T> extends FormField<T> {
enableFeedback: enableFeedback, enableFeedback: enableFeedback,
alignment: alignment, alignment: alignment,
borderRadius: borderRadius, borderRadius: borderRadius,
), inputDecoration: effectiveDecoration.copyWith(errorText: field.errorText),
isEmpty: isEmpty,
isFocused: Focus.of(context).hasFocus,
), ),
); );
}), }),
......
...@@ -577,6 +577,7 @@ void main() { ...@@ -577,6 +577,7 @@ void main() {
TestApp( TestApp(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: Material( child: Material(
child: Center(
child: DropdownButtonFormField<String>( child: DropdownButtonFormField<String>(
key: buttonKey, key: buttonKey,
value: value, value: value,
...@@ -591,10 +592,11 @@ void main() { ...@@ -591,10 +592,11 @@ void main() {
), ),
), ),
), ),
),
); );
final RenderBox box = tester.renderObject<RenderBox>(find.byType(dropdownButtonType)); final RenderBox box = tester.renderObject<RenderBox>(find.byType(dropdownButtonType));
expect(box.size.height, 24.0); expect(box.size.height, 48.0);
}); });
testWidgets('DropdownButtonFormField - custom text style', (WidgetTester tester) async { testWidgets('DropdownButtonFormField - custom text style', (WidgetTester tester) async {
......
...@@ -2395,13 +2395,13 @@ void main() { ...@@ -2395,13 +2395,13 @@ void main() {
final UniqueKey buttonKey = UniqueKey(); final UniqueKey buttonKey = UniqueKey();
final FocusNode focusNode = FocusNode(debugLabel: 'DropdownButton'); final FocusNode focusNode = FocusNode(debugLabel: 'DropdownButton');
await tester.pumpWidget(buildFrame(buttonKey: buttonKey, onChanged: onChanged, focusNode: focusNode, autofocus: true)); await tester.pumpWidget(buildFrame(buttonKey: buttonKey, onChanged: onChanged, focusNode: focusNode, autofocus: true));
await tester.pump(); // Pump a frame for autofocus to take effect. await tester.pumpAndSettle(); // Pump a frame for autofocus to take effect.
expect(focusNode.hasPrimaryFocus, isTrue); expect(focusNode.hasPrimaryFocus, isTrue);
final Finder buttonFinder = find.byKey(buttonKey); expect(find.byType(Material), paints..rect(rect: const Rect.fromLTRB(348.0, 276.0, 452.0, 324.0), color: const Color(0x1f000000)));
expect(buttonFinder, paints ..rrect(rrect: const RRect.fromLTRBXY(0.0, 0.0, 104.0, 48.0, 4.0, 4.0), color: const Color(0x1f000000)));
await tester.pumpWidget(buildFrame(buttonKey: buttonKey, onChanged: onChanged, focusNode: focusNode, focusColor: const Color(0xff00ff00))); await tester.pumpWidget(buildFrame(buttonKey: buttonKey, onChanged: onChanged, focusNode: focusNode, focusColor: const Color(0xff00ff00)));
expect(buttonFinder, paints ..rrect(rrect: const RRect.fromLTRBXY(0.0, 0.0, 104.0, 48.0, 4.0, 4.0), color: const Color(0xff00ff00))); await tester.pumpAndSettle(); // Pump a frame for autofocus to take effect.
expect(find.byType(Material), paints..rect(rect: const Rect.fromLTRB(348.0, 276.0, 452.0, 324.0), color: const Color(0x1f00ff00)));
}); });
testWidgets('DropdownButtonFormField can be focused, and has focusColor', (WidgetTester tester) async { testWidgets('DropdownButtonFormField can be focused, and has focusColor', (WidgetTester tester) async {
...@@ -2409,13 +2409,13 @@ void main() { ...@@ -2409,13 +2409,13 @@ void main() {
final UniqueKey buttonKey = UniqueKey(); final UniqueKey buttonKey = UniqueKey();
final FocusNode focusNode = FocusNode(debugLabel: 'DropdownButtonFormField'); final FocusNode focusNode = FocusNode(debugLabel: 'DropdownButtonFormField');
await tester.pumpWidget(buildFrame(isFormField: true, buttonKey: buttonKey, onChanged: onChanged, focusNode: focusNode, autofocus: true)); await tester.pumpWidget(buildFrame(isFormField: true, buttonKey: buttonKey, onChanged: onChanged, focusNode: focusNode, autofocus: true));
await tester.pump(); // Pump a frame for autofocus to take effect. await tester.pumpAndSettle(); // Pump a frame for autofocus to take effect.
expect(focusNode.hasPrimaryFocus, isTrue); expect(focusNode.hasPrimaryFocus, isTrue);
final Finder buttonFinder = find.descendant(of: find.byKey(buttonKey), matching: find.byType(InputDecorator)); expect(find.byType(Material), paints ..rect(rect: const Rect.fromLTRB(0.0, 264.0, 800.0, 336.0), color: const Color(0x1f000000)));
expect(buttonFinder, paints ..rrect(rrect: const RRect.fromLTRBXY(0.0, 12.0, 800.0, 60.0, 4.0, 4.0), color: const Color(0x1f000000)));
await tester.pumpWidget(buildFrame(isFormField: true, buttonKey: buttonKey, onChanged: onChanged, focusNode: focusNode, focusColor: const Color(0xff00ff00))); await tester.pumpWidget(buildFrame(isFormField: true, buttonKey: buttonKey, onChanged: onChanged, focusNode: focusNode, focusColor: const Color(0xff00ff00)));
expect(buttonFinder, paints ..rrect(rrect: const RRect.fromLTRBXY(0.0, 12.0, 800.0, 60.0, 4.0, 4.0), color: const Color(0xff00ff00))); await tester.pumpAndSettle(); // Pump a frame for autofocus to take effect.
expect(find.byType(Material), paints ..rect(rect: const Rect.fromLTRB(0.0, 264.0, 800.0, 336.0), color: const Color(0x1f00ff00)));
}); });
testWidgets("DropdownButton won't be focused if not enabled", (WidgetTester tester) async { testWidgets("DropdownButton won't be focused if not enabled", (WidgetTester tester) async {
...@@ -3413,7 +3413,7 @@ void main() { ...@@ -3413,7 +3413,7 @@ void main() {
await tester.tap(find.text('One')); await tester.tap(find.text('One'));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
await tester.tap(find.widgetWithText(InkWell, 'One')); await tester.tap(find.widgetWithText(InkWell, 'One').last);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(feedback.clickSoundCount, 1); expect(feedback.clickSoundCount, 1);
expect(feedback.hapticCount, 0); expect(feedback.hapticCount, 0);
...@@ -3426,7 +3426,7 @@ void main() { ...@@ -3426,7 +3426,7 @@ void main() {
await tester.tap(find.text('One')); await tester.tap(find.text('One'));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
await tester.tap(find.widgetWithText(InkWell, 'One')); await tester.tap(find.widgetWithText(InkWell, 'One').last);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(feedback.clickSoundCount, 0); expect(feedback.clickSoundCount, 0);
expect(feedback.hapticCount, 0); expect(feedback.hapticCount, 0);
...@@ -3437,7 +3437,7 @@ void main() { ...@@ -3437,7 +3437,7 @@ void main() {
await tester.tap(find.text('One')); await tester.tap(find.text('One'));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
await tester.tap(find.widgetWithText(InkWell, 'Two')); await tester.tap(find.widgetWithText(InkWell, 'Two').last);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(feedback.clickSoundCount, 1); expect(feedback.clickSoundCount, 1);
expect(feedback.hapticCount, 0); expect(feedback.hapticCount, 0);
......
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