From 2d62c532edb91a7ab53535e7c8f6423fef25b38c Mon Sep 17 00:00:00 2001 From: webtheory <11144268+sinups@users.noreply.github.com> Date: Thu, 22 Oct 2026 09:58:01 +0300 Subject: [PATCH 1/2] feat: a string answer that runs over several lines MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every string in an elicitation form was one line, however long the answer was meant to be: "what happened?" with room for 8000 characters was a field that showed about sixty of them. A string schema now takes `multiline: true`, and the field becomes a text area that grows from three lines to eight — `rows` moves where it starts. It never shares a row with another field, and the label, the limit and the required mark behave as they do on a one-line field. Without the flag nothing changes. --- .../src/elicitation/ElicitationForm.test.tsx | 42 +++++++++++++++++++ package/src/elicitation/ElicitationForm.tsx | 24 +++++++++++ package/src/elicitation/elicitation-schema.ts | 4 ++ site/app/data/component-docs.ts | 2 +- 4 files changed, 71 insertions(+), 1 deletion(-) diff --git a/package/src/elicitation/ElicitationForm.test.tsx b/package/src/elicitation/ElicitationForm.test.tsx index 1aed438..5a245cf 100644 --- a/package/src/elicitation/ElicitationForm.test.tsx +++ b/package/src/elicitation/ElicitationForm.test.tsx @@ -122,3 +122,45 @@ describe('elicitation/ElicitationForm', () => { expect(screen.queryByRole('link')).not.toBeInTheDocument(); }); }); + +describe('elicitation/a string over several lines', () => { + const multiline: ElicitationRequestedSchema = { + type: 'object', + properties: { + story: { type: 'string', title: 'What happened?', multiline: true, maxLength: 8000 }, + short: { type: 'string', title: 'Order number' }, + }, + required: ['story'], + }; + + it('gives a multiline string a text area and keeps the plain one an input', () => { + render(); + + expect(screen.getByLabelText(/What happened\?/).tagName).toBe('TEXTAREA'); + expect(screen.getByLabelText('Order number').tagName).toBe('INPUT'); + }); + + it('keeps the line breaks the person typed', async () => { + const onAccept = jest.fn(); + render(); + + const field = screen.getByLabelText(/What happened\?/); + await userEvent.type(field, 'First line{Enter}second line'); + await userEvent.click(screen.getByRole('button', { name: 'Submit' })); + + expect(onAccept).toHaveBeenCalledWith( + expect.objectContaining({ story: 'First line\nsecond line' }) + ); + }); + + it('keeps the required mark and the limit of the schema', async () => { + const onAccept = jest.fn(); + render(); + + expect(screen.getByLabelText(/What happened\?/)).toHaveAttribute('maxlength', '8000'); + + await userEvent.click(screen.getByRole('button', { name: 'Submit' })); + expect(screen.getByText('Required')).toBeInTheDocument(); + expect(onAccept).not.toHaveBeenCalled(); + }); +}); diff --git a/package/src/elicitation/ElicitationForm.tsx b/package/src/elicitation/ElicitationForm.tsx index ac17c06..1dbeaf2 100644 --- a/package/src/elicitation/ElicitationForm.tsx +++ b/package/src/elicitation/ElicitationForm.tsx @@ -13,6 +13,7 @@ import { Stack, Switch, Text, + Textarea, TextInput, } from '@mantine/core'; import { IconExternalLink, IconForms } from '@tabler/icons-react'; @@ -101,11 +102,17 @@ function isCompactField(field: ElicitationField): boolean { return true; } if (field.kind === 'text') { + if (field.schema.multiline) { + return false; + } return field.inputType !== 'text' || (field.schema.maxLength ?? Infinity) <= 120; } return field.kind === 'select' && field.options.length > 3; } +const MIN_TEXTAREA_ROWS = 3; +const MAX_TEXTAREA_ROWS = 8; + const EMPTY_SCHEMA: ElicitationRequestedSchema = { type: 'object', properties: {} }; type FieldControlProps = { @@ -128,6 +135,23 @@ function FieldControl({ field, value, error, disabled, onChange, onBlur }: Field switch (field.kind) { case 'text': + if (field.schema.multiline) { + return ( +