Unverified Commit ed36fe5d authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

Revert "Add focus nodes, hover, and shortcuts to switches, checkboxes, and...

Revert "Add focus nodes, hover, and shortcuts to switches, checkboxes, and radio buttons. (#43213)" (#43367)

This reverts commit 90006720.
parent 90006720
......@@ -4,8 +4,6 @@
import 'dart:math' as math;
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
......@@ -54,7 +52,7 @@ class Checkbox extends StatefulWidget {
/// * [onChanged], which is called when the value of the checkbox should
/// change. It can be set to null to disable the checkbox.
///
/// The values of [tristate] and [autofocus] must not be null.
/// The value of [tristate] must not be null.
const Checkbox({
Key key,
@required this.value,
......@@ -62,14 +60,9 @@ class Checkbox extends StatefulWidget {
@required this.onChanged,
this.activeColor,
this.checkColor,
this.focusColor,
this.hoverColor,
this.materialTapTargetSize,
this.focusNode,
this.autofocus = false,
}) : assert(tristate != null),
assert(tristate || value != null),
assert(autofocus != null),
super(key: key);
/// Whether this checkbox is checked.
......@@ -120,10 +113,10 @@ class Checkbox extends StatefulWidget {
///
/// Checkbox displays a dash when its value is null.
///
/// When a tri-state checkbox ([tristate] is true) is tapped, its [onChanged]
/// callback will be applied to true if the current value is false, to null if
/// value is true, and to false if value is null (i.e. it cycles through false
/// => true => null => false when tapped).
/// When a tri-state checkbox is tapped its [onChanged] callback will be
/// applied to true if the current value is null or false, false otherwise.
/// Typically tri-state checkboxes are disabled (the onChanged callback is
/// null) so they don't respond to taps.
///
/// If tristate is false (the default), [value] must not be null.
final bool tristate;
......@@ -137,18 +130,6 @@ class Checkbox extends StatefulWidget {
/// * [MaterialTapTargetSize], for a description of how this affects tap targets.
final MaterialTapTargetSize materialTapTargetSize;
/// The color for the checkbox's [Material] when it has the input focus.
final Color focusColor;
/// The color for the checkbox's [Material] when a pointer is hovering over it.
final Color hoverColor;
/// {@macro flutter.widgets.Focus.focusNode}
final FocusNode focusNode;
/// {@macro flutter.widgets.Focus.autofocus}
final bool autofocus;
/// The width of a checkbox widget.
static const double width = 18.0;
......@@ -157,68 +138,6 @@ class Checkbox extends StatefulWidget {
}
class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin {
bool get enabled => widget.onChanged != null;
Map<LocalKey, ActionFactory> _actionMap;
bool _showHighlight = false;
@override
void initState() {
super.initState();
_actionMap = <LocalKey, ActionFactory>{
SelectAction.key: _createAction,
if (!kIsWeb) ActivateAction.key: _createAction,
};
_updateHighlightMode(WidgetsBinding.instance.focusManager.highlightMode);
WidgetsBinding.instance.focusManager.addHighlightModeListener(_handleFocusHighlightModeChange);
}
void _actionHandler(FocusNode node, Intent intent){
if (widget.onChanged != null) {
switch (widget.value) {
case false:
widget.onChanged(true);
break;
case true:
widget.onChanged(widget.tristate ? null : false);
break;
default: // case null:
widget.onChanged(false);
break;
}
}
final RenderObject renderObject = node.context.findRenderObject();
renderObject.sendSemanticsEvent(const TapSemanticEvent());
}
Action _createAction() {
return CallbackAction(
SelectAction.key,
onInvoke: _actionHandler,
);
}
void _updateHighlightMode(FocusHighlightMode mode) {
switch (WidgetsBinding.instance.focusManager.highlightMode) {
case FocusHighlightMode.touch:
_showHighlight = false;
break;
case FocusHighlightMode.traditional:
_showHighlight = true;
break;
}
}
void _handleFocusHighlightModeChange(FocusHighlightMode mode) {
if (!mounted) {
return;
}
setState(() { _updateHighlightMode(mode); });
}
bool hovering = false;
void _handleMouseEnter(PointerEnterEvent event) => setState(() { hovering = true; });
void _handleMouseExit(PointerExitEvent event) => setState(() { hovering = false; });
@override
Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context));
......@@ -233,36 +152,15 @@ class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin {
break;
}
final BoxConstraints additionalConstraints = BoxConstraints.tight(size);
return MouseRegion(
onEnter: enabled ? _handleMouseEnter : null,
onExit: enabled ? _handleMouseExit : null,
child: Actions(
actions: _actionMap,
child: Focus(
focusNode: widget.focusNode,
autofocus: widget.autofocus,
canRequestFocus: enabled,
debugLabel: '${describeIdentity(widget)}(${widget.value})',
child: Builder(
builder: (BuildContext context) {
return _CheckboxRenderObjectWidget(
value: widget.value,
tristate: widget.tristate,
activeColor: widget.activeColor ?? themeData.toggleableActiveColor,
checkColor: widget.checkColor ?? const Color(0xFFFFFFFF),
inactiveColor: enabled ? themeData.unselectedWidgetColor : themeData.disabledColor,
focusColor: widget.focusColor ?? themeData.focusColor,
hoverColor: widget.hoverColor ?? themeData.hoverColor,
onChanged: widget.onChanged,
additionalConstraints: additionalConstraints,
vsync: this,
hasFocus: enabled && _showHighlight && Focus.of(context).hasFocus,
hovering: enabled && _showHighlight && hovering,
);
},
),
),
),
return _CheckboxRenderObjectWidget(
value: widget.value,
tristate: widget.tristate,
activeColor: widget.activeColor ?? themeData.toggleableActiveColor,
checkColor: widget.checkColor ?? const Color(0xFFFFFFFF),
inactiveColor: widget.onChanged != null ? themeData.unselectedWidgetColor : themeData.disabledColor,
onChanged: widget.onChanged,
additionalConstraints: additionalConstraints,
vsync: this,
);
}
}
......@@ -275,13 +173,9 @@ class _CheckboxRenderObjectWidget extends LeafRenderObjectWidget {
@required this.activeColor,
@required this.checkColor,
@required this.inactiveColor,
@required this.focusColor,
@required this.hoverColor,
@required this.onChanged,
@required this.vsync,
@required this.additionalConstraints,
@required this.hasFocus,
@required this.hovering,
}) : assert(tristate != null),
assert(tristate || value != null),
assert(activeColor != null),
......@@ -291,13 +185,9 @@ class _CheckboxRenderObjectWidget extends LeafRenderObjectWidget {
final bool value;
final bool tristate;
final bool hasFocus;
final bool hovering;
final Color activeColor;
final Color checkColor;
final Color inactiveColor;
final Color focusColor;
final Color hoverColor;
final ValueChanged<bool> onChanged;
final TickerProvider vsync;
final BoxConstraints additionalConstraints;
......@@ -309,13 +199,9 @@ class _CheckboxRenderObjectWidget extends LeafRenderObjectWidget {
activeColor: activeColor,
checkColor: checkColor,
inactiveColor: inactiveColor,
focusColor: focusColor,
hoverColor: hoverColor,
onChanged: onChanged,
vsync: vsync,
additionalConstraints: additionalConstraints,
hasFocus: hasFocus,
hovering: hovering,
);
@override
......@@ -326,13 +212,9 @@ class _CheckboxRenderObjectWidget extends LeafRenderObjectWidget {
..activeColor = activeColor
..checkColor = checkColor
..inactiveColor = inactiveColor
..focusColor = focusColor
..hoverColor = hoverColor
..onChanged = onChanged
..additionalConstraints = additionalConstraints
..vsync = vsync
..hasFocus = hasFocus
..hovering = hovering;
..vsync = vsync;
}
}
......@@ -347,12 +229,8 @@ class _RenderCheckbox extends RenderToggleable {
Color activeColor,
this.checkColor,
Color inactiveColor,
Color focusColor,
Color hoverColor,
BoxConstraints additionalConstraints,
ValueChanged<bool> onChanged,
bool hasFocus,
bool hovering,
@required TickerProvider vsync,
}) : _oldValue = value,
super(
......@@ -360,13 +238,9 @@ class _RenderCheckbox extends RenderToggleable {
tristate: tristate,
activeColor: activeColor,
inactiveColor: inactiveColor,
focusColor: focusColor,
hoverColor: hoverColor,
onChanged: onChanged,
additionalConstraints: additionalConstraints,
vsync: vsync,
hasFocus: hasFocus,
hovering: hovering,
);
bool _oldValue;
......
......@@ -489,25 +489,20 @@ class _InkResponseState<T extends InkResponse> extends State<T> with AutomaticKe
bool get highlightsExist => _highlights.values.where((InkHighlight highlight) => highlight != null).isNotEmpty;
void _handleAction(FocusNode node, Intent intent) {
_startSplash(context: node.context);
_handleTap(node.context);
}
Action _createAction() {
return CallbackAction(
ActivateAction.key,
onInvoke: _handleAction,
onInvoke: (FocusNode node, Intent intent) {
_startSplash(context: node.context);
_handleTap(node.context);
},
);
}
@override
void initState() {
super.initState();
_actionMap = <LocalKey, ActionFactory>{
SelectAction.key: _createAction,
if (!kIsWeb) ActivateAction.key: _createAction,
};
_actionMap = <LocalKey, ActionFactory>{ ActivateAction.key: _createAction };
WidgetsBinding.instance.focusManager.addHighlightModeListener(_handleFocusHighlightModeChange);
}
......
......@@ -2,8 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
......@@ -109,13 +107,8 @@ class Radio<T> extends StatefulWidget {
@required this.groupValue,
@required this.onChanged,
this.activeColor,
this.focusColor,
this.hoverColor,
this.materialTapTargetSize,
this.focusNode,
this.autofocus = false,
}) : assert(autofocus != null),
super(key: key);
}) : super(key: key);
/// The value represented by this radio button.
final T value;
......@@ -168,77 +161,15 @@ class Radio<T> extends StatefulWidget {
/// * [MaterialTapTargetSize], for a description of how this affects tap targets.
final MaterialTapTargetSize materialTapTargetSize;
/// The color for the radio's [Material] when it has the input focus.
final Color focusColor;
/// The color for the radio's [Material] when a pointer is hovering over it.
final Color hoverColor;
/// {@macro flutter.widgets.Focus.focusNode}
final FocusNode focusNode;
/// {@macro flutter.widgets.Focus.autofocus}
final bool autofocus;
@override
_RadioState<T> createState() => _RadioState<T>();
}
class _RadioState<T> extends State<Radio<T>> with TickerProviderStateMixin {
bool get enabled => widget.onChanged != null;
Map<LocalKey, ActionFactory> _actionMap;
bool _showHighlight = false;
@override
void initState() {
super.initState();
_actionMap = <LocalKey, ActionFactory>{
SelectAction.key: _createAction,
if (!kIsWeb) ActivateAction.key: _createAction,
};
_updateHighlightMode(WidgetsBinding.instance.focusManager.highlightMode);
WidgetsBinding.instance.focusManager.addHighlightModeListener(_handleFocusHighlightModeChange);
}
void _actionHandler(FocusNode node, Intent intent){
if (widget.onChanged != null) {
widget.onChanged(widget.value);
}
final RenderObject renderObject = node.context.findRenderObject();
renderObject.sendSemanticsEvent(const TapSemanticEvent());
}
Action _createAction() {
return CallbackAction(
SelectAction.key,
onInvoke: _actionHandler,
);
}
void _updateHighlightMode(FocusHighlightMode mode) {
switch (WidgetsBinding.instance.focusManager.highlightMode) {
case FocusHighlightMode.touch:
_showHighlight = false;
break;
case FocusHighlightMode.traditional:
_showHighlight = true;
break;
}
}
void _handleFocusHighlightModeChange(FocusHighlightMode mode) {
if (!mounted) {
return;
}
setState(() { _updateHighlightMode(mode); });
}
bool hovering = false;
void _handleMouseEnter(PointerEnterEvent event) => setState(() { hovering = true; });
void _handleMouseExit(PointerExitEvent event) => setState(() { hovering = false; });
bool get _enabled => widget.onChanged != null;
Color _getInactiveColor(ThemeData themeData) {
return enabled ? themeData.unselectedWidgetColor : themeData.disabledColor;
return _enabled ? themeData.unselectedWidgetColor : themeData.disabledColor;
}
void _handleChanged(bool selected) {
......@@ -260,34 +191,13 @@ class _RadioState<T> extends State<Radio<T>> with TickerProviderStateMixin {
break;
}
final BoxConstraints additionalConstraints = BoxConstraints.tight(size);
return MouseRegion(
onEnter: enabled ? _handleMouseEnter : null,
onExit: enabled ? _handleMouseExit : null,
child: Actions(
actions: _actionMap,
child: Focus(
focusNode: widget.focusNode,
autofocus: widget.autofocus,
canRequestFocus: enabled,
debugLabel: '${describeIdentity(widget)}(${widget.value})',
child: Builder(
builder: (BuildContext context) {
return _RadioRenderObjectWidget(
selected: widget.value == widget.groupValue,
activeColor: widget.activeColor ?? themeData.toggleableActiveColor,
inactiveColor: _getInactiveColor(themeData),
focusColor: widget.focusColor ?? themeData.focusColor,
hoverColor: widget.hoverColor ?? themeData.hoverColor,
onChanged: enabled ? _handleChanged : null,
additionalConstraints: additionalConstraints,
vsync: this,
hasFocus: enabled && _showHighlight && Focus.of(context).hasFocus,
hovering: enabled && _showHighlight && hovering,
);
},
),
),
),
return _RadioRenderObjectWidget(
selected: widget.value == widget.groupValue,
activeColor: widget.activeColor ?? themeData.toggleableActiveColor,
inactiveColor: _getInactiveColor(themeData),
onChanged: _enabled ? _handleChanged : null,
additionalConstraints: additionalConstraints,
vsync: this,
);
}
}
......@@ -298,13 +208,9 @@ class _RadioRenderObjectWidget extends LeafRenderObjectWidget {
@required this.selected,
@required this.activeColor,
@required this.inactiveColor,
@required this.focusColor,
@required this.hoverColor,
@required this.additionalConstraints,
this.onChanged,
@required this.vsync,
@required this.hasFocus,
@required this.hovering,
}) : assert(selected != null),
assert(activeColor != null),
assert(inactiveColor != null),
......@@ -312,12 +218,8 @@ class _RadioRenderObjectWidget extends LeafRenderObjectWidget {
super(key: key);
final bool selected;
final bool hasFocus;
final bool hovering;
final Color inactiveColor;
final Color activeColor;
final Color focusColor;
final Color hoverColor;
final ValueChanged<bool> onChanged;
final TickerProvider vsync;
final BoxConstraints additionalConstraints;
......@@ -327,13 +229,9 @@ class _RadioRenderObjectWidget extends LeafRenderObjectWidget {
value: selected,
activeColor: activeColor,
inactiveColor: inactiveColor,
focusColor: focusColor,
hoverColor: hoverColor,
onChanged: onChanged,
vsync: vsync,
additionalConstraints: additionalConstraints,
hasFocus: hasFocus,
hovering: hovering,
);
@override
......@@ -342,13 +240,9 @@ class _RadioRenderObjectWidget extends LeafRenderObjectWidget {
..value = selected
..activeColor = activeColor
..inactiveColor = inactiveColor
..focusColor = focusColor
..hoverColor = hoverColor
..onChanged = onChanged
..additionalConstraints = additionalConstraints
..vsync = vsync
..hasFocus = hasFocus
..hovering = hovering;
..vsync = vsync;
}
}
......@@ -357,25 +251,17 @@ class _RenderRadio extends RenderToggleable {
bool value,
Color activeColor,
Color inactiveColor,
Color focusColor,
Color hoverColor,
ValueChanged<bool> onChanged,
BoxConstraints additionalConstraints,
@required TickerProvider vsync,
bool hasFocus,
bool hovering,
}) : super(
value: value,
tristate: false,
activeColor: activeColor,
inactiveColor: inactiveColor,
focusColor: focusColor,
hoverColor: hoverColor,
onChanged: onChanged,
additionalConstraints: additionalConstraints,
vsync: vsync,
hasFocus: hasFocus,
hovering: hovering,
);
@override
......
......@@ -10,15 +10,9 @@ import 'package:flutter/scheduler.dart';
import 'constants.dart';
// Duration of the animation that moves the toggle from one state to another.
const Duration _kToggleDuration = Duration(milliseconds: 200);
// Radius of the radial reaction over time.
final Animatable<double> _kRadialReactionRadiusTween = Tween<double>(begin: 0.0, end: kRadialReactionRadius);
// Duration of the fade animation for the reaction when focus and hover occur.
const Duration _kReactionFadeDuration = Duration(milliseconds: 50);
/// A base class for material style toggleable controls with toggle animations.
///
/// This class handles storing the current value, dispatching ValueChanged on a
......@@ -34,13 +28,9 @@ abstract class RenderToggleable extends RenderConstrainedBox {
bool tristate = false,
@required Color activeColor,
@required Color inactiveColor,
Color hoverColor,
Color focusColor,
ValueChanged<bool> onChanged,
BoxConstraints additionalConstraints,
@required TickerProvider vsync,
bool hasFocus = false,
bool hovering = false,
}) : assert(tristate != null),
assert(tristate || value != null),
assert(activeColor != null),
......@@ -50,11 +40,7 @@ abstract class RenderToggleable extends RenderConstrainedBox {
_tristate = tristate,
_activeColor = activeColor,
_inactiveColor = inactiveColor,
_hoverColor = hoverColor ?? activeColor.withAlpha(kRadialReactionAlpha),
_focusColor = focusColor ?? activeColor.withAlpha(kRadialReactionAlpha),
_onChanged = onChanged,
_hasFocus = hasFocus,
_hovering = hovering,
_vsync = vsync,
super(additionalConstraints: additionalConstraints) {
_tap = TapGestureRecognizer()
......@@ -80,24 +66,6 @@ abstract class RenderToggleable extends RenderConstrainedBox {
parent: _reactionController,
curve: Curves.fastOutSlowIn,
)..addListener(markNeedsPaint);
_reactionHoverFadeController = AnimationController(
duration: _kReactionFadeDuration,
value: hovering || hasFocus ? 1.0 : 0.0,
vsync: vsync,
);
_reactionHoverFade = CurvedAnimation(
parent: _reactionHoverFadeController,
curve: Curves.fastOutSlowIn,
)..addListener(markNeedsPaint);
_reactionFocusFadeController = AnimationController(
duration: _kReactionFadeDuration,
value: hovering || hasFocus ? 1.0 : 0.0,
vsync: vsync,
);
_reactionFocusFade = CurvedAnimation(
parent: _reactionFocusFadeController,
curve: Curves.fastOutSlowIn,
)..addListener(markNeedsPaint);
}
/// Used by subclasses to manipulate the visual value of the control.
......@@ -134,66 +102,6 @@ abstract class RenderToggleable extends RenderConstrainedBox {
AnimationController _reactionController;
Animation<double> _reaction;
/// Used by subclasses to control the radial reaction's opacity animation for
/// [hasFocus] changes.
///
/// Some controls have a radial ink reaction to focus. This animation
/// controller can be used to start or stop these ink reaction fade-ins and
/// fade-outs.
///
/// Subclasses should call [paintRadialReaction] to actually paint the radial
/// reaction.
@protected
AnimationController get reactionFocusFadeController => _reactionFocusFadeController;
AnimationController _reactionFocusFadeController;
Animation<double> _reactionFocusFade;
/// Used by subclasses to control the radial reaction's opacity animation for
/// [hovering] changes.
///
/// Some controls have a radial ink reaction to pointer hover. This animation
/// controller can be used to start or stop these ink reaction fade-ins and
/// fade-outs.
///
/// Subclasses should call [paintRadialReaction] to actually paint the radial
/// reaction.
@protected
AnimationController get reactionHoverFadeController => _reactionHoverFadeController;
AnimationController _reactionHoverFadeController;
Animation<double> _reactionHoverFade;
/// True if this toggleable has the input focus.
bool get hasFocus => _hasFocus;
bool _hasFocus;
set hasFocus(bool value) {
assert(value != null);
if (value == _hasFocus)
return;
_hasFocus = value;
if (_hasFocus) {
_reactionFocusFadeController.forward();
} else {
_reactionFocusFadeController.reverse();
}
markNeedsPaint();
}
/// True if this toggleable is being hovered over by a pointer.
bool get hovering => _hovering;
bool _hovering;
set hovering(bool value) {
assert(value != null);
if (value == _hovering)
return;
_hovering = value;
if (_hovering) {
_reactionHoverFadeController.forward();
} else {
_reactionHoverFadeController.reverse();
}
markNeedsPaint();
}
/// The [TickerProvider] for the [AnimationController]s that run the animations.
TickerProvider get vsync => _vsync;
TickerProvider _vsync;
......@@ -284,54 +192,6 @@ abstract class RenderToggleable extends RenderConstrainedBox {
markNeedsPaint();
}
/// The color that should be used for the reaction when [hovering] is true.
///
/// Used when the toggleable needs to change the reaction color/transparency,
/// when it is being hovered over.
///
/// Defaults to the [activeColor] at alpha [kRadialReactionAlpha].
Color get hoverColor => _hoverColor;
Color _hoverColor;
set hoverColor(Color value) {
assert(value != null);
if (value == _hoverColor)
return;
_hoverColor = value;
markNeedsPaint();
}
/// The color that should be used for the reaction when [hasFocus] is true.
///
/// Used when the toggleable needs to change the reaction color/transparency,
/// when it has focus.
///
/// Defaults to the [activeColor] at alpha [kRadialReactionAlpha].
Color get focusColor => _focusColor;
Color _focusColor;
set focusColor(Color value) {
assert(value != null);
if (value == _focusColor)
return;
_focusColor = value;
markNeedsPaint();
}
/// The color that should be used for the reaction when drawn.
///
/// Used when the toggleable needs to change the reaction color/transparency
/// that is displayed when the toggleable is toggled by a tap.
///
/// Defaults to the [activeColor] at alpha [kRadialReactionAlpha].
Color get reactionColor => _reactionColor;
Color _reactionColor;
set reactionColor(Color value) {
assert(value != null);
if (value == _reactionColor)
return;
_reactionColor = value;
markNeedsPaint();
}
/// Called when the control changes value.
///
/// If the control is tapped, [onChanged] is called immediately with the new
......@@ -463,18 +323,12 @@ abstract class RenderToggleable extends RenderConstrainedBox {
/// point at which the user interacted with the control, which is handled
/// automatically).
void paintRadialReaction(Canvas canvas, Offset offset, Offset origin) {
if (!_reaction.isDismissed || !_reactionFocusFade.isDismissed || !_reactionHoverFade.isDismissed) {
final Paint reactionPaint = Paint()
..color = Color.lerp(
Color.lerp(activeColor.withAlpha(kRadialReactionAlpha), hoverColor, _reactionHoverFade.value),
focusColor,
_reactionFocusFade.value,
);
if (!_reaction.isDismissed) {
// TODO(abarth): We should have a different reaction color when position is zero.
final Paint reactionPaint = Paint()..color = activeColor.withAlpha(kRadialReactionAlpha);
final Offset center = Offset.lerp(_downPosition ?? origin, origin, _reaction.value);
final double reactionRadius = hasFocus || hovering
? kRadialReactionRadius
: _kRadialReactionRadiusTween.evaluate(_reaction);
canvas.drawCircle(center + offset, reactionRadius, reactionPaint);
final double radius = _kRadialReactionRadiusTween.evaluate(_reaction);
canvas.drawCircle(center + offset, radius, reactionPaint);
}
}
......
......@@ -376,8 +376,8 @@ class DoNothingAction extends Action {
/// An action that invokes the currently focused control.
///
/// This is an abstract class that serves as a base class for actions that
/// activate a control. By default, is bound to [LogicalKeyboardKey.enter] in
/// the default keyboard map in [WidgetsApp].
/// activate a control. It is bound to [LogicalKeyboardKey.enter] in the default
/// keyboard map in [WidgetsApp].
abstract class ActivateAction extends Action {
/// Creates a [ActivateAction] with a fixed [key];
const ActivateAction() : super(key);
......@@ -385,16 +385,3 @@ abstract class ActivateAction extends Action {
/// The [LocalKey] that uniquely identifies this action.
static const LocalKey key = ValueKey<Type>(ActivateAction);
}
/// An action that selects the currently focused control.
///
/// This is an abstract class that serves as a base class for actions that
/// select something, like a checkbox or a radio button. By default, it is bound
/// to [LogicalKeyboardKey.space] in the default keyboard map in [WidgetsApp].
abstract class SelectAction extends Action {
/// Creates a [SelectAction] with a fixed [key];
const SelectAction() : super(key);
/// The [LocalKey] that uniquely identifies this action.
static const LocalKey key = ValueKey<Type>(SelectAction);
}
......@@ -1046,7 +1046,6 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
LogicalKeySet(LogicalKeyboardKey.arrowDown): const DirectionalFocusIntent(TraversalDirection.down),
LogicalKeySet(LogicalKeyboardKey.arrowUp): const DirectionalFocusIntent(TraversalDirection.up),
LogicalKeySet(LogicalKeyboardKey.enter): const Intent(ActivateAction.key),
LogicalKeySet(LogicalKeyboardKey.space): const Intent(SelectAction.key),
};
final Map<LocalKey, ActionFactory> _actionMap = <LocalKey, ActionFactory>{
......
......@@ -897,8 +897,8 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<BuildContext>('context', context, defaultValue: null));
properties.add(FlagProperty('canRequestFocus', value: canRequestFocus, ifFalse: 'NOT FOCUSABLE', defaultValue: true));
properties.add(FlagProperty('hasFocus', value: hasFocus && !hasPrimaryFocus, ifTrue: 'IN FOCUS PATH', defaultValue: false));
properties.add(FlagProperty('hasPrimaryFocus', value: hasPrimaryFocus, ifTrue: 'PRIMARY FOCUS', defaultValue: false));
properties.add(FlagProperty('hasFocus', value: hasFocus, ifTrue: 'FOCUSED', defaultValue: false));
properties.add(StringProperty('debugLabel', debugLabel, defaultValue: null));
}
@override
......
......@@ -9,7 +9,6 @@ import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart';
import '../rendering/mock_canvas.dart';
import '../widgets/semantics_tester.dart';
void main() {
......@@ -132,7 +131,6 @@ void main() {
SemanticsFlag.hasCheckedState,
SemanticsFlag.hasEnabledState,
SemanticsFlag.isEnabled,
SemanticsFlag.isFocusable,
],
actions: <SemanticsAction>[
SemanticsAction.tap,
......@@ -159,7 +157,6 @@ void main() {
SemanticsFlag.isChecked,
SemanticsFlag.hasEnabledState,
SemanticsFlag.isEnabled,
SemanticsFlag.isFocusable,
],
actions: <SemanticsAction>[
SemanticsAction.tap,
......@@ -184,7 +181,6 @@ void main() {
SemanticsFlag.isInMutuallyExclusiveGroup,
SemanticsFlag.hasCheckedState,
SemanticsFlag.hasEnabledState,
SemanticsFlag.isFocusable,
],
),
],
......@@ -236,7 +232,7 @@ void main() {
));
await tester.tap(find.byKey(key));
final RenderObject object = tester.firstRenderObject(find.byType(Focus));
final RenderObject object = tester.firstRenderObject(find.byKey(key));
expect(radioValue, 1);
expect(semanticEvent, <String, dynamic>{
......@@ -286,227 +282,5 @@ void main() {
),
);
}, skip: isBrowser);
testWidgets('Radio is focusable and has correct focus color', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(debugLabel: 'Radio');
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
int groupValue = 0;
const Key radioKey = Key('radio');
Widget buildApp({bool enabled = true}) {
return MaterialApp(
home: Material(
child: Center(
child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
return Container(
width: 100,
height: 100,
color: Colors.white,
child: Radio<int>(
key: radioKey,
value: 0,
onChanged: enabled ? (int newValue) {
setState(() {
groupValue = newValue;
});
} : null,
focusColor: Colors.orange[500],
autofocus: true,
focusNode: focusNode,
groupValue: groupValue,
),
);
}),
),
),
);
}
await tester.pumpWidget(buildApp());
await tester.pumpAndSettle();
expect(focusNode.hasPrimaryFocus, isTrue);
expect(
Material.of(tester.element(find.byKey(radioKey))),
paints
..rect(
color: const Color(0xffffffff),
rect: const Rect.fromLTRB(350.0, 250.0, 450.0, 350.0))
..circle(color: Colors.orange[500])
..circle(color: const Color(0xff1e88e5))
..circle(color: const Color(0xff1e88e5)),
);
// Check when the radio isn't selected.
groupValue = 1;
await tester.pumpWidget(buildApp());
await tester.pumpAndSettle();
expect(focusNode.hasPrimaryFocus, isTrue);
expect(
Material.of(tester.element(find.byKey(radioKey))),
paints
..rect(
color: const Color(0xffffffff),
rect: const Rect.fromLTRB(350.0, 250.0, 450.0, 350.0))
..circle(color: Colors.orange[500])
..circle(color: const Color(0x8a000000), style: PaintingStyle.stroke, strokeWidth: 2.0)
);
// Check when the radio is selected, but disabled.
groupValue = 0;
await tester.pumpWidget(buildApp(enabled: false));
await tester.pumpAndSettle();
expect(focusNode.hasPrimaryFocus, isFalse);
expect(
Material.of(tester.element(find.byKey(radioKey))),
paints
..rect(
color: const Color(0xffffffff),
rect: const Rect.fromLTRB(350.0, 250.0, 450.0, 350.0))
..circle(color: const Color(0x61000000))
..circle(color: const Color(0x61000000)),
);
});
testWidgets('Radio can be hovered and has correct focus color', (WidgetTester tester) async {
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
int groupValue = 0;
const Key radioKey = Key('radio');
Widget buildApp({bool enabled = true}) {
return MaterialApp(
home: Material(
child: Center(
child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
return Container(
width: 100,
height: 100,
color: Colors.white,
child: Radio<int>(
key: radioKey,
value: 0,
onChanged: enabled ? (int newValue) {
setState(() {
groupValue = newValue;
});
} : null,
hoverColor: Colors.orange[500],
groupValue: groupValue,
),
);
}),
),
),
);
}
await tester.pumpWidget(buildApp());
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byKey(radioKey))),
paints
..rect(
color: const Color(0xffffffff),
rect: const Rect.fromLTRB(350.0, 250.0, 450.0, 350.0))
..circle(color: const Color(0xff1e88e5))
..circle(color: const Color(0xff1e88e5)),
);
// Start hovering
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
addTearDown(gesture.removePointer);
await gesture.moveTo(tester.getCenter(find.byKey(radioKey)));
// Check when the radio isn't selected.
groupValue = 1;
await tester.pumpWidget(buildApp());
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byKey(radioKey))),
paints
..rect(
color: const Color(0xffffffff),
rect: const Rect.fromLTRB(350.0, 250.0, 450.0, 350.0))
..circle(color: Colors.orange[500])
..circle(color: const Color(0x8a000000), style: PaintingStyle.stroke, strokeWidth: 2.0)
);
// Check when the radio is selected, but disabled.
groupValue = 0;
await tester.pumpWidget(buildApp(enabled: false));
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byKey(radioKey))),
paints
..rect(
color: const Color(0xffffffff),
rect: const Rect.fromLTRB(350.0, 250.0, 450.0, 350.0))
..circle(color: const Color(0x61000000))
..circle(color: const Color(0x61000000)),
);
});
testWidgets('Radio can be toggled by keyboard shortcuts', (WidgetTester tester) async {
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
int groupValue = 1;
const Key radioKey0 = Key('radio0');
const Key radioKey1 = Key('radio1');
final FocusNode focusNode1 = FocusNode(debugLabel: 'radio1');
Widget buildApp({bool enabled = true}) {
return MaterialApp(
home: Material(
child: Center(
child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
return Container(
width: 100,
height: 100,
color: Colors.white,
child: Row(
children: <Widget>[
Radio<int>(
key: radioKey0,
value: 0,
onChanged: enabled ? (int newValue) {
setState(() {
groupValue = newValue;
});
} : null,
hoverColor: Colors.orange[500],
groupValue: groupValue,
autofocus: true,
),
Radio<int>(
key: radioKey1,
value: 1,
onChanged: enabled ? (int newValue) {
setState(() {
groupValue = newValue;
});
} : null,
hoverColor: Colors.orange[500],
groupValue: groupValue,
focusNode: focusNode1,
),
],
),
);
}),
),
),
);
}
await tester.pumpWidget(buildApp());
await tester.pumpAndSettle();
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
await tester.pumpAndSettle();
expect(groupValue, equals(0));
focusNode1.requestFocus();
await tester.pumpAndSettle();
await tester.sendKeyEvent(LogicalKeyboardKey.space);
await tester.pumpAndSettle();
expect(groupValue, equals(1));
});
}
......@@ -2,7 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
......@@ -48,7 +47,6 @@ void main() {
Shortcuts(
shortcuts: <LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.enter): const Intent(ActivateAction.key),
LogicalKeySet(LogicalKeyboardKey.space): const Intent(SelectAction.key),
},
child: Directionality(
textDirection: TextDirection.ltr,
......@@ -76,12 +74,6 @@ void main() {
await tester.pumpAndSettle();
expect(pressed, isTrue);
pressed = false;
await tester.sendKeyEvent(LogicalKeyboardKey.space);
await tester.pumpAndSettle();
expect(pressed, kIsWeb ? isFalse : isTrue);
});
testWidgets('materialTapTargetSize.padded expands hit test area', (WidgetTester tester) async {
......
......@@ -518,7 +518,7 @@ void main() {
),
);
await tester.tap(find.byType(Switch));
final RenderObject object = tester.firstRenderObject(find.byType(Focus));
final RenderObject object = tester.firstRenderObject(find.byType(Switch));
expect(value, true);
expect(semanticEvent, <String, dynamic>{
......@@ -623,198 +623,4 @@ void main() {
});
testWidgets('Switch is focusable and has correct focus color', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(debugLabel: 'Switch');
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
bool value = true;
Widget buildApp({bool enabled = true}) {
return MaterialApp(
home: Material(
child: Center(
child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
return Switch(
value: value,
onChanged: enabled ? (bool newValue) {
setState(() {
value = newValue;
});
} : null,
focusColor: Colors.orange[500],
autofocus: true,
focusNode: focusNode,
);
}),
),
),
);
}
await tester.pumpWidget(buildApp());
await tester.pumpAndSettle();
expect(focusNode.hasPrimaryFocus, isTrue);
expect(
Material.of(tester.element(find.byType(Switch))),
paints
..rrect(
color: const Color(0x801e88e5),
rrect: RRect.fromLTRBR(
383.5, 293.0, 416.5, 307.0, const Radius.circular(7.0)))
..circle(color: Colors.orange[500])
..circle(color: const Color(0x33000000))
..circle(color: const Color(0x24000000))
..circle(color: const Color(0x1f000000))
..circle(color: const Color(0xff1e88e5)),
);
// Check the false value.
value = false;
await tester.pumpWidget(buildApp());
await tester.pumpAndSettle();
expect(focusNode.hasPrimaryFocus, isTrue);
expect(
Material.of(tester.element(find.byType(Switch))),
paints
..rrect(
color: const Color(0x52000000),
rrect: RRect.fromLTRBR(
383.5, 293.0, 416.5, 307.0, const Radius.circular(7.0)))
..circle(color: Colors.orange[500])
..circle(color: const Color(0x33000000))
..circle(color: const Color(0x24000000))
..circle(color: const Color(0x1f000000))
..circle(color: const Color(0xfffafafa)),
);
// Check what happens when disabled.
value = false;
await tester.pumpWidget(buildApp(enabled: false));
await tester.pumpAndSettle();
expect(focusNode.hasPrimaryFocus, isFalse);
expect(
Material.of(tester.element(find.byType(Switch))),
paints
..rrect(
color: const Color(0x1f000000),
rrect: RRect.fromLTRBR(
383.5, 293.0, 416.5, 307.0, const Radius.circular(7.0)))
..circle(color: const Color(0x33000000))
..circle(color: const Color(0x24000000))
..circle(color: const Color(0x1f000000))
..circle(color: const Color(0xffbdbdbd)),
);
});
testWidgets('Switch can be hovered and has correct hover color', (WidgetTester tester) async {
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
bool value = true;
Widget buildApp({bool enabled = true}) {
return MaterialApp(
home: Material(
child: Center(
child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
return Switch(
value: value,
onChanged: enabled ? (bool newValue) {
setState(() {
value = newValue;
});
} : null,
hoverColor: Colors.orange[500],
);
}),
),
),
);
}
await tester.pumpWidget(buildApp());
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Switch))),
paints
..rrect(
color: const Color(0x801e88e5),
rrect: RRect.fromLTRBR(
383.5, 293.0, 416.5, 307.0, const Radius.circular(7.0)))
..circle(color: const Color(0x33000000))
..circle(color: const Color(0x24000000))
..circle(color: const Color(0x1f000000))
..circle(color: const Color(0xff1e88e5)),
);
// Start hovering
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
addTearDown(gesture.removePointer);
await gesture.moveTo(tester.getCenter(find.byType(Switch)));
await tester.pumpWidget(buildApp());
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Switch))),
paints
..rrect(
color: const Color(0x801e88e5),
rrect: RRect.fromLTRBR(
383.5, 293.0, 416.5, 307.0, const Radius.circular(7.0)))
..circle(color: Colors.orange[500])
..circle(color: const Color(0x33000000))
..circle(color: const Color(0x24000000))
..circle(color: const Color(0x1f000000))
..circle(color: const Color(0xff1e88e5)),
);
// Check what happens when disabled.
await tester.pumpWidget(buildApp(enabled: false));
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Switch))),
paints
..rrect(
color: const Color(0x1f000000),
rrect: RRect.fromLTRBR(
383.5, 293.0, 416.5, 307.0, const Radius.circular(7.0)))
..circle(color: const Color(0x33000000))
..circle(color: const Color(0x24000000))
..circle(color: const Color(0x1f000000))
..circle(color: const Color(0xffbdbdbd)),
);
});
testWidgets('Switch can be toggled by keyboard shortcuts', (WidgetTester tester) async {
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
bool value = true;
Widget buildApp({bool enabled = true}) {
return MaterialApp(
home: Material(
child: Center(
child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
return Switch(
value: value,
onChanged: enabled ? (bool newValue) {
setState(() {
value = newValue;
});
} : null,
focusColor: Colors.orange[500],
autofocus: true,
);
}),
),
),
);
}
await tester.pumpWidget(buildApp());
await tester.pumpAndSettle();
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
await tester.pumpAndSettle();
expect(value, isFalse);
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
await tester.pumpAndSettle();
expect(value, isTrue);
await tester.sendKeyEvent(LogicalKeyboardKey.space);
await tester.pumpAndSettle();
expect(value, isFalse);
await tester.sendKeyEvent(LogicalKeyboardKey.space);
await tester.pumpAndSettle();
expect(value, isTrue);
});
}
......@@ -66,12 +66,9 @@ void main() {
FocusNode(
debugLabel: 'Label',
).debugFillProperties(builder);
final List<String> description = builder.properties.map((DiagnosticsNode n) => n.toString()).toList();
final List<String> description = builder.properties.where((DiagnosticsNode n) => !n.isFiltered(DiagnosticLevel.info)).map((DiagnosticsNode n) => n.toString()).toList();
expect(description, <String>[
'context: null',
'canRequestFocus: true',
'hasFocus: false',
'hasPrimaryFocus: false'
'debugLabel: "Label"',
]);
});
});
......@@ -624,12 +621,9 @@ void main() {
FocusScopeNode(
debugLabel: 'Scope Label',
).debugFillProperties(builder);
final List<String> description = builder.properties.map((DiagnosticsNode n) => n.toString()).toList();
final List<String> description = builder.properties.where((DiagnosticsNode n) => !n.isFiltered(DiagnosticLevel.info)).map((DiagnosticsNode n) => n.toString()).toList();
expect(description, <String>[
'context: null',
'canRequestFocus: true',
'hasFocus: false',
'hasPrimaryFocus: false'
'debugLabel: "Scope Label"',
]);
});
testWidgets('debugDescribeFocusTree produces correct output', (WidgetTester tester) async {
......@@ -669,36 +663,43 @@ void main() {
' │ primaryFocusCreator: Container-[GlobalKey#00000] ← [root]\n'
' │\n'
' └─rootScope: FocusScopeNode#00000(Root Focus Scope)\n'
' │ IN FOCUS PATH\n'
' │ FOCUSED\n'
' │ debugLabel: "Root Focus Scope"\n'
' │ focusedChildren: FocusScopeNode#00000\n'
' │\n'
' ├─Child 1: FocusScopeNode#00000(Scope 1)\n'
' │ │ context: Container-[GlobalKey#00000]\n'
' │ │ debugLabel: "Scope 1"\n'
' │ │\n'
' │ └─Child 1: FocusNode#00000(Parent 1)\n'
' │ │ context: Container-[GlobalKey#00000]\n'
' │ │ debugLabel: "Parent 1"\n'
' │ │\n'
' │ ├─Child 1: FocusNode#00000(Child 1)\n'
' │ │ context: Container-[GlobalKey#00000]\n'
' │ │ debugLabel: "Child 1"\n'
' │ │\n'
' │ └─Child 2: FocusNode#00000\n'
' │ context: Container-[GlobalKey#00000]\n'
' │\n'
' └─Child 2: FocusScopeNode#00000\n'
' │ context: Container-[GlobalKey#00000]\n'
' │ IN FOCUS PATH\n'
' │ FOCUSED\n'
' │ focusedChildren: FocusNode#00000(Child 4)\n'
' │\n'
' └─Child 1: FocusNode#00000(Parent 2)\n'
' │ context: Container-[GlobalKey#00000]\n'
' │ IN FOCUS PATH\n'
' │ FOCUSED\n'
' │ debugLabel: "Parent 2"\n'
' │\n'
' ├─Child 1: FocusNode#00000(Child 3)\n'
' │ context: Container-[GlobalKey#00000]\n'
' │ debugLabel: "Child 3"\n'
' │\n'
' └─Child 2: FocusNode#00000(Child 4)\n'
' context: Container-[GlobalKey#00000]\n'
' PRIMARY FOCUS\n'
' FOCUSED\n'
' debugLabel: "Child 4"\n'
));
});
});
......
......@@ -229,29 +229,34 @@ void main() {
parentFocusScope.toStringDeep(),
equalsIgnoringHashCodes('FocusScopeNode#00000(Parent Scope Node)\n'
' │ context: FocusScope\n'
' │ IN FOCUS PATH\n'
' │ FOCUSED\n'
' │ debugLabel: "Parent Scope Node"\n'
' │ focusedChildren: FocusNode#00000(Child)\n'
' │\n'
' └─Child 1: FocusNode#00000(Child)\n'
' context: Focus\n'
' PRIMARY FOCUS\n'),
' FOCUSED\n'
' debugLabel: "Child"\n'),
);
expect(WidgetsBinding.instance.focusManager.rootScope, hasAGoodToStringDeep);
expect(
WidgetsBinding.instance.focusManager.rootScope.toStringDeep(minLevel: DiagnosticLevel.info),
equalsIgnoringHashCodes('FocusScopeNode#00000(Root Focus Scope)\n'
' │ IN FOCUS PATH\n'
' │ FOCUSED\n'
' │ debugLabel: "Root Focus Scope"\n'
' │ focusedChildren: FocusScopeNode#00000(Parent Scope Node)\n'
' │\n'
' └─Child 1: FocusScopeNode#00000(Parent Scope Node)\n'
' │ context: FocusScope\n'
' │ IN FOCUS PATH\n'
' │ FOCUSED\n'
' │ debugLabel: "Parent Scope Node"\n'
' │ focusedChildren: FocusNode#00000(Child)\n'
' │\n'
' └─Child 1: FocusNode#00000(Child)\n'
' context: Focus\n'
' PRIMARY FOCUS\n'),
' FOCUSED\n'
' debugLabel: "Child"\n'),
);
// Add the child focus scope to the focus tree.
......
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