Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions packages/pin_code_fields/lib/src/core/pin_input.dart
Original file line number Diff line number Diff line change
Expand Up @@ -942,19 +942,21 @@ class _PinInputState extends State<PinInput>
? '●' * filledCount // Don't reveal obscured text
: currentText;

// Use custom hint builder if provided, otherwise use default
final semanticHint = widget.semanticHintBuilder?.call(filledCount, widget.length) ??
(filledCount < widget.length
? 'Enter ${widget.length - filledCount} more ${widget.length - filledCount == 1 ? 'digit' : 'digits'}'
: 'PIN complete');
// Suppress hint when disabled so the platform can announce "dimmed" unobstructed.
final semanticHint = widget.enabled
? (widget.semanticHintBuilder?.call(filledCount, widget.length) ??
(filledCount < widget.length
? 'Enter ${widget.length - filledCount} more ${widget.length - filledCount == 1 ? 'digit' : 'digits'}'
: 'PIN complete'))
: null;

return Semantics(
label: widget.semanticLabel ?? '${widget.length}-digit PIN code field',
value: semanticValue.isNotEmpty ? semanticValue : null,
hint: semanticHint,
textField: true,
enabled: widget.enabled,
focused: _focusNode.hasFocus,
focused: widget.enabled && _focusNode.hasFocus,
obscured: widget.obscureText,
child: content,
);
Expand Down
134 changes: 134 additions & 0 deletions packages/pin_code_fields/test/pin_input_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,140 @@ void main() {
expect(semantics.hasFlag(SemanticsFlag.isEnabled), false);
});

testWidgets('suppresses semantic hint when disabled', (tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: PinInput(
length: 6,
enabled: false,
builder: (context, cells) => Row(
children: cells.map((c) => Text(c.character ?? '-')).toList(),
),
),
),
),
);

final semanticsFinder = find.byWidgetPredicate((widget) {
if (widget is Semantics) {
return widget.properties.label?.contains('PIN code field') ?? false;
}
return false;
});
final semantics = tester.getSemantics(semanticsFinder);
// Hint must be absent so the platform can announce "dimmed" cleanly.
expect(semantics.hint, isEmpty);
});

testWidgets('suppresses semantic hint when disabled with pre-filled value',
(tester) async {
final controller = PinInputController(text: '1234');

await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: PinInput(
length: 4,
enabled: false,
pinController: controller,
builder: (context, cells) => Row(
children: cells.map((c) => Text(c.character ?? '-')).toList(),
),
),
),
),
);

await tester.pump();

final semanticsFinder = find.byWidgetPredicate((widget) {
if (widget is Semantics) {
return widget.properties.label?.contains('PIN code field') ?? false;
}
return false;
});
final semantics = tester.getSemantics(semanticsFinder);
// "PIN complete" hint must also be suppressed when disabled.
expect(semantics.hint, isEmpty);
});

testWidgets('suppresses custom semanticHintBuilder when disabled',
(tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: PinInput(
length: 4,
enabled: false,
semanticHintBuilder: (_, __) => 'Enter your security code',
builder: (context, cells) => Row(
children: cells.map((c) => Text(c.character ?? '-')).toList(),
),
),
),
),
);

final semanticsFinder = find.byWidgetPredicate((widget) {
if (widget is Semantics) {
return widget.properties.label?.contains('PIN code field') ?? false;
}
return false;
});
final semantics = tester.getSemantics(semanticsFinder);
// Custom hint builder output must be suppressed too.
expect(semantics.hint, isEmpty);
});

testWidgets(
'does not report focused state when field is disabled after focus',
(tester) async {
bool enabled = true;

await tester.pumpWidget(
StatefulBuilder(
builder: (context, setState) => MaterialApp(
home: Scaffold(
body: Column(
children: [
PinInput(
length: 4,
enabled: enabled,
autoFocus: true,
builder: (context, cells) => Row(
children:
cells.map((c) => Text(c.character ?? '-')).toList(),
),
),
ElevatedButton(
onPressed: () => setState(() => enabled = false),
child: const Text('Disable'),
),
],
),
),
),
),
);

await tester.pump();

// Disable the field while it potentially has focus.
await tester.tap(find.text('Disable'));
await tester.pump();

final semanticsFinder = find.byWidgetPredicate((widget) {
if (widget is Semantics) {
return widget.properties.label?.contains('PIN code field') ?? false;
}
return false;
});
final semantics = tester.getSemantics(semanticsFinder);
// ignore: deprecated_member_use
expect(semantics.hasFlag(SemanticsFlag.isFocused), false);
});

testWidgets('uses custom semanticHintBuilder for dynamic hints',
(tester) async {
await tester.pumpWidget(
Expand Down