Fix various problems with Chip delete button. (#90531)
This commit is contained in:
parent
6fb7381427
commit
cf89a787a9
@ -11,7 +11,6 @@ import 'chip_theme.dart';
|
|||||||
import 'colors.dart';
|
import 'colors.dart';
|
||||||
import 'constants.dart';
|
import 'constants.dart';
|
||||||
import 'debug.dart';
|
import 'debug.dart';
|
||||||
import 'feedback.dart';
|
|
||||||
import 'icons.dart';
|
import 'icons.dart';
|
||||||
import 'ink_well.dart';
|
import 'ink_well.dart';
|
||||||
import 'material.dart';
|
import 'material.dart';
|
||||||
@ -1589,8 +1588,6 @@ class _RawChipState extends State<RawChip> with MaterialStateMixin, TickerProvid
|
|||||||
late Animation<double> enableAnimation;
|
late Animation<double> enableAnimation;
|
||||||
late Animation<double> selectionFade;
|
late Animation<double> selectionFade;
|
||||||
|
|
||||||
final GlobalKey deleteIconKey = GlobalKey();
|
|
||||||
|
|
||||||
bool get hasDeleteButton => widget.onDeleted != null;
|
bool get hasDeleteButton => widget.onDeleted != null;
|
||||||
bool get hasAvatar => widget.avatar != null;
|
bool get hasAvatar => widget.avatar != null;
|
||||||
|
|
||||||
@ -1795,7 +1792,6 @@ class _RawChipState extends State<RawChip> with MaterialStateMixin, TickerProvid
|
|||||||
BuildContext context,
|
BuildContext context,
|
||||||
ThemeData theme,
|
ThemeData theme,
|
||||||
ChipThemeData chipTheme,
|
ChipThemeData chipTheme,
|
||||||
GlobalKey deleteIconKey,
|
|
||||||
) {
|
) {
|
||||||
if (!hasDeleteButton) {
|
if (!hasDeleteButton) {
|
||||||
return null;
|
return null;
|
||||||
@ -1806,15 +1802,12 @@ class _RawChipState extends State<RawChip> with MaterialStateMixin, TickerProvid
|
|||||||
child: _wrapWithTooltip(
|
child: _wrapWithTooltip(
|
||||||
widget.deleteButtonTooltipMessage ?? MaterialLocalizations.of(context).deleteButtonTooltip,
|
widget.deleteButtonTooltipMessage ?? MaterialLocalizations.of(context).deleteButtonTooltip,
|
||||||
widget.onDeleted,
|
widget.onDeleted,
|
||||||
GestureDetector(
|
InkWell(
|
||||||
key: deleteIconKey,
|
// Radius should be slightly less than the full size of the chip.
|
||||||
behavior: HitTestBehavior.opaque,
|
radius: (_kChipHeight + (widget.padding?.vertical ?? 0.0)) * .45,
|
||||||
onTap: widget.isEnabled
|
// Keeps the splash from being constrained to the icon alone.
|
||||||
? () {
|
splashFactory: _UnconstrainedInkSplashFactory(Theme.of(context).splashFactory),
|
||||||
Feedback.forTap(context);
|
onTap: widget.isEnabled ? widget.onDeleted : null,
|
||||||
widget.onDeleted!();
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
child: IconTheme(
|
child: IconTheme(
|
||||||
data: theme.iconTheme.copyWith(
|
data: theme.iconTheme.copyWith(
|
||||||
color: widget.deleteIconColor ?? chipTheme.deleteIconColor,
|
color: widget.deleteIconColor ?? chipTheme.deleteIconColor,
|
||||||
@ -1878,11 +1871,6 @@ class _RawChipState extends State<RawChip> with MaterialStateMixin, TickerProvid
|
|||||||
onTapDown: canTap ? _handleTapDown : null,
|
onTapDown: canTap ? _handleTapDown : null,
|
||||||
onTapCancel: canTap ? _handleTapCancel : null,
|
onTapCancel: canTap ? _handleTapCancel : null,
|
||||||
onHover: canTap ? updateMaterialState(MaterialState.hovered) : null,
|
onHover: canTap ? updateMaterialState(MaterialState.hovered) : null,
|
||||||
splashFactory: _LocationAwareInkRippleFactory(
|
|
||||||
hasDeleteButton,
|
|
||||||
context,
|
|
||||||
deleteIconKey,
|
|
||||||
),
|
|
||||||
customBorder: resolvedShape,
|
customBorder: resolvedShape,
|
||||||
child: AnimatedBuilder(
|
child: AnimatedBuilder(
|
||||||
animation: Listenable.merge(<Listenable>[selectController, enableController]),
|
animation: Listenable.merge(<Listenable>[selectController, enableController]),
|
||||||
@ -1916,7 +1904,7 @@ class _RawChipState extends State<RawChip> with MaterialStateMixin, TickerProvid
|
|||||||
deleteIcon: AnimatedSwitcher(
|
deleteIcon: AnimatedSwitcher(
|
||||||
duration: _kDrawerDuration,
|
duration: _kDrawerDuration,
|
||||||
switchInCurve: Curves.fastOutSlowIn,
|
switchInCurve: Curves.fastOutSlowIn,
|
||||||
child: _buildDeleteIcon(context, theme, chipTheme, deleteIconKey),
|
child: _buildDeleteIcon(context, theme, chipTheme),
|
||||||
),
|
),
|
||||||
brightness: chipTheme.brightness,
|
brightness: chipTheme.brightness,
|
||||||
padding: (widget.padding ?? chipTheme.padding).resolve(textDirection),
|
padding: (widget.padding ?? chipTheme.padding).resolve(textDirection),
|
||||||
@ -2531,13 +2519,14 @@ class _RenderChip extends RenderBox {
|
|||||||
if (!size.contains(position)) {
|
if (!size.contains(position)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
final bool tapIsOnDeleteIcon = _tapIsOnDeleteIcon(
|
final bool hitIsOnDeleteIcon = deleteIcon != null && _hitIsOnDeleteIcon(
|
||||||
hasDeleteButton: deleteIcon != null,
|
padding: theme.padding,
|
||||||
tapPosition: position,
|
tapPosition: position,
|
||||||
chipSize: size,
|
chipSize: size,
|
||||||
|
deleteButtonSize: deleteIcon!.size,
|
||||||
textDirection: textDirection!,
|
textDirection: textDirection!,
|
||||||
);
|
);
|
||||||
final RenderBox? hitTestChild = tapIsOnDeleteIcon
|
final RenderBox? hitTestChild = hitIsOnDeleteIcon
|
||||||
? (deleteIcon ?? label ?? avatar)
|
? (deleteIcon ?? label ?? avatar)
|
||||||
: (label ?? avatar);
|
: (label ?? avatar);
|
||||||
|
|
||||||
@ -2924,16 +2913,10 @@ class _ChipSizes {
|
|||||||
final Offset densityAdjustment;
|
final Offset densityAdjustment;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _LocationAwareInkRippleFactory extends InteractiveInkFeatureFactory {
|
class _UnconstrainedInkSplashFactory extends InteractiveInkFeatureFactory {
|
||||||
const _LocationAwareInkRippleFactory(
|
const _UnconstrainedInkSplashFactory(this.parentFactory);
|
||||||
this.hasDeleteButton,
|
|
||||||
this.chipContext,
|
|
||||||
this.deleteIconKey,
|
|
||||||
);
|
|
||||||
|
|
||||||
final bool hasDeleteButton;
|
final InteractiveInkFeatureFactory parentFactory;
|
||||||
final BuildContext chipContext;
|
|
||||||
final GlobalKey deleteIconKey;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
InteractiveInkFeature create({
|
InteractiveInkFeature create({
|
||||||
@ -2949,61 +2932,55 @@ class _LocationAwareInkRippleFactory extends InteractiveInkFeatureFactory {
|
|||||||
double? radius,
|
double? radius,
|
||||||
VoidCallback? onRemoved,
|
VoidCallback? onRemoved,
|
||||||
}) {
|
}) {
|
||||||
|
return parentFactory.create(
|
||||||
final bool tapIsOnDeleteIcon = _tapIsOnDeleteIcon(
|
|
||||||
hasDeleteButton: hasDeleteButton,
|
|
||||||
tapPosition: position,
|
|
||||||
chipSize: chipContext.size!,
|
|
||||||
textDirection: textDirection,
|
|
||||||
);
|
|
||||||
|
|
||||||
final BuildContext splashContext = tapIsOnDeleteIcon
|
|
||||||
? deleteIconKey.currentContext!
|
|
||||||
: chipContext;
|
|
||||||
|
|
||||||
final InteractiveInkFeatureFactory splashFactory = Theme.of(splashContext).splashFactory;
|
|
||||||
|
|
||||||
if (tapIsOnDeleteIcon) {
|
|
||||||
final RenderBox currentBox = referenceBox;
|
|
||||||
referenceBox = deleteIconKey.currentContext!.findRenderObject()! as RenderBox;
|
|
||||||
position = referenceBox.globalToLocal(currentBox.localToGlobal(position));
|
|
||||||
containedInkWell = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return splashFactory.create(
|
|
||||||
controller: controller,
|
controller: controller,
|
||||||
referenceBox: referenceBox,
|
referenceBox: referenceBox,
|
||||||
position: position,
|
position: position,
|
||||||
color: color,
|
color: color,
|
||||||
textDirection: textDirection,
|
containedInkWell: false,
|
||||||
containedInkWell: containedInkWell,
|
|
||||||
rectCallback: rectCallback,
|
rectCallback: rectCallback,
|
||||||
borderRadius: borderRadius,
|
borderRadius: borderRadius,
|
||||||
customBorder: customBorder,
|
customBorder: customBorder,
|
||||||
radius: radius,
|
radius: radius,
|
||||||
onRemoved: onRemoved,
|
onRemoved: onRemoved,
|
||||||
|
textDirection: textDirection,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _tapIsOnDeleteIcon({
|
bool _hitIsOnDeleteIcon({
|
||||||
required bool hasDeleteButton,
|
required EdgeInsetsGeometry padding,
|
||||||
required Offset tapPosition,
|
required Offset tapPosition,
|
||||||
required Size chipSize,
|
required Size chipSize,
|
||||||
|
required Size deleteButtonSize,
|
||||||
required TextDirection textDirection,
|
required TextDirection textDirection,
|
||||||
}) {
|
}) {
|
||||||
bool tapIsOnDeleteIcon;
|
// The chipSize includes the padding, so we need to deflate the size and adjust the
|
||||||
if (!hasDeleteButton) {
|
// tap position to account for the padding.
|
||||||
tapIsOnDeleteIcon = false;
|
final EdgeInsets resolvedPadding = padding.resolve(textDirection);
|
||||||
} else {
|
final Size deflatedSize = resolvedPadding.deflateSize(chipSize);
|
||||||
|
final Offset adjustedPosition = tapPosition - Offset(resolvedPadding.left, resolvedPadding.top);
|
||||||
|
// The delete button hit area should be at least the width of the delete button,
|
||||||
|
// but, if there's room, up to 24 pixels from the center of the delete icon
|
||||||
|
// (corresponding to part of a 48x48 square that Material would prefer for touch
|
||||||
|
// targets), but no more than half of the overall size of the chip when the chip is
|
||||||
|
// small.
|
||||||
|
//
|
||||||
|
// This isn't affected by materialTapTargetSize because it only applies to the
|
||||||
|
// width of the tappable region within the chip, not outside of the chip, which is
|
||||||
|
// handled elsewhere. Also because delete buttons aren't specified to be used on
|
||||||
|
// touch devices, only desktop devices.
|
||||||
|
final double accessibleDeleteButtonWidth = math.max(
|
||||||
|
deleteButtonSize.width,
|
||||||
|
math.min(
|
||||||
|
deflatedSize.width * 0.5,
|
||||||
|
24.0 + deleteButtonSize.width / 2.0,
|
||||||
|
),
|
||||||
|
);
|
||||||
switch (textDirection) {
|
switch (textDirection) {
|
||||||
case TextDirection.ltr:
|
case TextDirection.ltr:
|
||||||
tapIsOnDeleteIcon = tapPosition.dx / chipSize.width > 0.66;
|
return adjustedPosition.dx >= deflatedSize.width - accessibleDeleteButtonWidth;
|
||||||
break;
|
|
||||||
case TextDirection.rtl:
|
case TextDirection.rtl:
|
||||||
tapIsOnDeleteIcon = tapPosition.dx / chipSize.width < 0.33;
|
return adjustedPosition.dx <= accessibleDeleteButtonWidth;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return tapIsOnDeleteIcon;
|
|
||||||
}
|
|
||||||
|
@ -191,20 +191,23 @@ void _expectCheckmarkColor(Finder finder, Color color) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _doNothing() {}
|
||||||
|
|
||||||
Widget _chipWithOptionalDeleteButton({
|
Widget _chipWithOptionalDeleteButton({
|
||||||
UniqueKey? deleteButtonKey,
|
Key? deleteButtonKey,
|
||||||
UniqueKey? labelKey,
|
Key? labelKey,
|
||||||
required bool deletable,
|
required bool deletable,
|
||||||
TextDirection textDirection = TextDirection.ltr,
|
TextDirection textDirection = TextDirection.ltr,
|
||||||
bool hasDeleteButtonTooltip = true,
|
bool hasDeleteButtonTooltip = true,
|
||||||
|
VoidCallback? onPressed = _doNothing,
|
||||||
}) {
|
}) {
|
||||||
return _wrapForChip(
|
return _wrapForChip(
|
||||||
textDirection: textDirection,
|
textDirection: textDirection,
|
||||||
child: Wrap(
|
child: Wrap(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
RawChip(
|
RawChip(
|
||||||
onPressed: () {},
|
onPressed: onPressed,
|
||||||
onDeleted: deletable ? () {} : null,
|
onDeleted: deletable ? _doNothing : null,
|
||||||
deleteIcon: Icon(Icons.close, key: deleteButtonKey),
|
deleteIcon: Icon(Icons.close, key: deleteButtonKey),
|
||||||
useDeleteButtonTooltip: hasDeleteButtonTooltip,
|
useDeleteButtonTooltip: hasDeleteButtonTooltip,
|
||||||
label: Text(
|
label: Text(
|
||||||
@ -526,6 +529,59 @@ void main() {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
testWidgets('delete button tap target is the right proportion of the chip', (WidgetTester tester) async {
|
||||||
|
final UniqueKey deleteKey = UniqueKey();
|
||||||
|
bool calledDelete = false;
|
||||||
|
await tester.pumpWidget(
|
||||||
|
_wrapForChip(
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
Chip(
|
||||||
|
label: const Text('Really Long Label'),
|
||||||
|
deleteIcon: Icon(Icons.delete, key: deleteKey),
|
||||||
|
onDeleted: () {
|
||||||
|
calledDelete = true;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.tapAt(tester.getCenter(find.byKey(deleteKey)) - const Offset(24.0, 0.0));
|
||||||
|
await tester.pump();
|
||||||
|
expect(calledDelete, isTrue);
|
||||||
|
calledDelete = false;
|
||||||
|
|
||||||
|
await tester.tapAt(tester.getCenter(find.byKey(deleteKey)) - const Offset(25.0, 0.0));
|
||||||
|
await tester.pump();
|
||||||
|
expect(calledDelete, isFalse);
|
||||||
|
calledDelete = false;
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
_wrapForChip(
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
Chip(
|
||||||
|
label: const SizedBox(), // Short label
|
||||||
|
deleteIcon: Icon(Icons.delete, key: deleteKey),
|
||||||
|
onDeleted: () {
|
||||||
|
calledDelete = true;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.tapAt(tester.getCenter(find.byKey(deleteKey)) - const Offset(12.0, 0.0));
|
||||||
|
await tester.pump();
|
||||||
|
expect(calledDelete, isTrue);
|
||||||
|
calledDelete = false;
|
||||||
|
|
||||||
|
await tester.tapAt(tester.getCenter(find.byKey(deleteKey)) - const Offset(13.0, 0.0));
|
||||||
|
await tester.pump();
|
||||||
|
expect(calledDelete, isFalse);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('Chip elements are ordered horizontally for locale', (WidgetTester tester) async {
|
testWidgets('Chip elements are ordered horizontally for locale', (WidgetTester tester) async {
|
||||||
final UniqueKey iconKey = UniqueKey();
|
final UniqueKey iconKey = UniqueKey();
|
||||||
final Widget test = Overlay(
|
final Widget test = Overlay(
|
||||||
@ -1022,7 +1078,6 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('Chip creates centered, unique ripple when label is tapped', (WidgetTester tester) async {
|
testWidgets('Chip creates centered, unique ripple when label is tapped', (WidgetTester tester) async {
|
||||||
// Creates a chip with a delete button.
|
|
||||||
final UniqueKey labelKey = UniqueKey();
|
final UniqueKey labelKey = UniqueKey();
|
||||||
final UniqueKey deleteButtonKey = UniqueKey();
|
final UniqueKey deleteButtonKey = UniqueKey();
|
||||||
|
|
||||||
@ -1045,10 +1100,6 @@ void main() {
|
|||||||
// Waits for 100 ms.
|
// Waits for 100 ms.
|
||||||
await tester.pump(const Duration(milliseconds: 100));
|
await tester.pump(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
// There should be exactly one ink-creating widget.
|
|
||||||
expect(find.byType(InkWell), findsOneWidget);
|
|
||||||
expect(find.byType(InkResponse), findsNothing);
|
|
||||||
|
|
||||||
// There should be one unique, centered ink ripple.
|
// There should be one unique, centered ink ripple.
|
||||||
expect(box, ripplePattern(const Offset(163.0, 6.0), 20.9));
|
expect(box, ripplePattern(const Offset(163.0, 6.0), 20.9));
|
||||||
expect(box, uniqueRipplePattern(const Offset(163.0, 6.0), 20.9));
|
expect(box, uniqueRipplePattern(const Offset(163.0, 6.0), 20.9));
|
||||||
@ -1075,8 +1126,40 @@ void main() {
|
|||||||
await gesture.up();
|
await gesture.up();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('Delete button is focusable', (WidgetTester tester) async {
|
||||||
|
final GlobalKey labelKey = GlobalKey();
|
||||||
|
final GlobalKey deleteButtonKey = GlobalKey();
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
_chipWithOptionalDeleteButton(
|
||||||
|
labelKey: labelKey,
|
||||||
|
deleteButtonKey: deleteButtonKey,
|
||||||
|
deletable: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Focus.of(deleteButtonKey.currentContext!).requestFocus();
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
// They shouldn't have the same focus node.
|
||||||
|
expect(Focus.of(deleteButtonKey.currentContext!), isNot(equals(Focus.of(labelKey.currentContext!))));
|
||||||
|
expect(Focus.of(deleteButtonKey.currentContext!).hasFocus, isTrue);
|
||||||
|
expect(Focus.of(deleteButtonKey.currentContext!).hasPrimaryFocus, isTrue);
|
||||||
|
// Delete button is a child widget of the Chip, so the Chip should have focus if
|
||||||
|
// the delete button does.
|
||||||
|
expect(Focus.of(labelKey.currentContext!).hasFocus, isTrue);
|
||||||
|
expect(Focus.of(labelKey.currentContext!).hasPrimaryFocus, isFalse);
|
||||||
|
|
||||||
|
Focus.of(labelKey.currentContext!).requestFocus();
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(Focus.of(deleteButtonKey.currentContext!).hasFocus, isFalse);
|
||||||
|
expect(Focus.of(deleteButtonKey.currentContext!).hasPrimaryFocus, isFalse);
|
||||||
|
expect(Focus.of(labelKey.currentContext!).hasFocus, isTrue);
|
||||||
|
expect(Focus.of(labelKey.currentContext!).hasPrimaryFocus, isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('Delete button creates non-centered, unique ripple when tapped', (WidgetTester tester) async {
|
testWidgets('Delete button creates non-centered, unique ripple when tapped', (WidgetTester tester) async {
|
||||||
// Creates a chip with a delete button.
|
|
||||||
final UniqueKey labelKey = UniqueKey();
|
final UniqueKey labelKey = UniqueKey();
|
||||||
final UniqueKey deleteButtonKey = UniqueKey();
|
final UniqueKey deleteButtonKey = UniqueKey();
|
||||||
|
|
||||||
@ -1100,13 +1183,9 @@ void main() {
|
|||||||
await tester.pump(const Duration(milliseconds: 100));
|
await tester.pump(const Duration(milliseconds: 100));
|
||||||
await tester.pump(const Duration(milliseconds: 100));
|
await tester.pump(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
// There should be exactly one ink-creating widget.
|
|
||||||
expect(find.byType(InkWell), findsOneWidget);
|
|
||||||
expect(find.byType(InkResponse), findsNothing);
|
|
||||||
|
|
||||||
// There should be one unique ink ripple.
|
// There should be one unique ink ripple.
|
||||||
expect(box, ripplePattern(const Offset(3.0, 3.0), 3.5));
|
expect(box, ripplePattern(const Offset(3.0, 3.0), 1.44));
|
||||||
expect(box, uniqueRipplePattern(const Offset(3.0, 3.0), 3.5));
|
expect(box, uniqueRipplePattern(const Offset(3.0, 3.0), 1.44));
|
||||||
|
|
||||||
// There should be no tooltip.
|
// There should be no tooltip.
|
||||||
expect(findTooltipContainer('Delete'), findsNothing);
|
expect(findTooltipContainer('Delete'), findsNothing);
|
||||||
@ -1117,8 +1196,62 @@ void main() {
|
|||||||
|
|
||||||
// The ripple should grow, but the center should move,
|
// The ripple should grow, but the center should move,
|
||||||
// Towards the center of the delete icon.
|
// Towards the center of the delete icon.
|
||||||
expect(box, ripplePattern(const Offset(5.0, 5.0), 10.5));
|
expect(box, ripplePattern(const Offset(5.0, 5.0), 4.32));
|
||||||
expect(box, uniqueRipplePattern(const Offset(5.0, 5.0), 10.5));
|
expect(box, uniqueRipplePattern(const Offset(5.0, 5.0), 4.32));
|
||||||
|
|
||||||
|
// There should be no tooltip.
|
||||||
|
expect(findTooltipContainer('Delete'), findsNothing);
|
||||||
|
|
||||||
|
// Waits for a very long time.
|
||||||
|
// This is pressing and holding the delete button.
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// There should be a tooltip.
|
||||||
|
expect(findTooltipContainer('Delete'), findsOneWidget);
|
||||||
|
|
||||||
|
await gesture.up();
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Delete button in a chip with null onPressed creates ripple when tapped', (WidgetTester tester) async {
|
||||||
|
final UniqueKey labelKey = UniqueKey();
|
||||||
|
final UniqueKey deleteButtonKey = UniqueKey();
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
_chipWithOptionalDeleteButton(
|
||||||
|
labelKey: labelKey,
|
||||||
|
onPressed: null,
|
||||||
|
deleteButtonKey: deleteButtonKey,
|
||||||
|
deletable: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final RenderBox box = getMaterialBox(tester);
|
||||||
|
|
||||||
|
// Taps at a location close to the center of the delete icon.
|
||||||
|
final Offset centerOfDeleteButton = tester.getCenter(find.byKey(deleteButtonKey));
|
||||||
|
final Offset tapLocationOfDeleteButton = centerOfDeleteButton + const Offset(-10, -10);
|
||||||
|
final TestGesture gesture = await tester.startGesture(tapLocationOfDeleteButton);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
// Waits for 200 ms.
|
||||||
|
await tester.pump(const Duration(milliseconds: 100));
|
||||||
|
await tester.pump(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
|
// There should be one unique ink ripple.
|
||||||
|
expect(box, ripplePattern(const Offset(3.0, 3.0), 1.44));
|
||||||
|
expect(box, uniqueRipplePattern(const Offset(3.0, 3.0), 1.44));
|
||||||
|
|
||||||
|
// There should be no tooltip.
|
||||||
|
expect(findTooltipContainer('Delete'), findsNothing);
|
||||||
|
|
||||||
|
// Waits for 200 ms again.
|
||||||
|
await tester.pump(const Duration(milliseconds: 100));
|
||||||
|
await tester.pump(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
|
// The ripple should grow, but the center should move,
|
||||||
|
// Towards the center of the delete icon.
|
||||||
|
expect(box, ripplePattern(const Offset(5.0, 5.0), 4.32));
|
||||||
|
expect(box, uniqueRipplePattern(const Offset(5.0, 5.0), 4.32));
|
||||||
|
|
||||||
// There should be no tooltip.
|
// There should be no tooltip.
|
||||||
expect(findTooltipContainer('Delete'), findsNothing);
|
expect(findTooltipContainer('Delete'), findsNothing);
|
||||||
@ -1149,7 +1282,7 @@ void main() {
|
|||||||
|
|
||||||
// Taps at a location close to the center of the delete icon,
|
// Taps at a location close to the center of the delete icon,
|
||||||
// Which is on the left side of the chip.
|
// Which is on the left side of the chip.
|
||||||
final Offset topLeftOfInkWell = tester.getTopLeft(find.byType(InkWell));
|
final Offset topLeftOfInkWell = tester.getTopLeft(find.byType(InkWell).first);
|
||||||
final Offset tapLocation = topLeftOfInkWell + const Offset(8, 8);
|
final Offset tapLocation = topLeftOfInkWell + const Offset(8, 8);
|
||||||
final TestGesture gesture = await tester.startGesture(tapLocation);
|
final TestGesture gesture = await tester.startGesture(tapLocation);
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
@ -1236,7 +1369,7 @@ void main() {
|
|||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
selected: selected,
|
selected: selected,
|
||||||
label: Text('Chip', key: labelKey),
|
label: Text('Long Chip Label', key: labelKey),
|
||||||
shape: const StadiumBorder(),
|
shape: const StadiumBorder(),
|
||||||
showCheckmark: true,
|
showCheckmark: true,
|
||||||
tapEnabled: true,
|
tapEnabled: true,
|
||||||
@ -1254,7 +1387,7 @@ void main() {
|
|||||||
await pushChip(
|
await pushChip(
|
||||||
avatar: SizedBox(width: 40.0, height: 40.0, key: avatarKey),
|
avatar: SizedBox(width: 40.0, height: 40.0, key: avatarKey),
|
||||||
);
|
);
|
||||||
expect(tester.getSize(find.byType(RawChip)), equals(const Size(104.0, 48.0)));
|
expect(tester.getSize(find.byType(RawChip)), equals(const Size(258.0, 48.0)));
|
||||||
|
|
||||||
// Turn on selection.
|
// Turn on selection.
|
||||||
await pushChip(
|
await pushChip(
|
||||||
@ -1318,7 +1451,7 @@ void main() {
|
|||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
selected: selected,
|
selected: selected,
|
||||||
label: Text('Chip', key: labelKey),
|
label: Text('Long Chip Label', key: labelKey),
|
||||||
shape: const StadiumBorder(),
|
shape: const StadiumBorder(),
|
||||||
showCheckmark: true,
|
showCheckmark: true,
|
||||||
tapEnabled: true,
|
tapEnabled: true,
|
||||||
@ -1333,7 +1466,7 @@ void main() {
|
|||||||
|
|
||||||
// Without avatar, but not selectable.
|
// Without avatar, but not selectable.
|
||||||
await pushChip();
|
await pushChip();
|
||||||
expect(tester.getSize(find.byType(RawChip)), equals(const Size(80.0, 48.0)));
|
expect(tester.getSize(find.byType(RawChip)), equals(const Size(234.0, 48.0)));
|
||||||
|
|
||||||
// Turn on selection.
|
// Turn on selection.
|
||||||
await pushChip(selectable: true);
|
await pushChip(selectable: true);
|
||||||
@ -1395,7 +1528,7 @@ void main() {
|
|||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
selected: selected,
|
selected: selected,
|
||||||
label: Text('Chip', key: labelKey),
|
label: Text('Long Chip Label', key: labelKey),
|
||||||
shape: const StadiumBorder(),
|
shape: const StadiumBorder(),
|
||||||
showCheckmark: false,
|
showCheckmark: false,
|
||||||
tapEnabled: true,
|
tapEnabled: true,
|
||||||
@ -1737,6 +1870,7 @@ void main() {
|
|||||||
textDirection: TextDirection.ltr,
|
textDirection: TextDirection.ltr,
|
||||||
flags: <SemanticsFlag>[
|
flags: <SemanticsFlag>[
|
||||||
SemanticsFlag.isButton,
|
SemanticsFlag.isButton,
|
||||||
|
SemanticsFlag.isFocusable,
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -3196,7 +3330,7 @@ void main() {
|
|||||||
|
|
||||||
// Tap at the delete icon of the chip, which is at the right
|
// Tap at the delete icon of the chip, which is at the right
|
||||||
// side of the chip
|
// side of the chip
|
||||||
final Offset topRightOfInkwell = tester.getTopLeft(find.byType(InkWell));
|
final Offset topRightOfInkwell = tester.getTopLeft(find.byType(InkWell).first);
|
||||||
final Offset tapLocationOfDeleteButton = topRightOfInkwell + const Offset(8, 8);
|
final Offset tapLocationOfDeleteButton = topRightOfInkwell + const Offset(8, 8);
|
||||||
final TestGesture tapGesture = await tester.startGesture(tapLocationOfDeleteButton);
|
final TestGesture tapGesture = await tester.startGesture(tapLocationOfDeleteButton);
|
||||||
|
|
||||||
@ -3212,7 +3346,7 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('intrinsicHeight implementation meets constraints', (WidgetTester tester) async {
|
testWidgets('intrinsicHeight implementation meets constraints', (WidgetTester tester) async {
|
||||||
// Regression text for https://github.com/flutter/flutter/issues/49478.
|
// Regression test for https://github.com/flutter/flutter/issues/49478.
|
||||||
await tester.pumpWidget(_wrapForChip(
|
await tester.pumpWidget(_wrapForChip(
|
||||||
child: const Chip(
|
child: const Chip(
|
||||||
label: Text('text'),
|
label: Text('text'),
|
||||||
|
@ -1399,8 +1399,7 @@ class _SomethingPaintPredicate extends _PaintPredicate {
|
|||||||
currentCall = call.current;
|
currentCall = call.current;
|
||||||
if (!currentCall.invocation.isMethod)
|
if (!currentCall.invocation.isMethod)
|
||||||
throw 'It called $currentCall, which was not a method, when the paint pattern expected a method call';
|
throw 'It called $currentCall, which was not a method, when the paint pattern expected a method call';
|
||||||
call.moveNext();
|
} while (call.moveNext() && !_runPredicate(currentCall.invocation.memberName, currentCall.invocation.positionalArguments));
|
||||||
} while (!_runPredicate(currentCall.invocation.memberName, currentCall.invocation.positionalArguments));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _runPredicate(Symbol methodName, List<dynamic> arguments) {
|
bool _runPredicate(Symbol methodName, List<dynamic> arguments) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user