[Material] Add support for hovered, pressed, and focused text color on Buttons. (#33090)

* Support for stateful text colors in buttons

* Add color and a11y tests for buttons
This commit is contained in:
MH Johnson 2019-06-05 15:46:53 -04:00 committed by GitHub
parent 50ea6f32d3
commit 1b87f558d5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 1075 additions and 39 deletions

View file

@ -76,6 +76,7 @@ export 'src/material/list_tile.dart';
export 'src/material/material.dart';
export 'src/material/material_button.dart';
export 'src/material/material_localizations.dart';
export 'src/material/material_state.dart';
export 'src/material/mergeable_material.dart';
export 'src/material/outline_button.dart';
export 'src/material/page.dart';

View file

@ -13,6 +13,7 @@ import 'button_theme.dart';
import 'constants.dart';
import 'ink_well.dart';
import 'material.dart';
import 'material_state.dart';
import 'theme.dart';
import 'theme_data.dart';
@ -85,6 +86,14 @@ class RawMaterialButton extends StatefulWidget {
/// Defines the default text style, with [Material.textStyle], for the
/// button's [child].
///
/// If [textStyle.color] is a [MaterialStateColor], [MaterialStateColor.resolveColor]
/// is used for the following [MaterialState]s:
///
/// * [MaterialState.pressed].
/// * [MaterialState.hovered].
/// * [MaterialState.focused].
/// * [MaterialState.disabled].
final TextStyle textStyle;
/// The color of the button's [Material].
@ -231,14 +240,21 @@ class RawMaterialButton extends StatefulWidget {
}
class _RawMaterialButtonState extends State<RawMaterialButton> {
bool _highlight = false;
bool _focused = false;
bool _hovering = false;
final Set<MaterialState> _states = <MaterialState>{};
bool get _hovered => _states.contains(MaterialState.hovered);
bool get _focused => _states.contains(MaterialState.focused);
bool get _pressed => _states.contains(MaterialState.pressed);
bool get _disabled => _states.contains(MaterialState.disabled);
void _updateState(MaterialState state, bool value) {
value ? _states.add(state) : _states.remove(state);
}
void _handleHighlightChanged(bool value) {
if (_highlight != value) {
if (_pressed != value) {
setState(() {
_highlight = value;
_updateState(MaterialState.pressed, value);
if (widget.onHighlightChanged != null) {
widget.onHighlightChanged(value);
}
@ -246,46 +262,71 @@ class _RawMaterialButtonState extends State<RawMaterialButton> {
}
}
@override
void didUpdateWidget(RawMaterialButton oldWidget) {
super.didUpdateWidget(oldWidget);
if (_highlight && !widget.enabled) {
_highlight = false;
if (widget.onHighlightChanged != null) {
widget.onHighlightChanged(false);
}
void _handleHoveredChanged(bool value) {
if (_hovered != value) {
setState(() {
_updateState(MaterialState.hovered, value);
});
}
}
double _effectiveElevation() {
if (widget.enabled) {
// These conditionals are in order of precedence, so be careful about
// reorganizing them.
if (_highlight) {
return widget.highlightElevation;
}
if (_hovering) {
return widget.hoverElevation;
}
if (_focused) {
return widget.focusElevation;
}
return widget.elevation;
} else {
void _handleFocusedChanged(bool value) {
if (_focused != value) {
setState(() {
_updateState(MaterialState.focused, value);
});
}
}
@override
void initState() {
super.initState();
_updateState(MaterialState.disabled, !widget.enabled);
}
@override
void didUpdateWidget(RawMaterialButton oldWidget) {
super.didUpdateWidget(oldWidget);
_updateState(MaterialState.disabled, !widget.enabled);
// If the button is disabled while a press gesture is currently ongoing,
// InkWell makes a call to handleHighlightChanged. This causes an exception
// because it calls setState in the middle of a build. To preempt this, we
// manually update pressed to false when this situation occurs.
if (_disabled && _pressed) {
_handleHighlightChanged(false);
}
}
double get _effectiveElevation {
// These conditionals are in order of precedence, so be careful about
// reorganizing them.
if (_disabled) {
return widget.disabledElevation;
}
if (_pressed) {
return widget.highlightElevation;
}
if (_hovered) {
return widget.hoverElevation;
}
if (_focused) {
return widget.focusElevation;
}
return widget.elevation;
}
@override
Widget build(BuildContext context) {
final Color effectiveTextColor = MaterialStateColor.resolveColor(widget.textStyle?.color, _states);
final Widget result = Focus(
focusNode: widget.focusNode,
onFocusChange: (bool focused) => setState(() { _focused = focused; }),
onFocusChange: _handleFocusedChanged,
child: ConstrainedBox(
constraints: widget.constraints,
child: Material(
elevation: _effectiveElevation(),
textStyle: widget.textStyle,
elevation: _effectiveElevation,
textStyle: widget.textStyle?.copyWith(color: effectiveTextColor),
shape: widget.shape,
color: widget.fillColor,
type: widget.fillColor == null ? MaterialType.transparency : MaterialType.button,
@ -297,11 +338,11 @@ class _RawMaterialButtonState extends State<RawMaterialButton> {
highlightColor: widget.highlightColor,
focusColor: widget.focusColor,
hoverColor: widget.hoverColor,
onHover: (bool hovering) => setState(() => _hovering = hovering),
onHover: _handleHoveredChanged,
onTap: widget.onPressed,
customBorder: widget.shape,
child: IconTheme.merge(
data: IconThemeData(color: widget.textStyle?.color),
data: IconThemeData(color: effectiveTextColor),
child: Container(
padding: widget.padding,
child: Center(

View file

@ -10,6 +10,7 @@ import 'colors.dart';
import 'constants.dart';
import 'flat_button.dart';
import 'material_button.dart';
import 'material_state.dart';
import 'outline_button.dart';
import 'raised_button.dart';
import 'theme.dart';
@ -479,7 +480,13 @@ class ButtonThemeData extends Diagnosticable {
/// Returns the button's [MaterialButton.disabledColor] if it is non-null.
/// Otherwise the color scheme's [ColorScheme.onSurface] color is returned
/// with its opacity set to 0.30 if [getBrightness] is dark, 0.38 otherwise.
///
/// If [MaterialButton.textColor] is a [MaterialStateColor], it will be used
/// as the `disabledTextColor`. It will be resolved in the
/// [MaterialState.disabled] state.
Color getDisabledTextColor(MaterialButton button) {
if (button.textColor is MaterialStateColor)
return button.textColor;
if (button.disabledTextColor != null)
return button.disabledTextColor;
return _getDisabledColor(button);

View file

@ -100,6 +100,9 @@ class MaterialButton extends StatelessWidget {
/// The default text color depends on the button theme's text theme,
/// [ButtonThemeData.textTheme].
///
/// If [textColor] is a [MaterialStateColor], [disabledTextColor] will be
/// ignored.
///
/// See also:
///
/// * [disabledTextColor], the text color to use when the button has been
@ -114,6 +117,9 @@ class MaterialButton extends StatelessWidget {
/// The default value is the theme's disabled color,
/// [ThemeData.disabledColor].
///
/// If [textColor] is a [MaterialStateColor], [disabledTextColor] will be
/// ignored.
///
/// See also:
///
/// * [textColor] - The color to use for this button's text when the button is [enabled].

View file

@ -0,0 +1,186 @@
// 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 'dart:ui' show Color;
/// Interactive states that some of the Material widgets can take on when
/// receiving input from the user.
///
/// States are defined by https://material.io/design/interaction/states.html#usage.
///
/// Some Material widgets track their current state in a `Set<MaterialState>`.
///
/// See also:
/// * [MaterialStateColor], a color that has a `resolve` method that can
/// return a different color depending on the state of the widget that it
/// is used in.
enum MaterialState {
/// The state when the user drags their mouse cursor over the given widget.
///
/// See: https://material.io/design/interaction/states.html#hover.
hovered,
/// The state when the user navigates with the keyboard to a given widget.
///
/// This can also sometimes be triggered when a widget is tapped. For example,
/// when a [TextField] is tapped, it becomes [focused].
///
/// See: https://material.io/design/interaction/states.html#focus.
focused,
/// The state when the user is actively pressing down on the given widget.
///
/// See: https://material.io/design/interaction/states.html#pressed.
pressed,
/// The state when this widget is being dragged from one place to another by
/// the user.
///
/// https://material.io/design/interaction/states.html#dragged.
dragged,
/// The state when this item has been selected.
///
/// This applies to things that can be toggled (such as chips and checkboxes)
/// and things that are selected from a set of options (such as tabs and radio buttons).
///
/// See: https://material.io/design/interaction/states.html#selected.
selected,
/// The state when this widget disabled and can not be interacted with.
///
/// Disabled widgets should not respond to hover, focus, press, or drag
/// interactions.
///
/// See: https://material.io/design/interaction/states.html#disabled.
disabled,
/// The state when the widget has entered some form of invalid state.
///
/// See https://material.io/design/interaction/states.html#usage.
error,
}
/// Signature for the function that returns a color based on a given set of states.
typedef MaterialStateColorResolver = Color Function(Set<MaterialState> states);
/// Defines a [Color] whose value depends on a set of [MaterialState]s which
/// represent the interactive state of a component.
///
/// This is useful for improving the accessibility of text in different states
/// of a component. For example, in a [FlatButton] with blue text, the text will
/// become more difficult to read when the button is hovered, focused, or pressed,
/// because the contrast ratio between the button and the text will decrease. To
/// solve this, you can use [MaterialStateColor] to make the text darker when the
/// [FlatButton] is hovered, focused, or pressed.
///
/// To use a [MaterialStateColor], you can either:
/// 1. Create a subclass of [MaterialStateColor] and implement the abstract `resolve` method.
/// 2. Use [MaterialStateColor.resolveWith] and pass in a callback that
/// will be used to resolve the color in the given states.
///
/// This should only be used as parameters when they are documented to take
/// [MaterialStateColor], otherwise only the default state will be used.
///
/// {@tool sample}
///
/// This example shows how you could pass a `MaterialStateColor` to `FlatButton.textColor`.
/// Here, the text color will be `Colors.blue[900]` when the button is being
/// pressed, hovered, or focused. Otherwise, the text color will be `Colors.blue[600]`.
///
/// ```dart
/// Color getTextColor(Set<MaterialState> states) {
/// const Set<MaterialState> interactiveStates = <MaterialState>{
/// MaterialState.pressed,
/// MaterialState.hovered,
/// MaterialState.focused,
/// };
/// if (states.any(interactiveStates.contains)) {
/// return Colors.blue[900];
/// }
/// return Colors.blue[600];
/// }
///
/// FlatButton(
/// child: Text('FlatButton'),
/// onPressed: () {},
/// textColor: MaterialStateColor.resolveWith(getTextColor),
/// ),
/// ```
/// {@end-tool}
abstract class MaterialStateColor extends Color {
/// Creates a [MaterialStateColor].
///
/// If you want a `const` [MaterialStateColor], you'll need to extend
/// [MaterialStateColor] and override the [resolve] method. You'll also need
/// to provide a `defaultValue` to the super constructor, so that we can know
/// at compile-time what the value of the default [Color] is.
///
/// {@tool sample}
///
/// In this next example, we see how you can create a `MaterialStateColor` by
/// extending the abstract class and overriding the `resolve` method.
///
/// ```dart
/// class TextColor extends MaterialStateColor {
/// static const int _defaultColor = 0xcafefeed;
/// static const int _pressedColor = 0xdeadbeef;
///
/// const TextColor() : super(_defaultColor);
///
/// @override
/// Color resolve(Set<MaterialState> states) {
/// if (states.contains(MaterialState.pressed)) {
/// return const Color(_pressedColor);
/// }
/// return const Color(_defaultColor);
/// }
/// }
/// ```
/// {@end-tool}
const MaterialStateColor(int defaultValue) : super(defaultValue);
/// Creates a [MaterialStateColor] from a [MaterialStateColorResolver] callback function.
///
/// If used as a regular color, the color resolved in the default state (the
/// empty set of states) will be used.
///
/// The given callback parameter must return a non-null color in the default
/// state.
factory MaterialStateColor.resolveWith(MaterialStateColorResolver callback) => _MaterialStateColor(callback);
/// Returns a [Color] that's to be used when a Material component is in the
/// specified state.
Color resolve(Set<MaterialState> states);
/// Returns the color for the given set of states if `color` is a
/// [MaterialStateColor], otherwise returns the color itself.
///
/// This is useful for widgets that have parameters which can be [Color] or
/// [MaterialStateColor] values.
static Color resolveColor(Color color, Set<MaterialState> states) {
if (color is MaterialStateColor) {
return color.resolve(states);
}
return color;
}
}
/// A [MaterialStateColor] created from a [MaterialStateColorResolver] callback alone.
///
/// If used as a regular color, the color resolved in the default state will
/// be used.
///
/// Used by [MaterialStateColor.resolveWith].
class _MaterialStateColor extends MaterialStateColor {
_MaterialStateColor(this._resolve) : super(_resolve(_defaultStates).value);
final MaterialStateColorResolver _resolve;
/// The default state for a Material component, the empty set of interaction states.
static const Set<MaterialState> _defaultStates = <MaterialState>{};
@override
Color resolve(Set<MaterialState> states) => _resolve(states);
}

View file

@ -2,7 +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/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
@ -325,4 +327,79 @@ void main() {
expect(fooText, findsNWidgets(2));
expect(tester.getRect(fooText.at(0)), tester.getRect(fooText.at(1)));
});
testWidgets('button theme with stateful color changes color in states', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
const Color pressedColor = Color(1);
const Color hoverColor = Color(2);
const Color focusedColor = Color(3);
const Color defaultColor = Color(4);
Color getTextColor(Set<MaterialState> states) {
if (states.contains(MaterialState.pressed)) {
return pressedColor;
}
if (states.contains(MaterialState.hovered)) {
return hoverColor;
}
if (states.contains(MaterialState.focused)) {
return focusedColor;
}
return defaultColor;
}
const ColorScheme colorScheme = ColorScheme.light();
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: ButtonTheme(
colorScheme: colorScheme.copyWith(
primary: MaterialStateColor.resolveWith(getTextColor),
),
textTheme: ButtonTextTheme.primary,
child: FlatButton(
child: const Text('FlatButton'),
onPressed: () {},
focusNode: focusNode,
),
),
),
),
),
);
Color textColor() {
return tester.renderObject<RenderParagraph>(find.text('FlatButton')).text.style.color;
}
// Default, not disabled.
expect(textColor(), equals(defaultColor));
// Focused.
focusNode.requestFocus();
await tester.pumpAndSettle();
expect(textColor(), focusedColor);
// Hovered.
final Offset center = tester.getCenter(find.byType(FlatButton));
final TestGesture gesture = await tester.createGesture(
kind: PointerDeviceKind.mouse,
);
await gesture.addPointer();
await gesture.moveTo(center);
await tester.pumpAndSettle();
expect(textColor(), hoverColor);
// Highlighted (pressed).
await gesture.down(center);
await tester.pump(); // Start the splash and highlight animations.
await tester.pump(const Duration(milliseconds: 800)); // Wait for splash and highlight to be well under way.
expect(textColor(), pressedColor);
await gesture.removePointer();
},
semanticsEnabled: true,
);
}

View file

@ -4,6 +4,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
import '../rendering/mock_canvas.dart';
@ -33,6 +34,8 @@ void main() {
});
testWidgets('Default FlatButton meets a11y contrast guidelines', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
@ -40,6 +43,7 @@ void main() {
child: FlatButton(
child: const Text('FlatButton'),
onPressed: () { },
focusNode: focusNode,
),
),
),
@ -49,16 +53,266 @@ void main() {
// Default, not disabled.
await expectLater(tester, meetsGuideline(textContrastGuideline));
// Highlighted (pressed).
// Focused.
focusNode.requestFocus();
await tester.pumpAndSettle();
await expectLater(tester, meetsGuideline(textContrastGuideline));
// Hovered.
final Offset center = tester.getCenter(find.byType(FlatButton));
await tester.startGesture(center);
final TestGesture gesture = await tester.createGesture(
kind: PointerDeviceKind.mouse,
);
await gesture.addPointer();
await gesture.moveTo(center);
await tester.pumpAndSettle();
await expectLater(tester, meetsGuideline(textContrastGuideline));
// Highlighted (pressed).
await gesture.down(center);
await tester.pump(); // Start the splash and highlight animations.
await tester.pump(const Duration(milliseconds: 800)); // Wait for splash and highlight to be well under way.
await expectLater(tester, meetsGuideline(textContrastGuideline));
await gesture.removePointer();
},
semanticsEnabled: true,
);
testWidgets('FlatButton with colored theme meets a11y contrast guidelines', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
final ColorScheme colorScheme = ColorScheme.fromSwatch(primarySwatch: Colors.blue);
Color getTextColor(Set<MaterialState> states) {
final Set<MaterialState> interactiveStates = <MaterialState>{
MaterialState.pressed,
MaterialState.hovered,
MaterialState.focused,
};
if (states.any(interactiveStates.contains)) {
return Colors.blue[900];
}
return Colors.blue[800];
}
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: ButtonTheme(
colorScheme: colorScheme,
textTheme: ButtonTextTheme.primary,
child: FlatButton(
child: const Text('FlatButton'),
onPressed: () {},
focusNode: focusNode,
textColor: MaterialStateColor.resolveWith(getTextColor),
),
),
),
),
),
);
// Default, not disabled.
await expectLater(tester, meetsGuideline(textContrastGuideline));
// Focused.
focusNode.requestFocus();
await tester.pumpAndSettle();
await expectLater(tester, meetsGuideline(textContrastGuideline));
// Hovered.
final Offset center = tester.getCenter(find.byType(FlatButton));
final TestGesture gesture = await tester.createGesture(
kind: PointerDeviceKind.mouse,
);
await gesture.addPointer();
await gesture.moveTo(center);
await tester.pumpAndSettle();
await expectLater(tester, meetsGuideline(textContrastGuideline));
// Highlighted (pressed).
await gesture.down(center);
await tester.pump(); // Start the splash and highlight animations.
await tester.pump(const Duration(milliseconds: 800)); // Wait for splash and highlight to be well under way.
await expectLater(tester, meetsGuideline(textContrastGuideline));
await gesture.removePointer();
},
semanticsEnabled: true,
);
testWidgets('FlatButton uses stateful color for text color in different states', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
const Color pressedColor = Color(1);
const Color hoverColor = Color(2);
const Color focusedColor = Color(3);
const Color defaultColor = Color(4);
Color getTextColor(Set<MaterialState> states) {
if (states.contains(MaterialState.pressed)) {
return pressedColor;
}
if (states.contains(MaterialState.hovered)) {
return hoverColor;
}
if (states.contains(MaterialState.focused)) {
return focusedColor;
}
return defaultColor;
}
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: FlatButton(
child: const Text('FlatButton'),
onPressed: () {},
focusNode: focusNode,
textColor: MaterialStateColor.resolveWith(getTextColor),
),
),
),
),
);
Color textColor() {
return tester.renderObject<RenderParagraph>(find.text('FlatButton')).text.style.color;
}
// Default, not disabled.
expect(textColor(), equals(defaultColor));
// Focused.
focusNode.requestFocus();
await tester.pumpAndSettle();
expect(textColor(), focusedColor);
// Hovered.
final Offset center = tester.getCenter(find.byType(FlatButton));
final TestGesture gesture = await tester.createGesture(
kind: PointerDeviceKind.mouse,
);
await gesture.addPointer();
await gesture.moveTo(center);
await tester.pumpAndSettle();
expect(textColor(), hoverColor);
// Highlighted (pressed).
await gesture.down(center);
await tester.pump(); // Start the splash and highlight animations.
await tester.pump(const Duration(milliseconds: 800)); // Wait for splash and highlight to be well under way.
expect(textColor(), pressedColor);
await gesture.removePointer();
});
testWidgets('FlatButton uses stateful color for icon color in different states', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
final Key buttonKey = UniqueKey();
const Color pressedColor = Color(1);
const Color hoverColor = Color(2);
const Color focusedColor = Color(3);
const Color defaultColor = Color(4);
Color getTextColor(Set<MaterialState> states) {
if (states.contains(MaterialState.pressed)) {
return pressedColor;
}
if (states.contains(MaterialState.hovered)) {
return hoverColor;
}
if (states.contains(MaterialState.focused)) {
return focusedColor;
}
return defaultColor;
}
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: FlatButton.icon(
key: buttonKey,
icon: const Icon(Icons.add),
label: const Text('FlatButton'),
onPressed: () {},
focusNode: focusNode,
textColor: MaterialStateColor.resolveWith(getTextColor),
),
),
),
),
);
Color iconColor() => _iconStyle(tester, Icons.add).color;
// Default, not disabled.
expect(iconColor(), equals(defaultColor));
// Focused.
focusNode.requestFocus();
await tester.pumpAndSettle();
expect(iconColor(), focusedColor);
// Hovered.
final Offset center = tester.getCenter(find.byKey(buttonKey));
final TestGesture gesture = await tester.createGesture(
kind: PointerDeviceKind.mouse,
);
await gesture.addPointer();
await gesture.moveTo(center);
await tester.pumpAndSettle();
expect(iconColor(), hoverColor);
// Highlighted (pressed).
await gesture.down(center);
await tester.pump(); // Start the splash and highlight animations.
await tester.pump(const Duration(milliseconds: 800)); // Wait for splash and highlight to be well under way.
expect(iconColor(), pressedColor);
await gesture.removePointer();
});
testWidgets('FlatButton ignores disabled text color if text color is stateful', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
const Color disabledColor = Color(1);
const Color defaultColor = Color(2);
const Color unusedDisabledTextColor = Color(3);
Color getTextColor(Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return disabledColor;
}
return defaultColor;
}
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: FlatButton(
onPressed: null,
child: const Text('FlatButton'),
focusNode: focusNode,
textColor: MaterialStateColor.resolveWith(getTextColor),
disabledTextColor: unusedDisabledTextColor,
),
),
),
),
);
Color textColor() {
return tester.renderObject<RenderParagraph>(find.text('FlatButton')).text.style.color;
}
// Disabled.
expect(textColor(), equals(disabledColor));
expect(textColor(), isNot(unusedDisabledTextColor));
});
testWidgets('FlatButton has no clip by default', (WidgetTester tester) async {
await tester.pumpWidget(
Directionality(
@ -78,3 +332,10 @@ void main() {
);
});
}
TextStyle _iconStyle(WidgetTester tester, IconData icon) {
final RichText iconRichText = tester.widget<RichText>(
find.descendant(of: find.byIcon(icon), matching: find.byType(RichText)),
);
return iconRichText.text.style;
}

View file

@ -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_test/flutter_test.dart';
import 'package:flutter/rendering.dart';
@ -36,6 +37,8 @@ void main() {
});
testWidgets('Default OutlineButton meets a11y contrast guidelines', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
@ -43,6 +46,7 @@ void main() {
child: OutlineButton(
child: const Text('OutlineButton'),
onPressed: () {},
focusNode: focusNode,
),
),
),
@ -52,16 +56,266 @@ void main() {
// Default, not disabled.
await expectLater(tester, meetsGuideline(textContrastGuideline));
// Highlighted (pressed).
// Focused.
focusNode.requestFocus();
await tester.pumpAndSettle();
await expectLater(tester, meetsGuideline(textContrastGuideline));
// Hovered.
final Offset center = tester.getCenter(find.byType(OutlineButton));
await tester.startGesture(center);
final TestGesture gesture = await tester.createGesture(
kind: PointerDeviceKind.mouse,
);
await gesture.addPointer();
await gesture.moveTo(center);
await tester.pumpAndSettle();
await expectLater(tester, meetsGuideline(textContrastGuideline));
// Highlighted (pressed).
await gesture.down(center);
await tester.pump(); // Start the splash and highlight animations.
await tester.pump(const Duration(milliseconds: 800)); // Wait for splash and highlight to be well under way.
await expectLater(tester, meetsGuideline(textContrastGuideline));
await gesture.removePointer();
},
semanticsEnabled: true,
);
testWidgets('OutlineButton with colored theme meets a11y contrast guidelines', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
final ColorScheme colorScheme = ColorScheme.fromSwatch(primarySwatch: Colors.blue);
Color getTextColor(Set<MaterialState> states) {
final Set<MaterialState> interactiveStates = <MaterialState>{
MaterialState.pressed,
MaterialState.hovered,
MaterialState.focused,
};
if (states.any(interactiveStates.contains)) {
return Colors.blue[900];
}
return Colors.blue[800];
}
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: ButtonTheme(
colorScheme: colorScheme,
textTheme: ButtonTextTheme.primary,
child: OutlineButton(
child: const Text('OutlineButton'),
onPressed: () {},
focusNode: focusNode,
textColor: MaterialStateColor.resolveWith(getTextColor),
),
),
),
),
),
);
// Default, not disabled.
await expectLater(tester, meetsGuideline(textContrastGuideline));
// Focused.
focusNode.requestFocus();
await tester.pumpAndSettle();
await expectLater(tester, meetsGuideline(textContrastGuideline));
// Hovered.
final Offset center = tester.getCenter(find.byType(OutlineButton));
final TestGesture gesture = await tester.createGesture(
kind: PointerDeviceKind.mouse,
);
await gesture.addPointer();
await gesture.moveTo(center);
await tester.pumpAndSettle();
await expectLater(tester, meetsGuideline(textContrastGuideline));
// Highlighted (pressed).
await gesture.down(center);
await tester.pump(); // Start the splash and highlight animations.
await tester.pump(const Duration(milliseconds: 800)); // Wait for splash and highlight to be well under way.
await expectLater(tester, meetsGuideline(textContrastGuideline));
await gesture.removePointer();
},
semanticsEnabled: true,
);
testWidgets('OutlineButton uses stateful color for text color in different states', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
const Color pressedColor = Color(1);
const Color hoverColor = Color(2);
const Color focusedColor = Color(3);
const Color defaultColor = Color(4);
Color getTextColor(Set<MaterialState> states) {
if (states.contains(MaterialState.pressed)) {
return pressedColor;
}
if (states.contains(MaterialState.hovered)) {
return hoverColor;
}
if (states.contains(MaterialState.focused)) {
return focusedColor;
}
return defaultColor;
}
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: OutlineButton(
child: const Text('OutlineButton'),
onPressed: () {},
focusNode: focusNode,
textColor: MaterialStateColor.resolveWith(getTextColor),
),
),
),
),
);
Color textColor() {
return tester.renderObject<RenderParagraph>(find.text('OutlineButton')).text.style.color;
}
// Default, not disabled.
expect(textColor(), equals(defaultColor));
// Focused.
focusNode.requestFocus();
await tester.pumpAndSettle();
expect(textColor(), focusedColor);
// Hovered.
final Offset center = tester.getCenter(find.byType(OutlineButton));
final TestGesture gesture = await tester.createGesture(
kind: PointerDeviceKind.mouse,
);
await gesture.addPointer();
await gesture.moveTo(center);
await tester.pumpAndSettle();
expect(textColor(), hoverColor);
// Highlighted (pressed).
await gesture.down(center);
await tester.pump(); // Start the splash and highlight animations.
await tester.pump(const Duration(milliseconds: 800)); // Wait for splash and highlight to be well under way.
expect(textColor(), pressedColor);
await gesture.removePointer();
});
testWidgets('OutlineButton uses stateful color for icon color in different states', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
final Key buttonKey = UniqueKey();
const Color pressedColor = Color(1);
const Color hoverColor = Color(2);
const Color focusedColor = Color(3);
const Color defaultColor = Color(4);
Color getTextColor(Set<MaterialState> states) {
if (states.contains(MaterialState.pressed)) {
return pressedColor;
}
if (states.contains(MaterialState.hovered)) {
return hoverColor;
}
if (states.contains(MaterialState.focused)) {
return focusedColor;
}
return defaultColor;
}
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: OutlineButton.icon(
key: buttonKey,
icon: const Icon(Icons.add),
label: const Text('OutlineButton'),
onPressed: () {},
focusNode: focusNode,
textColor: MaterialStateColor.resolveWith(getTextColor),
),
),
),
),
);
Color iconColor() => _iconStyle(tester, Icons.add).color;
// Default, not disabled.
expect(iconColor(), equals(defaultColor));
// Focused.
focusNode.requestFocus();
await tester.pumpAndSettle();
expect(iconColor(), focusedColor);
// Hovered.
final Offset center = tester.getCenter(find.byKey(buttonKey));
final TestGesture gesture = await tester.createGesture(
kind: PointerDeviceKind.mouse,
);
await gesture.addPointer();
await gesture.moveTo(center);
await tester.pumpAndSettle();
expect(iconColor(), hoverColor);
// Highlighted (pressed).
await gesture.down(center);
await tester.pump(); // Start the splash and highlight animations.
await tester.pump(const Duration(milliseconds: 800)); // Wait for splash and highlight to be well under way.
expect(iconColor(), pressedColor);
await gesture.removePointer();
});
testWidgets('OutlineButton ignores disabled text color if text color is stateful', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
const Color disabledColor = Color(1);
const Color defaultColor = Color(2);
const Color unusedDisabledTextColor = Color(3);
Color getTextColor(Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return disabledColor;
}
return defaultColor;
}
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: OutlineButton(
onPressed: null,
child: const Text('OutlineButton'),
focusNode: focusNode,
textColor: MaterialStateColor.resolveWith(getTextColor),
disabledTextColor: unusedDisabledTextColor,
),
),
),
),
);
Color textColor() {
return tester.renderObject<RenderParagraph>(find.text('OutlineButton')).text.style.color;
}
// Disabled.
expect(textColor(), equals(disabledColor));
expect(textColor(), isNot(unusedDisabledTextColor));
});
testWidgets('Outline button responds to tap when enabled', (WidgetTester tester) async {
int pressedCount = 0;
@ -448,3 +702,10 @@ void _checkPhysicalLayer(Element element, Color expectedColor, { Path clipPath,
expect(expectedLayer.clipPath, coversSameAreaAs(clipPath, areaToCompare: clipRect.inflate(10.0)));
}
}
TextStyle _iconStyle(WidgetTester tester, IconData icon) {
final RichText iconRichText = tester.widget<RichText>(
find.descendant(of: find.byIcon(icon), matching: find.byType(RichText)),
);
return iconRichText.text.style;
}

View file

@ -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_test/flutter_test.dart';
import 'package:flutter/rendering.dart';
@ -33,6 +34,8 @@ void main() {
});
testWidgets('Default RaisedButton meets a11y contrast guidelines', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
@ -40,6 +43,7 @@ void main() {
child: RaisedButton(
child: const Text('RaisedButton'),
onPressed: () { },
focusNode: focusNode,
),
),
),
@ -49,15 +53,207 @@ void main() {
// Default, not disabled.
await expectLater(tester, meetsGuideline(textContrastGuideline));
// Highlighted (pressed).
// Focused.
focusNode.requestFocus();
await tester.pumpAndSettle();
await expectLater(tester, meetsGuideline(textContrastGuideline));
// Hovered.
final Offset center = tester.getCenter(find.byType(RaisedButton));
await tester.startGesture(center);
final TestGesture gesture = await tester.createGesture(
kind: PointerDeviceKind.mouse,
);
await gesture.addPointer();
await gesture.moveTo(center);
await tester.pumpAndSettle();
await expectLater(tester, meetsGuideline(textContrastGuideline));
// Highlighted (pressed).
await gesture.down(center);
await tester.pump(); // Start the splash and highlight animations.
await tester.pump(const Duration(milliseconds: 800)); // Wait for splash and highlight to be well under way.
await expectLater(tester, meetsGuideline(textContrastGuideline));
await gesture.removePointer();
},
semanticsEnabled: true,
);
testWidgets('RaisedButton uses stateful color for text color in different states', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
const Color pressedColor = Color(1);
const Color hoverColor = Color(2);
const Color focusedColor = Color(3);
const Color defaultColor = Color(4);
Color getTextColor(Set<MaterialState> states) {
if (states.contains(MaterialState.pressed)) {
return pressedColor;
}
if (states.contains(MaterialState.hovered)) {
return hoverColor;
}
if (states.contains(MaterialState.focused)) {
return focusedColor;
}
return defaultColor;
}
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: RaisedButton(
child: const Text('RaisedButton'),
onPressed: () {},
focusNode: focusNode,
textColor: MaterialStateColor.resolveWith(getTextColor),
),
),
),
),
);
Color textColor() {
return tester.renderObject<RenderParagraph>(find.text('RaisedButton')).text.style.color;
}
// Default, not disabled.
expect(textColor(), equals(defaultColor));
// Focused.
focusNode.requestFocus();
await tester.pumpAndSettle();
expect(textColor(), focusedColor);
// Hovered.
final Offset center = tester.getCenter(find.byType(RaisedButton));
final TestGesture gesture = await tester.createGesture(
kind: PointerDeviceKind.mouse,
);
await gesture.addPointer();
await gesture.moveTo(center);
await tester.pumpAndSettle();
expect(textColor(), hoverColor);
// Highlighted (pressed).
await gesture.down(center);
await tester.pump(); // Start the splash and highlight animations.
await tester.pump(const Duration(milliseconds: 800)); // Wait for splash and highlight to be well under way.
expect(textColor(), pressedColor);
await gesture.removePointer();
});
testWidgets('RaisedButton uses stateful color for icon color in different states', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
final Key buttonKey = UniqueKey();
const Color pressedColor = Color(1);
const Color hoverColor = Color(2);
const Color focusedColor = Color(3);
const Color defaultColor = Color(4);
Color getTextColor(Set<MaterialState> states) {
if (states.contains(MaterialState.pressed)) {
return pressedColor;
}
if (states.contains(MaterialState.hovered)) {
return hoverColor;
}
if (states.contains(MaterialState.focused)) {
return focusedColor;
}
return defaultColor;
}
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: RaisedButton.icon(
key: buttonKey,
icon: const Icon(Icons.add),
label: const Text('RaisedButton'),
onPressed: () {},
focusNode: focusNode,
textColor: MaterialStateColor.resolveWith(getTextColor),
),
),
),
),
);
Color iconColor() => _iconStyle(tester, Icons.add).color;
// Default, not disabled.
expect(iconColor(), equals(defaultColor));
// Focused.
focusNode.requestFocus();
await tester.pumpAndSettle();
expect(iconColor(), focusedColor);
// Hovered.
final Offset center = tester.getCenter(find.byKey(buttonKey));
final TestGesture gesture = await tester.createGesture(
kind: PointerDeviceKind.mouse,
);
await gesture.addPointer();
await gesture.moveTo(center);
await tester.pumpAndSettle();
expect(iconColor(), hoverColor);
// Highlighted (pressed).
await gesture.down(center);
await tester.pump(); // Start the splash and highlight animations.
await tester.pump(const Duration(milliseconds: 800)); // Wait for splash and highlight to be well under way.
expect(iconColor(), pressedColor);
await gesture.removePointer();
});
testWidgets('RaisedButton ignores disabled text color if text color is stateful', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
const Color disabledColor = Color(1);
const Color defaultColor = Color(2);
const Color unusedDisabledTextColor = Color(3);
Color getTextColor(Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return disabledColor;
}
return defaultColor;
}
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: RaisedButton(
onPressed: null,
child: const Text('RaisedButton'),
focusNode: focusNode,
textColor: MaterialStateColor.resolveWith(getTextColor),
disabledTextColor: unusedDisabledTextColor,
),
),
),
),
);
Color textColor() {
return tester.renderObject<RenderParagraph>(find.text('RaisedButton')).text.style.color;
}
// Disabled.
expect(textColor(), equals(disabledColor));
expect(textColor(), isNot(unusedDisabledTextColor));
});
}
TextStyle _iconStyle(WidgetTester tester, IconData icon) {
final RichText iconRichText = tester.widget<RichText>(
find.descendant(of: find.byIcon(icon), matching: find.byType(RichText)),
);
return iconRichText.text.style;
}