Skip to content

Commit 08d64ad

Browse files
committed
Conditions: an empty text variable is empty; obscure fixed-height password fields
isEmpty on a text variable holding "" was false: the empty text reads as null and isEmpty(null) returned false, so a form couldn't check that its error messages were cleared before submitting. A fixed-height text field expands, which leaves maxLines null, and only single-line fields were obscured, so a PIN or password field in a fixed box showed its text. An obscured field that isn't explicitly multi-line now keeps one line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011WedhUVWdWR5swqZYMuGfP
1 parent 95049ed commit 08d64ad

4 files changed

Lines changed: 114 additions & 6 deletions

File tree

‎lib/src/transformers/node_transformers/passive_text_field_transformer.dart‎

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -310,11 +310,20 @@ class _PassiveTextFieldWidgetState extends State<PassiveTextFieldWidget> {
310310
!widget.settings.isPreview &&
311311
(widget.autofocus ?? properties.autoFocus);
312312

313-
final (int? maxLines, int? minLines) = resolveTextFieldLines(
314-
maxLines: properties.maxLines,
315-
minLines: properties.minLines,
316-
expands: !widget.node.isVerticalWrap,
317-
);
313+
// Flutter only obscures single-line fields, so an obscured field that
314+
// isn't explicitly multi-line (a PIN or password in a fixed-height box)
315+
// keeps one line instead of expanding to the box.
316+
final bool singleLineObscured =
317+
obscureText &&
318+
(properties.maxLines ?? 1) <= 1 &&
319+
(properties.minLines ?? 1) <= 1;
320+
final (int? maxLines, int? minLines) = singleLineObscured
321+
? (1, null)
322+
: resolveTextFieldLines(
323+
maxLines: properties.maxLines,
324+
minLines: properties.minLines,
325+
expands: !widget.node.isVerticalWrap,
326+
);
318327

319328
Widget field = TextFormField(
320329
focusNode: focusNode,
@@ -352,7 +361,7 @@ class _PassiveTextFieldWidgetState extends State<PassiveTextFieldWidget> {
352361
],
353362
maxLines: maxLines,
354363
minLines: minLines,
355-
expands: !widget.node.isVerticalWrap,
364+
expands: !singleLineObscured && !widget.node.isVerticalWrap,
356365
obscuringCharacter: widget.node.properties.obscuringCharacter,
357366
style: TextUtils.retrieveTextStyleFromProp(
358367
widget.node.properties.inputStyle,

‎lib/src/transformers/utils/condition_evaluator.dart‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,9 @@ class ConditionEvaluator<R extends Object>
201201

202202
@override
203203
bool visitIsEmptyOperator(Object? left) {
204+
// An empty text variable reads as null (see _visitRawValue), and an unset
205+
// one is null too: both are empty.
206+
if (left == null) return true;
204207
if (left is List) return left.isEmpty;
205208
if (left is String) return left.isEmpty;
206209
if (left is Map) return left.isEmpty;

‎test/condition_is_empty_test.dart‎

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import 'package:codelessly_api/codelessly_api.dart';
2+
import 'package:codelessly_sdk/codelessly_sdk.dart';
3+
import 'package:flutter_test/flutter_test.dart';
4+
5+
/// isEmpty / isNotEmpty on text variables, the way a form validates its
6+
/// error messages before submitting.
7+
void main() {
8+
bool check(Map<String, Object?> vars, String path, String operator) {
9+
final BaseExpression expression = BaseExpression.fromJson({
10+
'type': 'Expression',
11+
'leftPart': {'type': 'Variable', 'valueString': '\${$path}'},
12+
'operator': operator,
13+
'rightPart': {'type': 'RawValue', 'value': ''},
14+
});
15+
return expression.accept<bool>(
16+
ConditionEvaluator<Object>(
17+
scopedValues: ScopedValues.preview(
18+
variables: {
19+
for (final MapEntry(:key, :value) in vars.entries)
20+
key: VariableData(id: key, name: key, value: value),
21+
},
22+
),
23+
),
24+
) ==
25+
true;
26+
}
27+
28+
test('an empty text variable is empty', () {
29+
expect(check({'error': ''}, 'error', 'isEmpty'), isTrue);
30+
expect(check({'error': ''}, 'error', 'isNotEmpty'), isFalse);
31+
});
32+
33+
test('a text variable with a value is not empty', () {
34+
expect(check({'error': 'Incorrect PIN'}, 'error', 'isEmpty'), isFalse);
35+
expect(check({'error': 'Incorrect PIN'}, 'error', 'isNotEmpty'), isTrue);
36+
});
37+
38+
test('a missing variable is empty', () {
39+
expect(check({}, 'missing', 'isEmpty'), isTrue);
40+
expect(check({}, 'missing', 'isNotEmpty'), isFalse);
41+
});
42+
43+
test('both errors empty, joined with and', () {
44+
final BaseExpression expression = BaseExpression.fromJson({
45+
'type': 'ExpressionGroup',
46+
'join': 'and',
47+
'leftExpression': {
48+
'type': 'Expression',
49+
'leftPart': {'type': 'Variable', 'valueString': r'${idError}'},
50+
'operator': 'isEmpty',
51+
'rightPart': {'type': 'RawValue', 'value': ''},
52+
},
53+
'rightExpression': {
54+
'type': 'Expression',
55+
'leftPart': {'type': 'Variable', 'valueString': r'${pinError}'},
56+
'operator': 'isEmpty',
57+
'rightPart': {'type': 'RawValue', 'value': ''},
58+
},
59+
});
60+
expect(
61+
expression.accept<bool>(
62+
ConditionEvaluator<Object>(
63+
scopedValues: ScopedValues.preview(
64+
variables: {
65+
'idError': VariableData(id: 'a', name: 'idError', value: ''),
66+
'pinError': VariableData(id: 'b', name: 'pinError', value: ''),
67+
},
68+
),
69+
),
70+
),
71+
isTrue,
72+
);
73+
});
74+
}

‎test/text_field_lines_test.dart‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,28 @@ void main() {
142142
_expectOnScreen(tester, size);
143143
});
144144

145+
testWidgets('fixed-height password field is obscured at $label', (
146+
tester,
147+
) async {
148+
await _pump(
149+
tester,
150+
_field(
151+
maxLines: 1,
152+
minLines: null,
153+
verticalFit: SizeFit.fixed,
154+
obscureText: true,
155+
),
156+
size,
157+
);
158+
expect(tester.takeException(), isNull);
159+
final TextField field = tester.widget(find.byType(TextField));
160+
expect(field.obscureText, isTrue);
161+
expect(field.expands, isFalse);
162+
expect(field.maxLines, 1);
163+
expect(tester.getSize(find.byType(TextField)).height, 120);
164+
_expectOnScreen(tester, size);
165+
});
166+
145167
testWidgets('fixed-height field expands with lines set at $label', (
146168
tester,
147169
) async {

0 commit comments

Comments
 (0)