From 6949298ea1c7fce65a4226a368ef3211e1211e27 Mon Sep 17 00:00:00 2001 From: "M.Samir" Date: Tue, 12 May 2026 23:57:07 +0300 Subject: [PATCH] fix: suppress semantic hint and focused state when disabled (#427) --- .../lib/src/core/pin_input.dart | 14 +- .../pin_code_fields/test/pin_input_test.dart | 134 ++++++++++++++++++ 2 files changed, 142 insertions(+), 6 deletions(-) diff --git a/packages/pin_code_fields/lib/src/core/pin_input.dart b/packages/pin_code_fields/lib/src/core/pin_input.dart index e087fd4c..0ad6ce9e 100644 --- a/packages/pin_code_fields/lib/src/core/pin_input.dart +++ b/packages/pin_code_fields/lib/src/core/pin_input.dart @@ -942,11 +942,13 @@ class _PinInputState extends State ? '●' * 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', @@ -954,7 +956,7 @@ class _PinInputState extends State hint: semanticHint, textField: true, enabled: widget.enabled, - focused: _focusNode.hasFocus, + focused: widget.enabled && _focusNode.hasFocus, obscured: widget.obscureText, child: content, ); diff --git a/packages/pin_code_fields/test/pin_input_test.dart b/packages/pin_code_fields/test/pin_input_test.dart index 3ef18659..f7e4d8e8 100644 --- a/packages/pin_code_fields/test/pin_input_test.dart +++ b/packages/pin_code_fields/test/pin_input_test.dart @@ -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(