WidgetStateInputBorder
(#157190)
**Changes** - Add `WidgetStateInputBorder` class, with `.resolveWith()` and `.fromMap()` constructors - Deprecate `MaterialStateOutlineInputBorder` and `MaterialStateUnderlineInputBorder` and provide data-driven fixes <br> **Other changes** based on https://github.com/flutter/flutter/pull/154972#pullrequestreview-2344092821 - Fix documentation copy-paste typo ("OutlinedBorder" â "InputBorder") - Add test to ensure borders are painted correctly - Add DartPad sample & relevant test
This commit is contained in:
parent
4af4a9b282
commit
ea0fda51ef
@ -0,0 +1,114 @@
|
||||
// Copyright 2014 The Flutter 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';
|
||||
|
||||
/// Flutter code sample for [WidgetStateInputBorder].
|
||||
|
||||
void main() => runApp(const WidgetStateInputBorderExampleApp());
|
||||
|
||||
/// This extension isn't necessary when WidgetState properties are
|
||||
/// configured using [WidgetStateMapper] objects.
|
||||
///
|
||||
/// But sometimes it makes sense to use a resolveWith() callback,
|
||||
/// and these getters make those callbacks a bit more readable!
|
||||
extension WidgetStateHelpers on Set<WidgetState> {
|
||||
bool get focused => contains(WidgetState.focused);
|
||||
bool get hovered => contains(WidgetState.hovered);
|
||||
bool get disabled => contains(WidgetState.disabled);
|
||||
}
|
||||
|
||||
class WidgetStateInputBorderExampleApp extends StatelessWidget {
|
||||
const WidgetStateInputBorderExampleApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
home: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('WidgetStateInputBorder Example'),
|
||||
),
|
||||
body: const Center(child: PageContent()),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PageContent extends StatefulWidget {
|
||||
const PageContent({super.key});
|
||||
|
||||
@override
|
||||
State<PageContent> createState() => _PageContentState();
|
||||
}
|
||||
|
||||
class _PageContentState extends State<PageContent> {
|
||||
bool enabled = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
const Spacer(flex: 8),
|
||||
Focus(
|
||||
child: WidgetStateInputBorderExample(enabled: enabled),
|
||||
),
|
||||
const Spacer(),
|
||||
FilterChip(
|
||||
label: const Text('enable text field'),
|
||||
selected: enabled,
|
||||
onSelected: (bool selected) {
|
||||
setState(() {
|
||||
enabled = selected;
|
||||
});
|
||||
},
|
||||
),
|
||||
const Spacer(flex: 8),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class WidgetStateInputBorderExample extends StatelessWidget {
|
||||
const WidgetStateInputBorderExample({super.key, required this.enabled});
|
||||
|
||||
final bool enabled;
|
||||
|
||||
/// A global or static function can be referenced in a `const` constructor,
|
||||
/// such as [WidgetStateInputBorder.resolveWith].
|
||||
///
|
||||
/// Constant values can be useful for promoting accurate equality checks,
|
||||
/// such as when rebuilding a [Theme] widget.
|
||||
static UnderlineInputBorder veryCoolBorder(Set<WidgetState> states) {
|
||||
if (states.disabled) {
|
||||
return const UnderlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.grey),
|
||||
);
|
||||
}
|
||||
|
||||
const Color dullViolet = Color(0xFF502080);
|
||||
|
||||
return UnderlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
width: states.hovered ? 6 : (states.focused ? 3 : 1.5),
|
||||
color: states.focused ? Colors.deepPurpleAccent : dullViolet,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final InputDecoration decoration = InputDecoration(
|
||||
border: const WidgetStateInputBorder.resolveWith(veryCoolBorder),
|
||||
labelText: enabled ? 'Type something awesome…' : '(click below to enable)',
|
||||
);
|
||||
|
||||
return AnimatedFractionallySizedBox(
|
||||
duration: Durations.medium1,
|
||||
curve: Curves.ease,
|
||||
widthFactor: Focus.of(context).hasFocus ? 0.9 : 0.6,
|
||||
child: TextField(decoration: decoration, enabled: enabled),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
// Copyright 2014 The Flutter 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/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_api_samples/material/widget_state_input_border/widget_state_input_border.0.dart' as example;
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('InputBorder appearance matches configuration', (WidgetTester tester) async {
|
||||
const WidgetStateInputBorder inputBorder = WidgetStateInputBorder.resolveWith(
|
||||
example.WidgetStateInputBorderExample.veryCoolBorder,
|
||||
);
|
||||
|
||||
void expectBorderToMatch(Set<WidgetState> states) {
|
||||
final RenderBox renderBox = tester.renderObject(
|
||||
find.descendant(
|
||||
of: find.byType(TextField),
|
||||
matching: find.byType(CustomPaint),
|
||||
),
|
||||
);
|
||||
|
||||
final BorderSide side = inputBorder.resolve(states).borderSide;
|
||||
expect(
|
||||
renderBox,
|
||||
paints..line(color: side.color, strokeWidth: side.width),
|
||||
);
|
||||
}
|
||||
|
||||
await tester.pumpWidget(
|
||||
const example.WidgetStateInputBorderExampleApp(),
|
||||
);
|
||||
expectBorderToMatch(const <WidgetState>{WidgetState.disabled});
|
||||
|
||||
await tester.tap(find.byType(FilterChip));
|
||||
await tester.pumpAndSettle();
|
||||
expectBorderToMatch(const <WidgetState>{});
|
||||
|
||||
await tester.tap(find.byType(TextField));
|
||||
await tester.pumpAndSettle();
|
||||
expectBorderToMatch(const <WidgetState>{WidgetState.focused});
|
||||
|
||||
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
|
||||
await gesture.addPointer(location: tester.getCenter(find.byType(TextField)));
|
||||
await tester.pumpAndSettle();
|
||||
expectBorderToMatch(const <WidgetState>{WidgetState.focused, WidgetState.hovered});
|
||||
});
|
||||
}
|
@ -26,6 +26,34 @@
|
||||
# * ThemeDate: fix_theme_data.yaml
|
||||
version: 1
|
||||
transforms:
|
||||
# Changes made in https://github.com/flutter/flutter/pull/154972
|
||||
- title: "Replace with 'WidgetStateInputBorder'"
|
||||
date: 2024-02-01
|
||||
element:
|
||||
uris: [ 'material.dart' ]
|
||||
constructor: 'resolveWith'
|
||||
inClass: 'MaterialStateOutlineInputBorder'
|
||||
changes:
|
||||
- kind: 'replacedBy'
|
||||
newElement:
|
||||
uris: [ 'material.dart' ]
|
||||
constructor: 'resolveWith'
|
||||
inClass: 'WidgetStateInputBorder'
|
||||
|
||||
# Changes made in https://github.com/flutter/flutter/pull/154972
|
||||
- title: "Replace with 'WidgetStateInputBorder'"
|
||||
date: 2024-02-01
|
||||
element:
|
||||
uris: [ 'material.dart' ]
|
||||
constructor: 'resolveWith'
|
||||
inClass: 'MaterialStateUnderlineInputBorder'
|
||||
changes:
|
||||
- kind: 'replacedBy'
|
||||
newElement:
|
||||
uris: [ 'material.dart' ]
|
||||
constructor: 'resolveWith'
|
||||
inClass: 'WidgetStateInputBorder'
|
||||
|
||||
# Changes made in https://github.com/flutter/flutter/pull/142151
|
||||
- title: "Replace with 'WidgetState'"
|
||||
date: 2024-02-01
|
||||
|
@ -12,6 +12,7 @@
|
||||
/// @docImport 'list_tile.dart';
|
||||
/// @docImport 'outlined_button.dart';
|
||||
/// @docImport 'text_button.dart';
|
||||
/// @docImport 'text_field.dart';
|
||||
/// @docImport 'time_picker_theme.dart';
|
||||
library;
|
||||
|
||||
@ -301,9 +302,19 @@ typedef MaterialStateTextStyle = WidgetStateTextStyle;
|
||||
/// [MaterialStateOutlineInputBorder] and override its [resolve] method. You'll also need
|
||||
/// to provide a `defaultValue` to the super constructor, so that we can know
|
||||
/// at compile-time what its default color is.
|
||||
@Deprecated(
|
||||
'Use WidgetStateInputBorder instead. '
|
||||
'Renamed to match other WidgetStateProperty objects. '
|
||||
'This feature was deprecated after v3.26.0-0.1.pre.'
|
||||
)
|
||||
abstract class MaterialStateOutlineInputBorder extends OutlineInputBorder implements MaterialStateProperty<InputBorder> {
|
||||
/// Abstract const constructor. This constructor enables subclasses to provide
|
||||
/// const constructors so that they can be used in const expressions.
|
||||
@Deprecated(
|
||||
'Use WidgetStateInputBorder instead. '
|
||||
'Renamed to match other WidgetStateProperty objects. '
|
||||
'This feature was deprecated after v3.26.0-0.1.pre.'
|
||||
)
|
||||
const MaterialStateOutlineInputBorder();
|
||||
|
||||
/// Creates a [MaterialStateOutlineInputBorder] from a [MaterialPropertyResolver<InputBorder>]
|
||||
@ -314,6 +325,11 @@ abstract class MaterialStateOutlineInputBorder extends OutlineInputBorder implem
|
||||
///
|
||||
/// The given callback parameter must return a non-null text style in the default
|
||||
/// state.
|
||||
@Deprecated(
|
||||
'Use WidgetStateInputBorder.resolveWith() instead. '
|
||||
'Renamed to match other WidgetStateProperty objects. '
|
||||
'This feature was deprecated after v3.26.0-0.1.pre.'
|
||||
)
|
||||
const factory MaterialStateOutlineInputBorder.resolveWith(MaterialPropertyResolver<InputBorder> callback) = _MaterialStateOutlineInputBorder;
|
||||
|
||||
/// Returns a [InputBorder] that's to be used when a Material component is in the
|
||||
@ -363,9 +379,19 @@ class _MaterialStateOutlineInputBorder extends MaterialStateOutlineInputBorder {
|
||||
/// [MaterialStateUnderlineInputBorder] and override its [resolve] method. You'll also need
|
||||
/// to provide a `defaultValue` to the super constructor, so that we can know
|
||||
/// at compile-time what its default color is.
|
||||
@Deprecated(
|
||||
'Use WidgetStateInputBorder instead. '
|
||||
'Renamed to match other WidgetStateProperty objects. '
|
||||
'This feature was deprecated after v3.26.0-0.1.pre.'
|
||||
)
|
||||
abstract class MaterialStateUnderlineInputBorder extends UnderlineInputBorder implements MaterialStateProperty<InputBorder> {
|
||||
/// Abstract const constructor. This constructor enables subclasses to provide
|
||||
/// const constructors so that they can be used in const expressions.
|
||||
@Deprecated(
|
||||
'Use WidgetStateInputBorder instead. '
|
||||
'Renamed to match other WidgetStateProperty objects. '
|
||||
'This feature was deprecated after v3.26.0-0.1.pre.'
|
||||
)
|
||||
const MaterialStateUnderlineInputBorder();
|
||||
|
||||
/// Creates a [MaterialStateUnderlineInputBorder] from a [MaterialPropertyResolver<InputBorder>]
|
||||
@ -376,6 +402,11 @@ abstract class MaterialStateUnderlineInputBorder extends UnderlineInputBorder im
|
||||
///
|
||||
/// The given callback parameter must return a non-null text style in the default
|
||||
/// state.
|
||||
@Deprecated(
|
||||
'Use WidgetStateInputBorder.resolveWith() instead. '
|
||||
'Renamed to match other WidgetStateProperty objects. '
|
||||
'This feature was deprecated after v3.26.0-0.1.pre.'
|
||||
)
|
||||
const factory MaterialStateUnderlineInputBorder.resolveWith(MaterialPropertyResolver<InputBorder> callback) = _MaterialStateUnderlineInputBorder;
|
||||
|
||||
/// Returns a [InputBorder] that's to be used when a Material component is in the
|
||||
@ -400,6 +431,66 @@ class _MaterialStateUnderlineInputBorder extends MaterialStateUnderlineInputBord
|
||||
InputBorder resolve(Set<MaterialState> states) => _resolve(states);
|
||||
}
|
||||
|
||||
/// Defines an [InputBorder] that is also a [WidgetStateProperty].
|
||||
///
|
||||
/// This class exists to enable widgets with [InputBorder] valued properties
|
||||
/// to also accept [WidgetStateProperty] objects.
|
||||
///
|
||||
/// [WidgetStateInputBorder] should only be used with widgets that document
|
||||
/// their support, like [InputDecoration.border].
|
||||
///
|
||||
/// A [WidgetStateInputBorder] can be created by:
|
||||
/// 1. Creating a class that extends [OutlineInputBorder] or [UnderlineInputBorder]
|
||||
/// and implements [WidgetStateInputBorder]. The class would also need to
|
||||
/// override the [resolve] method.
|
||||
/// 2. Using [WidgetStateInputBorder.resolveWith] with a callback that
|
||||
/// resolves the input border in the given states.
|
||||
/// 3. Using [WidgetStateInputBorder.fromMap] to assign a border with a [WidgetStateMap].
|
||||
///
|
||||
/// {@tool dartpad}
|
||||
/// This example shows how to use [WidgetStateInputBorder] to create
|
||||
/// a [TextField] with an appearance that responds to user interaction.
|
||||
///
|
||||
/// ** See code in examples/api/lib/material/widget_state_input_border/widget_state_input_border.0.dart **
|
||||
/// {@end-tool}
|
||||
abstract interface class WidgetStateInputBorder implements InputBorder, WidgetStateProperty<InputBorder> {
|
||||
/// Creates a [WidgetStateInputBorder] using a [WidgetPropertyResolver]
|
||||
/// callback.
|
||||
///
|
||||
/// This constructor should only be used for fields that support
|
||||
/// [WidgetStateInputBorder], such as [InputDecoration.border]
|
||||
/// (if used as a regular [InputBorder], it acts the same as
|
||||
/// an empty `OutlineInputBorder()` constructor).
|
||||
const factory WidgetStateInputBorder.resolveWith(
|
||||
WidgetPropertyResolver<InputBorder> callback,
|
||||
) = _WidgetStateInputBorder;
|
||||
|
||||
/// Creates a [WidgetStateOutlinedBorder] from a [WidgetStateMap].
|
||||
///
|
||||
/// {@macro flutter.widgets.WidgetStateProperty.fromMap}
|
||||
/// It should only be used for fields that support [WidgetStateOutlinedBorder]
|
||||
/// objects, such as [InputDecoration.border]
|
||||
/// (throws an error if used as a regular [OutlinedBorder]).
|
||||
///
|
||||
/// {@macro flutter.widgets.WidgetState.any}
|
||||
const factory WidgetStateInputBorder.fromMap(
|
||||
WidgetStateMap<InputBorder> map,
|
||||
) = _WidgetInputBorderMapper;
|
||||
}
|
||||
|
||||
class _WidgetStateInputBorder extends OutlineInputBorder implements WidgetStateInputBorder {
|
||||
const _WidgetStateInputBorder(this._resolve);
|
||||
|
||||
final WidgetPropertyResolver<InputBorder> _resolve;
|
||||
|
||||
@override
|
||||
InputBorder resolve(Set<WidgetState> states) => _resolve(states);
|
||||
}
|
||||
|
||||
class _WidgetInputBorderMapper extends WidgetStateMapper<InputBorder> implements WidgetStateInputBorder {
|
||||
const _WidgetInputBorderMapper(super.map);
|
||||
}
|
||||
|
||||
/// Interface for classes that [resolve] to a value of type `T` based
|
||||
/// on a widget's interactive "state", which is defined as a set
|
||||
/// of [MaterialState]s.
|
||||
|
@ -7189,6 +7189,57 @@ void main() {
|
||||
expect(decoration.constraints, const BoxConstraints(minWidth: 10, maxWidth: 20, minHeight: 30, maxHeight: 40));
|
||||
});
|
||||
|
||||
testWidgets('InputDecoration with WidgetStateInputBorder', (WidgetTester tester) async {
|
||||
const WidgetStateInputBorder outlineInputBorder = WidgetStateInputBorder.fromMap(
|
||||
<WidgetStatesConstraint, InputBorder>{
|
||||
WidgetState.focused: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.blue, width: 4.0),
|
||||
),
|
||||
WidgetState.hovered: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.cyan, width: 8.0),
|
||||
),
|
||||
WidgetState.any: OutlineInputBorder(),
|
||||
},
|
||||
);
|
||||
|
||||
RenderObject getBorder() {
|
||||
return tester.renderObject(
|
||||
find.descendant(
|
||||
of: find.byType(TextField),
|
||||
matching: find.byType(CustomPaint),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final FocusNode focusNode = FocusNode();
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: TextField(
|
||||
focusNode: focusNode,
|
||||
decoration: const InputDecoration(
|
||||
border: outlineInputBorder,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(getBorder(), paints..rrect(strokeWidth: 1.0));
|
||||
|
||||
focusNode.requestFocus();
|
||||
await tester.pumpAndSettle();
|
||||
expect(getBorder(), paints..rrect(color: Colors.blue, strokeWidth: 4.0));
|
||||
|
||||
focusNode.unfocus();
|
||||
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
|
||||
await gesture.addPointer(location: tester.getCenter(find.byType(TextField)));
|
||||
await tester.pumpAndSettle();
|
||||
expect(getBorder(), paints..rrect(color: Colors.cyan, strokeWidth: 8.0));
|
||||
|
||||
focusNode.dispose();
|
||||
});
|
||||
|
||||
testWidgets('InputDecorator constrained to 0x0', (WidgetTester tester) async {
|
||||
// Regression test for https://github.com/flutter/flutter/issues/17710
|
||||
await tester.pumpWidget(
|
||||
|
@ -306,4 +306,12 @@ void main() {
|
||||
|
||||
final PlatformMenuBar platformMenuBar = PlatformMenuBar(menus: <PlatformMenuItem>[], body: const SizedBox());
|
||||
final Widget bodyValue = platformMenuBar.body;
|
||||
|
||||
// Changes made in https://github.com/flutter/flutter/pull/154972
|
||||
final InputBorder outlineBorder = MaterialStateOutlineInputBorder.resolveWith(
|
||||
(states) => const OutlineInputBorder(),
|
||||
);
|
||||
final InputBorder underlineBorder = MaterialStateUnderlineInputBorder.resolveWith(
|
||||
(states) => const UnderlineInputBorder(),
|
||||
);
|
||||
}
|
||||
|
@ -302,4 +302,12 @@ void main() {
|
||||
|
||||
final PlatformMenuBar platformMenuBar = PlatformMenuBar(menus: <PlatformMenuItem>[], child: const SizedBox());
|
||||
final Widget bodyValue = platformMenuBar.child;
|
||||
|
||||
// Changes made in https://github.com/flutter/flutter/pull/154972
|
||||
final InputBorder outlineBorder = WidgetStateInputBorder.resolveWith(
|
||||
(states) => const OutlineInputBorder(),
|
||||
);
|
||||
final InputBorder underlineBorder = WidgetStateInputBorder.resolveWith(
|
||||
(states) => const UnderlineInputBorder(),
|
||||
);
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user