fix(form): FK select label overflow — truncate + ellipsis (наезжают on chevron)

Native <select> для FK references показывал label '<code> — <localized name>'
(например 'OPT_MIDRES_MS — Оптический мульти-резолюционный мульти-спектральный')
— текст бьётся на chevron в trigger area из-за тонкого contract'а browser-
specific overflow handling (наезжают per user screenshot 2026-05-11).

Fix:
- buildOption() обрезает name до MAX_FK_LABEL_CHARS=28 (code intact) +
  '…' suffix когда combined превышает limit
- select сам получает 'truncate overflow-hidden whitespace-nowrap text-ellipsis'
  как защита если browser native truncate не сработал
- pr-10 → pr-9 (38px) — точно совпадает с right-3 chevron position (12px)
  + ~16px icon + 8px gap, не overlap'aет.
This commit is contained in:
Zimin A.N.
2026-05-11 23:43:38 +03:00
parent 59067cfcf1
commit b2231a5a47
@@ -966,9 +966,13 @@ const ReferenceSelectField = ({
aria-invalid={Boolean(errorMsg)} aria-invalid={Boolean(errorMsg)}
aria-busy={isLoading} aria-busy={isLoading}
// appearance-none убирает OS default стрелку. // appearance-none убирает OS default стрелку.
// py-2 (8px) match'ит TextInput высоту. pr-10 — место под адорнмент. // pr-9 — место под custom адорнмент (chevron at right-3 + 12px).
// truncate/text-ellipsis форсят ellipsis на длинных option labels
// (предотвращает 'наезд' текста на chevron, см. buildOption — там
// ещё дополнительный length cap чтобы native select не overflowed).
className={[ className={[
'w-full appearance-none border rounded-sm pl-3 pr-10 py-2 text-body font-display', 'w-full appearance-none border rounded-sm pl-3 pr-9 py-2 text-body font-display',
'truncate overflow-hidden whitespace-nowrap text-ellipsis',
'bg-white text-black transition-colors', 'bg-white text-black transition-colors',
'focus:outline-none focus:ring-1 focus:ring-accent focus:border-accent', 'focus:outline-none focus:ring-1 focus:ring-accent focus:border-accent',
errorMsg ? 'border-mars' : 'border-line hover:border-ink/40', errorMsg ? 'border-mars' : 'border-line hover:border-ink/40',
@@ -1003,10 +1007,14 @@ const ReferenceSelectField = ({
/** /**
* Build a SelectOption from a record. Uses `record.data[refField]` as the * Build a SelectOption from a record. Uses `record.data[refField]` as the
* option id (the FK value). Label combines id with a localized name field * option id (the FK value). Label combines id с локализованным name'ом —
* when available — `OPERATIONAL — Действующий` is much friendlier than * `OPERATIONAL — Действующий` понятнее чем чистый `OPERATIONAL`. Если
* just `OPERATIONAL`. * комбинация длиннее MAX_LABEL chars, name truncate'ится с ellipsis —
* иначе native `<select>` trigger перекрывается chevron'ом (см. screenshot
* 2026-05-11 'наезжают'). Полный display name остаётся в hint поля.
*/ */
const MAX_FK_LABEL_CHARS = 28
const buildOption = ( const buildOption = (
record: FlattenedRecord, record: FlattenedRecord,
refField: string | undefined, refField: string | undefined,
@@ -1018,8 +1026,14 @@ const buildOption = (
const nameField = record.data?.['name'] const nameField = record.data?.['name']
const display = pickLocalized(nameField, defaultLocale) const display = pickLocalized(nameField, defaultLocale)
const label = display ? `${idStr}${display}` : idStr if (!display) return { id: idStr, label: idStr }
return { id: idStr, label }
const combined = `${idStr}${display}`
if (combined.length <= MAX_FK_LABEL_CHARS) return { id: idStr, label: combined }
// Truncate display, keep code intact (код — primary identifier).
const allowedDisplayLen = Math.max(4, MAX_FK_LABEL_CHARS - idStr.length - 4) // ' — ' + '…'
const truncated = `${display.slice(0, allowedDisplayLen).trimEnd()}`
return { id: idStr, label: `${idStr}${truncated}` }
} }
const countErrorsPerTab = ( const countErrorsPerTab = (