Skip to content

feat(input-opt): add input OTP masking options on parent, group and slot#1244

Open
liam-langstaff wants to merge 4 commits intospartan-ng:mainfrom
liam-langstaff:feat/masked-input-otp
Open

feat(input-opt): add input OTP masking options on parent, group and slot#1244
liam-langstaff wants to merge 4 commits intospartan-ng:mainfrom
liam-langstaff:feat/masked-input-otp

Conversation

@liam-langstaff
Copy link
Contributor

@liam-langstaff liam-langstaff commented Mar 3, 2026

PR Checklist

Please check if your PR fulfills the following requirements:

PR Type

What kind of change does this PR introduce?

  • Bugfix
  • Feature
  • Code style update (formatting, local variables)
  • Refactoring (no functional changes, no api changes)
  • Build related changes
  • CI related changes
  • Documentation content changes
  • Other... Please describe:

Which package are you modifying?

Primitives

  • accordion
  • alert
  • alert-dialog
  • aspect-ratio
  • autocomplete
  • avatar
  • badge
  • breadcrumb
  • button
  • button-group
  • calendar
  • card
  • carousel
  • checkbox
  • collapsible
  • combobox
  • command
  • context-menu
  • data-table
  • date-picker
  • dialog
  • empty
  • dropdown-menu
  • field
  • form-field
  • hover-card
  • icon
  • input
  • input-group
  • input-otp
  • item
  • kbd
  • label
  • menubar
  • native-select
  • navigation-menu
  • pagination
  • popover
  • progress
  • radio-group
  • resizable
  • scroll-area
  • select
  • separator
  • sheet
  • sidebar
  • skeleton
  • slider
  • sonner
  • spinner
  • switch
  • table
  • tabs
  • textarea
  • toggle
  • toggle-group
  • tooltip
  • typography

Others

  • trpc
  • nx
  • repo
  • cli

What is the current behavior?

--

Adds configurable masking for OTP slots across Brain and Helm.
• Introduces OTP character masking with brief reveal-then-mask behavior.
• Adds mask-value pipe and tests to cover masking timing and edge cases.
• Adds BrnInputOtpMask directive for group-level masking control.
• Wires group masking into Helm via hlmInputOtpGroup host directive.
• Supports per-slot [mask] overrides in hlm-input-otp-slot.
• Defines mask precedence as: slot mask -> group mask -> root brn-input-otp mask.
• Keeps existing behavior unchanged when masking is not enabled.

Does this PR introduce a breaking change?

  • Yes
  • No

Other information

masked_input_otp.mp4

@liam-langstaff liam-langstaff force-pushed the feat/masked-input-otp branch from 86e7f70 to e0c18c6 Compare March 3, 2026 20:01
@liam-langstaff liam-langstaff marked this pull request as ready for review March 3, 2026 20:20
@greptile-apps
Copy link

greptile-apps bot commented Mar 3, 2026

Greptile Summary

This PR adds configurable OTP character masking at three levels — root (brn-input-otp), group (hlmInputOtpGroup), and individual slot (hlm-input-otp-slot) — with a brief reveal-then-mask behaviour implemented via a new impure MaskValuePipe.

Key changes:

  • MaskValuePipe: new impure pipe that shows a freshly typed character for a configurable delay (default 200 ms) before replacing it with (U+2217). Timer lifecycle and CDR.markForCheck() on an OnPush host are handled correctly.
  • BrnInputOtpMask: minimal directive used as a hostDirective on HlmInputOtpGroup to provide group-level mask context to child slots via Angular's element injector DI chain.
  • BrnInputOtpSlot._hasMask computed: resolves effective mask via slot.mask() ?? groupMask?.mask() ?? root.mask() — precedence is correct and the nullish-coalescing chain handles explicit false overrides properly.
  • Tests cover pipe timing, falsy values, and the integration reveal-then-mask flow.

Issues found:

  • The mask input on BrnInputOtp is declared as input<boolean>(false) without a booleanAttribute transform, unlike disabled. Static attribute usage (<brn-input-otp mask>) will receive a string rather than a boolean, inconsistent with the rest of the API.
  • All three new example files contain an incorrect copy-pasted JSDoc comment — /** Overrides global formatDate */ — on the transformPaste function, which strips dashes from pasted OTP text and has nothing to do with date formatting.
  • Minor: missing blank line between the mask input and constructor() in hlm-input-otp-slot.ts.

Confidence Score: 4/5

  • Safe to merge with minor polish; no breaking changes and existing behaviour is unchanged when masking is not enabled.
  • Core masking logic, DI chain, timer management, and test coverage are all solid. The only noteworthy issue is the missing booleanAttribute transform on the root mask input, which creates a minor API inconsistency but doesn't affect the documented binding-based usage patterns.
  • libs/brain/input-otp/src/lib/brn-input-otp.ts — mask input should use booleanAttribute transform for consistency with disabled.

Important Files Changed

Filename Overview
libs/brain/input-otp/src/lib/util/mask-value-pipe.ts New impure pipe that briefly shows a typed character before replacing it with the mask glyph (U+2217) after a configurable delay. State management (last value, mask flag, timer) is handled correctly, the timer is properly cleaned up in ngOnDestroy, and CDR.markForCheck() correctly triggers re-rendering in OnPush hosts.
libs/brain/input-otp/src/lib/brn-input-otp-slot.ts Adds slot-level mask input and resolves the effective mask via a computed signal using the correct precedence: slot → group (BrnInputOtpMask injected optionally) → root. DI chain works correctly through Angular's element injector hierarchy.
libs/brain/input-otp/src/lib/brn-input-otp.ts Adds root-level mask input. Minor: the input lacks booleanAttribute transform, making static attribute usage (e.g. <brn-input-otp mask>) inconsistent with the disabled input.
libs/brain/input-otp/src/lib/brn-input-otp-mask.ts Minimal directive that carries a single optional boolean mask signal, used as a host directive on hlmInputOtpGroup to provide group-level mask context to child slots via DI.
libs/helm/input-otp/src/lib/hlm-input-otp-group.ts Wires BrnInputOtpMask as a hostDirective so that [mask] can be applied to the group element and be injected by child brn-input-otp-slot instances. Clean and correct.
libs/helm/input-otp/src/lib/hlm-input-otp-slot.ts Adds mask input and passes it through to brn-input-otp-slot. Minor style issue: missing blank line before the constructor.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["brn-input-otp\n[mask]=true/false (root)"] --> B["div[hlmInputOtpGroup]\n[mask]=true/false/undefined (group)\nHostDirective: BrnInputOtpMask"]
    B --> C["hlm-input-otp-slot\n[mask]=true/false/undefined (slot)"]
    C --> D["brn-input-otp-slot\n_hasMask computed"]

    D --> E{"slot.mask()\n!= undefined?"}
    E -- Yes --> F["Use slot mask"]
    E -- No --> G{"groupMask?.mask()\n!= undefined?"}
    G -- Yes --> H["Use group mask"]
    G -- No --> I["Use root mask\n(brn-input-otp.mask)"]

    F --> J["MaskValuePipe\ntransform(char, hasMask, 200ms)"]
    H --> J
    I --> J

    J --> K{"mask=false\nor char falsy?"}
    K -- Yes --> L["Render raw char"]
    K -- No --> M{"New char\nor mask toggled on?"}
    M -- Yes --> N["Show char briefly\nstart 200ms timer"]
    N --> O["Timer fires → _shouldMaskNow=true\nCDR.markForCheck()"]
    O --> P["Render ✱ (U+2217)"]
    M -- No --> Q{"_shouldMaskNow?"}
    Q -- Yes --> P
    Q -- No --> L
Loading

Last reviewed commit: 2c10352

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant