You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
A focus() call on a component right after it connects did nothing, because Lit renders in a microtask and the component has no inner control yet. In the checkbox, radio, select and switch, focus() and blur() threw a TypeError. A native control takes the focus at once.
Now the component stores the call and repeats it after its first render:
New internal module src/internals/deferred-focus.ts with deferFocus(host, options) and cancelDeferredFocus(host). The document has one focus, so the module keeps one request: a later request replaces it, and a focusin or a blur() of the host cancels it. It does nothing for a host that is not connected, has already rendered, has disabled set or has the focus.
All 16 component classes (22 tags) use one form in focus(): focus the inner target or call super.focus(), then call deferFocus(this, options). blur() calls cancelDeferredFocus(this) first. A rendered component still takes the focus at once.
The button, icon button, carousel, chip, color picker, date picker, date range picker and textarea now have their own focus() and blur(), so public-api.json lists them.
In the carousel, a deferred focus goes to a navigation button, because the indicators render only after the carousel finds its slides.
Known differences from native elements. All of them can occur only before the first render:
A move of the host in the same task keeps the request.
A focus() call on the element that already has the focus sends no focusin, so it does not cancel the request.
A host that cannot take the focus, for a reason other than its own disabled, replaces an earlier request. Examples: an inert or hidden ancestor, or a toggle button in a disabled group.
Type of Change
Bug fix (non-breaking change that fixes an issue)
Related Issues
Follow-up to #2539, which fixed the same problem only for the chat focusInput().
Testing
The new shared suite runFocusBeforeRenderTests() runs for all 22 tags. It checks that:
the deferred focus lands on the same element as a focus after the render
blur(), a later focus() on another element and a removal cancel the request, and a move keeps it
a host with disabled set does not cancel an earlier request
after the render, focus() and blur() work at once
Unit specs for deferred-focus.ts cover each guard, the interaction with native autofocus, and a host whose shouldUpdate() returns false.
The new tests fail when the source changes are reverted.
A new select spec covers autofocus set as a property.
The QR code spec now uses a string fixture for its invalid version, so lit-analyzer reports no problems.
Chromium: the full suite passes (5,813 passed, 1 skipped: the carousel has no disabled). The SSR smoke test renders 90 tags.
Firefox 157: no focus test fails in the 22 changed spec files.
- Radio, checkbox, switch and select read their inner input with a cached
query that is null until the first render. A focus() or blur() call
right after the element connected threw a TypeError. The calls now do
nothing, as in the other components.
- Add deferFocus() and cancelDeferredFocus() to internals. They store a
focus() call on a connected component that has not rendered and repeat
it after the first update. A later request, a focusin or a cancel drops
the stored call. No component uses them yet.
- Add the runFocusBeforeRenderTests() spec helper. It checks this behavior
for a component.
- A focus() call on a component right after it connects now moves the
focus to the component after its first render, as on a native control.
Before, the call did nothing. A blur() of the component, or a focus
change before the render, cancels the call.
- This applies to the button, icon button, toggle button, carousel,
checkbox, switch, radio, chip, color picker, combo, select, date picker,
date range picker, date time input, file input, input, mask input,
rating, slider, range slider and textarea. In the carousel, the focus
can go to a navigation button, because the indicators render after the
carousel finds its slides.
- The button, icon button, carousel, chip, color picker, date picker,
date range picker and textarea now list focus() and blur() in their
API.
- The specs use the shared runFocusBeforeRenderTests() suite. The
spec.md files and the CHANGELOG describe the change.
- All 16 components use one form: they focus the inner target or call
super.focus(), then call deferFocus(). The inner target is read once.
- deferFocus() returns nothing and checks hasUpdated first. It skips a
host with disabled set, so that host does not cancel an earlier
request.
- When shouldUpdate() returned false, the host skipped its render, and
the repeated focus() deferred again with no end. The repeat now runs
only after a render.
- The shared spec suite now also checks the disabled case, a move in the
same task, and focus() and blur() after the render. It replaces the
per-component "does not throw", disabled and old focus state tests,
uses getDeepActiveElement() and waits one frame to settle.
- A select spec covers autofocus set as a property. The QR code spec
uses a string fixture for an invalid version, so lit-analyzer no
longer warns.
- The spec.md test items now describe the suite exactly.
The reason will be displayed to describe this comment to others. Learn more.
🟡 Changes recommended
Unresolved deferred-focus bugs can steal focus after shadow-internal focus changes and drop pre-render carousel focus requests before indicators are ready.
…roots
- The deferred focus() now waits while the host has a pending update.
The carousel indicators come with the render after a slotchange, so a
carousel with hide-navigation now takes the focus, and every carousel
focuses the same element as after the render.
- A focus change inside one shadow root sends no focusin to the
document. The module now keeps the focused element of the request and
repeats the call only when that element, or the body, still has the
focus.
- The spec helper no longer has the sameElement option. The carousel
suite runs with and without hide-navigation. New unit specs cover the
shadow root change and a blur() of the element that had the focus.
Cancel pending focus after shadow-root focus round trips
src/internals/deferred-focus.ts:63
If focus moves from one input to another and back within the same shadow root while a request is pending, neither transition reaches the document's focusin listener. The final active element still equals pendingActive, so the deferred call steals focus despite the later explicit focus calls. Observe focus changes in the previously focused control's shadow root, clean up that listener with the request, and add a regression test for this round trip.
…focus
- A focusin does not leave the shadow root that holds both the old and
the new focus. The module now listens on the document of the host and
on each shadow root around the focused element, so any element that
takes the focus cancels the request, also when the focus comes back.
This replaces the check of the focused element and its body exception.
- The request is one object that holds only the host and its roots, and
the module drops it on every cleanup path.
- The module uses the document of the host again, so a host in an
iframe or a Picture-in-Picture window is tracked. The first wait goes
through updateComplete again, so a request made inside the first
update is kept.
- New unit specs cover a focus that leaves and comes back inside one
shadow root, an element that takes the focus and loses it, a follow-up
update that adds the element to focus, and a host in another document.
The shared suite checks that the host matches :focus and uses
fixtureSync for its containers.
Observe capture-phase focus to preserve focus redirects
src/internals/deferred-focus.ts:66
When a native control's focus handler appends a component and calls its focus(), the native control's trailing focusin immediately cancels the new request, leaving focus on the native control. Observe capture-phase focus events instead, with matching listener cleanup, so the earlier transition is not treated as a later focus change. Add a regression test for redirecting focus from a focus handler.
- The listeners now also cover each same-origin document that holds the
host in a frame, so a focus change in the parent page cancels the
request of a host in a frame.
- Shadow roots are detected by nodeType. A shadow root that a frame
creates belongs to the window of the frame and fails an instanceof
check in the window of the module.
- When the call repeats, the module compares the focused element, from
the top same-origin document down through open shadow roots and frames,
with the one at request time. A move into a frame or into the shadow
root of the focused host sends no focusin to the observed roots, and
this check catches it.
- New unit specs cover these moves, a shadow root in a frame, a focus
that leaves and comes back in a shadow root of the page, and an element
of an outer shadow root that takes the focus and loses it.
The reason will be displayed to describe this comment to others. Learn more.
🟡 Changes recommended
Focus changes within a same-origin iframe can go undetected when focus returns to its starting element before rendering, causing the pending parent component to steal focus incorrectly.
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
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.
Description
A
focus()call on a component right after it connects did nothing, because Lit renders in a microtask and the component has no inner control yet. In the checkbox, radio, select and switch,focus()andblur()threw aTypeError. A native control takes the focus at once.Now the component stores the call and repeats it after its first render:
src/internals/deferred-focus.tswithdeferFocus(host, options)andcancelDeferredFocus(host). The document has one focus, so the module keeps one request: a later request replaces it, and afocusinor ablur()of the host cancels it. It does nothing for a host that is not connected, has already rendered, hasdisabledset or has the focus.focus(): focus the inner target or callsuper.focus(), then calldeferFocus(this, options).blur()callscancelDeferredFocus(this)first. A rendered component still takes the focus at once.focus()andblur(), sopublic-api.jsonlists them.Known differences from native elements. All of them can occur only before the first render:
focus()call on the element that already has the focus sends nofocusin, so it does not cancel the request.disabled, replaces an earlier request. Examples: an inert or hidden ancestor, or a toggle button in a disabled group.Type of Change
Related Issues
Follow-up to #2539, which fixed the same problem only for the chat
focusInput().Testing
runFocusBeforeRenderTests()runs for all 22 tags. It checks that:blur(), a laterfocus()on another element and a removal cancel the request, and a move keeps itdisabledset does not cancel an earlier requestfocus()andblur()work at oncedeferred-focus.tscover each guard, the interaction with nativeautofocus, and a host whoseshouldUpdate()returnsfalse.autofocusset as a property.version, so lit-analyzer reports no problems.disabled). The SSR smoke test renders 90 tags.igc-button+144 B,igc-input+155 B,igc-select+135 B.Checklist
npm run public-api:updateand committedpublic-api.jsonCHANGELOG.mdentry under[Unreleased]