Skip to content

feat: Add text selection API for text input fields#9273

Open
Artur- wants to merge 9 commits into
mainfrom
feature/text-selection
Open

feat: Add text selection API for text input fields#9273
Artur- wants to merge 9 commits into
mainfrom
feature/text-selection

Conversation

@Artur-
Copy link
Copy Markdown
Member

@Artur- Artur- commented May 12, 2026

Summary

  • Add HasSelection mixin with selectAll, deselect, setSelectionRange, setCursorPosition, and a reactive Signal<SelectionRange> selectionSignal() so applications can drive the
    browser text selection from the server.
  • Implement on TextField, TextArea, PasswordField, and BigDecimalField. EmailField, NumberField, and IntegerField are deliberately excluded because their underlying <input type="email"> / type="number" elements throw InvalidStateError for these APIs per the HTML spec.

Fixes #1377

Details

Reactive read

selectionSignal() returns a Signal<SelectionRange> lazily backed by a ValueSignal. On first call it installs a client-side listener bundle (select, keyup, mouseup, input, focus) on the inner inputElement; each event dispatches a vaadin-selection-change event on the host, which the server picks up via Element.addEventListener(...).addEventData(...) and pushes into the signal. Subsequent calls return the same cached instance, surviving detach/re-attach.

This replaces the async getSelectionRange(callback) shape from the earlier PR #3194 attempt: a server-side click handler reads the latest selection synchronously via signal.peek() without an extra roundtrip, so timing races between user cursor moves and server reads no longer apply.

Imperative methods

SelectionRange is a record(int start, int end, String content) with length(), isEmpty(), and empty() helpers. Indices follow the HTMLInputElement.setSelectionRange() convention (zero-based, end-exclusive).

selectAll, setSelectionRange, and setCursorPosition focus the field by default so the highlight is rendered in the active color rather than the browser's faded inactive state. Each accepts a boolean focus overload to opt out. deselect is focus-neutral.

The generated JS applies the selection before focusing — inputElement.focus() scrolls to the current cursor position, so focusing first would scroll to the stale cursor (often end-of-value after a preceding setValue) and the subsequent range update would not re-scroll. selectAll uses setSelectionRange(0, length) rather than HTMLInputElement.select()
because the latter always implicitly focuses per the WHATWG spec, which would make the focus=false overload a lie. All mutators are wrapped in setTimeout(...,0) so they run after pending value/focus reflection on the web component, matching the workaround used in viritin/flow-viritin.

Artur- added 9 commits May 12, 2026 11:31
Adds a HasSelection mixin with selectAll, deselect, setSelectionRange,
setCursorPosition, and a reactive Signal<SelectionRange> selectionSignal().
TextField, TextArea, EmailField, PasswordField, IntegerField, NumberField
and BigDecimalField inherit the API via TextFieldBase.

Fixes #1377
Wrap selectAll, deselect, and setSelectionRange in setTimeout(...,0) so
the selection is applied after any pending value or focus reflection on
the web component. Without this, calling setValue + setSelectionRange or
focus + selectAll in the same request loses the selection when the
input re-renders. Same workaround used in viritin/flow-viritin.
Mirrors UC6: user selects text in a TextArea, clicks a server-side
button whose handler reads selectionSignal().peek() and uppercases the
selected substring in place. Verifies the result reflects the selection
that was active at click time and stays current across user actions —
the timing reliability that PR #3194's async getSelectionRange(callback)
could not guarantee.
Move the implements clause from TextFieldBase to the four subclasses
whose underlying input element actually supports the selection APIs:
TextField, TextArea, PasswordField, BigDecimalField.

EmailField, NumberField, and IntegerField wrap input types (email,
number) where the HTML spec disallows selectionStart and
setSelectionRange — Chrome and Firefox throw InvalidStateError. Having
HasSelection on those classes was a runtime trap. Drop the inheritance
so the type system reflects what works.
selectAll, setSelectionRange, and setCursorPosition now focus the field
as part of the call so the highlight is rendered in the active color
rather than the browser's faded inactive state. The previous Javadoc
told callers to focus the field themselves; in practice every use case
in the design doc wants focus, and forgetting it was a silent UX bug.

Each method gains a `boolean focus` overload for the rare cases where
the caller wants to change the selection without yanking focus.
deselect remains focus-neutral.
inputElement.focus() scrolls the field to the current cursor position.
With the previous order (focus → setSelectionRange) the field scrolled
to the old cursor location — typically the end of the value after a
preceding setValue — and the subsequent range update moved the cursor
without re-scrolling. Applying the range first and focusing second
makes focus scroll to the intended cursor location.

Also switch selectAll(boolean) from HTMLInputElement.select() to
setSelectionRange(0, length): per the WHATWG spec select() always
implicitly focuses the input, which made the focus=false opt-out a lie.
selectionSignal_serverHandlerSeesSelectionAtClickTime selects text in
the TextArea but the existing #selection-info div was bound to the
TextField's signal, so the waitUntil could never resolve and the test
timed out in CI. Add a separate #area-selection-info div bound to the
TextArea's signal and have the test wait on it.
@sonarqubecloud
Copy link
Copy Markdown

@Artur- Artur- marked this pull request as ready for review May 15, 2026 07:10
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.

TextField Selection API

1 participant