mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
fix https://github.com/flutter/flutter/issues/121493 `SegmentedButton` uses `TextButton` for each segments. When we have `MaterialTapTargetSize.padded` for `TextButton`, we make sure the minimum tap target size is 48.0( this value can be adjusted by visual density), even tough the actual button size is smaller. When `SegmentedButton` paints segments by using `MultiChildRenderObjectWidget`, it also includes the tap target size so the button that it actually draws always has the same height as the height of the tap target size. To fix it, this PR firstly calculate the actual height of a text button in `SegmentedButton` class, then we can get the height delta if there is. Then the the value of (Segmented button render box height - the delta) would be the actual button size that we should see. For now, we are not able to customize the min, max, fixed size in [`SegmentedButton` style](https://api.flutter.dev/flutter/material/SegmentedButton/style.html). So the standard button height is always 40 and can only be customized by `style.visualDensity` and `style.tapTargetSize`; `SegmentedButton` only simulates the `TextButton` behavior when `TextButton`'s height is its default value.  https://github.com/flutter/flutter/assets/36861262/15ca6034-e6e0-4cc6-8fe3-808b4bd6a920
863 lines
30 KiB
Dart
863 lines
30 KiB
Dart
// 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.
|
|
|
|
// This file is run as part of a reduced test set in CI on Mac and Windows
|
|
// machines.
|
|
import 'dart:ui';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/rendering.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
import '../widgets/semantics_tester.dart';
|
|
|
|
Widget boilerplate({required Widget child}) {
|
|
return Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Center(child: child),
|
|
);
|
|
}
|
|
|
|
void main() {
|
|
testWidgets('SegmentsButton when compositing does not crash', (WidgetTester tester) async {
|
|
// Regression test for https://github.com/flutter/flutter/issues/135747
|
|
// If the render object holds on to a stale canvas reference, this will
|
|
// throw an exception.
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: SegmentedButton<int>(
|
|
segments: const <ButtonSegment<int>>[
|
|
ButtonSegment<int>(
|
|
value: 0,
|
|
label: Opacity(
|
|
opacity: 0.5,
|
|
child: Text('option'),
|
|
),
|
|
icon: Opacity(
|
|
opacity: 0.5,
|
|
child: Icon(Icons.add),
|
|
),
|
|
),
|
|
],
|
|
selected: const <int>{0},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(find.byType(SegmentedButton<int>), findsOneWidget);
|
|
expect(tester.takeException(), isNull);
|
|
});
|
|
|
|
testWidgets('SegmentedButton releases state controllers for deleted segments', (WidgetTester tester) async {
|
|
final ThemeData theme = ThemeData(useMaterial3: true);
|
|
final Key key = UniqueKey();
|
|
|
|
Widget buildApp(Widget button) {
|
|
return MaterialApp(
|
|
theme: theme,
|
|
home: Scaffold(
|
|
body: Center(
|
|
child: button,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
await tester.pumpWidget(
|
|
buildApp(
|
|
SegmentedButton<int>(
|
|
key: key,
|
|
segments: const <ButtonSegment<int>>[
|
|
ButtonSegment<int>(value: 1, label: Text('1')),
|
|
ButtonSegment<int>(value: 2, label: Text('2')),
|
|
],
|
|
selected: const <int>{2},
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.pumpWidget(
|
|
buildApp(
|
|
SegmentedButton<int>(
|
|
key: key,
|
|
segments: const <ButtonSegment<int>>[
|
|
ButtonSegment<int>(value: 2, label: Text('2')),
|
|
ButtonSegment<int>(value: 3, label: Text('3')),
|
|
],
|
|
selected: const <int>{2},
|
|
),
|
|
),
|
|
);
|
|
|
|
final SegmentedButtonState<int> state = tester.state(find.byType(SegmentedButton<int>));
|
|
expect(state.statesControllers, hasLength(2));
|
|
expect(state.statesControllers.keys.first.value, 2);
|
|
expect(state.statesControllers.keys.last.value, 3);
|
|
});
|
|
|
|
testWidgets('SegmentedButton is built with Material of type MaterialType.transparency', (WidgetTester tester) async {
|
|
final ThemeData theme = ThemeData(useMaterial3: true);
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: theme,
|
|
home: Scaffold(
|
|
body: Center(
|
|
child: SegmentedButton<int>(
|
|
segments: const <ButtonSegment<int>>[
|
|
ButtonSegment<int>(value: 1, label: Text('1')),
|
|
ButtonSegment<int>(value: 2, label: Text('2')),
|
|
ButtonSegment<int>(value: 3, label: Text('3'), enabled: false),
|
|
],
|
|
selected: const <int>{2},
|
|
onSelectionChanged: (Set<int> selected) { },
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
// Expect SegmentedButton to be built with type MaterialType.transparency.
|
|
final Finder text = find.text('1');
|
|
final Finder parent = find.ancestor(of: text, matching: find.byType(Material)).first;
|
|
final Finder parentMaterial = find.ancestor(of: parent, matching: find.byType(Material)).first;
|
|
final Material material = tester.widget<Material>(parentMaterial);
|
|
expect(material.type, MaterialType.transparency);
|
|
});
|
|
|
|
testWidgets('SegmentedButton supports exclusive choice by default', (WidgetTester tester) async {
|
|
int callbackCount = 0;
|
|
int selectedSegment = 2;
|
|
|
|
Widget frameWithSelection(int selected) {
|
|
return Material(
|
|
child: boilerplate(
|
|
child: SegmentedButton<int>(
|
|
segments: const <ButtonSegment<int>>[
|
|
ButtonSegment<int>(value: 1, label: Text('1')),
|
|
ButtonSegment<int>(value: 2, label: Text('2')),
|
|
ButtonSegment<int>(value: 3, label: Text('3')),
|
|
],
|
|
selected: <int>{selected},
|
|
onSelectionChanged: (Set<int> selected) {
|
|
assert(selected.length == 1);
|
|
selectedSegment = selected.first;
|
|
callbackCount += 1;
|
|
},
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
await tester.pumpWidget(frameWithSelection(selectedSegment));
|
|
expect(selectedSegment, 2);
|
|
expect(callbackCount, 0);
|
|
|
|
// Tap on segment 1.
|
|
await tester.tap(find.text('1'));
|
|
await tester.pumpAndSettle();
|
|
expect(callbackCount, 1);
|
|
expect(selectedSegment, 1);
|
|
|
|
// Update the selection in the widget
|
|
await tester.pumpWidget(frameWithSelection(1));
|
|
|
|
// Tap on segment 1 again should do nothing.
|
|
await tester.tap(find.text('1'));
|
|
await tester.pumpAndSettle();
|
|
expect(callbackCount, 1);
|
|
expect(selectedSegment, 1);
|
|
|
|
// Tap on segment 3.
|
|
await tester.tap(find.text('3'));
|
|
await tester.pumpAndSettle();
|
|
expect(callbackCount, 2);
|
|
expect(selectedSegment, 3);
|
|
});
|
|
|
|
testWidgets('SegmentedButton supports multiple selected segments', (WidgetTester tester) async {
|
|
int callbackCount = 0;
|
|
Set<int> selection = <int>{1};
|
|
|
|
Widget frameWithSelection(Set<int> selected) {
|
|
return Material(
|
|
child: boilerplate(
|
|
child: SegmentedButton<int>(
|
|
multiSelectionEnabled: true,
|
|
segments: const <ButtonSegment<int>>[
|
|
ButtonSegment<int>(value: 1, label: Text('1')),
|
|
ButtonSegment<int>(value: 2, label: Text('2')),
|
|
ButtonSegment<int>(value: 3, label: Text('3')),
|
|
],
|
|
selected: selected,
|
|
onSelectionChanged: (Set<int> selected) {
|
|
selection = selected;
|
|
callbackCount += 1;
|
|
},
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
await tester.pumpWidget(frameWithSelection(selection));
|
|
expect(selection, <int>{1});
|
|
expect(callbackCount, 0);
|
|
|
|
// Tap on segment 2.
|
|
await tester.tap(find.text('2'));
|
|
await tester.pumpAndSettle();
|
|
expect(callbackCount, 1);
|
|
expect(selection, <int>{1, 2});
|
|
|
|
// Update the selection in the widget
|
|
await tester.pumpWidget(frameWithSelection(<int>{1, 2}));
|
|
await tester.pumpAndSettle();
|
|
|
|
// Tap on segment 1 again should remove it from selection.
|
|
await tester.tap(find.text('1'));
|
|
await tester.pumpAndSettle();
|
|
expect(callbackCount, 2);
|
|
expect(selection, <int>{2});
|
|
|
|
// Update the selection in the widget
|
|
await tester.pumpWidget(frameWithSelection(<int>{2}));
|
|
await tester.pumpAndSettle();
|
|
|
|
// Tap on segment 3.
|
|
await tester.tap(find.text('3'));
|
|
await tester.pumpAndSettle();
|
|
expect(callbackCount, 3);
|
|
expect(selection, <int>{2, 3});
|
|
});
|
|
|
|
testWidgets('SegmentedButton allows for empty selection', (WidgetTester tester) async {
|
|
int callbackCount = 0;
|
|
int? selectedSegment = 1;
|
|
|
|
Widget frameWithSelection(int? selected) {
|
|
return Material(
|
|
child: boilerplate(
|
|
child: SegmentedButton<int>(
|
|
emptySelectionAllowed: true,
|
|
segments: const <ButtonSegment<int>>[
|
|
ButtonSegment<int>(value: 1, label: Text('1')),
|
|
ButtonSegment<int>(value: 2, label: Text('2')),
|
|
ButtonSegment<int>(value: 3, label: Text('3')),
|
|
],
|
|
selected: <int>{if (selected != null) selected},
|
|
onSelectionChanged: (Set<int> selected) {
|
|
selectedSegment = selected.isEmpty ? null : selected.first;
|
|
callbackCount += 1;
|
|
},
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
await tester.pumpWidget(frameWithSelection(selectedSegment));
|
|
expect(selectedSegment,1);
|
|
expect(callbackCount, 0);
|
|
|
|
// Tap on segment 1 should deselect it and make the selection empty.
|
|
await tester.tap(find.text('1'));
|
|
await tester.pumpAndSettle();
|
|
expect(callbackCount, 1);
|
|
expect(selectedSegment, null);
|
|
|
|
// Update the selection in the widget
|
|
await tester.pumpWidget(frameWithSelection(null));
|
|
|
|
// Tap on segment 2 should select it.
|
|
await tester.tap(find.text('2'));
|
|
await tester.pumpAndSettle();
|
|
expect(callbackCount, 2);
|
|
expect(selectedSegment, 2);
|
|
|
|
// Update the selection in the widget
|
|
await tester.pumpWidget(frameWithSelection(2));
|
|
|
|
// Tap on segment 3.
|
|
await tester.tap(find.text('3'));
|
|
await tester.pumpAndSettle();
|
|
expect(callbackCount, 3);
|
|
expect(selectedSegment, 3);
|
|
});
|
|
|
|
testWidgets('SegmentedButton shows checkboxes for selected segments', (WidgetTester tester) async {
|
|
Widget frameWithSelection(int selected) {
|
|
return Material(
|
|
child: boilerplate(
|
|
child: SegmentedButton<int>(
|
|
segments: const <ButtonSegment<int>>[
|
|
ButtonSegment<int>(value: 1, label: Text('1')),
|
|
ButtonSegment<int>(value: 2, label: Text('2')),
|
|
ButtonSegment<int>(value: 3, label: Text('3')),
|
|
],
|
|
selected: <int>{selected},
|
|
onSelectionChanged: (Set<int> selected) {},
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Finder textHasIcon(String text, IconData icon) {
|
|
return find.descendant(
|
|
of: find.widgetWithText(Row, text),
|
|
matching: find.byIcon(icon)
|
|
);
|
|
}
|
|
|
|
await tester.pumpWidget(frameWithSelection(1));
|
|
expect(textHasIcon('1', Icons.check), findsOneWidget);
|
|
expect(find.byIcon(Icons.check), findsOneWidget);
|
|
|
|
await tester.pumpWidget(frameWithSelection(2));
|
|
expect(textHasIcon('2', Icons.check), findsOneWidget);
|
|
expect(find.byIcon(Icons.check), findsOneWidget);
|
|
|
|
await tester.pumpWidget(frameWithSelection(2));
|
|
expect(textHasIcon('2', Icons.check), findsOneWidget);
|
|
expect(find.byIcon(Icons.check), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('SegmentedButton shows selected checkboxes in place of icon if it has a label as well', (WidgetTester tester) async {
|
|
Widget frameWithSelection(int selected) {
|
|
return Material(
|
|
child: boilerplate(
|
|
child: SegmentedButton<int>(
|
|
segments: const <ButtonSegment<int>>[
|
|
ButtonSegment<int>(value: 1, icon: Icon(Icons.add), label: Text('1')),
|
|
ButtonSegment<int>(value: 2, icon: Icon(Icons.add_a_photo), label: Text('2')),
|
|
ButtonSegment<int>(value: 3, icon: Icon(Icons.add_alarm), label: Text('3')),
|
|
],
|
|
selected: <int>{selected},
|
|
onSelectionChanged: (Set<int> selected) {},
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Finder textHasIcon(String text, IconData icon) {
|
|
return find.descendant(
|
|
of: find.widgetWithText(Row, text),
|
|
matching: find.byIcon(icon)
|
|
);
|
|
}
|
|
|
|
await tester.pumpWidget(frameWithSelection(1));
|
|
expect(textHasIcon('1', Icons.check), findsOneWidget);
|
|
expect(find.byIcon(Icons.add), findsNothing);
|
|
expect(textHasIcon('2', Icons.add_a_photo), findsOneWidget);
|
|
expect(textHasIcon('3', Icons.add_alarm), findsOneWidget);
|
|
|
|
await tester.pumpWidget(frameWithSelection(2));
|
|
expect(textHasIcon('1', Icons.add), findsOneWidget);
|
|
expect(textHasIcon('2', Icons.check), findsOneWidget);
|
|
expect(find.byIcon(Icons.add_a_photo), findsNothing);
|
|
expect(textHasIcon('3', Icons.add_alarm), findsOneWidget);
|
|
|
|
await tester.pumpWidget(frameWithSelection(3));
|
|
expect(textHasIcon('1', Icons.add), findsOneWidget);
|
|
expect(textHasIcon('2', Icons.add_a_photo), findsOneWidget);
|
|
expect(textHasIcon('3', Icons.check), findsOneWidget);
|
|
expect(find.byIcon(Icons.add_alarm), findsNothing);
|
|
});
|
|
|
|
testWidgets('SegmentedButton shows selected checkboxes next to icon if there is no label', (WidgetTester tester) async {
|
|
Widget frameWithSelection(int selected) {
|
|
return Material(
|
|
child: boilerplate(
|
|
child: SegmentedButton<int>(
|
|
segments: const <ButtonSegment<int>>[
|
|
ButtonSegment<int>(value: 1, icon: Icon(Icons.add)),
|
|
ButtonSegment<int>(value: 2, icon: Icon(Icons.add_a_photo)),
|
|
ButtonSegment<int>(value: 3, icon: Icon(Icons.add_alarm)),
|
|
],
|
|
selected: <int>{selected},
|
|
onSelectionChanged: (Set<int> selected) {},
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Finder rowWithIcons(IconData icon1, IconData icon2) {
|
|
return find.descendant(
|
|
of: find.widgetWithIcon(Row, icon1),
|
|
matching: find.byIcon(icon2)
|
|
);
|
|
}
|
|
|
|
await tester.pumpWidget(frameWithSelection(1));
|
|
expect(rowWithIcons(Icons.add, Icons.check), findsOneWidget);
|
|
expect(rowWithIcons(Icons.add_a_photo, Icons.check), findsNothing);
|
|
expect(rowWithIcons(Icons.add_alarm, Icons.check), findsNothing);
|
|
|
|
await tester.pumpWidget(frameWithSelection(2));
|
|
expect(rowWithIcons(Icons.add, Icons.check), findsNothing);
|
|
expect(rowWithIcons(Icons.add_a_photo, Icons.check), findsOneWidget);
|
|
expect(rowWithIcons(Icons.add_alarm, Icons.check), findsNothing);
|
|
|
|
await tester.pumpWidget(frameWithSelection(3));
|
|
expect(rowWithIcons(Icons.add, Icons.check), findsNothing);
|
|
expect(rowWithIcons(Icons.add_a_photo, Icons.check), findsNothing);
|
|
expect(rowWithIcons(Icons.add_alarm, Icons.check), findsOneWidget);
|
|
|
|
});
|
|
|
|
testWidgets('SegmentedButtons have correct semantics', (WidgetTester tester) async {
|
|
final SemanticsTester semantics = SemanticsTester(tester);
|
|
|
|
await tester.pumpWidget(
|
|
Material(
|
|
child: boilerplate(
|
|
child: SegmentedButton<int>(
|
|
segments: const <ButtonSegment<int>>[
|
|
ButtonSegment<int>(value: 1, label: Text('1')),
|
|
ButtonSegment<int>(value: 2, label: Text('2')),
|
|
ButtonSegment<int>(value: 3, label: Text('3'), enabled: false),
|
|
],
|
|
selected: const <int>{2},
|
|
onSelectionChanged: (Set<int> selected) {},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(
|
|
semantics,
|
|
hasSemantics(
|
|
TestSemantics.root(
|
|
children: <TestSemantics>[
|
|
// First is an unselected, enabled button.
|
|
TestSemantics(
|
|
flags: <SemanticsFlag>[
|
|
SemanticsFlag.isButton,
|
|
SemanticsFlag.isEnabled,
|
|
SemanticsFlag.hasEnabledState,
|
|
SemanticsFlag.hasCheckedState,
|
|
SemanticsFlag.isFocusable,
|
|
SemanticsFlag.isInMutuallyExclusiveGroup,
|
|
],
|
|
label: '1',
|
|
actions: <SemanticsAction>[
|
|
SemanticsAction.tap,
|
|
],
|
|
),
|
|
|
|
// Second is a selected, enabled button.
|
|
TestSemantics(
|
|
flags: <SemanticsFlag>[
|
|
SemanticsFlag.isButton,
|
|
SemanticsFlag.isEnabled,
|
|
SemanticsFlag.hasEnabledState,
|
|
SemanticsFlag.hasCheckedState,
|
|
SemanticsFlag.isChecked,
|
|
SemanticsFlag.isFocusable,
|
|
SemanticsFlag.isInMutuallyExclusiveGroup,
|
|
],
|
|
label: '2',
|
|
actions: <SemanticsAction>[
|
|
SemanticsAction.tap,
|
|
],
|
|
),
|
|
|
|
// Third is an unselected, disabled button.
|
|
TestSemantics(
|
|
flags: <SemanticsFlag>[
|
|
SemanticsFlag.isButton,
|
|
SemanticsFlag.hasEnabledState,
|
|
SemanticsFlag.hasCheckedState,
|
|
SemanticsFlag.isInMutuallyExclusiveGroup,
|
|
],
|
|
label: '3',
|
|
),
|
|
],
|
|
),
|
|
ignoreId: true,
|
|
ignoreRect: true,
|
|
ignoreTransform: true,
|
|
),
|
|
);
|
|
|
|
semantics.dispose();
|
|
});
|
|
|
|
|
|
testWidgets('Multi-select SegmentedButtons have correct semantics', (WidgetTester tester) async {
|
|
final SemanticsTester semantics = SemanticsTester(tester);
|
|
|
|
await tester.pumpWidget(
|
|
Material(
|
|
child: boilerplate(
|
|
child: SegmentedButton<int>(
|
|
segments: const <ButtonSegment<int>>[
|
|
ButtonSegment<int>(value: 1, label: Text('1')),
|
|
ButtonSegment<int>(value: 2, label: Text('2')),
|
|
ButtonSegment<int>(value: 3, label: Text('3'), enabled: false),
|
|
],
|
|
selected: const <int>{1, 3},
|
|
onSelectionChanged: (Set<int> selected) {},
|
|
multiSelectionEnabled: true,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(
|
|
semantics,
|
|
hasSemantics(
|
|
TestSemantics.root(
|
|
children: <TestSemantics>[
|
|
// First is selected, enabled button.
|
|
TestSemantics(
|
|
flags: <SemanticsFlag>[
|
|
SemanticsFlag.isButton,
|
|
SemanticsFlag.isEnabled,
|
|
SemanticsFlag.hasEnabledState,
|
|
SemanticsFlag.hasCheckedState,
|
|
SemanticsFlag.isChecked,
|
|
SemanticsFlag.isFocusable,
|
|
],
|
|
label: '1',
|
|
actions: <SemanticsAction>[
|
|
SemanticsAction.tap,
|
|
],
|
|
),
|
|
|
|
// Second is an unselected, enabled button.
|
|
TestSemantics(
|
|
flags: <SemanticsFlag>[
|
|
SemanticsFlag.isButton,
|
|
SemanticsFlag.isEnabled,
|
|
SemanticsFlag.hasEnabledState,
|
|
SemanticsFlag.hasCheckedState,
|
|
SemanticsFlag.isFocusable,
|
|
],
|
|
label: '2',
|
|
actions: <SemanticsAction>[
|
|
SemanticsAction.tap,
|
|
],
|
|
),
|
|
|
|
// Third is a selected, disabled button.
|
|
TestSemantics(
|
|
flags: <SemanticsFlag>[
|
|
SemanticsFlag.isButton,
|
|
SemanticsFlag.hasEnabledState,
|
|
SemanticsFlag.isChecked,
|
|
SemanticsFlag.hasCheckedState,
|
|
],
|
|
label: '3',
|
|
),
|
|
],
|
|
),
|
|
ignoreId: true,
|
|
ignoreRect: true,
|
|
ignoreTransform: true,
|
|
),
|
|
);
|
|
|
|
semantics.dispose();
|
|
});
|
|
|
|
testWidgets('SegmentedButton default overlayColor and foregroundColor resolve pressed state', (WidgetTester tester) async {
|
|
final ThemeData theme = ThemeData(useMaterial3: true);
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: theme,
|
|
home: Scaffold(
|
|
body: Center(
|
|
child: SegmentedButton<int>(
|
|
segments: const <ButtonSegment<int>>[
|
|
ButtonSegment<int>(value: 1, label: Text('1')),
|
|
ButtonSegment<int>(value: 2, label: Text('2')),
|
|
],
|
|
selected: const <int>{1},
|
|
onSelectionChanged: (Set<int> selected) {},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
RenderObject overlayColor() {
|
|
return tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures');
|
|
}
|
|
|
|
final Material material = tester.widget<Material>(find.descendant(
|
|
of: find.byType(TextButton),
|
|
matching: find.byType(Material),
|
|
));
|
|
|
|
// Hovered.
|
|
final Offset center = tester.getCenter(find.text('2'));
|
|
final TestGesture gesture = await tester.createGesture(
|
|
kind: PointerDeviceKind.mouse,
|
|
);
|
|
await gesture.addPointer();
|
|
await gesture.moveTo(center);
|
|
await tester.pumpAndSettle();
|
|
expect(overlayColor(), paints..rect(color: theme.colorScheme.onSurface.withOpacity(0.08)));
|
|
expect(material.textStyle?.color, theme.colorScheme.onSurface);
|
|
|
|
// Highlighted (pressed).
|
|
await gesture.down(center);
|
|
await tester.pumpAndSettle();
|
|
expect(overlayColor(), paints..rect()..rect(color: theme.colorScheme.onSurface.withOpacity(0.12)));
|
|
expect(material.textStyle?.color, theme.colorScheme.onSurface);
|
|
});
|
|
|
|
testWidgets('SegmentedButton has no tooltips by default', (WidgetTester tester) async {
|
|
final ThemeData theme = ThemeData(useMaterial3: true);
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: theme,
|
|
home: Scaffold(
|
|
body: Center(
|
|
child: SegmentedButton<int>(
|
|
segments: const <ButtonSegment<int>>[
|
|
ButtonSegment<int>(value: 1, label: Text('1')),
|
|
ButtonSegment<int>(value: 2, label: Text('2')),
|
|
ButtonSegment<int>(value: 3, label: Text('3'), enabled: false),
|
|
],
|
|
selected: const <int>{2},
|
|
onSelectionChanged: (Set<int> selected) { },
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(find.byType(Tooltip), findsNothing);
|
|
});
|
|
|
|
testWidgets('SegmentedButton has correct tooltips', (WidgetTester tester) async {
|
|
final ThemeData theme = ThemeData(useMaterial3: true);
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: theme,
|
|
home: Scaffold(
|
|
body: Center(
|
|
child: SegmentedButton<int>(
|
|
segments: const <ButtonSegment<int>>[
|
|
ButtonSegment<int>(value: 1, label: Text('1')),
|
|
ButtonSegment<int>(value: 2, label: Text('2'), tooltip: 't2'),
|
|
ButtonSegment<int>(
|
|
value: 3,
|
|
label: Text('3'),
|
|
tooltip: 't3',
|
|
enabled: false,
|
|
),
|
|
],
|
|
selected: const <int>{2},
|
|
onSelectionChanged: (Set<int> selected) { },
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(find.byType(Tooltip), findsNWidgets(2));
|
|
expect(find.byTooltip('t2'), findsOneWidget);
|
|
expect(find.byTooltip('t3'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('SegmentedButton.styleFrom is applied to the SegmentedButton', (WidgetTester tester) async {
|
|
const Color foregroundColor = Color(0xfffffff0);
|
|
const Color backgroundColor = Color(0xfffffff1);
|
|
const Color selectedBackgroundColor = Color(0xfffffff2);
|
|
const Color selectedForegroundColor = Color(0xfffffff3);
|
|
const Color disabledBackgroundColor = Color(0xfffffff4);
|
|
const Color disabledForegroundColor = Color(0xfffffff5);
|
|
const MouseCursor enabledMouseCursor = SystemMouseCursors.text;
|
|
const MouseCursor disabledMouseCursor = SystemMouseCursors.grab;
|
|
|
|
final ButtonStyle styleFromStyle = SegmentedButton.styleFrom(
|
|
foregroundColor: foregroundColor,
|
|
backgroundColor: backgroundColor,
|
|
selectedForegroundColor: selectedForegroundColor,
|
|
selectedBackgroundColor: selectedBackgroundColor,
|
|
disabledForegroundColor: disabledForegroundColor,
|
|
disabledBackgroundColor: disabledBackgroundColor,
|
|
shadowColor: const Color(0xfffffff6),
|
|
surfaceTintColor: const Color(0xfffffff7),
|
|
elevation: 1,
|
|
textStyle: const TextStyle(color: Color(0xfffffff8)),
|
|
padding: const EdgeInsets.all(2),
|
|
side: const BorderSide(color: Color(0xfffffff9)),
|
|
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(3))),
|
|
enabledMouseCursor: enabledMouseCursor,
|
|
disabledMouseCursor: disabledMouseCursor,
|
|
visualDensity: VisualDensity.compact,
|
|
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
|
animationDuration: const Duration(milliseconds: 100),
|
|
enableFeedback: true,
|
|
alignment: Alignment.center,
|
|
splashFactory: NoSplash.splashFactory,
|
|
);
|
|
|
|
await tester.pumpWidget(MaterialApp(
|
|
home: Scaffold(
|
|
body: Center(
|
|
child: SegmentedButton<int>(
|
|
style: styleFromStyle,
|
|
segments: const <ButtonSegment<int>>[
|
|
ButtonSegment<int>(value: 1, label: Text('1')),
|
|
ButtonSegment<int>(value: 2, label: Text('2')),
|
|
ButtonSegment<int>(value: 3, label: Text('3'), enabled: false),
|
|
],
|
|
selected: const <int>{2},
|
|
onSelectionChanged: (Set<int> selected) { },
|
|
selectedIcon: const Icon(Icons.alarm),
|
|
),
|
|
),
|
|
),
|
|
));
|
|
|
|
// Test provided button style is applied to the enabled button segment.
|
|
ButtonStyle? buttonStyle = tester.widget<TextButton>(find.byType(TextButton).first).style;
|
|
expect(buttonStyle?.foregroundColor?.resolve(enabled), foregroundColor);
|
|
expect(buttonStyle?.backgroundColor?.resolve(enabled), backgroundColor);
|
|
expect(buttonStyle?.overlayColor, styleFromStyle.overlayColor);
|
|
expect(buttonStyle?.surfaceTintColor, styleFromStyle.surfaceTintColor);
|
|
expect(buttonStyle?.elevation, styleFromStyle.elevation);
|
|
expect(buttonStyle?.textStyle, styleFromStyle.textStyle);
|
|
expect(buttonStyle?.padding, styleFromStyle.padding);
|
|
expect(buttonStyle?.mouseCursor?.resolve(enabled), enabledMouseCursor);
|
|
expect(buttonStyle?.visualDensity, styleFromStyle.visualDensity);
|
|
expect(buttonStyle?.tapTargetSize, styleFromStyle.tapTargetSize);
|
|
expect(buttonStyle?.animationDuration, styleFromStyle.animationDuration);
|
|
expect(buttonStyle?.enableFeedback, styleFromStyle.enableFeedback);
|
|
expect(buttonStyle?.alignment, styleFromStyle.alignment);
|
|
expect(buttonStyle?.splashFactory, styleFromStyle.splashFactory);
|
|
|
|
// Test provided button style is applied selected button segment.
|
|
buttonStyle = tester.widget<TextButton>(find.byType(TextButton).at(1)).style;
|
|
expect(buttonStyle?.foregroundColor?.resolve(selected), selectedForegroundColor);
|
|
expect(buttonStyle?.backgroundColor?.resolve(selected), selectedBackgroundColor);
|
|
expect(buttonStyle?.mouseCursor?.resolve(enabled), enabledMouseCursor);
|
|
|
|
// Test provided button style is applied disabled button segment.
|
|
buttonStyle = tester.widget<TextButton>(find.byType(TextButton).last).style;
|
|
expect(buttonStyle?.foregroundColor?.resolve(disabled), disabledForegroundColor);
|
|
expect(buttonStyle?.backgroundColor?.resolve(disabled), disabledBackgroundColor);
|
|
expect(buttonStyle?.mouseCursor?.resolve(disabled), disabledMouseCursor);
|
|
|
|
// Test provided button style is applied to the segmented button material.
|
|
final Material material = tester.widget<Material>(find.descendant(
|
|
of: find.byType(SegmentedButton<int>),
|
|
matching: find.byType(Material),
|
|
).first);
|
|
expect(material.elevation, styleFromStyle.elevation?.resolve(enabled));
|
|
expect(material.shadowColor, styleFromStyle.shadowColor?.resolve(enabled));
|
|
expect(material.surfaceTintColor, styleFromStyle.surfaceTintColor?.resolve(enabled));
|
|
|
|
// Test provided button style border is applied to the segmented button border.
|
|
expect(
|
|
find.byType(SegmentedButton<int>),
|
|
paints..line(color: styleFromStyle.side?.resolve(enabled)?.color),
|
|
);
|
|
|
|
// Test foreground color is applied to the overlay color.
|
|
RenderObject overlayColor() {
|
|
return tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures');
|
|
}
|
|
final TestGesture gesture = await tester.createGesture(
|
|
kind: PointerDeviceKind.mouse,
|
|
);
|
|
await gesture.addPointer();
|
|
await gesture.down(tester.getCenter(find.text('1')));
|
|
await tester.pumpAndSettle();
|
|
expect(overlayColor(), paints..rect(color: foregroundColor.withOpacity(0.08)));
|
|
});
|
|
|
|
testWidgets('Disabled SegmentedButton has correct states when rebuilding', (WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: Center(
|
|
child: StatefulBuilder(
|
|
builder: (BuildContext context, StateSetter setState) {
|
|
return Column(
|
|
children: <Widget>[
|
|
SegmentedButton<int>(
|
|
segments: const <ButtonSegment<int>>[
|
|
ButtonSegment<int>(value: 0, label: Text('foo')),
|
|
],
|
|
selected: const <int>{0},
|
|
),
|
|
ElevatedButton(
|
|
onPressed: () => setState(() {}),
|
|
child: const Text('Trigger rebuild'),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
final Set<MaterialState> states = <MaterialState>{ MaterialState.selected, MaterialState.disabled };
|
|
// Check the initial states.
|
|
SegmentedButtonState<int> state = tester.state(find.byType(SegmentedButton<int>));
|
|
expect(state.statesControllers.values.first.value, states);
|
|
// Trigger a rebuild.
|
|
await tester.tap(find.byType(ElevatedButton));
|
|
await tester.pumpAndSettle();
|
|
// Check the states after the rebuild.
|
|
state = tester.state(find.byType(SegmentedButton<int>));
|
|
expect(state.statesControllers.values.first.value, states);
|
|
});
|
|
|
|
testWidgets('Min button hit target height is 48.0 and min (painted) button height is 40 '
|
|
'by default with standard density and MaterialTapTargetSize.padded', (WidgetTester tester) async {
|
|
final ThemeData theme = ThemeData();
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: theme,
|
|
home: Scaffold(
|
|
body: Center(
|
|
child: Column(
|
|
children: <Widget>[
|
|
SegmentedButton<int>(
|
|
segments: const <ButtonSegment<int>>[
|
|
ButtonSegment<int>(value: 0, label: Text('Day'), icon: Icon(Icons.calendar_view_day)),
|
|
ButtonSegment<int>(value: 1, label: Text('Week'), icon: Icon(Icons.calendar_view_week)),
|
|
ButtonSegment<int>(value: 2, label: Text('Month'), icon: Icon(Icons.calendar_view_month)),
|
|
ButtonSegment<int>(value: 3, label: Text('Year'), icon: Icon(Icons.calendar_today)),
|
|
],
|
|
selected: const <int>{0},
|
|
onSelectionChanged: (Set<int> value) {},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(theme.visualDensity, VisualDensity.standard);
|
|
expect(theme.materialTapTargetSize, MaterialTapTargetSize.padded);
|
|
|
|
final Finder button = find.byType(SegmentedButton<int>);
|
|
expect(tester.getSize(button).height, 48.0);
|
|
expect(
|
|
find.byType(SegmentedButton<int>),
|
|
paints..rrect(
|
|
style: PaintingStyle.stroke,
|
|
strokeWidth: 1.0,
|
|
// Button border height is button.bottom(43.5) - button.top(4.5) + stoke width(1) = 40.
|
|
rrect: RRect.fromLTRBR(0.5, 4.5, 497.5, 43.5, const Radius.circular(19.5))
|
|
)
|
|
);
|
|
});
|
|
}
|
|
|
|
Set<MaterialState> enabled = const <MaterialState>{};
|
|
Set<MaterialState> disabled = const <MaterialState>{ MaterialState.disabled };
|
|
Set<MaterialState> selected = const <MaterialState>{ MaterialState.selected };
|