Skip to content

Commit b178efe

Browse files
authored
Merge branch 'main' into scroll-notifier
2 parents ebf4a8a + dedb23a commit b178efe

22 files changed

+424
-92
lines changed

Diff for: integration_test/framework/test_helper.dart

+5-3
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@ class TestHelper {
1717
/// the result but no longer available for the next run, causing the
1818
/// subsequent tests to hang. For this reason, call this in your Test class's
1919
/// setupApp() once before running the tests.
20-
static Future<EnsembleConfig> setupApp({required String appName}) async {
21-
I18nProps i18nProps = I18nProps('en', 'en', false);
22-
i18nProps.path = 'ensemble/i18n';
20+
static Future<EnsembleConfig> setupApp(
21+
{required String appName, String? forcedLocale}) async {
22+
I18nProps i18nProps =
23+
I18nProps('en', 'en', false, forcedLocale: forcedLocale);
24+
i18nProps.path = 'integration_test/local/$appName/i18n';
2325

2426
EnsembleConfig config = EnsembleConfig(
2527
definitionProvider: LocalDefinitionProvider(

Diff for: integration_test/local/customWidget_test.dart

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import 'package:ensemble/ensemble.dart';
2+
import 'package:ensemble/widget/button.dart';
3+
import 'package:flutter/material.dart';
4+
import 'package:flutter_test/flutter_test.dart';
5+
6+
import '../framework/test_helper.dart';
7+
8+
void main() {
9+
late EnsembleConfig config;
10+
setUpAll(() async {
11+
config = await TestHelper.setupApp(appName: 'widgets');
12+
});
13+
14+
group('Custom Widgets', () {
15+
testWidgets("test basic widget features", (tester) async {
16+
await TestHelper.loadScreen(tester, 'Custom Widget', config);
17+
18+
// test visibility
19+
expect(find.text('I am visible'), findsOneWidget);
20+
expect(find.text('I am really visible'), findsOneWidget);
21+
expect(find.text('I am invisible'), findsNothing);
22+
23+
await tester.tap(find.descendant(
24+
of: find.byType(Button), matching: find.text('Toggle Visibility')));
25+
await tester.pumpAndSettle();
26+
expect(find.text('I am visible'), findsNothing);
27+
expect(find.text('I am really visible'), findsNothing);
28+
expect(find.text('I am invisible'), findsOneWidget);
29+
30+
// test flex and its modification
31+
32+
// equal distribution with default flex
33+
expect(tester.getSize(find.text("hello")).width,
34+
tester.getSize(find.text("world")).width);
35+
36+
// changing flex under FlexBox is a problem because the parent FlexBox already
37+
// make the child Expanded if not specified, so if we update it later, there will
38+
// be duplicate Expanded.
39+
// ALmost like we want to travel up to the Flexbox and ask it to refresh itself,
40+
// but that is not recommended. TODO: figure this out
41+
// change "world" to 2x
42+
// await tester.tap(find.descendant(
43+
// of: find.byType(Button), matching: find.text('Change Flex')));
44+
// await tester.pumpAndSettle();
45+
// expect(tester.getSize(find.text("hello")).width, 100);
46+
// expect(tester.getSize(find.text("world")).width, 200);
47+
48+
expect(tester.getSize(find.text("1x")).width, 100);
49+
expect(tester.getSize(find.text("2x")).width, 200);
50+
});
51+
});
52+
}

Diff for: integration_test/local/defaultApp/Widget Bindings.yaml renamed to integration_test/local/defaultApp/Custom Widget.yaml

+12-2
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ View:
2222

2323
Custom:
2424
inputs: [txt]
25+
onLoad:
26+
showToast:
27+
message: Hello ${txt}
2528
body:
2629
Column:
2730
children:
@@ -35,6 +38,13 @@ Custom:
3538

3639
Custom2:
3740
inputs: [text]
41+
onLoad: |-
42+
setByJS.text = "Hi " + text;
43+
3844
body:
39-
Text:
40-
text: "Custom Custom Widget: ${text}"
45+
Column:
46+
children:
47+
- Text:
48+
text: "Custom Custom Widget: ${text}"
49+
- Text:
50+
id: setByJS

Diff for: integration_test/local/defaultApp_test.dart

+12-2
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,22 @@ void main() {
2424
/// test that binding to a TextInput works properly in the same scope
2525
/// and also in a custom widget's scope
2626
testWidgets("Bindings to widget's value", (tester) async {
27-
await TestHelper.loadScreen(tester, 'Widget Bindings', config);
28-
// await tester.pumpAndSettle();
27+
await TestHelper.loadScreen(tester, 'Custom Widget', config);
2928

3029
// TextInput has initial value of 'first'
3130
// so first make sure our EnsembleText is correctly bind to that
3231
Finder text = find.descendant(
3332
of: find.byType(EnsembleText), matching: find.text('first'));
3433
expect(text, findsOneWidget);
3534

35+
// Custom Widget's onLoad can access inputs
36+
// search for toast message
37+
await tester.pumpAndSettle();
38+
expect(find.text('Hello first'), findsOneWidget);
39+
40+
// ensure Nested widget's onLoad can access inputs via JS too
41+
expect(find.text('Hi first'), findsOneWidget);
42+
3643
// Custom Widget's text should also bind to the same value
3744
Finder customText = find.descendant(
3845
of: find.byType(EnsembleText),
@@ -68,6 +75,9 @@ void main() {
6875
of: find.byType(EnsembleText),
6976
matching: find.text('Custom Custom Widget: second'));
7077
expect(customCustomText, findsOneWidget);
78+
79+
// ensure onLoad never run again and still retain original value
80+
expect(find.text('Hi first'), findsOneWidget);
7181
});
7282

7383
/// test bindings to API is working properly

Diff for: integration_test/local/dialog_test.dart renamed to integration_test/local/dialogsAndToast_test.dart

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import '../framework/test_helper.dart';
88
void main() {
99
late EnsembleConfig config;
1010
setUpAll(() async {
11-
config = await TestHelper.setupApp(appName: 'dialog');
11+
config = await TestHelper.setupApp(appName: 'dialogsAndToast');
1212
});
1313

1414
group('Testing Toast', () {

Diff for: integration_test/local/translation/Basic.yaml

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
View:
2+
styles:
3+
useSafeArea: true
4+
body:
5+
Column:
6+
children:
7+
- TextInput:
8+
id: myName
9+
value: Peter
10+
- Text:
11+
text: [email protected] ${myName.value}
12+
- Button:
13+
label: Show Toast
14+
onTap:
15+
showToast:
16+
message: [email protected] [email protected] ${myName.value}

Diff for: integration_test/local/translation/i18n/en.yaml

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
home:
2+
hello: Hello

Diff for: integration_test/local/translation/i18n/es.yaml

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
home:
2+
hello: Hola

Diff for: integration_test/local/widgets/Custom Widget.yaml

+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
View:
2+
styles:
3+
useSafeArea: true
4+
body:
5+
Column:
6+
children:
7+
# visibility test
8+
- TextWidget:
9+
id: visibleWidget
10+
inputs:
11+
text: I am visible
12+
- TextWidget:
13+
id: reallyVisibleWidget
14+
inputs:
15+
text: I am really visible
16+
styles:
17+
visible: true
18+
- TextWidget:
19+
id: invisibleWidget
20+
inputs:
21+
text: I am invisible
22+
styles:
23+
visible: false
24+
- Button:
25+
label: Toggle Visibility
26+
onTap: |-
27+
visibleWidget.visible = false;
28+
reallyVisibleWidget.visible = false;
29+
invisibleWidget.visible = true;
30+
31+
# default flex - equal distribution
32+
- FlexRow:
33+
styles:
34+
width: 300
35+
children:
36+
- TextWidget:
37+
inputs:
38+
text: hello
39+
- TextWidget:
40+
id: world
41+
inputs:
42+
text: world
43+
44+
# Doesn't work well yet. See note in the test
45+
# - Button:
46+
# label: Change Flex
47+
# onTap: |-
48+
# world.flex = 2;
49+
50+
- FlexRow:
51+
styles:
52+
width: 300
53+
children:
54+
- TextWidget:
55+
inputs:
56+
text: 1x
57+
- TextWidget:
58+
inputs:
59+
text: 2x
60+
styles:
61+
flex: 2
62+
63+
64+
TextWidget:
65+
inputs: [text]
66+
body:
67+
Text:
68+
text: ${text}
69+
70+
Rectangle:
71+
inputs: [color]
72+
body:
73+
Shape:
74+
type: rectangle
75+
styles:
76+
height: 50
77+
backgroundColor: ${color}

Diff for: integration_test/translation_test.dart

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import 'package:ensemble/ensemble.dart';
2+
import 'package:flutter_test/flutter_test.dart';
3+
4+
import 'framework/test_helper.dart';
5+
6+
void main() {
7+
group('Default English locale', () {
8+
testWidgets("default locale should work properly", (tester) async {
9+
EnsembleConfig config = await TestHelper.setupApp(appName: 'translation');
10+
await TestHelper.loadScreen(tester, 'Basic', config);
11+
12+
expect(find.text("Hello Peter"), findsOneWidget);
13+
14+
await tester.tap(find.text("Show Toast"));
15+
await tester.pumpAndSettle();
16+
expect(find.text("Hello Hello Peter"), findsOneWidget);
17+
});
18+
});
19+
20+
group('Forcing Spanish locale', () {
21+
testWidgets("forcing Spanish locale should work", (tester) async {
22+
EnsembleConfig config =
23+
await TestHelper.setupApp(appName: 'translation', forcedLocale: 'es');
24+
await TestHelper.loadScreen(tester, 'Basic', config);
25+
26+
expect(find.text("Hola Peter"), findsOneWidget);
27+
28+
await tester.tap(find.text("Show Toast"));
29+
await tester.pumpAndSettle();
30+
expect(find.text("Hola Hola Peter"), findsOneWidget);
31+
});
32+
});
33+
}

Diff for: lib/action/bottom_sheet_actions.dart

+41-7
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import 'package:collection/collection.dart';
12
import 'package:ensemble/framework/action.dart';
23
import 'package:ensemble/framework/data_context.dart';
34
import 'package:ensemble/framework/error_handling.dart';
@@ -21,6 +22,8 @@ class ShowBottomSheetAction extends EnsembleAction {
2122
});
2223

2324
static const defaultTopBorderRadius = Radius.circular(16);
25+
static const dragHandleHeight = 3.0;
26+
static const dragHandleVerticalMargin = 10.0;
2427

2528
final Map payload;
2629
final dynamic body;
@@ -85,6 +88,14 @@ class ShowBottomSheetAction extends EnsembleAction {
8588
min: 0,
8689
max: 1);
8790

91+
bool isSnap(scopeManager) =>
92+
Utils.optionalBool(
93+
eval(payload["scrollOptions"]?["snap"], scopeManager)) ??
94+
false;
95+
96+
List<double>? additionalSnaps(scopeManager) => Utils.getList<double>(
97+
eval(payload["scrollOptions"]?["additionalViewportSnaps"], scopeManager));
98+
8899
@override
89100
Future<dynamic> execute(BuildContext context, ScopeManager scopeManager) {
90101
if (body != null) {
@@ -123,7 +134,21 @@ class ShowBottomSheetAction extends EnsembleAction {
123134
}
124135

125136
Widget getBodyWidget(ScopeManager scopeManager, BuildContext context) {
126-
var widget = scopeManager.buildWidgetFromDefinition(body);
137+
// We have to handle the BottomSheet's padding directly around the widget,
138+
// such that it is inside the Scrollable area to be able to move up and down.
139+
var sheetPadding = padding(scopeManager) ?? EdgeInsets.zero;
140+
141+
// account for the drag handle
142+
if (showDragHandle(scopeManager)) {
143+
var additionalPaddingTop =
144+
dragHandleVerticalMargin * 2 + dragHandleHeight;
145+
sheetPadding =
146+
sheetPadding.copyWith(top: sheetPadding.top + additionalPaddingTop);
147+
}
148+
149+
var widget = Padding(
150+
padding: sheetPadding,
151+
child: scopeManager.buildWidgetFromDefinition(body));
127152

128153
if (isScrollable(scopeManager) == true) {
129154
// fix the viewport numbers if used incorrectly
@@ -140,14 +165,22 @@ class ShowBottomSheetAction extends EnsembleAction {
140165
initialViewport = (minViewport + maxViewport) / 2.0;
141166
}
142167

168+
bool useSnap = isSnap(scopeManager);
169+
List<double>? snaps = additionalSnaps(scopeManager)
170+
?.where((item) => item > minViewport && item < maxViewport)
171+
.toList();
172+
snaps?.sort();
173+
143174
// On platforms with a mouse (Web/desktop), there is no min/maxViewport due to platform consistency,
144175
// so the height will be fixed to initialViewport, and content will just scroll within it.
145176
// https://docs.flutter.dev/release/breaking-changes/default-scroll-behavior-drag
146177
return DraggableScrollableSheet(
147-
expand: false,
178+
expand: true,
148179
minChildSize: minViewport,
149180
maxChildSize: maxViewport,
150181
initialChildSize: initialViewport,
182+
snap: useSnap,
183+
snapSizes: useSnap ? snaps : null,
151184
builder: (context, scrollController) =>
152185
buildRootContainer(scopeManager, context,
153186
child: SingleChildScrollView(
@@ -164,8 +197,6 @@ class ShowBottomSheetAction extends EnsembleAction {
164197
Widget buildRootContainer(ScopeManager scopeManager, BuildContext context,
165198
{required Widget child, required bool isScrollable}) {
166199
Widget rootWidget = Container(
167-
margin: margin(scopeManager),
168-
padding: padding(scopeManager),
169200
decoration: BoxDecoration(
170201
color: getBackgroundColor(scopeManager) ??
171202
Theme.of(context).dialogBackgroundColor,
@@ -185,14 +216,17 @@ class ShowBottomSheetAction extends EnsembleAction {
185216
children: [rootWidget, _buildDragHandle(scopeManager)],
186217
);
187218
}
188-
return rootWidget;
219+
// This is the Margin of the bottom sheet.
220+
// Padding will be handled separately inside the scrollable area
221+
return Padding(
222+
padding: margin(scopeManager) ?? EdgeInsets.zero, child: rootWidget);
189223
}
190224

191225
Widget _buildDragHandle(ScopeManager scopeManager) {
192226
return Container(
193-
margin: const EdgeInsets.only(top: 10),
227+
margin: const EdgeInsets.only(top: dragHandleVerticalMargin),
194228
width: 32,
195-
height: 3,
229+
height: dragHandleHeight,
196230
decoration: BoxDecoration(
197231
color: dragHandleColor(scopeManager) ?? Colors.grey[500],
198232
borderRadius: BorderRadius.circular(12),

0 commit comments

Comments
 (0)