feat: Add text selection API for text input fields#9273
Open
Artur- wants to merge 9 commits into
Open
Conversation
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.
|
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.



Summary
HasSelectionmixin withselectAll,deselect,setSelectionRange,setCursorPosition, and a reactiveSignal<SelectionRange> selectionSignal()so applications can drive thebrowser text selection from the server.
TextField,TextArea,PasswordField, andBigDecimalField.EmailField,NumberField, andIntegerFieldare deliberately excluded because their underlying<input type="email">/type="number"elements throwInvalidStateErrorfor these APIs per the HTML spec.Fixes #1377
Details
Reactive read
selectionSignal()returns aSignal<SelectionRange>lazily backed by aValueSignal. On first call it installs a client-side listener bundle (select,keyup,mouseup,input,focus) on the innerinputElement; each event dispatches avaadin-selection-changeevent on the host, which the server picks up viaElement.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 viasignal.peek()without an extra roundtrip, so timing races between user cursor moves and server reads no longer apply.Imperative methods
SelectionRangeis arecord(int start, int end, String content)withlength(),isEmpty(), andempty()helpers. Indices follow theHTMLInputElement.setSelectionRange()convention (zero-based, end-exclusive).selectAll,setSelectionRange, andsetCursorPositionfocus the field by default so the highlight is rendered in the active color rather than the browser's faded inactive state. Each accepts aboolean focusoverload to opt out.deselectis 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 precedingsetValue) and the subsequent range update would not re-scroll.selectAllusessetSelectionRange(0, length)rather thanHTMLInputElement.select()because the latter always implicitly focuses per the WHATWG spec, which would make the
focus=falseoverload a lie. All mutators are wrapped insetTimeout(...,0)so they run after pending value/focus reflection on the web component, matching the workaround used inviritin/flow-viritin.