Skip to content

fix(Typeahead): keep the field's width when a value is selected - #5682

Open
freddymeta wants to merge 3 commits into
mainfrom
fix/typeahead-collapse-width
Open

fix(Typeahead): keep the field's width when a value is selected#5682
freddymeta wants to merge 3 commits into
mainfrom
fix/typeahead-collapse-width

Conversation

@freddymeta

@freddymeta freddymeta commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Fixes #5560.

It is the component, not the docsite

The docsite only exposes it. Its preview is centred, a centred child is a flex item, and a flex item is sized to its content — a perfectly ordinary layout that every other field survives. Three fields in identical display: flex parents, measured in Chromium at a 1000px viewport, before and after giving each one a value:

before after
TextInput 199px 199px unchanged
Tokenizer 199px 151px −48px
Typeahead 199px 44px −155px

TextInput is the control: same parent, same viewport, no movement. So the layout is not what is wrong.

inline-block reproduces it exactly (199 → 44). A block-level parent hides it completely (984 → 984 for all three), which is why no story caught it: every Typeahead story renders in a fixed-width container.

Cause

A field's width must not depend on its value, and every other field keeps that promise for free: its <input> stays in flow, so the field is as wide as the input's own default size. Typeahead takes the input out of flow when the token shows —

inputXStyle={showToken ? styles.inputHidden : undefined}
// inputHidden: { width: 0, minWidth: 0, flex: '0 0 0', position: 'absolute', … }

— and the input is the only child with an intrinsic width. Neither the Typeahead wrapper nor the shared inputWrapperStyles.base sets one. Remove the input and the only thing left to measure is the token, so a content-sized parent shrinks the field onto it. At 44px the token's own label is clipped to one letter and the clear button lands on top of it.

The two approaches that don't work

Both were measured, not reasoned about:

Keeping the collapsed input in flow — the issue's own suggestion, and my first instinct — moves the field from 44px to 95px. It does not fix it. width: 0 removes the input's intrinsic contribution whether or not the input is in flow, and dropping position: absolute gives back only the flex gap.

min(200px, 100%), the shape this repo reaches for elsewhere to mean "yield when there is no room", silently does nothing — the field stayed at 44px. A percentage min-width resolves against an indefinite containing block during shrink-to-fit, so it computes to 0 and min() picks it. Worth knowing before someone copies that idiom into another intrinsic-sizing context.

The fix

The field states the width it already had instead of inheriting it from the input:

inputCollapsedWidth: {
  '--typeahead-min-width': '200px',
  minWidth: 'var(--typeahead-min-width)',
},

Applied only while the token shows, so an unselected field is byte-identical to today. min-width, not width, so a block-level or stretched field still fills exactly as it does now — this only stops the collapse. The default is the width the field already measures: 181px, which is what a browser gives an <input> at the base font, plus this field's own 19px of padding and border.

The value is public and themeable, because the right minimum for a field is a design decision rather than a constant:

typeahead: {base: {'--typeahead-min-width': '16rem'}}

Measured

before and after

Chromium, before → after selection:

parent before after
display: flex 199 → 44 199 → 200
inline-block 199 → 44 199 → 200
display: block 984 → 984 984 → 984

The remaining 1px is the gap between the browser's font-derived default and the stated floor; a theme that wants them identical can say so.

(Assets live on the assets/pr-5560 branch — asset-only, deletable with the PR. I have no fork.)

Tests and story

Two unit tests, in the probe-class style this repo already uses for declarations jsdom cannot measure: the floor is present when a token shows, and absent when one does not. Reverting the fix fails the first and leaves the second passing.

One story, With Selected Value. No Typeahead story rendered a selected value, and every story renders inside a fixed-width container — between them, that is exactly why a bug this visible survived. The new one shows a token in a flex parent, the case that used to collapse.

Full build, core typecheck, docs typecheck, Storybook typecheck, check:repo and lint:strict pass; 286 test files green.

Two things CI caught, both fair

A documented var has to be registered. derivedVarRegistry's test requires every documented var to either map onto a standard CSS property or be listed as unmappable with a reason. min-width maps onto this one, so it gets the entry — which also means a theme can write the standard property and have both it and the var emitted:

typeahead: {base: {minWidth: '16rem'}}

A var no element declares is a var no theme can reach. theme-var-reachability walks the built Storybook asking which element sets each documented var, and --typeahead-min-width had no answer: it was declared inside the collapsed-state style, and no story ever selected a value. The declaration moved up to the wrapper — the element carrying the typeahead theme target, always rendered — and only the min-width reading it stays conditional. Same behavior, and the same shape Spinner settled on for its own public vars. Reachability now reports .astryx-typeahead sets it (200px).

Left alone

Tokenizer collapses its input the same way and shrinks for the same reason (199 → 151). Its input is multi-token and wrapping, so the right floor there is a different question than a single-value field's, and worth its own change.

Long values still widen the field (a 49-character value takes it to 278px). That is the same invariant seen from the other side, and it predates this issue; fixing it properly means laying the token over the input rather than beside it, which changes long values from widening the field to truncating in place — a product decision rather than a bug fix, so not smuggled in here.

Selecting a value shrank the field — 199px to 44px for a one-word value,
measured in Chromium — in any layout that sizes it to its content: a flex
item, `inline-block`, a shrink-to-fit grid track. The docsite's own Typeahead
page is one, since its preview is centred and a centred child is a flex item.

A field's width must not depend on its value, and every other field keeps that
promise for free: its `<input>` stays in flow and the field is as wide as the
input's own default size. This one takes the input out of flow when the token
shows, and the input is the only child with an intrinsic width, so the field
was left measuring the token. Block-level parents hid it, because they fill
their container whatever their content is — which is why no story caught it.

So the field now states the width it already had rather than inheriting it
from the input, as a floor applied only while the token shows: an unselected
field is untouched, and a block-level or stretched one still fills. The value
is public and themeable as `--typeahead-min-width`, since the right minimum
for a field is a design decision, not a constant.

Measured in Chromium in a flex parent, before -> after selection: 199 -> 44
becomes 199 -> 200. In a block parent both are 984 -> 984, unchanged.

Note for the issue's suggested fix, which was to keep the collapsed input in
flow: measured, that moves the field from 44px to 95px, because `width: 0`
removes the input's intrinsic contribution whether or not it is in flow. The
percentage form the repo usually reaches for, `min(200px, 100%)`, is also no
use here — a percentage min-width resolves against an indefinite containing
block during shrink-to-fit, computing to 0, and the field stayed at 44px.

Tokenizer collapses its input the same way and shrinks for the same reason
(199 -> 151); left alone here, since its multi-token wrapping makes the right
floor a different question.
@vercel

vercel Bot commented Aug 29, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
astryx Ready Ready Preview Aug 29, 2026 3:15pm

Request Review

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Aug 29, 2026
@github-actions github-actions Bot added community Authored by a community contributor (not on the eng/design team) needs:code-review High-risk change (new package/component/API) — needs human code review before merge labels Aug 29, 2026
@github-actions

github-actions Bot commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

PR Analysis Report

📚 Storybook Preview

View Storybook for this PR
GitHub Pages may take up to a minute to hydrate after deploy.

🧪 Sandbox Preview

View Sandbox for this PR
GitHub Pages may take up to a minute to hydrate after deploy.

Modified Components

Typeahead (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Bundle Size (ESM) N/A N/A N/A
Lines of Code N/A 1227 -
Complexity N/A Very High (175) -

Bundle Size Summary

Package Size (ESM) Size (CJS) Gzipped
@astryxdesign/core N/A 4.8KB 1.2KB

Accessibility Audit

Status: No accessibility violations detected.

Visual Regression

Status: Skipped — Broad stable scope is deferred to the daily release gate. It covers 1964 trusted baseline shots instead of recapturing them for this PR. View the report


Generated by PR Enrichment workflow | Storybook | Sandbox | View full report

The var was documented but not mapped, which derivedVarRegistry's own test
catches: a documented var either maps onto a standard CSS property through a
derived[] entry, or is listed as unmappable with a reason. `min-width` maps
onto this one directly, so it gets the entry rather than the exemption — which
also means a theme can write the standard property instead of the var:

  typeahead: {base: {minWidth: '16rem'}}

That emits both the property and the var, so a theme raising the field's
minimum raises the collapsed floor with it rather than letting the two drift.
…ith a story

theme-var-reachability walks the built Storybook and asks, for every
documented var, which element sets it. `--typeahead-min-width` had no answer:
it was declared inside the collapsed-state style, and no Typeahead story ever
selects a value, so nothing in the built stories carried it. A var no element
declares is a var no theme can reach, which is the gate's whole point.

The declaration moves up to the wrapper, which is the element carrying the
`typeahead` theme target and is always rendered. Only the `min-width` that
reads it stays conditional, so the behavior is unchanged — an unselected field
is still untouched — while a theme always has an element to set the value on.
This is also the shape Spinner settled on for its own public vars.

The missing story is worth having on its own account: no story rendered a
selected value, and every one of them renders in a fixed-width container,
which is exactly why a bug this visible survived. `With Selected Value` shows
a token in a flex parent — the case that used to collapse.

Verified against the built Storybook: reachability now reports
`.astryx-typeahead sets it (200px)`, and the a11y audit is unchanged at 0
violations across all 17 Typeahead stories.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Meta Open Source bot. community Authored by a community contributor (not on the eng/design team) needs:code-review High-risk change (new package/component/API) — needs human code review before merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Typeahead shrinks when a value is selected, in any content-sized layout (the docsite's own preview is one)

1 participant