Repository navigation
Description
Problem
The instructions field is a string[] of "Brief sequential instructions":
OpenDataCapture/packages/runtime-core/src/types/instrument.base.ts
Lines 49 to 50 in 432dec8
| /** Brief sequential instructions for how the subject should complete the instrument. */ | |
| instructions?: InstrumentUIOption<TLanguage, string[]>; |
The instrument guidelines show each entry as one full sentence:
OpenDataCapture/packages/instrument-guidelines/AGENTS.md
Lines 743 to 744 in 432dec8
| instructions: { | |
| en: ['Please answer all required questions.', 'Conditional questions will appear based on your answers.'], |
en: ['Please answer all required questions.', 'Conditional questions will appear based on your answers.'],But both renderers join the entries with ', ' into one paragraph:
OpenDataCapture/packages/react-core/src/components/InstrumentOverview/InstrumentOverview.tsx
Line 51 in 432dec8
<p className="text-muted-foreground text-sm">{instructions!.join(', ')}</p> <p className="text-muted-foreground text-sm">{instructions?.join(', ')}</p>
Steps to reproduce
- Make an instrument with
clientDetails.instructionsset to the guidelines example above. - Open the instrument overview, or open the instructions dialog in the form.
Expected
Each instruction shows as its own line or list item:
Please answer all required questions.
Conditional questions will appear based on your answers.
Actual
Please answer all required questions., Conditional questions will appear based on your answers.
Note
The instruments in instrument-library use only one entry, so they do not show the problem. Instruments that follow the documented example do show it.
Suggested fix
Render each entry as its own element in both components. For example, use an <ol>/<ul> (the entries are "sequential"), or one <p> for each entry. Do not use join(', ').
cc @thomasbeaudry (last change to these files)
Activity
joshunrau commentedon Oct 26, 2026
Still reproduces on
main(d86740e). No commit has touched either component since this was filed, and no open PR touches them. Both are internal parts ofScalarInstrumentRendererandSeriesInstrumentRenderer, so one fix in@opendatacapture/react-corecovers web, gateway, playground andserve-instrument.Gotchas
InstrumentOverview.test.tsx:39expects'Step one, Step two'andFormContent.test.tsx:72expects'Answer honestly, Be brief'.<p>per entry, not a list. Tailwind's preflight setslist-style: noneonolandul(tailwindcss4.3.3,preflight.css:202-206), so a list shows no numbers withoutlist-decimal, and with it every built-in instrument shows1.before its one sentence. VoiceOver also stops announcing an unstyled list as a list (Tailwind docs), and the usual fix,role="list", failsjsx-a11y/no-redundant-roles, which the shared eslint config sets toerror.apps/web/src/routes/_app/group/manage.tsx:291), and the series overview renders it through the sameInstrumentOverview. Putwhitespace-pre-lineon each<p>. That also fixes series already stored this way, which splitting the text inmanage.tsxwould not, soapps/webneeds no change.z.array(z.string().min(1)),packages/schemas/src/instrument/instrument.base.ts:90), so key each<p>by index, not by text.FormContent.tsx:27-29) has no accessible name, and is already listed in Icon-only buttons on the instrument toolbar, results summary and navigation have no accessible name #1701. The e2e test needs a name to find it, so add anaria-labelhere and take it off Icon-only buttons on the instrument toolbar, results summary and navigation have no accessible name #1701's list. The dialog title already usest({ en: 'Instructions', es: 'Instrucciones', fr: 'Instructions' }); put that in one constant for both.Plan
packages/react-core/src/components/InstrumentInstructions/(InstrumentInstructions.tsxandindex.ts). It takesinstructions: string[]and renders<div className="text-muted-foreground space-y-1 text-sm">with one<p className="whitespace-pre-line">per entry. Do not export it fromsrc/index.ts.<p>atInstrumentOverview.tsx:51andFormContent.tsx:42, and add thearia-label. In the overview, changeBoolean(instructions?.length) &&toinstructions && instructions.length > 0 &&, which narrows, so the!goes.Tests
Unit: add
InstrumentInstructions/__tests__/InstrumentInstructions.test.tsx. Rewrite the two joined-text assertions as onegetByTextper entry. MakegetInfoButton(FormContent.test.tsx:35) usegetByRole('button', { name: 'Instructions' }).e2e:
testing/src/specs/playground.spec.tsalready renders share links throughgeneratePlaygroundURL. Add a form with two entries,'Read each question.'and'Answer'+ newline +'honestly.'. That spec builds the instrument source in a template literal, so write the newline as\\n; a bare\nputs a raw line break inside a string literal and the instrument does not compile. Assert:getByText(..., { exact: true }). Withoutexact, Playwright matches substrings, so the joined paragraph would pass too.innerText()is'Answer\nhonestly.'. A browser keeps that line break only underpre-line.Begin, the dialog opened withgetByRole('button', { name: 'Instructions' })shows the same two entries.fix: resolve 26 low-difficulty issues, one commit each #1825 and build: upgrade dependencies held back by breaking changes #1660 also add tests to that spec, so expect a small rebase.
Triage note written with Claude Code. Code references point at
main@d86740e.fix(react-core): render each instruction in its own paragraph