Unverified Commit bb3c6605 authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

Implements focus handling and hover for Material buttons. (#31438)

This implements focus and hover handling for Material buttons. It inserts Focus widgets into the tree in order to allow buttons to be focusable via keyboard traversal (a.k.a. TAB traversal), and Listener widgets into the InkWell to allow the detection of hover states for widgets.

Addresses #11344, #1608, and #13264.
parent ed90d055
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
void main() {
runApp(const MaterialApp(
title: 'Focus Demo',
home: FocusDemo(),
));
}
class DemoButton extends StatelessWidget {
const DemoButton({this.name});
final String name;
void _handleOnPressed() {
print('Button $name pressed.');
}
@override
Widget build(BuildContext context) {
return FlatButton(
focusColor: Colors.red,
hoverColor: Colors.blue,
onPressed: () => _handleOnPressed(),
child: Text(name),
);
}
}
class FocusDemo extends StatefulWidget {
const FocusDemo({Key key}) : super(key: key);
@override
_FocusDemoState createState() => _FocusDemoState();
}
class _FocusDemoState extends State<FocusDemo> {
FocusNode outlineFocus;
@override
void initState() {
super.initState();
outlineFocus = FocusNode(debugLabel: 'Demo Focus Node');
}
@override
void dispose() {
outlineFocus.dispose();
super.dispose();
}
bool _handleKeyPress(FocusNode node, RawKeyEvent event) {
if (event is RawKeyDownEvent) {
print('Scope got key event: ${event.logicalKey}, $node');
print('Keys down: ${RawKeyboard.instance.keysPressed}');
if (event.logicalKey == LogicalKeyboardKey.tab) {
debugDumpFocusTree();
if (event.isShiftPressed) {
print('Moving to previous.');
node.previousFocus();
return true;
} else {
print('Moving to next.');
node.nextFocus();
return true;
}
}
if (event.logicalKey == LogicalKeyboardKey.arrowLeft) {
node.focusInDirection(TraversalDirection.left);
return true;
}
if (event.logicalKey == LogicalKeyboardKey.arrowRight) {
node.focusInDirection(TraversalDirection.right);
return true;
}
if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
node.focusInDirection(TraversalDirection.up);
return true;
}
if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
node.focusInDirection(TraversalDirection.down);
return true;
}
}
return false;
}
@override
Widget build(BuildContext context) {
final TextTheme textTheme = Theme.of(context).textTheme;
return DefaultFocusTraversal(
policy: ReadingOrderTraversalPolicy(),
child: FocusScope(
debugLabel: 'Scope',
onKey: _handleKeyPress,
autofocus: true,
child: DefaultTextStyle(
style: textTheme.display1,
child: Scaffold(
appBar: AppBar(
title: const Text('Focus Demo'),
),
floatingActionButton: FloatingActionButton(
child: const Text('+'),
onPressed: () {},
),
body: Center(
child: Builder(builder: (BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: const <Widget>[
DemoButton(name: 'One'),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: const <Widget>[
DemoButton(name: 'Two'),
DemoButton(name: 'Three'),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: const <Widget>[
DemoButton(name: 'Four'),
DemoButton(name: 'Five'),
DemoButton(name: 'Six'),
],
),
OutlineButton(onPressed: () => print('pressed'), child: const Text('PRESS ME')),
Padding(
padding: const EdgeInsets.all(8.0),
child: TextField(
decoration: InputDecoration(labelText: 'Enter Text', filled: true),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: TextField(
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: 'Enter Text',
filled: false,
),
),
),
],
);
}),
),
),
),
),
);
}
}
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
void main() {
runApp(const MaterialApp(
title: 'Hover Demo',
home: HoverDemo(),
));
}
class DemoButton extends StatelessWidget {
const DemoButton({this.name});
final String name;
void _handleOnPressed() {
print('Button $name pressed.');
}
@override
Widget build(BuildContext context) {
return FlatButton(
onPressed: () => _handleOnPressed(),
child: Text(name),
);
}
}
class HoverDemo extends StatefulWidget {
const HoverDemo({Key key}) : super(key: key);
@override
_HoverDemoState createState() => _HoverDemoState();
}
class _HoverDemoState extends State<HoverDemo> {
@override
Widget build(BuildContext context) {
final TextTheme textTheme = Theme.of(context).textTheme;
return DefaultTextStyle(
style: textTheme.display1,
child: Scaffold(
appBar: AppBar(
title: const Text('Hover Demo'),
),
floatingActionButton: FloatingActionButton(
child: const Text('+'),
onPressed: () {},
),
body: Center(
child: Builder(builder: (BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
RaisedButton(
onPressed: () => print('Button pressed.'),
child: const Text('Button'),
focusColor: Colors.deepOrangeAccent,
),
FlatButton(
onPressed: () => print('Button pressed.'),
child: const Text('Button'),
focusColor: Colors.deepOrangeAccent,
),
IconButton(
onPressed: () => print('Button pressed'),
icon: const Icon(Icons.access_alarm),
focusColor: Colors.deepOrangeAccent,
),
Padding(
padding: const EdgeInsets.all(8.0),
child: TextField(
decoration: InputDecoration(labelText: 'Enter Text', filled: true),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: TextField(
autofocus: false,
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: 'Enter Text',
filled: false,
),
),
),
],
);
}),
),
),
);
}
}
......@@ -60,6 +60,7 @@ class _CategoryItem extends StatelessWidget {
return RepaintBoundary(
child: RawMaterialButton(
padding: EdgeInsets.zero,
hoverColor: theme.primaryColor.withOpacity(0.05),
splashColor: theme.primaryColor.withOpacity(0.12),
highlightColor: Colors.transparent,
onPressed: onTap,
......
......@@ -475,43 +475,45 @@ class _BottomNavigationTile extends StatelessWidget {
container: true,
header: true,
selected: selected,
child: Stack(
children: <Widget>[
InkResponse(
onTap: onTap,
child: Padding(
padding: EdgeInsets.only(top: topPadding, bottom: bottomPadding),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
_TileIcon(
colorTween: colorTween,
animation: animation,
iconSize: iconSize,
selected: selected,
item: item,
selectedIconTheme: selectedIconTheme,
unselectedIconTheme: unselectedIconTheme,
),
_Label(
colorTween: colorTween,
animation: animation,
item: item,
selectedLabelStyle: selectedLabelStyle,
unselectedLabelStyle: unselectedLabelStyle,
showSelectedLabels: showSelectedLabels,
showUnselectedLabels: showUnselectedLabels,
),
],
child: Focus(
child: Stack(
children: <Widget>[
InkResponse(
onTap: onTap,
child: Padding(
padding: EdgeInsets.only(top: topPadding, bottom: bottomPadding),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
_TileIcon(
colorTween: colorTween,
animation: animation,
iconSize: iconSize,
selected: selected,
item: item,
selectedIconTheme: selectedIconTheme,
unselectedIconTheme: unselectedIconTheme,
),
_Label(
colorTween: colorTween,
animation: animation,
item: item,
selectedLabelStyle: selectedLabelStyle,
unselectedLabelStyle: unselectedLabelStyle,
showSelectedLabels: showSelectedLabels,
showUnselectedLabels: showUnselectedLabels,
),
],
),
),
),
),
Semantics(
label: indexLabel,
),
],
Semantics(
label: indexLabel,
),
],
),
),
),
);
......
......@@ -78,6 +78,8 @@ class ButtonTheme extends InheritedWidget {
bool alignedDropdown = false,
Color buttonColor,
Color disabledColor,
Color focusColor,
Color hoverColor,
Color highlightColor,
Color splashColor,
ColorScheme colorScheme,
......@@ -98,6 +100,8 @@ class ButtonTheme extends InheritedWidget {
layoutBehavior: layoutBehavior,
buttonColor: buttonColor,
disabledColor: disabledColor,
focusColor: focusColor,
hoverColor: hoverColor,
highlightColor: highlightColor,
splashColor: splashColor,
colorScheme: colorScheme,
......@@ -139,6 +143,8 @@ class ButtonTheme extends InheritedWidget {
bool alignedDropdown = false,
Color buttonColor,
Color disabledColor,
Color focusColor,
Color hoverColor,
Color highlightColor,
Color splashColor,
ColorScheme colorScheme,
......@@ -158,6 +164,8 @@ class ButtonTheme extends InheritedWidget {
layoutBehavior: layoutBehavior,
buttonColor: buttonColor,
disabledColor: disabledColor,
focusColor: focusColor,
hoverColor: hoverColor,
highlightColor: highlightColor,
splashColor: splashColor,
colorScheme: colorScheme,
......@@ -220,6 +228,8 @@ class ButtonThemeData extends Diagnosticable {
this.alignedDropdown = false,
Color buttonColor,
Color disabledColor,
Color focusColor,
Color hoverColor,
Color highlightColor,
Color splashColor,
this.colorScheme,
......@@ -231,6 +241,8 @@ class ButtonThemeData extends Diagnosticable {
assert(layoutBehavior != null),
_buttonColor = buttonColor,
_disabledColor = disabledColor,
_focusColor = focusColor,
_hoverColor = hoverColor,
_highlightColor = highlightColor,
_splashColor = splashColor,
_padding = padding,
......@@ -350,6 +362,10 @@ class ButtonThemeData extends Diagnosticable {
///
/// This property is null by default.
///
/// If the button is in the focused, hovering, or highlighted state, then the
/// [focusColor], [hoverColor], or [highlightColor] will take precedence over
/// the [focusColor].
///
/// See also:
///
/// * [getFillColor], which is used by [RaisedButton] to compute its
......@@ -366,6 +382,32 @@ class ButtonThemeData extends Diagnosticable {
/// background fill color.
final Color _disabledColor;
/// The fill color of the button when it has the input focus.
///
/// This property is null by default.
///
/// If the button is in the hovering or highlighted state, then the [hoverColor]
/// or [highlightColor] will take precedence over the [focusColor].
///
/// See also:
///
/// * [getFocusColor], which is used by [RaisedButton], [OutlineButton]
/// and [FlatButton].
final Color _focusColor;
/// The fill color of the button when a pointer is hovering over it.
///
/// This property is null by default.
///
/// If the button is in the highlighted state, then the [highlightColor] will
/// take precedence over the [hoverColor].
///
/// See also:
///
/// * [getHoverColor], which is used by [RaisedButton], [OutlineButton]
/// and [FlatButton].
final Color _hoverColor;
/// The color of the overlay that appears when a button is pressed.
///
/// This property is null by default.
......@@ -590,6 +632,32 @@ class ButtonThemeData extends Diagnosticable {
return getTextColor(button).withOpacity(0.12);
}
/// The fill color of the button when it has input focus.
///
/// Returns the button's [MaterialButton.focusColor] if it is non-null.
/// Otherwise the focus color depends on [getTextTheme]:
///
/// * [ButtonTextTheme.normal], [ButtonTextTheme.accent]: returns the
/// value of the `focusColor` constructor parameter if it is non-null,
/// otherwise the value of [getTextColor] with opacity 0.12.
/// * [ButtonTextTheme.primary], returns [Colors.transparent].
Color getFocusColor(MaterialButton button) {
return button.focusColor ?? _focusColor ?? getTextColor(button).withOpacity(0.12);
}
/// The fill color of the button when it has input focus.
///
/// Returns the button's [MaterialButton.focusColor] if it is non-null.
/// Otherwise the focus color depends on [getTextTheme]:
///
/// * [ButtonTextTheme.normal], [ButtonTextTheme.accent],
/// [ButtonTextTheme.primary]: returns the value of the `focusColor`
/// constructor parameter if it is non-null, otherwise the value of
/// [getTextColor] with opacity 0.04.
Color getHoverColor(MaterialButton button) {
return button.hoverColor ?? _hoverColor ?? getTextColor(button).withOpacity(0.04);
}
/// The color of the overlay that appears when the [button] is pressed.
///
/// Returns the button's [MaterialButton.highlightColor] if it is non-null.
......@@ -628,6 +696,38 @@ class ButtonThemeData extends Diagnosticable {
return 2.0;
}
/// The [button]'s elevation when it is enabled and has focus.
///
/// Returns the button's [MaterialButton.focusElevation] if it is non-null.
///
/// If button is a [FlatButton] or an [OutlineButton] then the focus
/// elevation is 0.0, otherwise the highlight elevation is 4.0.
double getFocusElevation(MaterialButton button) {
if (button.focusElevation != null)
return button.focusElevation;
if (button is FlatButton)
return 0.0;
if (button is OutlineButton)
return 0.0;
return 4.0;
}
/// The [button]'s elevation when it is enabled and has focus.
///
/// Returns the button's [MaterialButton.hoverElevation] if it is non-null.
///
/// If button is a [FlatButton] or an [OutlineButton] then the hover
/// elevation is 0.0, otherwise the highlight elevation is 4.0.
double getHoverElevation(MaterialButton button) {
if (button.hoverElevation != null)
return button.hoverElevation;
if (button is FlatButton)
return 0.0;
if (button is OutlineButton)
return 0.0;
return 4.0;
}
/// The [button]'s elevation when it is enabled and has been pressed.
///
/// Returns the button's [MaterialButton.highlightElevation] if it is non-null.
......@@ -737,6 +837,8 @@ class ButtonThemeData extends Diagnosticable {
bool alignedDropdown,
Color buttonColor,
Color disabledColor,
Color focusColor,
Color hoverColor,
Color highlightColor,
Color splashColor,
ColorScheme colorScheme,
......@@ -752,6 +854,8 @@ class ButtonThemeData extends Diagnosticable {
alignedDropdown: alignedDropdown ?? this.alignedDropdown,
buttonColor: buttonColor ?? _buttonColor,
disabledColor: disabledColor ?? _disabledColor,
focusColor: focusColor ?? _focusColor,
hoverColor: hoverColor ?? _hoverColor,
highlightColor: highlightColor ?? _highlightColor,
splashColor: splashColor ?? _splashColor,
colorScheme: colorScheme ?? this.colorScheme,
......@@ -772,6 +876,8 @@ class ButtonThemeData extends Diagnosticable {
&& alignedDropdown == typedOther.alignedDropdown
&& _buttonColor == typedOther._buttonColor
&& _disabledColor == typedOther._disabledColor
&& _focusColor == typedOther._focusColor
&& _hoverColor == typedOther._hoverColor
&& _highlightColor == typedOther._highlightColor
&& _splashColor == typedOther._splashColor
&& colorScheme == typedOther.colorScheme
......@@ -789,6 +895,8 @@ class ButtonThemeData extends Diagnosticable {
alignedDropdown,
_buttonColor,
_disabledColor,
_focusColor,
_hoverColor,
_highlightColor,
_splashColor,
colorScheme,
......@@ -812,6 +920,8 @@ class ButtonThemeData extends Diagnosticable {
));
properties.add(DiagnosticsProperty<Color>('buttonColor', _buttonColor, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('disabledColor', _disabledColor, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('focusColor', _focusColor, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('hoverColor', _hoverColor, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('highlightColor', _highlightColor, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('splashColor', _splashColor, defaultValue: null));
properties.add(DiagnosticsProperty<ColorScheme>('colorScheme', colorScheme, defaultValue: defaultTheme.colorScheme));
......
......@@ -102,12 +102,15 @@ class FlatButton extends MaterialButton {
Color disabledTextColor,
Color color,
Color disabledColor,
Color focusColor,
Color hoverColor,
Color highlightColor,
Color splashColor,
Brightness colorBrightness,
EdgeInsetsGeometry padding,
ShapeBorder shape,
Clip clipBehavior = Clip.none,
Clip clipBehavior,
FocusNode focusNode,
MaterialTapTargetSize materialTapTargetSize,
@required Widget child,
}) : super(
......@@ -119,12 +122,15 @@ class FlatButton extends MaterialButton {
disabledTextColor: disabledTextColor,
color: color,
disabledColor: disabledColor,
focusColor: focusColor,
hoverColor: hoverColor,
highlightColor: highlightColor,
splashColor: splashColor,
colorBrightness: colorBrightness,
padding: padding,
shape: shape,
clipBehavior: clipBehavior,
focusNode: focusNode,
materialTapTargetSize: materialTapTargetSize,
child: child,
);
......@@ -145,12 +151,15 @@ class FlatButton extends MaterialButton {
Color disabledTextColor,
Color color,
Color disabledColor,
Color focusColor,
Color hoverColor,
Color highlightColor,
Color splashColor,
Brightness colorBrightness,
EdgeInsetsGeometry padding,
ShapeBorder shape,
Clip clipBehavior,
FocusNode focusNode,
MaterialTapTargetSize materialTapTargetSize,
@required Widget icon,
@required Widget label,
......@@ -163,39 +172,27 @@ class FlatButton extends MaterialButton {
return RawMaterialButton(
onPressed: onPressed,
onHighlightChanged: onHighlightChanged,
clipBehavior: clipBehavior ?? Clip.none,
fillColor: buttonTheme.getFillColor(this),
textStyle: theme.textTheme.button.copyWith(color: buttonTheme.getTextColor(this)),
focusColor: buttonTheme.getFocusColor(this),
hoverColor: buttonTheme.getHoverColor(this),
highlightColor: buttonTheme.getHighlightColor(this),
splashColor: buttonTheme.getSplashColor(this),
elevation: buttonTheme.getElevation(this),
focusElevation: buttonTheme.getFocusElevation(this),
hoverElevation: buttonTheme.getHoverElevation(this),
highlightElevation: buttonTheme.getHighlightElevation(this),
disabledElevation: buttonTheme.getDisabledElevation(this),
padding: buttonTheme.getPadding(this),
constraints: buttonTheme.getConstraints(this),
shape: buttonTheme.getShape(this),
animationDuration: buttonTheme.getAnimationDuration(this),
clipBehavior: clipBehavior ?? Clip.none,
focusNode: focusNode,
materialTapTargetSize: buttonTheme.getMaterialTapTargetSize(this),
animationDuration: buttonTheme.getAnimationDuration(this),
child: child,
);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(ObjectFlagProperty<VoidCallback>('onPressed', onPressed, ifNull: 'disabled'));
properties.add(DiagnosticsProperty<ButtonTextTheme>('textTheme', textTheme, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('textColor', textColor, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('disabledTextColor', disabledTextColor, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('color', color, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('disabledColor', disabledColor, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('highlightColor', highlightColor, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('splashColor', splashColor, defaultValue: null));
properties.add(DiagnosticsProperty<Brightness>('colorBrightness', colorBrightness, defaultValue: null));
properties.add(DiagnosticsProperty<EdgeInsetsGeometry>('padding', padding, defaultValue: null));
properties.add(DiagnosticsProperty<ShapeBorder>('shape', shape, defaultValue: null));
properties.add(DiagnosticsProperty<MaterialTapTargetSize>('materialTapTargetSize', materialTapTargetSize, defaultValue: null));
}
}
/// The type of of FlatButtons created with [FlatButton.icon].
......@@ -212,12 +209,15 @@ class _FlatButtonWithIcon extends FlatButton with MaterialButtonWithIconMixin {
Color disabledTextColor,
Color color,
Color disabledColor,
Color focusColor,
Color hoverColor,
Color highlightColor,
Color splashColor,
Brightness colorBrightness,
EdgeInsetsGeometry padding,
ShapeBorder shape,
Clip clipBehavior,
FocusNode focusNode,
MaterialTapTargetSize materialTapTargetSize,
@required Widget icon,
@required Widget label,
......@@ -232,12 +232,15 @@ class _FlatButtonWithIcon extends FlatButton with MaterialButtonWithIconMixin {
disabledTextColor: disabledTextColor,
color: color,
disabledColor: disabledColor,
focusColor: focusColor,
hoverColor: hoverColor,
highlightColor: highlightColor,
splashColor: splashColor,
colorBrightness: colorBrightness,
padding: padding,
shape: shape,
clipBehavior: clipBehavior,
focusNode: focusNode,
materialTapTargetSize: materialTapTargetSize,
child: Row(
mainAxisSize: MainAxisSize.min,
......
......@@ -4,6 +4,7 @@
import 'dart:math' as math;
import 'package:flutter/foundation.dart';
import 'package:flutter/painting.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
......@@ -126,17 +127,24 @@ class FloatingActionButton extends StatelessWidget {
this.tooltip,
this.foregroundColor,
this.backgroundColor,
this.focusColor,
this.hoverColor,
this.heroTag = const _DefaultHeroTag(),
this.elevation,
this.focusElevation,
this.hoverElevation,
this.highlightElevation,
this.disabledElevation,
@required this.onPressed,
this.mini = false,
this.shape,
this.clipBehavior = Clip.none,
this.focusNode,
this.materialTapTargetSize,
this.isExtended = false,
}) : assert(elevation == null || elevation >= 0.0),
assert(focusElevation == null || focusElevation >= 0.0),
assert(hoverElevation == null || hoverElevation >= 0.0),
assert(highlightElevation == null || highlightElevation >= 0.0),
assert(disabledElevation == null || disabledElevation >= 0.0),
assert(mini != null),
......@@ -155,8 +163,12 @@ class FloatingActionButton extends StatelessWidget {
this.tooltip,
this.foregroundColor,
this.backgroundColor,
this.focusColor,
this.hoverColor,
this.heroTag = const _DefaultHeroTag(),
this.elevation,
this.focusElevation,
this.hoverElevation,
this.highlightElevation,
this.disabledElevation,
@required this.onPressed,
......@@ -164,13 +176,15 @@ class FloatingActionButton extends StatelessWidget {
this.isExtended = true,
this.materialTapTargetSize,
this.clipBehavior = Clip.none,
this.focusNode,
Widget icon,
@required Widget label,
}) : assert(elevation == null || elevation >= 0.0),
assert(focusElevation == null || focusElevation >= 0.0),
assert(hoverElevation == null || hoverElevation >= 0.0),
assert(highlightElevation == null || highlightElevation >= 0.0),
assert(disabledElevation == null || disabledElevation >= 0.0),
assert(isExtended != null),
assert(clipBehavior != null),
_sizeConstraints = _kExtendedSizeConstraints,
mini = false,
child = _ChildOverflowBox(
......@@ -214,6 +228,17 @@ class FloatingActionButton extends StatelessWidget {
/// Defaults to [ThemeData.accentColor] for the current theme.
final Color backgroundColor;
/// The color to use for filling the button when the button has input focus.
///
/// Defaults to [ThemeData.focusColor] for the current theme.
final Color focusColor;
/// The color to use for filling the button when the button has a pointer
/// hovering over it.
///
/// Defaults to [ThemeData.hoverColor] for the current theme.
final Color hoverColor;
/// The tag to apply to the button's [Hero] widget.
///
/// Defaults to a tag that matches other floating action buttons.
......@@ -246,6 +271,36 @@ class FloatingActionButton extends StatelessWidget {
/// * [disabledElevation], the elevation when the button is disabled.
final double elevation;
/// The z-coordinate at which to place this button relative to its parent when
/// the button has the input focus.
///
/// This controls the size of the shadow below the floating action button.
///
/// Defaults to 8, the appropriate elevation for floating action buttons
/// while they have focus. The value is always non-negative.
///
/// See also:
///
/// * [elevation], the default elevation.
/// * [highlightElevation], the elevation when the button is pressed.
/// * [disabledElevation], the elevation when the button is disabled.
final double focusElevation;
/// The z-coordinate at which to place this button relative to its parent when
/// the button is enabled and has a pointer hovering over it.
///
/// This controls the size of the shadow below the floating action button.
///
/// Defaults to 8, the appropriate elevation for floating action buttons while
/// they have a pointer hovering over them. The value is always non-negative.
///
/// See also:
///
/// * [elevation], the default elevation.
/// * [highlightElevation], the elevation when the button is pressed.
/// * [disabledElevation], the elevation when the button is disabled.
final double hoverElevation;
/// The z-coordinate at which to place this button relative to its parent when
/// the user is touching the button.
///
......@@ -305,6 +360,14 @@ class FloatingActionButton extends StatelessWidget {
/// floating action buttons are scaled and faded in.
final bool isExtended;
/// An optional focus node to use for requesting focus when pressed.
///
/// If not supplied, the button will create and host its own [FocusNode].
///
/// If supplied, the given focusNode will be _hosted_ by this widget. See
/// [FocusNode] for more information on what that implies.
final FocusNode focusNode;
/// Configures the minimum size of the tap target.
///
/// Defaults to [ThemeData.materialTapTargetSize].
......@@ -317,6 +380,10 @@ class FloatingActionButton extends StatelessWidget {
final BoxConstraints _sizeConstraints;
static const double _defaultElevation = 6;
// TODO(gspencer): verify focus and hover elevation values are correct
// according to spec.
static const double _defaultFocusElevation = 8;
static const double _defaultHoverElevation = 10;
static const double _defaultHighlightElevation = 12;
static const ShapeBorder _defaultShape = CircleBorder();
static const ShapeBorder _defaultExtendedShape = StadiumBorder();
......@@ -326,16 +393,28 @@ class FloatingActionButton extends StatelessWidget {
final ThemeData theme = Theme.of(context);
final FloatingActionButtonThemeData floatingActionButtonTheme = theme.floatingActionButtonTheme;
final Color backgroundColor = this.backgroundColor
?? floatingActionButtonTheme.backgroundColor
?? theme.colorScheme.secondary;
final Color foregroundColor = this.foregroundColor
?? floatingActionButtonTheme.foregroundColor
?? theme.accentIconTheme.color
?? theme.colorScheme.onSecondary;
final Color backgroundColor = this.backgroundColor
?? floatingActionButtonTheme.backgroundColor
?? theme.colorScheme.secondary;
final Color focusColor = this.focusColor
?? floatingActionButtonTheme.focusColor
?? theme.focusColor;
final Color hoverColor = this.hoverColor
?? floatingActionButtonTheme.hoverColor
?? theme.hoverColor;
final double elevation = this.elevation
?? floatingActionButtonTheme.elevation
?? _defaultElevation;
final double focusElevation = this.focusElevation
?? floatingActionButtonTheme.focusElevation
?? _defaultFocusElevation;
final double hoverElevation = this.hoverElevation
?? floatingActionButtonTheme.hoverElevation
?? _defaultHoverElevation;
final double disabledElevation = this.disabledElevation
?? floatingActionButtonTheme.disabledElevation
?? elevation;
......@@ -366,14 +445,19 @@ class FloatingActionButton extends StatelessWidget {
result = RawMaterialButton(
onPressed: onPressed,
elevation: elevation,
focusElevation: focusElevation,
hoverElevation: hoverElevation,
highlightElevation: highlightElevation,
disabledElevation: disabledElevation,
constraints: _sizeConstraints,
materialTapTargetSize: materialTapTargetSize,
fillColor: backgroundColor,
focusColor: focusColor,
hoverColor: hoverColor,
textStyle: textStyle,
shape: shape,
clipBehavior: clipBehavior,
clipBehavior: clipBehavior ?? Clip.none,
focusNode: focusNode,
child: result,
);
......@@ -395,6 +479,27 @@ class FloatingActionButton extends StatelessWidget {
return result;
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(ObjectFlagProperty<VoidCallback>('onPressed', onPressed, ifNull: 'disabled'));
properties.add(StringProperty('tooltip', tooltip, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('foregroundColor', foregroundColor, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('backgroundColor', backgroundColor, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('focusColor', focusColor, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('hoverColor', hoverColor, defaultValue: null));
properties.add(ObjectFlagProperty<Object>('heroTag', heroTag, ifPresent: 'hero'));
properties.add(DiagnosticsProperty<double>('elevation', elevation, defaultValue: null));
properties.add(DiagnosticsProperty<double>('focusElevation', focusElevation, defaultValue: null));
properties.add(DiagnosticsProperty<double>('hoverElevation', hoverElevation, defaultValue: null));
properties.add(DiagnosticsProperty<double>('highlightElevation', highlightElevation, defaultValue: null));
properties.add(DiagnosticsProperty<double>('disabledElevation', disabledElevation, defaultValue: null));
properties.add(DiagnosticsProperty<ShapeBorder>('shape', shape, defaultValue: null));
properties.add(DiagnosticsProperty<FocusNode>('focusNode', focusNode, defaultValue: null));
properties.add(FlagProperty('isExtended', value: isExtended, ifTrue: 'extended'));
properties.add(DiagnosticsProperty<MaterialTapTargetSize>('materialTapTargetSize', materialTapTargetSize, defaultValue: null));
}
}
// This widget's size matches its child's size unless its constraints
......
......@@ -30,26 +30,49 @@ class FloatingActionButtonThemeData extends Diagnosticable {
/// Creates a theme that can be used for
/// [ThemeData.floatingActionButtonTheme].
const FloatingActionButtonThemeData({
this.backgroundColor,
this.foregroundColor,
this.backgroundColor,
this.focusColor,
this.hoverColor,
this.elevation,
this.focusElevation,
this.hoverElevation,
this.disabledElevation,
this.highlightElevation,
this.shape,
});
/// Color to be used for the unselected, enabled [FloatingActionButton]'s
/// foreground.
final Color foregroundColor;
/// Color to be used for the unselected, enabled [FloatingActionButton]'s
/// background.
final Color backgroundColor;
/// Color to be used for the unselected, enabled [FloatingActionButton]'s
/// foreground.
final Color foregroundColor;
/// The color to use for filling the button when the button has input focus.
final Color focusColor;
/// The color to use for filling the button when the button has a pointer
/// hovering over it.
final Color hoverColor;
/// The z-coordinate to be used for the unselected, enabled
/// [FloatingActionButton]'s elevation foreground.
final double elevation;
/// The z-coordinate at which to place this button relative to its parent when
/// the button has the input focus.
///
/// This controls the size of the shadow below the floating action button.
final double focusElevation;
/// The z-coordinate at which to place this button relative to its parent when
/// the button is enabled and has a pointer hovering over it.
///
/// This controls the size of the shadow below the floating action button.
final double hoverElevation;
/// The z-coordinate to be used for the disabled [FloatingActionButton]'s
/// elevation foreground.
final double disabledElevation;
......@@ -64,17 +87,25 @@ class FloatingActionButtonThemeData extends Diagnosticable {
/// Creates a copy of this object with the given fields replaced with the
/// new values.
FloatingActionButtonThemeData copyWith({
Color backgroundColor,
Color foregroundColor,
Color backgroundColor,
Color focusColor,
Color hoverColor,
double elevation,
double focusElevation,
double hoverElevation,
double disabledElevation,
double highlightElevation,
ShapeBorder shape,
}) {
return FloatingActionButtonThemeData(
backgroundColor: backgroundColor ?? this.backgroundColor,
foregroundColor: foregroundColor ?? this.foregroundColor,
backgroundColor: backgroundColor ?? this.backgroundColor,
focusColor: focusColor ?? this.focusColor,
hoverColor: hoverColor ?? this.hoverColor,
elevation: elevation ?? this.elevation,
focusElevation: focusElevation ?? this.focusElevation,
hoverElevation: hoverElevation ?? this.hoverElevation,
disabledElevation: disabledElevation ?? this.disabledElevation,
highlightElevation: highlightElevation ?? this.highlightElevation,
shape: shape ?? this.shape,
......@@ -91,9 +122,13 @@ class FloatingActionButtonThemeData extends Diagnosticable {
if (a == null && b == null)
return null;
return FloatingActionButtonThemeData(
backgroundColor: Color.lerp(a?.backgroundColor, b?.backgroundColor, t),
foregroundColor: Color.lerp(a?.foregroundColor, b?.foregroundColor, t),
backgroundColor: Color.lerp(a?.backgroundColor, b?.backgroundColor, t),
focusColor: Color.lerp(a?.focusColor, b?.focusColor, t),
hoverColor: Color.lerp(a?.hoverColor, b?.hoverColor, t),
elevation: lerpDouble(a?.elevation, b?.elevation, t),
focusElevation: lerpDouble(a?.focusElevation, b?.focusElevation, t),
hoverElevation: lerpDouble(a?.hoverElevation, b?.hoverElevation, t),
disabledElevation: lerpDouble(a?.disabledElevation, b?.disabledElevation, t),
highlightElevation: lerpDouble(a?.highlightElevation, b?.highlightElevation, t),
shape: ShapeBorder.lerp(a?.shape, b?.shape, t),
......@@ -103,9 +138,13 @@ class FloatingActionButtonThemeData extends Diagnosticable {
@override
int get hashCode {
return hashValues(
backgroundColor,
foregroundColor,
backgroundColor,
focusColor,
hoverColor,
elevation,
focusElevation,
hoverElevation,
disabledElevation,
highlightElevation,
shape,
......@@ -119,9 +158,13 @@ class FloatingActionButtonThemeData extends Diagnosticable {
if (other.runtimeType != runtimeType)
return false;
final FloatingActionButtonThemeData otherData = other;
return otherData.backgroundColor == backgroundColor
&& otherData.foregroundColor == foregroundColor
return otherData.foregroundColor == foregroundColor
&& otherData.backgroundColor == backgroundColor
&& otherData.focusColor == focusColor
&& otherData.hoverColor == hoverColor
&& otherData.elevation == elevation
&& otherData.focusElevation == focusElevation
&& otherData.hoverElevation == hoverElevation
&& otherData.disabledElevation == disabledElevation
&& otherData.highlightElevation == highlightElevation
&& otherData.shape == shape;
......@@ -132,9 +175,13 @@ class FloatingActionButtonThemeData extends Diagnosticable {
super.debugFillProperties(properties);
const FloatingActionButtonThemeData defaultData = FloatingActionButtonThemeData();
properties.add(DiagnosticsProperty<Color>('backgroundColor', backgroundColor, defaultValue: defaultData.backgroundColor));
properties.add(DiagnosticsProperty<Color>('foregroundColor', foregroundColor, defaultValue: defaultData.foregroundColor));
properties.add(DiagnosticsProperty<Color>('backgroundColor', backgroundColor, defaultValue: defaultData.backgroundColor));
properties.add(DiagnosticsProperty<Color>('focusColor', focusColor, defaultValue: defaultData.focusColor));
properties.add(DiagnosticsProperty<Color>('hoverColor', hoverColor, defaultValue: defaultData.hoverColor));
properties.add(DiagnosticsProperty<double>('elevation', elevation, defaultValue: defaultData.elevation));
properties.add(DiagnosticsProperty<double>('focusElevation', focusElevation, defaultValue: defaultData.focusElevation));
properties.add(DiagnosticsProperty<double>('hoverElevation', hoverElevation, defaultValue: defaultData.hoverElevation));
properties.add(DiagnosticsProperty<double>('disabledElevation', disabledElevation, defaultValue: defaultData.disabledElevation));
properties.add(DiagnosticsProperty<double>('highlightElevation', highlightElevation, defaultValue: defaultData.highlightElevation));
properties.add(DiagnosticsProperty<ShapeBorder>('shape', shape, defaultValue: defaultData.shape));
......
......@@ -142,10 +142,13 @@ class IconButton extends StatelessWidget {
this.alignment = Alignment.center,
@required this.icon,
this.color,
this.focusColor,
this.hoverColor,
this.highlightColor,
this.splashColor,
this.disabledColor,
@required this.onPressed,
this.focusNode,
this.tooltip,
}) : assert(iconSize != null),
assert(padding != null),
......@@ -195,6 +198,16 @@ class IconButton extends StatelessWidget {
/// See [Icon], [ImageIcon].
final Widget icon;
/// The color for the button's icon when it has the input focus.
///
/// Defaults to [ThemeData.focusColor] of the ambient theme.
final Color focusColor;
/// The color for the button's icon when a pointer is hovering over it.
///
/// Defaults to [ThemeData.hoverColor] of the ambient theme.
final Color hoverColor;
/// The color to use for the icon inside the button, if the icon is enabled.
/// Defaults to leaving this up to the [icon] widget.
///
......@@ -242,6 +255,14 @@ class IconButton extends StatelessWidget {
/// If this is set to null, the button will be disabled.
final VoidCallback onPressed;
/// An optional focus node to use for requesting focus when pressed.
///
/// If not supplied, the button will create and host its own [FocusNode].
///
/// If supplied, the given focusNode will be _hosted_ by this widget. See
/// [FocusNode] for more information on what that implies.
final FocusNode focusNode;
/// Text that describes the action that will occur when the button is pressed.
///
/// This text is displayed when the user long-presses on the button and is
......@@ -257,25 +278,21 @@ class IconButton extends StatelessWidget {
else
currentColor = disabledColor ?? Theme.of(context).disabledColor;
Widget result = Semantics(
button: true,
enabled: onPressed != null,
child: ConstrainedBox(
constraints: const BoxConstraints(minWidth: _kMinButtonSize, minHeight: _kMinButtonSize),
child: Padding(
padding: padding,
child: SizedBox(
height: iconSize,
width: iconSize,
child: Align(
alignment: alignment,
child: IconTheme.merge(
data: IconThemeData(
size: iconSize,
color: currentColor,
),
child: icon,
Widget result = ConstrainedBox(
constraints: const BoxConstraints(minWidth: _kMinButtonSize, minHeight: _kMinButtonSize),
child: Padding(
padding: padding,
child: SizedBox(
height: iconSize,
width: iconSize,
child: Align(
alignment: alignment,
child: IconTheme.merge(
data: IconThemeData(
size: iconSize,
color: currentColor,
),
child: icon,
),
),
),
......@@ -288,15 +305,25 @@ class IconButton extends StatelessWidget {
child: result,
);
}
return InkResponse(
onTap: onPressed,
child: result,
highlightColor: highlightColor ?? Theme.of(context).highlightColor,
splashColor: splashColor ?? Theme.of(context).splashColor,
radius: math.max(
Material.defaultSplashRadius,
(iconSize + math.min(padding.horizontal, padding.vertical)) * 0.7,
// x 0.5 for diameter -> radius and + 40% overflow derived from other Material apps.
return Semantics(
button: true,
enabled: onPressed != null,
child: Focus(
focusNode: focusNode,
child: InkResponse(
onTap: onPressed,
child: result,
focusColor: focusColor ?? Theme.of(context).focusColor,
hoverColor: hoverColor ?? Theme.of(context).hoverColor,
highlightColor: highlightColor ?? Theme.of(context).highlightColor,
splashColor: splashColor ?? Theme.of(context).splashColor,
radius: math.max(
Material.defaultSplashRadius,
(iconSize + math.min(padding.horizontal, padding.vertical)) * 0.7,
// x 0.5 for diameter -> radius and + 40% overflow derived from other Material apps.
),
),
),
);
}
......@@ -305,7 +332,15 @@ class IconButton extends StatelessWidget {
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<Widget>('icon', icon, showName: false));
properties.add(ObjectFlagProperty<VoidCallback>('onPressed', onPressed, ifNull: 'disabled'));
properties.add(StringProperty('tooltip', tooltip, defaultValue: null, quoted: false));
properties.add(ObjectFlagProperty<VoidCallback>('onPressed', onPressed, ifNull: 'disabled'));
properties.add(DiagnosticsProperty<Color>('color', color, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('disabledColor', disabledColor, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('focusColor', focusColor, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('hoverColor', hoverColor, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('highlightColor', highlightColor, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('splashColor', splashColor, defaultValue: null));
properties.add(DiagnosticsProperty<EdgeInsetsGeometry>('padding', padding, defaultValue: null));
properties.add(DiagnosticsProperty<FocusNode>('focusNode', focusNode, defaultValue: null));
}
}
......@@ -8,7 +8,7 @@ import 'package:flutter/widgets.dart';
import 'ink_well.dart' show InteractiveInkFeature;
import 'material.dart';
const Duration _kHighlightFadeDuration = Duration(milliseconds: 200);
const Duration _kDefaultHighlightFadeDuration = Duration(milliseconds: 200);
/// A visual emphasis on a part of a [Material] receiving user interaction.
///
......@@ -45,16 +45,18 @@ class InkHighlight extends InteractiveInkFeature {
ShapeBorder customBorder,
RectCallback rectCallback,
VoidCallback onRemoved,
Duration fadeDuration = _kDefaultHighlightFadeDuration,
}) : assert(color != null),
assert(shape != null),
assert(textDirection != null),
assert(fadeDuration != null),
_shape = shape,
_borderRadius = borderRadius ?? BorderRadius.zero,
_customBorder = customBorder,
_textDirection = textDirection,
_rectCallback = rectCallback,
super(controller: controller, referenceBox: referenceBox, color: color, onRemoved: onRemoved) {
_alphaController = AnimationController(duration: _kHighlightFadeDuration, vsync: controller.vsync)
_alphaController = AnimationController(duration: fadeDuration, vsync: controller.vsync)
..addListener(controller.markNeedsPaint)
..addStatusListener(_handleAlphaStatusChanged)
..forward();
......
......@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
......@@ -50,15 +52,20 @@ class MaterialButton extends StatelessWidget {
this.disabledTextColor,
this.color,
this.disabledColor,
this.focusColor,
this.hoverColor,
this.highlightColor,
this.splashColor,
this.colorBrightness,
this.elevation,
this.focusElevation,
this.hoverElevation,
this.highlightElevation,
this.disabledElevation,
this.padding,
this.shape,
this.clipBehavior = Clip.none,
this.focusNode,
this.materialTapTargetSize,
this.animationDuration,
this.minWidth,
......@@ -145,6 +152,19 @@ class MaterialButton extends StatelessWidget {
/// factory, [ThemeData.splashFactory].
final Color splashColor;
/// The fill color of the button's [Material] when it has the input focus.
///
/// The button changed focus color when the button has the input focus. It
/// appears behind the button's child.
final Color focusColor;
/// The fill color of the button's [Material] when a pointer is hovering over
/// it.
///
/// The button changes fill color when a pointer is hovering over the button.
/// It appears behind the button's child.
final Color hoverColor;
/// The highlight color of the button's [InkWell].
///
/// The highlight indicates that the button is actively being pressed. It
......@@ -166,10 +186,40 @@ class MaterialButton extends StatelessWidget {
/// See also:
///
/// * [FlatButton], a button with no elevation or fill color.
/// * [focusElevation], the elevation when the button is focused.
/// * [hoverElevation], the elevation when a pointer is hovering over the
/// button.
/// * [disabledElevation], the elevation when the button is disabled.
/// * [highlightElevation], the elevation when the button is pressed.
final double elevation;
/// The elevation for the button's [Material] when the button
/// is [enabled] and a pointer is hovering over it.
///
/// Defaults to 4.0. The value is always non-negative.
///
/// See also:
///
/// * [elevation], the default elevation.
/// * [focusElevation], the elevation when the button is focused.
/// * [disabledElevation], the elevation when the button is disabled.
/// * [highlightElevation], the elevation when the button is pressed.
final double hoverElevation;
/// The elevation for the button's [Material] when the button
/// is [enabled] and has the input focus.
///
/// Defaults to 4.0. The value is always non-negative.
///
/// See also:
///
/// * [elevation], the default elevation.
/// * [hoverElevation], the elevation when a pointer is hovering over the
/// button.
/// * [disabledElevation], the elevation when the button is disabled.
/// * [highlightElevation], the elevation when the button is pressed.
final double focusElevation;
/// The elevation for the button's [Material] relative to its parent when the
/// button is [enabled] and pressed.
///
......@@ -182,6 +232,9 @@ class MaterialButton extends StatelessWidget {
/// See also:
///
/// * [elevation], the default elevation.
/// * [focusElevation], the elevation when the button is focused.
/// * [hoverElevation], the elevation when a pointer is hovering over the
/// button.
/// * [disabledElevation], the elevation when the button is disabled.
final double highlightElevation;
......@@ -231,6 +284,14 @@ class MaterialButton extends StatelessWidget {
/// {@macro flutter.widgets.Clip}
final Clip clipBehavior;
/// An optional focus node to use for requesting focus when pressed.
///
/// If not supplied, the button will create and host its own [FocusNode].
///
/// If supplied, the given focusNode will be _hosted_ by this widget. See
/// [FocusNode] for more information on what that implies.
final FocusNode focusNode;
/// Defines the duration of animated changes for [shape] and [elevation].
///
/// The default value is [kThemeChangeDuration].
......@@ -265,9 +326,13 @@ class MaterialButton extends StatelessWidget {
onHighlightChanged: onHighlightChanged,
fillColor: buttonTheme.getFillColor(this),
textStyle: theme.textTheme.button.copyWith(color: buttonTheme.getTextColor(this)),
focusColor: focusColor ?? buttonTheme.getFocusColor(this) ?? theme.focusColor,
hoverColor: hoverColor ?? buttonTheme.getHoverColor(this) ?? theme.hoverColor,
highlightColor: highlightColor ?? theme.highlightColor,
splashColor: splashColor ?? theme.splashColor,
elevation: buttonTheme.getElevation(this),
focusElevation: buttonTheme.getFocusElevation(this),
hoverElevation: buttonTheme.getHoverElevation(this),
highlightElevation: buttonTheme.getHighlightElevation(this),
padding: buttonTheme.getPadding(this),
constraints: buttonTheme.getConstraints(this).copyWith(
......@@ -276,6 +341,7 @@ class MaterialButton extends StatelessWidget {
),
shape: buttonTheme.getShape(this),
clipBehavior: clipBehavior ?? Clip.none,
focusNode: focusNode,
animationDuration: buttonTheme.getAnimationDuration(this),
child: child,
materialTapTargetSize: materialTapTargetSize ?? theme.materialTapTargetSize,
......@@ -285,7 +351,21 @@ class MaterialButton extends StatelessWidget {
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(FlagProperty('enabled', value: enabled, ifFalse: 'disabled'));
properties.add(ObjectFlagProperty<VoidCallback>('onPressed', onPressed, ifNull: 'disabled'));
properties.add(DiagnosticsProperty<ButtonTextTheme>('textTheme', textTheme, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('textColor', textColor, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('disabledTextColor', disabledTextColor, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('color', color, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('disabledColor', disabledColor, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('focusColor', focusColor, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('hoverColor', hoverColor, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('highlightColor', highlightColor, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('splashColor', splashColor, defaultValue: null));
properties.add(DiagnosticsProperty<Brightness>('colorBrightness', colorBrightness, defaultValue: null));
properties.add(DiagnosticsProperty<EdgeInsetsGeometry>('padding', padding, defaultValue: null));
properties.add(DiagnosticsProperty<ShapeBorder>('shape', shape, defaultValue: null));
properties.add(DiagnosticsProperty<FocusNode>('focusNode', focusNode, defaultValue: null));
properties.add(DiagnosticsProperty<MaterialTapTargetSize>('materialTapTargetSize', materialTapTargetSize, defaultValue: null));
}
}
......
......@@ -64,6 +64,8 @@ class OutlineButton extends MaterialButton {
Color textColor,
Color disabledTextColor,
Color color,
Color focusColor,
Color hoverColor,
Color highlightColor,
Color splashColor,
double highlightElevation,
......@@ -72,7 +74,8 @@ class OutlineButton extends MaterialButton {
this.highlightedBorderColor,
EdgeInsetsGeometry padding,
ShapeBorder shape,
Clip clipBehavior = Clip.none,
Clip clipBehavior,
FocusNode focusNode,
Widget child,
}) : assert(highlightElevation == null || highlightElevation >= 0.0),
super(
......@@ -82,12 +85,15 @@ class OutlineButton extends MaterialButton {
textColor: textColor,
disabledTextColor: disabledTextColor,
color: color,
focusColor: focusColor,
hoverColor: hoverColor,
highlightColor: highlightColor,
splashColor: splashColor,
highlightElevation: highlightElevation,
padding: padding,
shape: shape,
clipBehavior: clipBehavior,
focusNode: focusNode,
child: child,
);
......@@ -106,6 +112,8 @@ class OutlineButton extends MaterialButton {
Color textColor,
Color disabledTextColor,
Color color,
Color focusColor,
Color hoverColor,
Color highlightColor,
Color splashColor,
double highlightElevation,
......@@ -115,6 +123,7 @@ class OutlineButton extends MaterialButton {
EdgeInsetsGeometry padding,
ShapeBorder shape,
Clip clipBehavior,
FocusNode focusNode,
@required Widget icon,
@required Widget label,
}) = _OutlineButtonWithIcon;
......@@ -151,6 +160,8 @@ class OutlineButton extends MaterialButton {
textColor: buttonTheme.getTextColor(this),
disabledTextColor: buttonTheme.getDisabledTextColor(this),
color: color,
focusColor: buttonTheme.getFocusColor(this),
hoverColor: buttonTheme.getHoverColor(this),
highlightColor: buttonTheme.getHighlightColor(this),
splashColor: buttonTheme.getSplashColor(this),
highlightElevation: buttonTheme.getHighlightElevation(this),
......@@ -160,6 +171,7 @@ class OutlineButton extends MaterialButton {
padding: buttonTheme.getPadding(this),
shape: buttonTheme.getShape(this),
clipBehavior: clipBehavior,
focusNode: focusNode,
child: child,
);
}
......@@ -167,19 +179,9 @@ class OutlineButton extends MaterialButton {
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(ObjectFlagProperty<VoidCallback>('onPressed', onPressed, ifNull: 'disabled'));
properties.add(DiagnosticsProperty<ButtonTextTheme>('textTheme', textTheme, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('textColor', textColor, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('disabledTextColor', disabledTextColor, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('color', color, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('highlightColor', highlightColor, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('splashColor', splashColor, defaultValue: null));
properties.add(DiagnosticsProperty<double>('highlightElevation', highlightElevation, defaultValue: null));
properties.add(DiagnosticsProperty<BorderSide>('borderSide', borderSide, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('disabledBorderColor', disabledBorderColor, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('highlightedBorderColor', highlightedBorderColor, defaultValue: null));
properties.add(DiagnosticsProperty<EdgeInsetsGeometry>('padding', padding, defaultValue: null));
properties.add(DiagnosticsProperty<ShapeBorder>('shape', shape, defaultValue: null));
}
}
......@@ -195,6 +197,8 @@ class _OutlineButtonWithIcon extends OutlineButton with MaterialButtonWithIconMi
Color textColor,
Color disabledTextColor,
Color color,
Color focusColor,
Color hoverColor,
Color highlightColor,
Color splashColor,
double highlightElevation,
......@@ -204,6 +208,7 @@ class _OutlineButtonWithIcon extends OutlineButton with MaterialButtonWithIconMi
EdgeInsetsGeometry padding,
ShapeBorder shape,
Clip clipBehavior,
FocusNode focusNode,
@required Widget icon,
@required Widget label,
}) : assert(highlightElevation == null || highlightElevation >= 0.0),
......@@ -216,6 +221,8 @@ class _OutlineButtonWithIcon extends OutlineButton with MaterialButtonWithIconMi
textColor: textColor,
disabledTextColor: disabledTextColor,
color: color,
focusColor: focusColor,
hoverColor: hoverColor,
highlightColor: highlightColor,
splashColor: splashColor,
highlightElevation: highlightElevation,
......@@ -225,6 +232,7 @@ class _OutlineButtonWithIcon extends OutlineButton with MaterialButtonWithIconMi
padding: padding,
shape: shape,
clipBehavior: clipBehavior,
focusNode: focusNode,
child: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
......@@ -245,6 +253,8 @@ class _OutlineButton extends StatefulWidget {
this.textColor,
this.disabledTextColor,
this.color,
this.focusColor,
this.hoverColor,
this.highlightColor,
this.splashColor,
@required this.highlightElevation,
......@@ -254,6 +264,7 @@ class _OutlineButton extends StatefulWidget {
this.padding,
this.shape,
this.clipBehavior,
this.focusNode,
this.child,
}) : assert(highlightElevation != null && highlightElevation >= 0.0),
assert(highlightedBorderColor != null),
......@@ -266,6 +277,8 @@ class _OutlineButton extends StatefulWidget {
final Color disabledTextColor;
final Color color;
final Color splashColor;
final Color focusColor;
final Color hoverColor;
final Color highlightColor;
final double highlightElevation;
final BorderSide borderSide;
......@@ -274,6 +287,7 @@ class _OutlineButton extends StatefulWidget {
final EdgeInsetsGeometry padding;
final ShapeBorder shape;
final Clip clipBehavior;
final FocusNode focusNode;
final Widget child;
bool get enabled => onPressed != null;
......@@ -391,11 +405,15 @@ class _OutlineButtonState extends State<_OutlineButton> with SingleTickerProvide
disabledTextColor: widget.disabledTextColor,
color: _getFillColor(),
splashColor: widget.splashColor,
focusColor: widget.focusColor,
hoverColor: widget.hoverColor,
highlightColor: widget.highlightColor,
disabledColor: Colors.transparent,
onPressed: widget.onPressed,
elevation: 0.0,
disabledElevation: 0.0,
focusElevation: 0.0,
hoverElevation: 0.0,
highlightElevation: _getHighlightElevation(),
onHighlightChanged: _handleHighlightChanged,
padding: widget.padding,
......@@ -404,6 +422,7 @@ class _OutlineButtonState extends State<_OutlineButton> with SingleTickerProvide
side: _getOutline(),
),
clipBehavior: widget.clipBehavior,
focusNode: widget.focusNode,
animationDuration: _kElevationDuration,
child: widget.child,
);
......
......@@ -113,19 +113,26 @@ class RaisedButton extends MaterialButton {
Color disabledTextColor,
Color color,
Color disabledColor,
Color focusColor,
Color hoverColor,
Color highlightColor,
Color splashColor,
Brightness colorBrightness,
double elevation,
double focusElevation,
double hoverElevation,
double highlightElevation,
double disabledElevation,
EdgeInsetsGeometry padding,
ShapeBorder shape,
Clip clipBehavior = Clip.none,
Clip clipBehavior,
FocusNode focusNode,
MaterialTapTargetSize materialTapTargetSize,
Duration animationDuration,
Widget child,
}) : assert(elevation == null || elevation >= 0.0),
assert(focusElevation == null || focusElevation >= 0.0),
assert(hoverElevation == null || hoverElevation >= 0.0),
assert(highlightElevation == null || highlightElevation >= 0.0),
assert(disabledElevation == null || disabledElevation >= 0.0),
super(
......@@ -137,15 +144,20 @@ class RaisedButton extends MaterialButton {
disabledTextColor: disabledTextColor,
color: color,
disabledColor: disabledColor,
focusColor: focusColor,
hoverColor: hoverColor,
highlightColor: highlightColor,
splashColor: splashColor,
colorBrightness: colorBrightness,
elevation: elevation,
focusElevation: focusElevation,
hoverElevation: hoverElevation,
highlightElevation: highlightElevation,
disabledElevation: disabledElevation,
padding: padding,
shape: shape,
clipBehavior: clipBehavior,
focusNode: focusNode,
materialTapTargetSize: materialTapTargetSize,
animationDuration: animationDuration,
child: child,
......@@ -168,6 +180,8 @@ class RaisedButton extends MaterialButton {
Color disabledTextColor,
Color color,
Color disabledColor,
Color focusColor,
Color hoverColor,
Color highlightColor,
Color splashColor,
Brightness colorBrightness,
......@@ -176,6 +190,7 @@ class RaisedButton extends MaterialButton {
double disabledElevation,
ShapeBorder shape,
Clip clipBehavior,
FocusNode focusNode,
MaterialTapTargetSize materialTapTargetSize,
Duration animationDuration,
@required Widget icon,
......@@ -192,14 +207,19 @@ class RaisedButton extends MaterialButton {
clipBehavior: clipBehavior ?? Clip.none,
fillColor: buttonTheme.getFillColor(this),
textStyle: theme.textTheme.button.copyWith(color: buttonTheme.getTextColor(this)),
focusColor: buttonTheme.getFocusColor(this),
hoverColor: buttonTheme.getHoverColor(this),
highlightColor: buttonTheme.getHighlightColor(this),
splashColor: buttonTheme.getSplashColor(this),
elevation: buttonTheme.getElevation(this),
focusElevation: buttonTheme.getFocusElevation(this),
hoverElevation: buttonTheme.getHoverElevation(this),
highlightElevation: buttonTheme.getHighlightElevation(this),
disabledElevation: buttonTheme.getDisabledElevation(this),
padding: buttonTheme.getPadding(this),
constraints: buttonTheme.getConstraints(this),
shape: buttonTheme.getShape(this),
focusNode: focusNode,
animationDuration: buttonTheme.getAnimationDuration(this),
materialTapTargetSize: buttonTheme.getMaterialTapTargetSize(this),
child: child,
......@@ -209,19 +229,11 @@ class RaisedButton extends MaterialButton {
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(ObjectFlagProperty<VoidCallback>('onPressed', onPressed, ifNull: 'disabled'));
properties.add(DiagnosticsProperty<Color>('textColor', textColor, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('disabledTextColor', disabledTextColor, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('color', color, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('disabledColor', disabledColor, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('highlightColor', highlightColor, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('splashColor', splashColor, defaultValue: null));
properties.add(DiagnosticsProperty<Brightness>('colorBrightness', colorBrightness, defaultValue: null));
properties.add(DiagnosticsProperty<double>('elevation', elevation, defaultValue: null));
properties.add(DiagnosticsProperty<double>('focusElevation', focusElevation, defaultValue: null));
properties.add(DiagnosticsProperty<double>('hoverElevation', hoverElevation, defaultValue: null));
properties.add(DiagnosticsProperty<double>('highlightElevation', highlightElevation, defaultValue: null));
properties.add(DiagnosticsProperty<double>('disabledElevation', disabledElevation, defaultValue: null));
properties.add(DiagnosticsProperty<EdgeInsetsGeometry>('padding', padding, defaultValue: null));
properties.add(DiagnosticsProperty<ShapeBorder>('shape', shape, defaultValue: null));
}
}
......@@ -239,6 +251,8 @@ class _RaisedButtonWithIcon extends RaisedButton with MaterialButtonWithIconMixi
Color disabledTextColor,
Color color,
Color disabledColor,
Color focusColor,
Color hoverColor,
Color highlightColor,
Color splashColor,
Brightness colorBrightness,
......@@ -247,6 +261,7 @@ class _RaisedButtonWithIcon extends RaisedButton with MaterialButtonWithIconMixi
double disabledElevation,
ShapeBorder shape,
Clip clipBehavior = Clip.none,
FocusNode focusNode,
MaterialTapTargetSize materialTapTargetSize,
Duration animationDuration,
@required Widget icon,
......@@ -265,6 +280,8 @@ class _RaisedButtonWithIcon extends RaisedButton with MaterialButtonWithIconMixi
disabledTextColor: disabledTextColor,
color: color,
disabledColor: disabledColor,
focusColor: focusColor,
hoverColor: hoverColor,
highlightColor: highlightColor,
splashColor: splashColor,
colorBrightness: colorBrightness,
......@@ -273,6 +290,7 @@ class _RaisedButtonWithIcon extends RaisedButton with MaterialButtonWithIconMixi
disabledElevation: disabledElevation,
shape: shape,
clipBehavior: clipBehavior,
focusNode: focusNode,
materialTapTargetSize: materialTapTargetSize,
animationDuration: animationDuration,
child: Row(
......
......@@ -355,7 +355,7 @@ class _SearchPageState<T> extends State<_SearchPage<T>> {
@override
void initState() {
super.initState();
queryTextController.addListener(_onQueryChanged);
widget.delegate._queryTextController.addListener(_onQueryChanged);
widget.animation.addStatusListener(_onAnimationStatusChanged);
widget.delegate._currentBodyNotifier.addListener(_onSearchBodyChanged);
focusNode.addListener(_onFocusChanged);
......@@ -365,7 +365,7 @@ class _SearchPageState<T> extends State<_SearchPage<T>> {
@override
void dispose() {
super.dispose();
queryTextController.removeListener(_onQueryChanged);
widget.delegate._queryTextController.removeListener(_onQueryChanged);
widget.animation.removeStatusListener(_onAnimationStatusChanged);
widget.delegate._currentBodyNotifier.removeListener(_onSearchBodyChanged);
widget.delegate._focusNode = null;
......@@ -382,6 +382,19 @@ class _SearchPageState<T> extends State<_SearchPage<T>> {
}
}
@override
void didUpdateWidget(_SearchPage<T> oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.delegate != oldWidget.delegate) {
oldWidget.delegate._queryTextController.removeListener(_onQueryChanged);
widget.delegate._queryTextController.addListener(_onQueryChanged);
oldWidget.delegate._currentBodyNotifier.removeListener(_onSearchBodyChanged);
widget.delegate._currentBodyNotifier.addListener(_onSearchBodyChanged);
oldWidget.delegate._focusNode = null;
widget.delegate._focusNode = focusNode;
}
}
void _onFocusChanged() {
if (focusNode.hasFocus && widget.delegate._currentBody != _SearchBody.suggestions) {
widget.delegate.showSuggestions(context);
......@@ -443,7 +456,7 @@ class _SearchPageState<T> extends State<_SearchPage<T>> {
brightness: theme.primaryColorBrightness,
leading: widget.delegate.buildLeading(context),
title: TextField(
controller: queryTextController,
controller: widget.delegate._queryTextController,
focusNode: focusNode,
style: theme.textTheme.title,
textInputAction: TextInputAction.search,
......@@ -464,6 +477,4 @@ class _SearchPageState<T> extends State<_SearchPage<T>> {
),
);
}
TextEditingController get queryTextController => widget.delegate._queryTextController;
}
......@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:ui' show Color, hashValues;
import 'dart:ui' show Color, hashList;
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
......@@ -125,6 +125,8 @@ class ThemeData extends Diagnosticable {
Color bottomAppBarColor,
Color cardColor,
Color dividerColor,
Color focusColor,
Color hoverColor,
Color highlightColor,
Color splashColor,
InteractiveInkFeatureFactory splashFactory,
......@@ -204,7 +206,7 @@ class ThemeData extends Diagnosticable {
// Spec doesn't specify a dark theme secondaryHeaderColor, this is a guess.
secondaryHeaderColor ??= isDark ? Colors.grey[700] : primarySwatch[50];
textSelectionColor ??= isDark ? accentColor : primarySwatch[200];
// todo (sandrasandeep): change to color provided by Material Design team
// TODO(sandrasandeep): change to color provided by Material Design team
cursorColor = cursorColor ?? const Color.fromRGBO(66, 133, 244, 1.0);
textSelectionHandleColor ??= isDark ? Colors.tealAccent[400] : primarySwatch[300];
backgroundColor ??= isDark ? Colors.grey[700] : primarySwatch[200];
......@@ -235,10 +237,14 @@ class ThemeData extends Diagnosticable {
// Used as the default color (fill color) for RaisedButtons. Computing the
// default for ButtonThemeData for the sake of backwards compatibility.
buttonColor ??= isDark ? primarySwatch[600] : Colors.grey[300];
focusColor ??= buttonColor;
hoverColor ??= buttonColor;
buttonTheme ??= ButtonThemeData(
colorScheme: colorScheme,
buttonColor: buttonColor,
disabledColor: disabledColor,
focusColor: focusColor,
hoverColor: hoverColor,
highlightColor: highlightColor,
splashColor: splashColor,
materialTapTargetSize: materialTapTargetSize,
......@@ -276,6 +282,8 @@ class ThemeData extends Diagnosticable {
bottomAppBarColor: bottomAppBarColor,
cardColor: cardColor,
dividerColor: dividerColor,
focusColor: focusColor,
hoverColor: hoverColor,
highlightColor: highlightColor,
splashColor: splashColor,
splashFactory: splashFactory,
......@@ -343,6 +351,8 @@ class ThemeData extends Diagnosticable {
@required this.bottomAppBarColor,
@required this.cardColor,
@required this.dividerColor,
@required this.focusColor,
@required this.hoverColor,
@required this.highlightColor,
@required this.splashColor,
@required this.splashFactory,
......@@ -396,6 +406,8 @@ class ThemeData extends Diagnosticable {
assert(bottomAppBarColor != null),
assert(cardColor != null),
assert(dividerColor != null),
assert(focusColor != null),
assert(hoverColor != null),
assert(highlightColor != null),
assert(splashColor != null),
assert(splashFactory != null),
......@@ -530,6 +542,13 @@ class ThemeData extends Diagnosticable {
/// [Divider.createBorderSide].
final Color dividerColor;
/// The focus color used indicate that a component has the input focus.
final Color focusColor;
/// The hover color used to indicate when a pointer is hovering over a
/// component.
final Color hoverColor;
/// The highlight color used during ink splash animations or to
/// indicate an item in a menu is selected.
final Color highlightColor;
......@@ -727,6 +746,8 @@ class ThemeData extends Diagnosticable {
Color bottomAppBarColor,
Color cardColor,
Color dividerColor,
Color focusColor,
Color hoverColor,
Color highlightColor,
Color splashColor,
InteractiveInkFeatureFactory splashFactory,
......@@ -783,6 +804,8 @@ class ThemeData extends Diagnosticable {
bottomAppBarColor: bottomAppBarColor ?? this.bottomAppBarColor,
cardColor: cardColor ?? this.cardColor,
dividerColor: dividerColor ?? this.dividerColor,
focusColor: focusColor ?? this.focusColor,
hoverColor: hoverColor ?? this.hoverColor,
highlightColor: highlightColor ?? this.highlightColor,
splashColor: splashColor ?? this.splashColor,
splashFactory: splashFactory ?? this.splashFactory,
......@@ -917,6 +940,8 @@ class ThemeData extends Diagnosticable {
bottomAppBarColor: Color.lerp(a.bottomAppBarColor, b.bottomAppBarColor, t),
cardColor: Color.lerp(a.cardColor, b.cardColor, t),
dividerColor: Color.lerp(a.dividerColor, b.dividerColor, t),
focusColor: Color.lerp(a.focusColor, b.focusColor, t),
hoverColor: Color.lerp(a.hoverColor, b.hoverColor, t),
highlightColor: Color.lerp(a.highlightColor, b.highlightColor, t),
splashColor: Color.lerp(a.splashColor, b.splashColor, t),
splashFactory: t < 0.5 ? a.splashFactory : b.splashFactory,
......@@ -1026,11 +1051,10 @@ class ThemeData extends Diagnosticable {
@override
int get hashCode {
// The hashValues() function supports up to 20 arguments.
return hashValues(
// Warning: make sure these properties are in the exact same order as in
// operator == and in the raw constructor and in the order of fields in
// the class and in the lerp() method.
// Warning: For the sanity of the reader, please make sure these properties
// are in the exact same order as in operator == and in the raw constructor
// and in the order of fields in the class and in the lerp() method.
final List<Object> values = <Object>[
brightness,
primaryColor,
primaryColorBrightness,
......@@ -1043,6 +1067,8 @@ class ThemeData extends Diagnosticable {
bottomAppBarColor,
cardColor,
dividerColor,
focusColor,
hoverColor,
highlightColor,
splashColor,
splashFactory,
......@@ -1050,45 +1076,42 @@ class ThemeData extends Diagnosticable {
unselectedWidgetColor,
disabledColor,
buttonTheme,
hashValues(
buttonColor,
toggleableActiveColor,
secondaryHeaderColor,
textSelectionColor,
cursorColor,
textSelectionHandleColor,
backgroundColor,
dialogBackgroundColor,
indicatorColor,
hintColor,
errorColor,
textTheme,
primaryTextTheme,
accentTextTheme,
inputDecorationTheme,
iconTheme,
primaryIconTheme,
accentIconTheme,
sliderTheme,
hashValues(
tabBarTheme,
cardTheme,
chipTheme,
platform,
materialTapTargetSize,
pageTransitionsTheme,
appBarTheme,
bottomAppBarTheme,
colorScheme,
dialogTheme,
floatingActionButtonTheme,
typography,
cupertinoOverrideTheme,
snackBarTheme,
bottomSheetTheme,
),
),
);
buttonColor,
toggleableActiveColor,
secondaryHeaderColor,
textSelectionColor,
cursorColor,
textSelectionHandleColor,
backgroundColor,
dialogBackgroundColor,
indicatorColor,
hintColor,
errorColor,
textTheme,
primaryTextTheme,
accentTextTheme,
inputDecorationTheme,
iconTheme,
primaryIconTheme,
accentIconTheme,
sliderTheme,
tabBarTheme,
cardTheme,
chipTheme,
platform,
materialTapTargetSize,
pageTransitionsTheme,
appBarTheme,
bottomAppBarTheme,
colorScheme,
dialogTheme,
floatingActionButtonTheme,
typography,
cupertinoOverrideTheme,
snackBarTheme,
bottomSheetTheme,
];
return hashList(values);
}
@override
......@@ -1106,6 +1129,8 @@ class ThemeData extends Diagnosticable {
properties.add(DiagnosticsProperty<Color>('bottomAppBarColor', bottomAppBarColor, defaultValue: defaultData.bottomAppBarColor));
properties.add(DiagnosticsProperty<Color>('cardColor', cardColor, defaultValue: defaultData.cardColor));
properties.add(DiagnosticsProperty<Color>('dividerColor', dividerColor, defaultValue: defaultData.dividerColor));
properties.add(DiagnosticsProperty<Color>('focusColor', focusColor, defaultValue: defaultData.focusColor));
properties.add(DiagnosticsProperty<Color>('hoverColor', hoverColor, defaultValue: defaultData.hoverColor));
properties.add(DiagnosticsProperty<Color>('highlightColor', highlightColor, defaultValue: defaultData.highlightColor));
properties.add(DiagnosticsProperty<Color>('splashColor', splashColor, defaultValue: defaultData.splashColor));
properties.add(DiagnosticsProperty<Color>('selectedRowColor', selectedRowColor, defaultValue: defaultData.selectedRowColor));
......
......@@ -340,11 +340,21 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
FocusNode({
String debugLabel,
FocusOnKeyCallback onKey,
}) : _onKey = onKey {
this.skipTraversal = false,
}) : assert(skipTraversal != null),
_onKey = onKey {
// Set it via the setter so that it does nothing on release builds.
this.debugLabel = debugLabel;
}
/// If true, tells the focus traversal policy to skip over this node for
/// purposes of the traversal algorithm.
///
/// This may be used to place nodes in the focus tree that may be focused, but
/// not traversed, allowing them to receive key events as part of the focus
/// chain, but not be traversed to via focus traversal.
bool skipTraversal;
/// The context that was supplied to [attach].
///
/// This is typically the context for the widget that is being focused, as it
......@@ -374,6 +384,10 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
Iterable<FocusNode> get children => _children;
final List<FocusNode> _children = <FocusNode>[];
/// An iterator over the children that are allowed to be traversed by the
/// [FocusTraversalPolicy].
Iterable<FocusNode> get traversalChildren => children.where((FocusNode node) => !node.skipTraversal);
/// A debug label that is used for diagnostic output.
///
/// Will always return null in release builds.
......@@ -398,6 +412,9 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
}
}
/// Returns all descendants which do not have the [skipTraversal] flag set.
Iterable<FocusNode> get traversalDescendants => descendants.where((FocusNode node) => !node.skipTraversal);
/// An [Iterable] over the ancestors of this node.
///
/// Iterates the ancestors of this node starting at the parent and iterating
......
......@@ -93,13 +93,9 @@ import 'inherited_notifier.dart';
/// return GestureDetector(
/// onTap: () {
/// if (hasFocus) {
/// setState(() {
/// focusNode.unfocus();
/// });
/// focusNode.unfocus();
/// } else {
/// setState(() {
/// focusNode.requestFocus();
/// });
/// focusNode.requestFocus();
/// }
/// },
/// child: Center(
......@@ -150,8 +146,10 @@ class Focus extends StatefulWidget {
this.onFocusChange,
this.onKey,
this.debugLabel,
this.skipTraversal = false,
}) : assert(child != null),
assert(autofocus != null),
assert(skipTraversal != null),
super(key: key);
/// A debug label for this widget.
......@@ -212,6 +210,13 @@ class Focus extends StatefulWidget {
/// node when needed.
final FocusNode focusNode;
/// Sets the [FocusNode.skipTraversal] flag on the focus node so that it won't
/// be visited by the [FocusTraversalPolicy].
///
/// This is sometimes useful if a Focus widget should receive key events as
/// part of the focus chain, but shouldn't be accessible via focus traversal.
final bool skipTraversal;
/// Returns the [focusNode] of the [Focus] that most tightly encloses the given
/// [BuildContext].
///
......@@ -238,7 +243,7 @@ class Focus extends StatefulWidget {
super.debugFillProperties(properties);
properties.add(StringProperty('debugLabel', debugLabel, defaultValue: null));
properties.add(FlagProperty('autofocus', value: autofocus, ifTrue: 'AUTOFOCUS', defaultValue: false));
properties.add(DiagnosticsProperty<FocusScopeNode>('node', focusNode, defaultValue: null));
properties.add(DiagnosticsProperty<FocusNode>('node', focusNode, defaultValue: null));
}
@override
......@@ -263,6 +268,7 @@ class _FocusState extends State<Focus> {
// Only create a new node if the widget doesn't have one.
_internalNode ??= _createNode();
}
node.skipTraversal = widget.skipTraversal;
_focusAttachment = node.attach(context, onKey: widget.onKey);
_hasFocus = node.hasFocus;
// Add listener even if the _internalNode existed before, since it should
......
......@@ -227,7 +227,7 @@ mixin DirectionalFocusTraversalPolicyMixin on FocusTraversalPolicy {
}
FocusNode _sortAndFindInitial(FocusNode currentNode, { bool vertical, bool first }) {
final Iterable<FocusNode> nodes = currentNode.nearestScope.descendants;
final Iterable<FocusNode> nodes = currentNode.nearestScope.traversalDescendants;
final List<FocusNode> sorted = nodes.toList();
sorted.sort((FocusNode a, FocusNode b) {
if (vertical) {
......@@ -261,7 +261,7 @@ mixin DirectionalFocusTraversalPolicyMixin on FocusTraversalPolicy {
FocusNode nearestScope,
) {
assert(direction == TraversalDirection.left || direction == TraversalDirection.right);
final Iterable<FocusNode> nodes = nearestScope.descendants;
final Iterable<FocusNode> nodes = nearestScope.traversalDescendants;
assert(!nodes.contains(nearestScope));
final List<FocusNode> sorted = nodes.toList();
sorted.sort((FocusNode a, FocusNode b) => a.rect.center.dx.compareTo(b.rect.center.dx));
......@@ -397,7 +397,7 @@ mixin DirectionalFocusTraversalPolicyMixin on FocusTraversalPolicy {
final Iterable<FocusNode> eligibleNodes = _sortAndFilterVertically(
direction,
focusedChild.rect,
nearestScope.descendants,
nearestScope.traversalDescendants,
);
if (eligibleNodes.isEmpty) {
break;
......@@ -434,7 +434,7 @@ mixin DirectionalFocusTraversalPolicyMixin on FocusTraversalPolicy {
final Rect band = Rect.fromLTRB(-double.infinity, focusedChild.rect.top, double.infinity, focusedChild.rect.bottom);
final Iterable<FocusNode> inBand = sorted.where((FocusNode node) => !node.rect.intersect(band).isEmpty);
if (inBand.isNotEmpty) {
// The inBand list is already sorted by horizontal distance, so pick the closest one.
// The inBand list is already sorted by vertical distance, so pick the closest one.
found = inBand.first;
break;
}
......@@ -483,8 +483,8 @@ class WidgetOrderFocusTraversalPolicy extends FocusTraversalPolicy with Directio
// doesn't have a focusedChild, or a non-scope is encountered.
FocusNode candidate = scope.focusedChild;
if (candidate == null) {
if (scope.children.isNotEmpty) {
candidate = scope.children.first;
if (scope.traversalChildren.isNotEmpty) {
candidate = scope.traversalChildren.first;
} else {
candidate = currentNode;
}
......@@ -513,7 +513,7 @@ class WidgetOrderFocusTraversalPolicy extends FocusTraversalPolicy with Directio
FocusNode firstNode;
FocusNode lastNode;
bool visit(FocusNode node) {
for (FocusNode visited in node.children) {
for (FocusNode visited in node.traversalChildren) {
firstNode ??= visited;
if (!visit(visited)) {
return false;
......@@ -600,7 +600,7 @@ class ReadingOrderTraversalPolicy extends FocusTraversalPolicy with DirectionalF
// doesn't have a focusedChild, or a non-scope is encountered.
FocusNode candidate = scope.focusedChild;
while (candidate == null) {
if (candidate.nearestScope.children.isNotEmpty) {
if (candidate.nearestScope.traversalChildren.isNotEmpty) {
candidate = _sortByGeometry(scope).first;
}
if (candidate is FocusScopeNode) {
......@@ -611,7 +611,7 @@ class ReadingOrderTraversalPolicy extends FocusTraversalPolicy with DirectionalF
}
if (candidate == null) {
if (scope.children.isNotEmpty) {
if (scope.traversalChildren.isNotEmpty) {
candidate = _sortByGeometry(scope).first;
} else {
candidate = currentNode;
......@@ -627,7 +627,7 @@ class ReadingOrderTraversalPolicy extends FocusTraversalPolicy with DirectionalF
// Sorts the list of nodes based on their geometry into the desired reading
// order based on the directionality of the context for each node.
Iterable<FocusNode> _sortByGeometry(FocusNode scope) {
final Iterable<FocusNode> nodes = scope.descendants;
final Iterable<FocusNode> nodes = scope.traversalDescendants;
if (nodes.length <= 1) {
return nodes;
}
......
......@@ -228,7 +228,7 @@ void main() {
expect(boxDecoration.color, const Color(0x00FF00));
});
testWidgets('Botton respects themes', (WidgetTester tester) async {
testWidgets('Button respects themes', (WidgetTester tester) async {
TextStyle textStyle;
await tester.pumpWidget(
......
......@@ -2,8 +2,9 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/rendering.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
......@@ -66,7 +67,6 @@ void main() {
expect(material.textStyle.fontWeight, FontWeight.w500);
expect(material.type, MaterialType.transparency);
// Disabled MaterialButton
await tester.pumpWidget(
const Directionality(
......@@ -321,6 +321,217 @@ void main() {
expect(material.type, MaterialType.button);
});
testWidgets('Do buttons work with hover', (WidgetTester tester) async {
const Color hoverColor = Color(0xff001122);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: MaterialButton(
hoverColor: hoverColor,
onPressed: () { },
child: const Text('button'),
),
),
);
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
await gesture.moveTo(tester.getCenter(find.byType(MaterialButton)));
await tester.pumpAndSettle();
RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures');
expect(inkFeatures, paints..rect(color: hoverColor));
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: FlatButton(
hoverColor: hoverColor,
onPressed: () { },
child: const Text('button'),
),
),
);
await tester.pumpAndSettle();
inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures');
expect(inkFeatures, paints..rect(color: hoverColor));
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: OutlineButton(
hoverColor: hoverColor,
onPressed: () { },
child: const Text('button'),
),
),
);
await tester.pumpAndSettle();
inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures');
expect(inkFeatures, paints..rect(color: hoverColor));
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: RaisedButton(
hoverColor: hoverColor,
onPressed: () { },
child: const Text('button'),
),
),
);
await tester.pumpAndSettle();
inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures');
expect(inkFeatures, paints..rect(color: hoverColor));
gesture.removePointer();
});
testWidgets('Do buttons work with focus', (WidgetTester tester) async {
const Color focusColor = Color(0xff001122);
FocusNode focusNode = FocusNode(debugLabel: 'MaterialButton Node');
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: MaterialButton(
focusColor: focusColor,
focusNode: focusNode,
onPressed: () { },
child: const Text('button'),
),
),
);
focusNode.requestFocus();
await tester.pumpAndSettle();
RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures');
expect(inkFeatures, paints..rect(color: focusColor));
focusNode = FocusNode(debugLabel: 'FlatButton Node');
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: FlatButton(
focusColor: focusColor,
focusNode: focusNode,
onPressed: () { },
child: const Text('button'),
),
),
);
focusNode.requestFocus();
await tester.pumpAndSettle();
inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures');
expect(inkFeatures, paints..rect(color: focusColor));
focusNode = FocusNode(debugLabel: 'RaisedButton Node');
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: RaisedButton(
focusColor: focusColor,
focusNode: focusNode,
onPressed: () { },
child: const Text('button'),
),
),
);
focusNode.requestFocus();
await tester.pumpAndSettle();
inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures');
expect(inkFeatures, paints..rect(color: focusColor));
focusNode = FocusNode(debugLabel: 'OutlineButton Node');
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: OutlineButton(
focusColor: focusColor,
focusNode: focusNode,
onPressed: () { },
child: const Text('button'),
),
),
);
focusNode.requestFocus();
await tester.pumpAndSettle();
inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures');
expect(inkFeatures, paints..rect(color: focusColor));
});
testWidgets('Button elevation and colors have proper precedence', (WidgetTester tester) async {
const double elevation = 10.0;
const double focusElevation = 11.0;
const double hoverElevation = 12.0;
const double highlightElevation = 13.0;
const Color focusColor = Color(0xff001122);
const Color hoverColor = Color(0xff112233);
const Color highlightColor = Color(0xff223344);
final Finder rawButtonMaterial = find.descendant(
of: find.byType(MaterialButton),
matching: find.byType(Material),
);
final FocusNode focusNode = FocusNode(debugLabel: 'MaterialButton Node');
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: MaterialButton(
focusColor: focusColor,
hoverColor: hoverColor,
highlightColor: highlightColor,
elevation: elevation,
focusElevation: focusElevation,
hoverElevation: hoverElevation,
highlightElevation: highlightElevation,
focusNode: focusNode,
onPressed: () { },
child: const Text('button'),
),
),
);
await tester.pumpAndSettle();
// Base elevation
Material material = tester.widget<Material>(rawButtonMaterial);
expect(material.elevation, equals(elevation));
// Focus elevation overrides base
focusNode.requestFocus();
await tester.pumpAndSettle();
material = tester.widget<Material>(rawButtonMaterial);
RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures');
expect(inkFeatures, paints..rect(color: focusColor));
expect(focusNode.hasPrimaryFocus, isTrue);
expect(material.elevation, equals(focusElevation));
// Hover elevation overrides focus
TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
await gesture.moveTo(tester.getCenter(find.byType(MaterialButton)));
await tester.pumpAndSettle();
material = tester.widget<Material>(rawButtonMaterial);
inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures');
expect(inkFeatures, paints..rect(color: focusColor)..rect(color: hoverColor));
expect(material.elevation, equals(hoverElevation));
await gesture.removePointer();
// Highlight elevation overrides hover
gesture = await tester.startGesture(tester.getCenter(find.byType(MaterialButton)));
await tester.pumpAndSettle();
material = tester.widget<Material>(rawButtonMaterial);
inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures');
expect(inkFeatures, paints..rect(color: focusColor)..rect(color: highlightColor));
expect(material.elevation, equals(highlightElevation));
await gesture.up();
});
testWidgets('Does FlatButton contribute semantics', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(
......@@ -952,7 +1163,7 @@ void main() {
expect(tester.widget<Material>(rawButtonMaterial).shape, const StadiumBorder());
});
testWidgets('MaterialButton defaults', (WidgetTester tester) async {
testWidgets('MaterialButton disabled default is correct.', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/30012.
final Finder rawButtonMaterial = find.descendant(
......
......@@ -105,8 +105,25 @@ void main() {
);
expect(find.text('Add'), findsNothing);
// Test hover for tooltip.
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
await gesture.moveTo(tester.getCenter(find.byType(FloatingActionButton)));
await tester.pumpAndSettle();
expect(find.text('Add'), findsOneWidget);
await gesture.moveTo(Offset.zero);
await gesture.removePointer();
await tester.pumpAndSettle();
expect(find.text('Add'), findsNothing);
// Test long press for tooltip.
await tester.longPress(find.byType(FloatingActionButton));
await tester.pumpAndSettle();
expect(find.text('Add'), findsOneWidget);
});
......@@ -687,20 +704,25 @@ void main() {
});
testWidgets('Floating Action Button has no clip by default', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Material(
child: FloatingActionButton(
onPressed: () { /* to make sure the button is enabled */ },
),
textDirection: TextDirection.ltr,
child: Material(
child: FloatingActionButton(
focusNode: focusNode,
onPressed: () { /* to make sure the button is enabled */ },
),
),
),
);
focusNode.unfocus();
await tester.pump();
expect(
tester.renderObject(find.byType(FloatingActionButton)),
paintsExactlyCountTimes(#clipPath, 0),
tester.renderObject(find.byType(FloatingActionButton)),
paintsExactlyCountTimes(#clipPath, 0),
);
});
}
......
......@@ -171,8 +171,8 @@ void main() {
.toList();
expect(description, <String>[
'backgroundColor: Color(0xcafecafe)',
'foregroundColor: Color(0xfeedfeed)',
'backgroundColor: Color(0xcafecafe)',
'elevation: 23.0',
'disabledElevation: 11.0',
'highlightElevation: 43.0',
......
......@@ -15,32 +15,30 @@ void main() {
testWidgets('InkWell gestures control test', (WidgetTester tester) async {
final List<String> log = <String>[];
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Material(
child: Center(
child: InkWell(
onTap: () {
log.add('tap');
},
onDoubleTap: () {
log.add('double-tap');
},
onLongPress: () {
log.add('long-press');
},
onTapDown: (TapDownDetails details) {
log.add('tap-down');
},
onTapCancel: () {
log.add('tap-cancel');
},
),
await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: Material(
child: Center(
child: InkWell(
onTap: () {
log.add('tap');
},
onDoubleTap: () {
log.add('double-tap');
},
onLongPress: () {
log.add('long-press');
},
onTapDown: (TapDownDetails details) {
log.add('tap-down');
},
onTapCancel: () {
log.add('tap-cancel');
},
),
),
)
);
),
));
await tester.tap(find.byType(InkWell), pointer: 1);
......@@ -92,6 +90,71 @@ void main() {
await tester.pump(const Duration(seconds: 1));
});
testWidgets('ink well changes color on hover', (WidgetTester tester) async {
await tester.pumpWidget(Material(
child: Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: Container(
width: 100,
height: 100,
child: InkWell(
hoverColor: const Color(0xff00ff00),
splashColor: const Color(0xffff0000),
focusColor: const Color(0xff0000ff),
highlightColor: const Color(0xf00fffff),
onTap: () {},
onLongPress: () {},
onHover: (bool hover) {}
),
),
),
),
));
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
await gesture.moveTo(tester.getCenter(find.byType(Container)));
await tester.pumpAndSettle();
final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures');
expect(inkFeatures, paints..rect(rect: const Rect.fromLTRB(350.0, 250.0, 450.0, 350.0), color: const Color(0xff00ff00)));
await gesture.removePointer();
});
testWidgets('ink response changes color on focus', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(debugLabel: 'Ink Focus');
await tester.pumpWidget(Material(
child: Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: Focus(
focusNode: focusNode,
child: Container(
width: 100,
height: 100,
child: InkWell(
hoverColor: const Color(0xff00ff00),
splashColor: const Color(0xffff0000),
focusColor: const Color(0xff0000ff),
highlightColor: const Color(0xf00fffff),
onTap: () {},
onLongPress: () {},
onHover: (bool hover) {}
),
),
),
),
),
));
await tester.pumpAndSettle();
final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures');
expect(inkFeatures, paintsExactlyCountTimes(#rect, 0));
focusNode.requestFocus();
await tester.pumpAndSettle();
expect(inkFeatures, paints
..rect(rect: const Rect.fromLTRB(350.0, 250.0, 450.0, 350.0), color: const Color(0xff0000ff)));
});
group('feedback', () {
FeedbackTester feedback;
......@@ -109,8 +172,8 @@ void main() {
textDirection: TextDirection.ltr,
child: Center(
child: InkWell(
onTap: () { },
onLongPress: () { },
onTap: () {},
onLongPress: () {},
),
),
),
......@@ -137,8 +200,8 @@ void main() {
textDirection: TextDirection.ltr,
child: Center(
child: InkWell(
onTap: () { },
onLongPress: () { },
onTap: () {},
onLongPress: () {},
enableFeedback: false,
),
),
......@@ -162,13 +225,14 @@ void main() {
Directionality(
textDirection: TextDirection.ltr,
child: Material(
child: CompositedTransformFollower( // forces a layer, which makes the paints easier to separate out
child: CompositedTransformFollower(
// forces a layer, which makes the paints easier to separate out
link: LayerLink(),
child: ListView(
addAutomaticKeepAlives: keepAlive,
dragStartBehavior: DragStartBehavior.down,
children: <Widget>[
Container(height: 500.0, child: InkWell(onTap: () { }, child: const Placeholder())),
Container(height: 500.0, child: InkWell(onTap: () {}, child: const Placeholder())),
Container(height: 500.0),
Container(height: 500.0),
],
......@@ -191,6 +255,7 @@ void main() {
keepAlive ? (paints..circle()) : isNot(paints..circle()),
);
}
await runTest(true);
await runTest(false);
});
......@@ -202,7 +267,7 @@ void main() {
textDirection: TextDirection.ltr,
child: Material(
child: InkWell(
onTap: () { },
onTap: () {},
child: const Text('Button'),
),
),
......@@ -213,7 +278,7 @@ void main() {
textDirection: TextDirection.ltr,
child: Material(
child: InkWell(
onTap: () { },
onTap: () {},
child: const Text('Button'),
excludeFromSemantics: true,
),
......
......@@ -13,7 +13,7 @@ void main() {
testWidgets('OutlineButton implements debugFillProperties', (WidgetTester tester) async {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
OutlineButton(
onPressed: () { },
onPressed: () {},
textColor: const Color(0xFF00FF00),
disabledTextColor: const Color(0xFFFF0000),
color: const Color(0xFF000000),
......@@ -42,7 +42,7 @@ void main() {
body: Center(
child: OutlineButton(
child: const Text('OutlineButton'),
onPressed: () { },
onPressed: () {},
),
),
),
......@@ -108,7 +108,7 @@ void main() {
);
}
await tester.pumpWidget(buildFrame(() { }));
await tester.pumpWidget(buildFrame(() {}));
await tester.press(find.byType(OutlineButton));
await tester.pumpAndSettle();
await tester.pumpWidget(buildFrame(null));
......@@ -176,7 +176,7 @@ void main() {
// Pump a new button with a no-op onPressed callback to make it enabled.
await tester.pumpWidget(
buildFrame(onPressed: () { }),
buildFrame(onPressed: () {}),
);
// Wait for the border color to change from disabled to enabled.
......@@ -236,9 +236,9 @@ void main() {
child: Material(
child: Center(
child: OutlineButton(
key: buttonKey,
onPressed: () { },
child: const Text('ABC'),
key: buttonKey,
onPressed: () {},
child: const Text('ABC'),
),
),
),
......@@ -259,7 +259,7 @@ void main() {
child: Material(
child: Center(
child: OutlineButton(
onPressed: () { },
onPressed: () {},
child: const Text('ABC'),
),
),
......@@ -300,7 +300,7 @@ void main() {
data: const MediaQueryData(textScaleFactor: 1.0),
child: Center(
child: OutlineButton(
onPressed: () { },
onPressed: () {},
child: const Text('ABC'),
),
),
......@@ -321,7 +321,7 @@ void main() {
data: const MediaQueryData(textScaleFactor: 1.3),
child: Center(
child: FlatButton(
onPressed: () { },
onPressed: () {},
child: const Text('ABC'),
),
),
......@@ -345,7 +345,7 @@ void main() {
data: const MediaQueryData(textScaleFactor: 3.0),
child: Center(
child: FlatButton(
onPressed: () { },
onPressed: () {},
child: const Text('ABC'),
),
),
......@@ -369,7 +369,7 @@ void main() {
home: Scaffold(
body: Center(
child: OutlineButton(
onPressed: () { },
onPressed: () {},
// Causes the button to be filled with the theme's canvasColor
// instead of Colors.transparent before the button material's
// elevation is animated to 2.0.
......
......@@ -297,7 +297,7 @@ void main() {
),
);
await tester.tap(find.text('PUSH'));
expect(await tester.pumpAndSettle(const Duration(minutes: 1)), 2);
expect(await tester.pumpAndSettle(const Duration(minutes: 1)), 3);
expect(find.text('PUSH'), findsNothing);
expect(find.text('HELLO'), findsOneWidget);
final Offset helloPosition1 = tester.getCenter(find.text('HELLO'));
......@@ -342,7 +342,7 @@ void main() {
expect(helloPosition3.dy, helloPosition4.dy);
await gesture.moveBy(const Offset(500.0, 0.0));
await gesture.up();
expect(await tester.pumpAndSettle(const Duration(minutes: 1)), 2);
expect(await tester.pumpAndSettle(const Duration(minutes: 1)), 3);
expect(find.text('PUSH'), findsOneWidget);
expect(find.text('HELLO'), findsNothing);
});
......
......@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
......@@ -189,7 +190,7 @@ void main() {
expect(find.byKey(key).hitTestable(), findsOneWidget);
});
testWidgets('RawMaterialButton can be expanded by parent constraints', (WidgetTester tester) async {
testWidgets('$RawMaterialButton can be expanded by parent constraints', (WidgetTester tester) async {
const Key key = Key('test');
await tester.pumpWidget(
MaterialApp(
......@@ -208,4 +209,61 @@ void main() {
expect(tester.getSize(find.byKey(key)), const Size(800.0, 48.0));
});
testWidgets('$RawMaterialButton handles focus', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(debugLabel: 'Button Focus');
const Key key = Key('test');
const Color focusColor = Color(0xff00ff00);
await tester.pumpWidget(
MaterialApp(
home: Center(
child: RawMaterialButton(
key: key,
focusNode: focusNode,
focusColor: focusColor,
onPressed: () {},
child: Container(width: 100, height: 100, color: const Color(0xffff0000)),
),
),
),
);
final RenderBox box = Material.of(tester.element(find.byType(InkWell))) as dynamic;
expect(box, isNot(paints..rect(color: focusColor)));
focusNode.requestFocus();
await tester.pumpAndSettle(const Duration(seconds: 1));
expect(box, paints..rect(color: focusColor));
});
testWidgets('$RawMaterialButton handles hover', (WidgetTester tester) async {
const Key key = Key('test');
const Color hoverColor = Color(0xff00ff00);
await tester.pumpWidget(
MaterialApp(
home: Center(
child: RawMaterialButton(
key: key,
hoverColor: hoverColor,
hoverElevation: 10.5,
onPressed: () {},
child: Container(width: 100, height: 100, color: const Color(0xffff0000)),
),
),
),
);
final RenderBox box = Material.of(tester.element(find.byType(InkWell))) as dynamic;
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
expect(box, isNot(paints..rect(color: hoverColor)));
await gesture.moveTo(tester.getCenter(find.byType(RawMaterialButton)));
await tester.pumpAndSettle(const Duration(seconds: 1));
expect(box, paints..rect(color: hoverColor));
await gesture.removePointer();
});
}
......@@ -101,11 +101,11 @@ void main() {
await tester.pumpAndSettle();
expect(delegate.query, '');
expect(delegate.querysForSuggestions.last, '');
expect(delegate.querysForResults, hasLength(0));
expect(delegate.queriesForSuggestions.last, '');
expect(delegate.queriesForResults, hasLength(0));
// Type W o w into search field
delegate.querysForSuggestions.clear();
delegate.queriesForSuggestions.clear();
await tester.enterText(find.byType(TextField), 'W');
await tester.pumpAndSettle();
expect(delegate.query, 'W');
......@@ -116,8 +116,8 @@ void main() {
await tester.pumpAndSettle();
expect(delegate.query, 'Wow');
expect(delegate.querysForSuggestions, <String>['W', 'Wo', 'Wow']);
expect(delegate.querysForResults, hasLength(0));
expect(delegate.queriesForSuggestions, <String>['W', 'Wo', 'Wow']);
expect(delegate.queriesForResults, hasLength(0));
});
testWidgets('Shows Results and closes search', (WidgetTester tester) async {
......@@ -143,7 +143,7 @@ void main() {
final TextField textField = tester.widget(find.byType(TextField));
expect(textField.focusNode.hasFocus, isFalse);
expect(delegate.querysForResults, <String>['Wow']);
expect(delegate.queriesForResults, <String>['Wow']);
// Close search
await tester.tap(find.byTooltip('Back'));
......@@ -170,13 +170,13 @@ void main() {
expect(find.text('Results'), findsNothing);
// Typing query Wow
delegate.querysForSuggestions.clear();
delegate.queriesForSuggestions.clear();
await tester.enterText(find.byType(TextField), 'Wow');
await tester.pumpAndSettle();
expect(delegate.query, 'Wow');
expect(delegate.querysForSuggestions, <String>['Wow']);
expect(delegate.querysForResults, hasLength(0));
expect(delegate.queriesForSuggestions, <String>['Wow']);
expect(delegate.queriesForResults, hasLength(0));
await tester.tap(find.text('Suggestions'));
await tester.pumpAndSettle();
......@@ -186,13 +186,13 @@ void main() {
expect(find.text('Results'), findsOneWidget);
expect(delegate.query, 'Wow');
expect(delegate.querysForSuggestions, <String>['Wow']);
expect(delegate.querysForResults, <String>['Wow']);
expect(delegate.queriesForSuggestions, <String>['Wow']);
expect(delegate.queriesForResults, <String>['Wow']);
TextField textField = tester.widget(find.byType(TextField));
expect(textField.focusNode.hasFocus, isFalse);
// Taping search field to go back to suggestions
// Tapping search field to go back to suggestions
await tester.tap(find.byType(TextField));
await tester.pumpAndSettle();
......@@ -201,15 +201,15 @@ void main() {
expect(find.text('Suggestions'), findsOneWidget);
expect(find.text('Results'), findsNothing);
expect(delegate.querysForSuggestions, <String>['Wow', 'Wow']);
expect(delegate.querysForResults, <String>['Wow']);
expect(delegate.queriesForSuggestions, <String>['Wow', 'Wow']);
expect(delegate.queriesForResults, <String>['Wow']);
await tester.enterText(find.byType(TextField), 'Foo');
await tester.pumpAndSettle();
expect(delegate.query, 'Foo');
expect(delegate.querysForSuggestions, <String>['Wow', 'Wow', 'Foo']);
expect(delegate.querysForResults, <String>['Wow']);
expect(delegate.queriesForSuggestions, <String>['Wow', 'Wow', 'Foo']);
expect(delegate.queriesForResults, <String>['Wow']);
// Go to results again
await tester.tap(find.text('Suggestions'));
......@@ -219,8 +219,8 @@ void main() {
expect(find.text('Results'), findsOneWidget);
expect(delegate.query, 'Foo');
expect(delegate.querysForSuggestions, <String>['Wow', 'Wow', 'Foo']);
expect(delegate.querysForResults, <String>['Wow', 'Foo']);
expect(delegate.queriesForSuggestions, <String>['Wow', 'Wow', 'Foo']);
expect(delegate.queriesForResults, <String>['Wow', 'Foo']);
textField = tester.widget(find.byType(TextField));
expect(textField.focusNode.hasFocus, isFalse);
......@@ -656,12 +656,12 @@ class _TestSearchDelegate extends SearchDelegate<String> {
);
}
final List<String> querysForSuggestions = <String>[];
final List<String> querysForResults = <String>[];
final List<String> queriesForSuggestions = <String>[];
final List<String> queriesForResults = <String>[];
@override
Widget buildSuggestions(BuildContext context) {
querysForSuggestions.add(query);
queriesForSuggestions.add(query);
return MaterialButton(
onPressed: () {
showResults(context);
......@@ -672,7 +672,7 @@ class _TestSearchDelegate extends SearchDelegate<String> {
@override
Widget buildResults(BuildContext context) {
querysForResults.add(query);
queriesForResults.add(query);
return const Text('Results');
}
......
......@@ -3263,6 +3263,8 @@ void main() {
),
),
);
focusNode.requestFocus();
await tester.pump();
const String testValue = 'a big house\njumped over a mouse'; // 11 \n 19
await tester.enterText(find.byType(TextField), testValue);
......
......@@ -167,7 +167,7 @@ void main() {
await tester.pump(const Duration(milliseconds: 10));
expect(tester.hasRunningAnimations, isFalse);
await tester.pumpAndSettle();
expect(await tester.pumpAndSettle(), 1);
transformWidget = tester.firstWidget(find.byType(Transform));
// Icon has not rotated.
......
......@@ -631,8 +631,8 @@ void main() {
FocusScope.of(keyB.currentContext).requestFocus(keyB.currentState.focusNode);
FocusScope.of(keyA.currentContext).requestFocus(keyA.currentState.focusNode);
final FocusScopeNode bScope = FocusScope.of(keyB.currentContext);
final FocusScopeNode aScope = FocusScope.of(keyA.currentContext);
final FocusScopeNode bScope = FocusScope.of(keyB.currentContext);
WidgetsBinding.instance.focusManager.rootScope.setFirstFocus(bScope);
WidgetsBinding.instance.focusManager.rootScope.setFirstFocus(aScope);
......@@ -777,8 +777,8 @@ void main() {
FocusScope.of(keyB.currentContext).requestFocus(keyB.currentState.focusNode);
FocusScope.of(keyA.currentContext).requestFocus(keyA.currentState.focusNode);
final FocusScopeNode aScope = FocusScope.of(keyA.currentContext);
final FocusScopeNode bScope = FocusScope.of(keyB.currentContext);
final FocusScopeNode aScope = FocusScope.of(keyA.currentContext);
WidgetsBinding.instance.focusManager.rootScope.setFirstFocus(bScope);
WidgetsBinding.instance.focusManager.rootScope.setFirstFocus(aScope);
......
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="manual_tests - focus" type="FlutterRunConfigurationType" factoryName="Flutter" singleton="false">
<option name="filePath" value="$PROJECT_DIR$/dev/manual_tests/lib/focus.dart" />
<method v="2" />
</configuration>
</component>
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="manual_tests - hover" type="FlutterRunConfigurationType" factoryName="Flutter" singleton="false">
<option name="filePath" value="$PROJECT_DIR$/dev/manual_tests/lib/hover.dart" />
<method v="2" />
</configuration>
</component>
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