Files
pos-system/packages/blazor-ui/Components/Auth/OtpInput.razor
Ho Ngoc Hai af0461f233 fix(frontend): resolve 4 P2 architecture issues (Wave 3)
FRONT-I-01: Extract Auth components to Razor Class Library packages/blazor-ui/
- Created GoodGo.BlazorUi RCL (net10.0, MudBlazor 8.15) at packages/blazor-ui/
- Moved AuthButton, AuthCard, AuthInput, OtpInput, BrandPanel, SocialLogin, LanguageSwitcher
- Referenced RCL from WebClientTpos.Client via ProjectReference
- Added GoodGo.BlazorUi.Components.Auth/Common namespaces to _Imports.razor

FRONT-I-02: Add ARIA/accessibility attributes (WCAG 2.1 AA)
- AuthButton: aria-label, aria-busy, aria-disabled, aria-hidden on decorative icons
- OtpInput: role=group, aria-label per digit, autocomplete=one-time-code
- PosLayout: aria-expanded + aria-controls on sidebar/order toggles, aria-label on all icon buttons

FRONT-I-03: Implement Style Dictionary design token pipeline
- Created packages/design-tokens/ with token JSON (color, spacing, typography, border)
- Style Dictionary config outputs: CSS custom properties → wwwroot/css/tokens.generated.css
- Second output: C# constants → packages/blazor-ui/DesignTokens/DesignTokens.g.cs
- Added tokens:build script to root package.json
- Added tokens.generated.css link to index.html (before app.css for cascade correctness)

FRONT-I-04: Replace eval() in OtpInput with safe JS interop
- Created wwwroot/js/otp-input.js with window.focusOtpInput(index) helper
- Replaced JS.InvokeVoidAsync("eval", ...) with JS.InvokeVoidAsync("focusOtpInput", index)
- Eliminates CSP-violating eval(), improves security and debuggability

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-23 09:50:13 +07:00

100 lines
3.2 KiB
Plaintext

@*
EN: 6-digit OTP input with auto-focus, auto-advance, and backspace support.
VI: Input OTP 6 chữ số với auto-focus, tự chuyển ô, và hỗ trợ backspace.
*@
@inject IJSRuntime JS
<div class="auth-otp-group"
role="group"
aria-label="@GroupAriaLabel">
@for (int i = 0; i < DigitCount; i++)
{
var index = i;
<input id="otp-@index"
type="text"
inputmode="numeric"
maxlength="1"
autocomplete="one-time-code"
aria-label="@($"Digit {index + 1} of {DigitCount}")"
class="auth-otp-input @(UseBlueTheme ? "auth-otp-input--blue" : "") @(!string.IsNullOrEmpty(_digits[index]) ? "auth-otp-input--filled" : "")"
value="@_digits[index]"
@oninput="(e) => HandleInput(e, index)"
@onkeydown="(e) => HandleKeyDown(e, index)" />
}
</div>
@code {
private string[] _digits = new string[6];
/// <summary>
/// EN: Number of OTP digits.
/// VI: Số chữ số OTP.
/// </summary>
[Parameter] public int DigitCount { get; set; } = 6;
/// <summary>
/// EN: Use blue theme (for 2FA authenticator).
/// VI: Dùng theme xanh dương (cho 2FA authenticator).
/// </summary>
[Parameter] public bool UseBlueTheme { get; set; }
/// <summary>
/// EN: Accessible group label for screen readers.
/// VI: Nhãn nhóm accessible cho screen reader.
/// </summary>
[Parameter] public string GroupAriaLabel { get; set; } = "Enter OTP code";
/// <summary>
/// EN: Callback when all digits are entered.
/// VI: Callback khi tất cả chữ số được nhập.
/// </summary>
[Parameter] public EventCallback<string> OnComplete { get; set; }
protected override void OnInitialized()
{
_digits = new string[DigitCount];
}
private async Task HandleInput(ChangeEventArgs e, int index)
{
var value = e.Value?.ToString() ?? "";
// EN: Only allow numeric input
// VI: Chỉ cho phép nhập số
if (!string.IsNullOrEmpty(value) && !char.IsDigit(value[0]))
{
_digits[index] = "";
return;
}
_digits[index] = value;
if (!string.IsNullOrEmpty(value) && index < DigitCount - 1)
{
// EN: Auto-advance to next input
// VI: Tự động chuyển sang ô tiếp theo
await JS.InvokeVoidAsync("focusOtpInput", index + 1);
}
// EN: Check if all digits are filled
// VI: Kiểm tra tất cả ô đã nhập xong chưa
if (_digits.All(d => !string.IsNullOrEmpty(d)))
{
var code = string.Join("", _digits);
await OnComplete.InvokeAsync(code);
}
}
private async Task HandleKeyDown(KeyboardEventArgs e, int index)
{
if (e.Key == "Backspace" && string.IsNullOrEmpty(_digits[index]) && index > 0)
{
// EN: Move to previous input on backspace
// VI: Chuyển về ô trước khi nhấn backspace
_digits[index - 1] = "";
await JS.InvokeVoidAsync("focusOtpInput", index - 1);
}
}
}