Skip to content

Commit 7f01ef4

Browse files
committed
Improve OTPInput paste handling and JS interop safety
- Allow multi-digit paste by setting maxlength=6 on input fields. - Distribute pasted digits across OTP fields and update focus accordingly. - Add SafeInvokeVoidAsync to safely wrap JS interop calls. - Replace direct JSRuntime.InvokeVoidAsync calls with SafeInvokeVoidAsync to handle component disposal and JS runtime disconnects gracefully. - Remove old logic that only used the last digit on multi-digit input. - Improves robustness and user experience during input and navigation.
1 parent 976ac09 commit 7f01ef4

2 files changed

Lines changed: 57 additions & 10 deletions

File tree

blazorbootstrap/Components/Form/OTPInput/OTPInput.razor

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
id="@inputId"
1212
class="@ClassNames"
1313
style="@StyleNames"
14-
maxlength="1"
14+
maxlength="6"
1515
inputmode="numeric"
1616
value="@otpValues[index]"
1717
@oninput="(e) => OnInput(e, index)"

blazorbootstrap/Components/Form/OTPInput/OTPInput.razor.cs

Lines changed: 56 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
namespace BlazorBootstrap;
22

3+
using Microsoft.JSInterop;
4+
35
public partial class OTPInput : BlazorBootstrapComponentBase
46
{
57
#region Fields and Constants
@@ -22,6 +24,23 @@ protected override void OnParametersSet()
2224
}
2325
}
2426

27+
private async Task SafeInvokeVoidAsync(string identifier, params object?[] args)
28+
{
29+
try
30+
{
31+
await JSRuntime.InvokeVoidAsync(identifier, args);
32+
}
33+
catch (TaskCanceledException)
34+
{
35+
// Component/DOM likely got removed (navigation, conditional render, etc.)
36+
// Treat as benign for focus/value updates.
37+
}
38+
catch (JSDisconnectedException)
39+
{
40+
// JS runtime no longer available (more common on Server, but safe here too).
41+
}
42+
}
43+
2544
/// <summary>
2645
/// Clears the OTP input fields.
2746
/// </summary>
@@ -34,7 +53,7 @@ public async Task ClearAsync()
3453
await NotifyChangesAsync();
3554

3655
if (Length > 0)
37-
await JSRuntime.InvokeVoidAsync(JsInteropUtils.FocusInputElement, GetInputId(0));
56+
await SafeInvokeVoidAsync(JsInteropUtils.FocusInputElement, GetInputId(0));
3857

3958
await InvokeAsync(StateHasChanged);
4059
}
@@ -64,28 +83,56 @@ private async Task OnInput(ChangeEventArgs e, int index)
6483
// Clear the input element if it contained invalid characters
6584
if (!string.IsNullOrEmpty(rawValue))
6685
{
67-
await JSRuntime.InvokeVoidAsync(JsInteropUtils.SetInputElementValue, GetInputId(index), string.Empty);
86+
await SafeInvokeVoidAsync(JsInteropUtils.SetInputElementValue, GetInputId(index), string.Empty);
6887
}
6988

7089
await NotifyChangesAsync();
7190
return;
7291
}
7392

74-
// If multiple digits were entered (e.g. fast typing or paste), use the last one
75-
var digit = numericValue.Length > 1 ? numericValue[^1].ToString() : numericValue;
93+
// If multiple digits were entered (e.g. paste), distribute them across the input fields
94+
if (numericValue.Length > 1)
95+
{
96+
var digits = numericValue.ToCharArray();
97+
var currentInputLength = digits.Length;
98+
99+
for (int i = 0; i < currentInputLength; i++)
100+
{
101+
var targetIndex = index + i;
102+
if (targetIndex < Length)
103+
{
104+
otpValues[targetIndex] = digits[i].ToString();
105+
106+
// Update the UI value for the current input and subsequent inputs
107+
await SafeInvokeVoidAsync(JsInteropUtils.SetInputElementValue, GetInputId(targetIndex), otpValues[targetIndex]);
108+
}
109+
}
110+
111+
// Move focus to the next input field after the last pasted digit
112+
var nextIndex = index + currentInputLength;
113+
if (nextIndex < Length)
114+
await SafeInvokeVoidAsync(JsInteropUtils.FocusInputElement, GetInputId(nextIndex));
115+
else
116+
await SafeInvokeVoidAsync(JsInteropUtils.FocusInputElement, GetInputId(Length - 1));
117+
118+
await NotifyChangesAsync();
119+
return;
120+
}
121+
122+
var digit = numericValue;
76123

77124
otpValues[index] = digit;
78125

79126
// Reset the input value on the client side if it doesn't match the sanitized digit
80127
if (rawValue != digit)
81128
{
82-
await JSRuntime.InvokeVoidAsync(JsInteropUtils.SetInputElementValue, GetInputId(index), digit);
129+
await SafeInvokeVoidAsync(JsInteropUtils.SetInputElementValue, GetInputId(index), digit);
83130
}
84131

85132
// Move focus to the next input field
86133
if (index < Length - 1)
87134
{
88-
await JSRuntime.InvokeVoidAsync(JsInteropUtils.FocusInputElement, GetInputId(index + 1));
135+
await SafeInvokeVoidAsync(JsInteropUtils.FocusInputElement, GetInputId(index + 1));
89136
}
90137

91138
await NotifyChangesAsync();
@@ -97,19 +144,19 @@ private async Task OnKeyUp(KeyboardEventArgs e, int index)
97144
if (e.Key == "Backspace" && index > 0)
98145
{
99146
otpValues[index] = string.Empty;
100-
await JSRuntime.InvokeVoidAsync(JsInteropUtils.FocusInputElement, GetInputId(index - 1));
147+
await SafeInvokeVoidAsync(JsInteropUtils.FocusInputElement, GetInputId(index - 1));
101148

102149
// Notify changes
103150
await NotifyChangesAsync();
104151
}
105152

106153
// Handle left arrow key to focus on the previous input
107154
if (e.Key == "ArrowLeft" && index > 0)
108-
await JSRuntime.InvokeVoidAsync(JsInteropUtils.FocusInputElement, GetInputId(index - 1));
155+
await SafeInvokeVoidAsync(JsInteropUtils.FocusInputElement, GetInputId(index - 1));
109156

110157
// Handle right arrow key to focus on the next input
111158
if (e.Key == "ArrowRight" && index < Length - 1)
112-
await JSRuntime.InvokeVoidAsync(JsInteropUtils.FocusInputElement, GetInputId(index + 1));
159+
await SafeInvokeVoidAsync(JsInteropUtils.FocusInputElement, GetInputId(index + 1));
113160
}
114161

115162
#endregion

0 commit comments

Comments
 (0)