[material-ui] Render the theme.focusVisible ring on inputs - #49152
Open
siriwatknp wants to merge 5 commits into
Open
siriwatknp wants to merge 5 commits into
siriwatknp wants to merge 5 commits into
Conversation
disableUnderline drops the only focus indicator, so the theme ring takes its place. Keyed on Mui-focused, same state the underline it replaces uses. Docs: new full-demo row for the filled + disableUnderline TextField, and Row gains a secondary caption under the label. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ACTuQZLtuDTErvbWTCuMaP
Deploy previewBundle size
Check out the code infra dashboard for more information about this PR. |
Same gap as FilledInput: the underline is the only focus indicator, so disableUnderline leaves the standard input with none. Drop outsetFocusRing from both. Outset is already the var fallback; the reset only matters for components that nest inside a clip-prone root, and a text input inside a Tab/MenuItem/ListItemButton is nested-interactive. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ACTuQZLtuDTErvbWTCuMaP
disableUnderlinedisableUnderline
Move the ring to InputBase, mirroring ButtonBase: the base draws it, and internalDisabledThemeFocusVisible lets each wrapper decide. A bare InputBase has no indicator, so it gets the ring. Input and FilledInput opt out while the underline is drawn. OutlinedInput always opts out; its notched outline recolors on focus. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ACTuQZLtuDTErvbWTCuMaP
disableUnderlinetheme.focusVisible ring on inputs
The dashed box and radius were demo chrome that made the ring look rounder than a bare InputBase is. Keep only the placeholder, so the row shows the real default: nothing at rest, square ring on focus. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ACTuQZLtuDTErvbWTCuMaP
siriwatknp
commented
Sep 17, 2026
siriwatknp
commented
Sep 17, 2026
| inputProps: inputPropsProp = {}, | ||
| inputRef: inputRefProp, | ||
| /* eslint-disable react/prop-types */ | ||
| // private prop to let a wrapper (like OutlinedInput) draw its own focus indicator instead |
Member
Author
There was a problem hiding this comment.
Suggested change
| // private prop to let a wrapper (like OutlinedInput) draw its own focus indicator instead | |
| // private prop to let a wrapper draw its own focus indicator instead |
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ACTuQZLtuDTErvbWTCuMaP
| <InputBase placeholder="Test" /> | ||
| </Row> | ||
| <Row label="TextField" secondary="(filled + disableUnderline)"> | ||
| <TextField |
Member
Member
There was a problem hiding this comment.
another choice is to wrap the whole container in the outline, including the label.
Member
There was a problem hiding this comment.
or maybe the label overlapping the outline, like the Autocomplete example. in any case, I'm advocating for consistency here.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Follow-up to #48743.
Summary
theme.focusVisiblecovered everyButtonBase-derived component, but nothing on the input side. That left real gaps: a bareInputBasehas no focus indicator at all, and a filled or standard text field withdisableUnderlineloses the underline that was its only one — in a theme whose whole point is that nothing is left without an indicator.This PR gives
InputBasethe ring, following the same shapeButtonBasealready uses: the base draws it, and each wrapper opts out when it already has an indicator of its own.InputBaseInput,FilledInputwithdisableUnderlineInput,FilledInputOutlinedInputThe bottom three rows are the control: the components that already had an indicator are untouched, so there is no double ring anywhere. And the whole variant is a no-op when
focusVisibleis not set on the theme, so this is zero visual diff for anyone not opted in.The full focus visible demo gains rows for
InputBaseand bothdisableUnderlinevariants, and theRowhelper now takes a secondary caption so a variant qualifier reads as supporting text rather than swelling the bold label.For Reviewers
The shape mirrors
ButtonBase.InputBaseRootdraws the ring behind ainternalDisabledThemeFocusVisiblevariant, the private prop is destructured with the other private props and carried onownerState— the same three touch pointsButtonBaseuses, and the same name, so the two halves of the feature read alike. There are exactly three consumers ofInputBaseRoot(Input,FilledInput,OutlinedInput) and each now states its decision at theInputBasecall site, next to the other props it forwards.An earlier revision put the ring directly on
FilledInputandInputinstead. Inverting it is better: it covers the bareInputBase, it puts the decision where a reader looks for it, and any future wrapper gets the ring by default and has to say so to decline.Since the prop is destructured it never reaches
other, so it cannot leak to the DOM — there is a test for that.Why it keys on
Mui-focused, not a keyboard-only state. Two reasons, one principled and one practical.The principled one: on the wrappers this ring is a substitute for the underline it replaces, and the underline has always been
Mui-focused-driven. Keying the replacement on a different state would meandisableUnderlinesilently changes when the field reports focus, not just how it looks.The practical one: the feature is class-driven throughout — that is what lets the visual-regression fixtures render already-focused, and what lets users force the state in their own tests.
InputBasetracksMui-focusedand nothing finer. A keyboard-only ring would mean introducing aMui-focusVisiblestate onInputBase— a new public class, a new API surface, and a second focus-tracking path through a component many others compose. That is a larger change than this gap warrants, and it can be layered on later without breaking anything here.The trade-off worth naming: a filled
SelectwithdisableUnderlineshows the ring on mouse click, where native:focus-visiblewould not. For a text field it makes no practical difference — browsers match:focus-visibleon text inputs regardless of how focus arrived.On the standard variant specifically. It has no surface of its own, so the ring is the only box the field gets and a focused field ends up reading close to the outlined variant. That is a real cost, and it is called out in the guide. It still beats no indicator at all, and an author who dislikes the shape can pass a custom
focusVisible— which is the whole point of the theme key.No
outsetFocusRinghere. Worth flagging since every other consumer of the ring spreads it. Outset is already the fallback (var(--_focusVisible-offset, 1)), so that helper is purely a reset for inherited inset vars — it earns its place onButtonBase,Checkbox,Sliderand friends because those genuinely nest inside a clip-prone root (anIconButtoninside aMenuItem, aCheckboxinside aListItemButton). The inset vars are only ever set byTab,MenuItem,ListItemButton,CardActionArea,BottomNavigationActionand the Autocomplete option — all interactive leaf roots, so a text input inside one would be nested-interactive markup. Verified by removing it: the offset still computes to2px.Tests. Computed-style tests on
InputBase(ring on focus; suppressed when a wrapper opts out; the prop does not reach the DOM),FilledInputandInput(ring withdisableUnderline, none without), andOutlinedInput(never a ring). The negative cases are the ones that matter — they pin the opt-out, which is the only way this change could regress a default path.🤖 Generated with Claude Code
https://claude.ai/code/session_01ACTuQZLtuDTErvbWTCuMaP