Skip to content

fix: company-input-v2 — explicit selection only, no clear icon, "Use "<typed>"" row - #289

Merged
smarcet merged 6 commits into
mainfrom
feature/company-input-v2-freetext-use-option
Jul 14, 2026
Merged

fix: company-input-v2 — explicit selection only, no clear icon, "Use "<typed>"" row#289
smarcet merged 6 commits into
mainfrom
feature/company-input-v2-freetext-use-option

Conversation

@JpMaxMan

@JpMaxMan JpMaxMan commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

The bug

CompanyInputV2 used MUI's autoSelect, which commits the currently highlighted option on blur. Users would mouse over a suggestion, then tab away intending to keep their typed text — and the moused-over company got saved instead (wrong company populated).

Changes (src/components/inputs/company-input-v2.js)

  • Remove autoSelect — selection is now explicit (click / Enter) only. This is the root-cause fix.
  • New onBlur — tab/click-away commits exactly what was typed: resolved to an existing company only on an exact (case-insensitive) name match, otherwise a free-text { id: 0, name }. Never commits a merely-highlighted option.
  • disableClearable — removes the MUI clear (x) icon (the field commits free text, so an explicit clear affordance isn't wanted; delete the text to empty).
  • Use "<typed>" rowfilterOptions appends a synthetic option so users can explicitly commit their typed text; skipped when it already matches a listed company. Commits a clean { id: 0, name } (display-only marker stripped). renderOption shows the Use "…" label; getOptionLabel still returns the plain name.
  • Predictive typeahead unchanged (freeSolo + server-side queryRegistrationCompanies).

⚠️ Shared-component behavior change

This affects all CompanyInputV2 consumers, not just the requesting screen: no more clear (x) icon, and blur no longer auto-selects a highlighted option (it keeps the typed text). Both are intentional — flagging for reviewers.

Note: the Formik variant CompanyInputMUI (mui/formik-inputs/company-input-mui.js) is a separate implementation and is not touched here; if a Formik form needs the same behavior we'll do it in a follow-up.

Tests

+5 tests; the file's suite is 25/25 green:

  • blur keeps the typed text and never commits a highlighted option (pins the bug);
  • blur resolves an exact case-insensitive match to the canonical company;
  • clear icon never renders, even with a real selected value;
  • Use "…" row commits clean free text; no redundant Use row on an exact match.

Verified by running jest directly (node_modules/.bin/jest) — CI's Node 18 avoids a local node-sass/Node-25 build issue unrelated to this change. package.json version intentionally not bumped (done separately at release).

Summary by CodeRabbit

  • New Features
    • Added a synthetic “Use ‘…’” option to commit a company name that isn’t in the suggestions.
    • “Use ‘…’” is shown only when it doesn’t already match an available option (trimmed, case-insensitive).
  • Bug Fixes
    • Leaving the field (including browser autofill) now correctly commits typed/free-text values when there’s no exact match; exact-name matches commit the existing option instead.
    • Removed the clear (x) icon to keep the selection behavior consistent and prevent redundant updates.
  • Tests
    • Expanded blur/autofill/clear test coverage and updated assertions for the “Use ‘…’” row behavior.

…"<typed>"" row

Root cause of the wrong-company bug: `autoSelect` committed the currently
*highlighted* option on blur, so merely mousing over a suggestion and then
tabbing away silently populated the wrong company.

- Remove `autoSelect`. Selection is now explicit (click / Enter) only.
- New onBlur: tab/click-away commits exactly what was typed — resolved to an
  existing company only on an exact (case-insensitive) name match, else a
  free-text { id: 0, name }. Never commits a merely-highlighted option.
- `disableClearable`: remove the MUI clear (x) icon; the field commits free
  text, so an explicit clear affordance isn't wanted (delete text to empty).
- Append a synthetic 'Use "<typed>"' row so users can explicitly commit their
  text; skipped when it already matches a listed company. Commits a clean
  { id: 0, name }, dropping the display-only marker.
- Predictive typeahead unchanged (freeSolo + server-side queryRegistrationCompanies).

Tests: +5 (blur keeps typed text / never a highlighted option; clear icon never
renders even with a real value; "Use" row commits clean free text; no redundant
"Use" row on exact match). 25/25 green.
@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ac6c7aea-13dd-4afa-b510-2dd95550d5b9

📥 Commits

Reviewing files that changed from the base of the PR and between 0f79e52 and c279cc7.

📒 Files selected for processing (1)
  • src/components/inputs/company-input-v2.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/components/inputs/company-input-v2.js

📝 Walkthrough

Walkthrough

CompanyInputV2 commits typed text on blur, resolves case-insensitive company matches, supports explicit Use "<typed>" free-text options, disables the clear icon, and adds integration coverage for these behaviors.

Changes

CompanyInputV2 selection behavior

Layer / File(s) Summary
Selection and blur normalization
src/components/inputs/company-input-v2.js, src/components/inputs/__tests__/company-input-v2.test.js
Blur commits matching companies or { id: 0, name } free-text entries, handles browser autofill and clearing, while onChange normalizes string and synthetic free-text selections.
Synthetic free-text option UI
src/components/inputs/company-input-v2.js, src/components/inputs/__tests__/company-input-v2.test.js
Autocomplete preserves options, adds and labels Use "<typed>" rows, disables the clear icon, and tests matching-option suppression and free-text selection.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant Autocomplete
  participant CompanyInputV2
  participant Parent
  User->>Autocomplete: type or autofill company text
  Autocomplete->>CompanyInputV2: blur with DOM input value
  CompanyInputV2->>Parent: commit company, free-text entry, or null
Loading

Possibly related PRs

Suggested reviewers: smarcet

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately highlights the main behavior changes: explicit selection, disabled clear icon, and the free-text row.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/company-input-v2-freetext-use-option

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/inputs/company-input-v2.js`:
- Around line 113-139: Update the onBlur handler to clear the committed company
when the trimmed inputValue is empty, by calling fireChange with null or the
component’s established clear value. Preserve the existing exact-match and
free-text behavior for non-empty input, avoid duplicate changes when already
cleared, and add a regression test covering delete-all-text followed by blur.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ecfeb9fa-e11a-4a27-bcea-861a0f48f74e

📥 Commits

Reviewing files that changed from the base of the PR and between 5688cf7 and 294bc61.

📒 Files selected for processing (2)
  • src/components/inputs/__tests__/company-input-v2.test.js
  • src/components/inputs/company-input-v2.js

Comment thread src/components/inputs/company-input-v2.js
…241)

Follow-up to removing autoSelect. autoSelect (added in #241) committed the
input's DOM value on blur, which is how it captured iOS-Chrome browser autofill
(autofill writes to the DOM without firing onInputChange). The initial onBlur
here read React input state, which would be stale for autofill and reintroduce
the #241 required-field-validation bug.

- onBlur now reads event.target.value (the DOM value), not React state, so
  typed AND autofilled values propagate on blur — while still committing the
  field text, never a highlighted option (the autoSelect-on-hover bug fix
  stands).
- Test: commits a browser-autofilled value on blur even when onInputChange
  never fired. 26/26 green.

Needs a Chrome-iOS device re-test of the #241 autofill path before merge.
@JpMaxMan

Copy link
Copy Markdown
Contributor Author

Re: autoSelect and the #241 autofill fix — addressed in 1bc112a

@gcutrini flagged (via Slack) that autoSelect was added in #241 to fix a real bug: in freeSolo mode MUI only propagates a value on explicit selection, so typing-and-tabbing and browser autofill (iOS Chrome) never reached the parent and required-field validation failed. Correct — and simply removing autoSelect would regress that.

Reconciliation (latest commit): autoSelect stays removed — it's what committed the currently highlighted option on blur, so mousing over a suggestion and tabbing away saved the wrong company. In its place, onBlur now reads the input's DOM value (event.target.value) — the same source autoSelect read internally — and commits it as the field value. So:

Suite: 26/26 (jest run directly; CI Node 18 avoids a local node-sass/Node-25 issue).

⚠️ Merge gate: the autofill test passes in jsdom, but jsdom ≠ iOS Chrome. Reading event.target.value is strictly the more-robust source (it's what autoSelect used), but the real iOS-Chrome autofill path needs a device re-test. @gcutrini — could you verify that path on this branch, since you have the #241 repro? Happy to pair if the behavior differs.

…odeRabbit)

With disableClearable there's no (x), so delete-all-text + blur is the only way
to clear the field — but onBlur previously fired nothing for empty input,
leaving the prior company committed (a required field still read as filled).

- onBlur now propagates null when the field is emptied, guarded so an
  already-cleared field doesn't fire a redundant change. Non-empty
  exact-match / free-text behavior unchanged.
- Tests: clear-on-empty-and-blur; no redundant change on already-empty blur.
  28/28 green.
@JpMaxMan

Copy link
Copy Markdown
Contributor Author

CodeRabbit finding (clear-on-empty in onBlur) — addressed in 0f79e52

Verified against the current code: valid, and worth fixing precisely because this PR removes the clear (x) icon (disableClearable), making delete-all-text + blur the only way to clear the field. Previously onBlur fired nothing for empty input, so the prior company stayed committed (a required field would still read as filled).

Fix (minimal):

  • onBlur now propagates null when the field is emptied, guarded by if (normalizedValue) so an already-cleared field doesn't emit a redundant change.
  • Non-empty exact-match / free-text behavior unchanged.
  • Regression tests added: "clears the committed company when the field is emptied (delete-all-text) and blurred" and "does not fire a redundant change when an already-empty field is blurred". Suite 28/28.

@gcutrini

Copy link
Copy Markdown
Contributor

Small UX nit: the Use "<typed>" row lands at the bottom. Feels better at the top. Typed text is the user's primary intent, and arrow-down-then-Enter would commit it without scrolling past matches. One-line change in filterOptions:

return trimmed && !alreadyListed
  ? [{ id: 0, name: trimmed, isFreeTextOption: true }, ...opts]
  : opts;

gcutrini added 3 commits July 14, 2026 12:52
…pdown

Prepend the synthetic free-text row instead of appending. Puts the
user's typed text as the primary action (arrow-down + Enter commits it
without scrolling past API suggestions) and matches the intent that
they took the trouble to type.
getOptionLabel, filterOptions, and renderOption each computed the
option-shape-to-name mapping inline. Extracts a single exported helper
so the string / company-object / malformed cases live in one place.
…layLabel

The local variable shadowed the outer `label` prop, which is confusing
to skim. Same-block only, no behavior change.
@gcutrini
gcutrini requested a review from smarcet July 14, 2026 17:50
smarcet pushed a commit that referenced this pull request Jul 14, 2026
…main (v4.x) (#290)

Port of PR #289 + layered commits from main:
- Remove `autoSelect` (root cause of hover-then-tab wrong-company commits)
- Explicit onBlur that reads event.target.value (DOM value; falls back to
  React input state) so browser autofill still propagates on blur --
  preserves #241/#246's iOS Chrome autofill fix
- `disableClearable`: with free-text supported, an explicit clear (x)
  isn't wanted; users empty by deleting the text
- Synthetic "Use "<typed>"" row prepended to dropdown when the typed text
  isn't already listed; committed cleanly (marker stripped) via onChange
- Extract `getOptionName` helper used by getOptionLabel, filterOptions,
  renderOption; rename local `label` in renderOption to `displayLabel`
  to stop shadowing the outer prop

Tests: +7 (28/28 green), including regression coverage for the hover-then-tab
guard, the DOM-value autofill path, and the Use/free-text commits.

@smarcet smarcet left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@smarcet
smarcet merged commit fc351ae into main Jul 14, 2026
5 checks passed
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.

3 participants