feat(autocomplete, tags, tooltip, popover, dropdown, core): anchor pop-ups to the text caret (#DS-4857, #DS-4858) - #2065
lskramarov wants to merge 20 commits into
Conversation
|
Visit the preview URL for this PR (updated for commit b2c7c2e): https://koobiq-next--prs-2065-u5o91tsc.web.app (expires Sat, 26 Sep 2026 16:32:30 GMT) 🔥 via Firebase Hosting GitHub Action 🌎 Sign: c9e37e518febda70d0317d07e8ceb35ac43c534c |
🚨 E2E tests failedReview the report for details. 💡 Comment |
3c845dc to
3bdadbf
Compare
|
/redeploy-preview |
|
🔄 Redeploying documentation preview. |
There was a problem hiding this comment.
Copilot review overview
🔵 Needs a closer look
It adds a large, low-level caret-measurement and overlay-positioning surface across five components with new public API and visual baselines, which warrants final human review.
Review effort: Balanced
Findings: 1
Open (1)
What changed in this PR
This PR adds opt-in caret-anchored positioning to Koobiq's pop-up components and a text-completion mode to the autocomplete. It introduces new caret-measurement helpers to @koobiq/components/core (kbqGetCaretRect, kbqGetSelectionRect, kbqCreateCaretOrigin, kbqGetTextQuery, kbqListenForCaretMoves, the KbqOverlayOrigin type, and a text-mirror), then wires them into the tooltip (kbqRelativeToCaret), autocomplete (kbqAutocompleteRelativeToCaret + text mode), and gives popover/dropdown their own origin and auto-focus inputs so an overlay can follow the caret while keeping focus in the field. Default behavior of every touched component is unchanged.
Changes:
- New core caret/overlay-origin helpers and a docs-private text mirror, plus
getAnchorSize()/origin plumbing onKbqPopUpTriggerso any pop-up can anchor to a rectangle in viewport coordinates. - Tooltip caret anchoring, autocomplete text mode with inline hint and WAI-ARIA 1.2 combobox semantics, and
kbqPopoverOrigin/kbqDropdownTriggerOrigin+ auto-focus / active-descendant navigation. - New examples, bilingual docs sections, dev-app and e2e scenarios, refreshed screenshot baselines, and updated public-API guard goldens.
| File | Description |
|---|---|
| tools/public_api_guard/components/tooltip.api.md | Records new KbqCaretVerticalAnchor, relativeToCaret(Vertical), applyRelativeToCaret, getAnchorSize. |
| tools/public_api_guard/components/popover.api.md | Records new autoFocus and popoverOrigin inputs. |
| tools/public_api_guard/components/dropdown.api.md | Records activeDescendantNavigation, autoFocus, origin, updatePosition. |
| tools/public_api_guard/components/core.api.md | Records new caret/overlay-origin public API surface. |
| packages/components/dropdown/dropdown-trigger.directive.ts | Adds origin/autoFocus inputs and updatePosition(); hasPointOrigin carries a copy-pasted JSDoc that misdescribes it. |
| packages/components/core/overlay/overlay-origin.ts | New KbqOverlayOrigin type and resolve/size helpers (SSR-safe duck typing). |
| packages/components/core/form-field/text-query.ts | New word/trigger query extraction with surrogate-aware walk-back. |
| packages/components/core/form-field/caret-rect.ts, text-mirror.ts, caret-origin.ts | Caret measurement, transparent mirror hint, and per-microtask cached origin. |
| packages/components/core/pop-up/pop-up-trigger.ts | Origin plumbing: updateOrigin, getResolvedOrigin, getAnchorSize. |
| packages/components/{tooltip,popover}/*.ts | Caret anchoring / auto-focus and origin inputs. |
| packages/components/{tags,autocomplete,dropdown,popover}/e2e.ts + *.playwright-spec.ts + screenshots | New caret scenarios and baselines. |
| packages/docs-examples/**, apps/docs/**, *.mdx | New examples, generated module/loader, bilingual docs. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
Пример с поповером над выделенным текстом. Заметил в примере странное поведение: когда выделяю текст мышкой слева направо, поповер появляется еще до того, как я закончил выделение. Из-за этого он в итоге оказывается не по центру выделенного текста, а ближе к левому краю. Посоветовался с Codex и посмотрел похожие примеры.
Итого Смотреть со звуком. Комментарию хорошую реализацию из интернета |
|
Пример «Проверка по мере ввода» в статье Validation. Мне кажется, поведение примера не соответствует описанию выше по тексту в доках. Там написано, что сообщение пропадает при вводе корректного символа или потере фокуса. Но сейчас оно просто скрывается по таймеру и не закрывается в этих случаях. Можно доработать отдельно |
…(#DS-4857) Adds `kbqGetCaretRect` and `kbqGetSelectionRect` to the core entry point and `[kbqRelativeToCaret]` to the tooltip trigger, which anchors the overlay to the caret of the field it is attached to and follows it while the user types, moves the caret and scrolls the field. The caret is handed to CDK as a virtual rectangle, so every placement stays available and the usual fallback placements keep the tooltip on screen - unlike `kbqRelativeToPointer`, which passes a bare point and is limited to top/bottom. An `<input>` is measured with the existing text ruler, a `<textarea>` with a wrapping mirror added next to it, and a `contenteditable` through the range the browser already laid out. Fields that render something other than their value and right-to-left text fall back to the field box.
…#DS-4858) Adds `kbqAutocompleteRelativeToCaret`, which opens the autocomplete panel from the text caret, right below the caret's line and only as wide as its options, so that tag autocomplete in a wide field keeps the options next to the typed text. Adds text mode to the autocomplete (`kbqAutocompleteTextMode`): the query is the word before the caret or the text after one of `kbqAutocompleteTriggers`, choosing an option replaces only that query, and an inline hint draws the rest of the active option after the caret, accepted with Tab or the right arrow. The form value stays the whole text; the default mode is unchanged. Adds `kbqRelativeToCaretVertical` to the tooltip: by default a single-line input keeps the tooltip above the field, as in the DS-4857 mockup, and a textarea keeps it at the caret line. The core gains the query parser, the caret listener shared by both, and the text mirror that renders the hint in the field's own layout.
…on list (#DS-4858) The trigger told assistive technology nothing about its panel. A text input now follows the WAI-ARIA 1.2 combobox pattern: `role="combobox"`, `aria-autocomplete`, `aria-expanded` while the panel is open, `aria-controls` on the option list and `aria-activedescendant` on the active option. ARIA in HTML allows the role on a text input only, so a textarea and the search, email, tel and url inputs keep their native role and get the rest, while other input types get none of it. Text mode shares these bindings and still reports `aria-autocomplete="both"` for the inline hint. A disabled autocomplete drops them all. The option list had no accessible name, which axe reports as `aria-input-field-name`. It now takes the name of the field that opens the panel: its `aria-labelledby`, its `aria-label`, its label or its placeholder. `aria-controls` points at a new id on the list, so `KbqAutocomplete.id` stays on the panel element.
…etion in step with the field (#DS-4858) The caret is measured again whenever the overlay is positioned instead of once on opening, so a caret-anchored panel or tooltip stays on the caret through page scrolls, window resizes and layout shifts. A panel near the right edge of the viewport may end at the caret instead of running off screen, a tooltip following the caret can still flip, and its arrow points at the caret rather than at the middle of the field. Text mode no longer acts on a stale query or hint: a value written by the form, text changed without an input event and switching text mode, the hint or the autocomplete off all drop them, so Tab and the right arrow cannot write an option into text it was not offered for. An option chosen from a panel opened with `open()` goes in at the caret. Shift with an arrow selects text, an option made active while the options arrive is no longer selected, and an OnPush view is told about the active option. The word before the caret is found without a pattern that backtracks over long tokens, and it keeps combining marks. The caret measurement lays out the rest of the word, follows the field's `white-space` and falls back to the field for centred and right-aligned inputs; the inline hint is only offered where the field would draw it. Showing a tooltip measures nothing, so a server render no longer throws. The option list takes its name from a label without an id or from `title`, and the boolean inputs of text mode accept `null` and `undefined` in strict templates. The docs describe where the caret cannot be located and no longer contradict the panel width rules.
…ve examples (#DS-4858) The documentation pages are compiled from MDX now, where an HTML comment is not valid syntax, so the examples added for the caret are embedded with `<Example id="..." />` like the rest of each page. The regenerated example module lists their ids in `LiveExampleId`.
…rary fields and the whole panel (#DS-4858) The tooltip scenario used bare browser fields, so its screenshot showed native controls instead of the library's; it now puts `kbqInput` and `kbqTextarea` into `kbq-form-field`. The tag scenario filled the field with tags up to its right edge, and the panel opened from the caret there ran out of the captured area; two tags keep the caret, and the panel, inside it. Adds the baselines of the new screenshots, rendered in the Docker image CI uses.
…lection (#DS-4857) Extends caret anchoring past the tooltip: `KbqPopUpTrigger` takes an origin of its own, and the popover and the dropdown expose it as `kbqPopoverOrigin` and `kbqDropdownTriggerOrigin`. An origin is an element or a rectangle in viewport coordinates, so the rectangle `kbqCreateCaretOrigin` keeps on the caret is handed straight to it. Two documentation examples come with it: a formatting toolbar next to a text selection, and a menu opened by `/` at the caret of a contenteditable editor. Both need the panel to leave the caret alone, hence `kbqPopoverAutoFocus`, `kbqDropdownTriggerAutoFocus` and `activeDescendantNavigation`, which moves the dropdown's highlight without taking focus.
…selection toolbar work (#DS-4857) The slash-menu editor was a hand-drawn box: no background, no typography, and two of its `var()` references did not exist — `--kbq-states-line-theme`, so the focus border never changed, and the `font: var(--kbq-typography-text-compact)` shorthand, which the design tokens do not publish. It takes its background, border, radius and focus ring from the form-field tokens now, and the footer keeps the styles the dropdown already gives it, spaces included. The toolbar of the popover example did nothing: its button toggles carried no handler. It is a button group now, and the buttons wrap the selection in `<strong>`, `<em>` or `<u>`, unwrap it on a second click, and report through `aria-pressed` the formats the selection already carries. The panel no longer scrolls: `.kbq-popover__content` grows a scrollbar from a single pixel of overflow.
…ge (#DS-4857) The tables duplicated the generated API tab, and no other overview page carries an API section: of 144 pages the heading appeared in the two tooltip files alone. The page ends on Recommendations now, as the rest of them do.
…4857) The repo compiles against `lib: ["ES2022", "DOM"]`, so spreading a `NodeList` does not type-check; `Array.from` takes the array-like instead. The dev app builds accept the spread, the docs-examples library build and `ssr:build` do not.
…S-4858) The keys were inline code, and the key column wrapped `Tab, →` onto two lines. They are `docs-hot-key-button` spans now, in a min-width key column, as on the modal, sidepanel and app-switcher pages, and the section is named after theirs: "Управление с клавиатуры" and "Keyboard navigation".
…(#DS-4857) The footer read "Press Esc to close" as one line of text. It holds "Close menu" and the key now, pushed to the two ends of the row, with the key in the secondary text color.
…orners (#DS-4857) A button group squares the corners where its buttons meet, so the active button lost its rounding on the inner side, and the buttons touched the edge of the panel. The toolbar is a plain group of transparent buttons now, each with its own corners, inside a 4px padding.
…r the blur tooltip to the caret (#DS-4857) Both come from the review. `hasPointOrigin` carried the JSDoc of `shouldMatchTriggerWidth`, which describes neither what it reports nor what it drives: pushing a point-anchored panel back into the viewport and skipping the trigger width. The tooltip of the validation-on-blur example still pointed at the field, although it is shown while typing, exactly as the one in validation-on-type.
…e pop-up (#DS-4857) A tooltip closes on scroll through `scrollStrategies.close()`, which detaches the overlay without going through `hide()`. The pop-up is destroyed, its visibility stream completes instead of emitting `false`, and the trigger is left `isOpen` for good: the validation examples, which show a tooltip only while it reports closed, never show it again. `detach` resets the state and emits it now. Keyed on `isOpen` rather than on `visible`, which mirrors the requested state and is already set by the time `show()` detaches a previous overlay.
…n is done (#DS-4857) Shown on the first `selectionchange`, the toolbar anchored to the first characters of a selection the user was still dragging out, and stayed there. It opens on `pointerup` now, or after a pause for a keyboard selection, and an open panel is moved to a selection that changed — `updatePosition` re-reads the origin only when asked to re-apply the position. `Tab` moves into the toolbar as well: the panel is in an overlay at the end of the document, so the tab order walks past it.
… for an optional numeric input (#DS-4857) Four components carried the same transform, and no two of them agreed: `null` and an empty string became `undefined` in the autocomplete, `undefined` and `0` in the dl, and `0` in the other two, because Angular's `numberAttribute` reads them as zero. The core helper keeps the strictest reading, so an input bound to nothing falls back to its default instead of to `0`.
…857) `kbqOptionalNumberAttribute` takes `unknown`, as Angular's own `numberAttribute` does: an input transform is handed whatever the template binds. Narrowing it would narrow the write type of the four inputs built on it, so the count is recorded rather than the parameter changed.
5d9cd4e to
faf207e
Compare
… is typed (#DS-4857) The hint now behaves the way the validation page already documents it: it goes away on the first valid character and on focus loss, instead of only after a three-second timeout.
поправил |

Summary
Opens the tooltip and the autocomplete panel at the text caret instead of the whole field (DS-4857, DS-4858), and adds a text mode to the autocomplete that completes the word or the trigger query at the caret — suggestions in a textarea, commands, mentions — with an inline hint. Everything is opt-in through new inputs; the default behavior of both components does not change.
List of notable changes:
kbqRelativeToCaretandkbqRelativeToCaretVerticalto the tooltip: it anchors to the caret or the selection of the field and follows it, with every placement and fallback available. By default it stays above a single-line input and at the caret line in a textarea.kbqAutocompleteRelativeToCaret: the panel opens right below the caret line and is only as wide as its options (panelMinWidthdoes not apply). Tag autocomplete in a wide field uses it.kbqAutocompleteTextMode,kbqAutocompleteTriggers,kbqAutocompleteMinLength,kbqAutocompleteInlineHintand(kbqAutocompleteQueryChange). The query is the word before the caret or the text after a trigger such as/or@; choosing an option replaces only that query;Tabor→accept the hint; the form value stays the whole text.kbqPopoverOriginandkbqDropdownTriggerOrigin:KbqPopUpTriggertakes an origin of its own — an element or a rectangle in viewport coordinates — so the rectanglekbqCreateCaretOriginkeeps on the caret drives a popover or a dropdown too.kbqPopoverAutoFocus,kbqDropdownTriggerAutoFocusandactiveDescendantNavigationkeep the focus in the field while the panel is open. Two examples come with it: a formatting toolbar next to a text selection, and a menu opened by/at the caret of acontenteditableeditor.@koobiq/components/core:kbqGetCaretRect,kbqGetSelectionRect,kbqCreateCaretOrigin,kbqGetTextQuery,kbqListenForCaretMovesand theKbqOverlayOrigintype withkbqResolveOverlayOrigin, plus the docs-private text mirror behind the hint.KbqPopUpTriggergains a protectedgetAnchorSize(), so the arrow offset can be taken from the caret.What should reviewers focus on?
core/form-field/caret-rect.ts: a text ruler for an input, a wrapping mirror for a textarea (it lays out the rest of the word, so a word the field wraps takes the caret along) and the selection range forcontenteditable.password-like fields, fields without a text selection (email,number), right-to-left text and centred or right-aligned inputs fall back to the field box.kbqCreateCaretOrigin), so scrolls and resizes keep the overlay on the caret. The tooltip lifts its position lock only while following the caret, so that it can still flip.autocomplete-trigger.directive.ts: the query and the hint are dropped when the form writes a value, when the text changes without an input event and when text mode, the hint or the autocomplete is switched off. Insertion goes throughsetRangeTextand a dispatchedinputevent, so native undo does not cover it.core/form-field/text-mirror.ts) is drawn in a transparent mirror of the field and offered only where the field would lay it out: on the caret's row and inside the visible part of the field.03, tags06, tooltip02— were rendered in the e2e Docker image.core/pop-up/pop-up-trigger.ts:updateOrigin()moves the position origin only — the host element keeps the event listeners, the focus target and the scroll containers the pop-up follows, which is whatsetExternalNativeElementmoves — andnullputs the position back on the host.