From 4fa96bffed074448d848739ba66ac5d7103e90cf Mon Sep 17 00:00:00 2001 From: "Zimin A.N." Date: Wed, 6 May 2026 19:52:12 +0300 Subject: [PATCH] =?UTF-8?q?fix(form):=20FK=20select=20=D0=BD=D0=B0=20nativ?= =?UTF-8?q?e=20 делегирует popup positioning браузеру: auto-flip вверх когда мало места снизу, native scroll внутри dropdown, type-ahead search через клавиатуру (если ввести 'OP' — прыгнет на OPERATIONAL), mobile-friendly. Stylinglypko под @nstart/ui (border-regolith / border-mars / focus-ultramarain) — визуально встаёт в линейку с TextInput / DatePicker. SingleSelect остаётся для enum-полей (короткие закрытые списки, чаще ≤5 опций — там popup overflow не возникает). --- .../components/form/SchemaDrivenForm.test.tsx | 26 ++++++------ .../src/components/form/SchemaDrivenForm.tsx | 41 ++++++++++++++----- 2 files changed, 43 insertions(+), 24 deletions(-) diff --git a/ordinis-admin-ui/src/components/form/SchemaDrivenForm.test.tsx b/ordinis-admin-ui/src/components/form/SchemaDrivenForm.test.tsx index 2168755..676cc60 100644 --- a/ordinis-admin-ui/src/components/form/SchemaDrivenForm.test.tsx +++ b/ordinis-admin-ui/src/components/form/SchemaDrivenForm.test.tsx @@ -7,11 +7,14 @@ import type { FlattenedRecord, JsonSchema } from '@/api/client' // Stub useReferencedRecords so tests don't need a QueryClient. Each test // can override via mockReturnValue. -const mockUseReferencedRecords = vi.fn(() => ({ - data: undefined, - isLoading: false, - isError: false, -})) +type MockResult = { + data: FlattenedRecord[] | undefined + isLoading: boolean + isError: boolean +} +const mockUseReferencedRecords = vi.fn<(name: string | undefined) => MockResult>( + () => ({ data: undefined, isLoading: false, isError: false }), +) vi.mock('@/api/queries', () => ({ useReferencedRecords: (name: string | undefined) => mockUseReferencedRecords(name), })) @@ -144,7 +147,6 @@ describe('SchemaDrivenForm', () => { }, } as JsonSchema - const user = userEvent.setup() renderWithI18n( { expect(mockUseReferencedRecords).toHaveBeenCalledWith('spacecraft_status') // Hint показывает FK target. expect(screen.getByText(/Статус КА/)).toBeInTheDocument() - // Открываем SingleSelect → видим обе локализованные опции. - // SingleSelect рендерит trigger как button с placeholder "—". - const triggers = screen.getAllByRole('button') - const selectTrigger = triggers.find((b) => b.textContent === '—') - expect(selectTrigger).toBeDefined() - await user.click(selectTrigger!) - expect(await screen.findByText('OPERATIONAL — Действующий')).toBeInTheDocument() - expect(screen.getByText('LOST — Утерян')).toBeInTheDocument() + // Native вместо SingleSelect: для FK с длинным списком + // (50+ статусов, типов КА) и tight modal'ом native browser dropdown + // лучше — popup auto-positioning не клипается viewport'ом, native + // scroll и type-ahead search работают OOTB. SingleSelect popup + // через createPortal с position:fixed не делает auto-flip и уходит + // за нижний край когда триггер близко к низу modal'а. return ( ( - +
+ {label} + + {hint} + {errorMsg} +
)} /> )