ZuloOne
English
Sign in
Menu

The reference lookup: typing, the pause, and the popup

Updated

ReferenceLookup (frontend/src/components/ReferenceSelect.tsx) is the editor of every reference value — record forms, table-part cells, report filters. It is a text box the user can type into AND a dropdown whose popup is a searchable record grid. Those two halves compete for the same keystrokes.

← Developer wiki

What opens and closes the popup#

Gesture Effect
Click anywhere in the field opens (never toggles — a second click to move the caret keeps it open)
Click the chevron toggles
F4, ArrowDown opens
Enter binds a unique match, otherwise opens the list
Esc reverts the text to the current value and closes
A typing pause of LOOKUP_TYPE_DEBOUNCE_MS (200 ms) binds a unique match; opens the list when the text is ambiguous
Clicking a row binds and closes
Blur while closed unique → bind, ambiguous → open, no match → revert

Focus stays on the text input while the popup is open, so narrowing by typing is never interrupted.

The decisions are pure functions#

The component owns no matching logic. lib/formDataEntry.ts decides:

  • matchReferenceItems — ranks exact code, then code prefix, then a substring of the display text;
  • resolveTypedLookupEnter / resolveTypedLookupBlur / resolveTypedLookupIdle — each returns commit / open / revert / noop;
  • lookupWindow / centredScrollTop — which rows the popup lists, and where it is scrolled when it opens.

They are unit-tested in lib/formDataEntry.test.ts; the component is tested in components/ReferenceSelect.test.tsx. New behaviour belongs in a resolver, not in an if inside the JSX.

The echoed value is not typing#

Warning

Focusing a filled lookup copies the current record's display text into the input. That text matches its own record — uniquely. So the idle resolver, 200 ms after the user clicked the field, "found a unique match", bound the value that was already bound and closed the list the user had just opened. From the user's side: the dropdown flashes open and hides itself again.

A typedByUser ref is the guard: only a real onChange from the input arms the idle resolver. Focus, pick(), blur-revert, Esc and going disabled all clear it. The paste / barcode-scanner path still works — a pasted value arrives as an input event, which is real typing.

The general rule: state that means "the user did X" must not be set by code that merely restores a display. Echoing a value into a box the user can type in looks identical to typing, one render later.

The list opens onto the current value#

A filled lookup opens scrolled to its own record, centred in the popup, not at row 1. Two pieces make that work, both pure functions in formDataEntry.ts:

  • lookupWindow(items, selectedIndex, limit) — the popup lists at most LOOKUP_LIMIT (50) rows. When the current record sits past the cap the window slides to hold it, so the list always contains the value the field shows.
  • centredScrollTop(viewport, row) — the scroll offset that centres the selected row, clamped at 0 so the first rows are not dragged out of reach.

The scroll is applied by hand on the ScrollArea viewport (viewportRef), inside a requestAnimationFrame after the popup lays out. scrollIntoView() is not used: it scrolls every scrollable ancestor, so the form behind the popup jumps too. The row offsets are read from client rects, because a row's offsetParent is the ScrollArea root, not the scrolling viewport.

The table header is stickyHeader for the same reason — a list that opens mid-way would otherwise show no column titles.

Where the display text comes from#

  • useReferenceItems(dictionaryId) — the dictionary's list, refetched on every open so records created in another tab appear.
  • useReferenceItem(dictionaryId, id) — resolves a single id. Needed because the current value may be outside the list (filtered, archived, paged out).
  • useReferenceTexts(ids) — the same text as a plain string, for grids. See Grid columns.

The closed field shows displayValue: the dictionary's Display format ({Name} ({Code})). That string is what the user typed against, and what a grid cell prints.

Columns in the popup#

The popup is a grid of Lookup properties, not one concatenated description. On the dictionary (and document type) designer the Lookup properties tab is a Mix LookupProperties list — the field names, in order. The same ticks sit on the Properties / Header grid as a Lookup column.

When the list is empty the server fills it the Mix way: ID plus every placeholder of Display format, then Name if nothing else remains. Each reference item carries those fields in values. The popup headers are the fields' captions (ID reads as Code). Typing in the description search box matches any of those columns.

Another model can overlay that list without rewriting the owner CSV. On the extension aggregate (lookupPropertiesAdd / lookupPropertiesHide) extras append in layer order and hides drop names. The designer Lookup tab, when the working model is not the owner, writes that overlay — ticks save immediately.

Display format still wins for the closed-field text. Clearing it falls back to joining the lookup columns with an em dash.

Where next#

Was this page helpful?