Skip to content

fix(input): make leading and trailing slots usable - #230

Merged
ndlabdev merged 1 commit into
devfrom
fix/229-input-slots
Sep 27, 2026
Merged

ndlabdev merged 1 commit into
devfrom
fix/229-input-slots

Conversation

@ndlabdev

Copy link
Copy Markdown
Owner

Summary

leadingSlot and trailingSlot are part of the public API of Input and had no test coverage at all, which is how three separate defects survived in them. This came out of #228, where the reporter suggested composing Input with a button as the alternative to a dedicated component. I tried exactly that and it did not work.

Measured in a real browser before changing anything, with the supported leadingIcon prop as the control case.

The slots reserved no room in the field:

prop leadingIcon | padding-left 36px, text starts 324, icon ends 320 -> no overlap
leadingSlot      | padding-left 12px, text starts 300, icon ends 322 -> OVERLAPS by 22px

Interactive content in a slot could not be clicked:

trailingSlot            -> blocked, counter stays at 0
leadingSlot             -> blocked, counter stays at 0
same code + ui override -> click registers

The cursor still changed to a pointer on hover, so it looked clickable. The workaround was ui={{ trailing: 'pointer-events-auto pe-1', base: 'pe-10' }}, which had to be reverse engineered and whose padding value had to be guessed per size.

A slot also swallowed the loading spinner. Three inputs in a loading state on one page produced one spinner and three disabled fields.

Closes #229

Type of change

  • 🐛 Bug fix
  • ✨ New feature / component
  • 📖 Documentation
  • ♻️ Refactor / chore
  • ⚠️ Breaking change

Changes

  • isLeading and isTrailing account for the slots, so the padding compound variants that already existed now apply. No new classes were added for this.
  • Pointer events are re-enabled only on a wrapper that actually holds a slot, and only while the field is neither disabled nor loading. A decorative icon stays click through, so clicking it still focuses the field, and a control inside a disabled field stays inert.
  • The loading branch takes precedence over a slot, matching how Button swaps its leading icon for the spinner.
  • Corrected the loading prop documentation, which claimed it optionally disables interaction while the input is always disabled while loading. That text ships in the published type declarations and shows up in editor tooltips.

Checklist

  • Linked the related issue (Closes #229)
  • pnpm check passes (0 errors, 0 warnings)
  • pnpm lint passes
  • pnpm test passes
  • Added or updated tests for the change
  • Updated CHANGELOG.md under [Unreleased]
  • Followed component conventions (no comments outside *.types.ts, Material 3 design tokens)

Notes

input.variants.ts is untouched. An earlier attempt added two boolean variants for the pointer events, which turned out to be unnecessary: the slot function already merges its class argument, so a conditional entry in the array that was already there resolves correctly and keeps ui overrides working. Verified that a caller passing ui={{ trailing: 'pointer-events-none' }} still wins.

Verified on a throwaway page in a real browser. Every wrapper ends up in the right state:

leadingIcon prop, decorative  -> none   (click through preserved)
leadingSlot with a button     -> auto
trailingSlot with a button    -> auto
loading, with or without slot -> none
disabled, with slot           -> none

Eight regression tests cover padding on both sides, pointer events on and off, the decorative case, the disabled and loading cases, spinner precedence, and slot rendering when idle. Tests: 3868 passing across 107 files.

The two slots were part of the public API but had no test coverage, and
three separate things were wrong with them.

isLeading and isTrailing ignored the slots, so the padding compound
variants never applied. The same icon measured 36px of padding through
the leadingIcon prop and 12px through the slot, leaving the content
overlapping the text by 22px. Both flags now account for the slots.

The slot wrapper carries pointer-events-none so that a decorative icon
stays click through and clicking it focuses the field. That also made
interactive slot content impossible to click: a button in a slot never
received a single click, while the cursor still changed on hover.
Pointer events are now re-enabled only on a wrapper that actually holds
a slot, and only while the field is neither disabled nor loading, so a
control inside a disabled field stays inert.

A slot also took precedence over the loading branch, so a field with a
slot went disabled during loading with no spinner and no other
indicator. Loading now wins, matching how Button swaps its leading icon
for the spinner.

Also corrected the loading prop documentation, which claimed it
optionally disables interaction while the input is always disabled while
loading. That text ships in the published type declarations.

Closes #229
@ndlabdev ndlabdev added bug Something isn't working priority: P1 High — important, schedule soon labels Sep 25, 2026
@ndlabdev ndlabdev self-assigned this Sep 25, 2026
@ndlabdev
ndlabdev merged commit 0bd6334 into dev Sep 27, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working priority: P1 High — important, schedule soon

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant