Skip to content

Form editor: real-time input mapping panel with palette highlighting#357

Open
morissette wants to merge 9 commits intoCodeForPhilly:mainfrom
morissette:form-input-mapping-helper
Open

Form editor: real-time input mapping panel with palette highlighting#357
morissette wants to merge 9 commits intoCodeForPhilly:mainfrom
morissette:form-input-mapping-helper

Conversation

@morissette
Copy link
Contributor

What this does

While building a form for a screener, I ran into a frustrating UX gap: there was no way to see what data fields the benefits required, or which form component types were compatible, without leaving the Form Editor tab. This PR adds real-time visibility directly in the form editor toolbar.

Changes

Compact input status strip (top of Form Editor)

A horizontal pill bar replaces the previous hidden/drawer validation approach. It shows every required input path at a glance — no button click needed.

  • Red pill (✗) — input path not yet mapped to any form field key
  • Green pill (✓) — input path is covered by a field in the form

Palette highlighting on hover/click

Hovering a pill dims incompatible component types in the left-hand palette and outlines compatible ones with a sky-blue ring — so you know exactly what to drag onto the canvas.

Clicking a pill pins the highlight so it stays visible while you work. Click again to unpin.

Tooltip

A tooltip on each pill lists the compatible component types in human-readable form ("Text field", "Dropdown", etc.).

ARIA / keyboard support

  • role="button", tabIndex, aria-pressed (pin state), aria-label (path + status + compatible types)
  • Enter/Space activate the pin toggle
  • Focus triggers the same palette highlight as hover
  • role="tooltip" on tooltip divs; tooltip visible on group-focus-within as well as hover
  • Decorative icon spans ( ) marked aria-hidden

Bug fix

extractInputPaths now correctly handles nullable JSON Schema union types (["null", "string"]) by skipping "null" before resolving the leaf type — previously it would take index 0, silently produce no compatible components for optional fields, and break the key dropdown filter.

Cleanup

  • Removed two stray console.log statements in customKeyDropdownProvider and pathOptionsService

Screenshot

Form editor showing the input strip with one missing input (red), two mapped inputs (green), and the boolean-compatible palette components highlighted after hovering simpleChecks.tenYearTaxAbatement:

Testing

  1. Create or open a screener with at least one benefit that has eligibility checks
  2. Open Form Editor tab
  3. Verify the FORM INPUTS strip appears above the canvas with red/green pills
  4. Hover a pill → incompatible palette fields dim, compatible ones get a sky-blue outline
  5. Click a pill → highlight pins (pill shows a ring); click again to unpin
  6. Tab to a pill and press Enter/Space → same pin/unpin behaviour as click
  7. Tab to a pill → tooltip appears and palette highlight activates without mouse

morissette and others added 2 commits March 10, 2026 22:59
When building a form, users had no visibility into what inputs needed
to be mapped or what form component types to use for each input.

- formSchemaUtils: add extractInputPaths() to flatten a JSONSchema7
  inputDefinition into { path, type } pairs
- pathOptionsService: export TYPE_COMPATIBILITY, COMPONENT_TYPE_LABELS,
  and compatibleComponentLabels() for use outside Form-JS modules
- SelectedEligibilityCheck: render Required Form Inputs section showing
  each input path, its type, and compatible component types (filling in
  the existing placeholder comment)
- FormValidationDrawer: show compatible component type hints on each
  missing input row so users know what field type to add
- FormValidationDrawer: rename trigger to 'Required Inputs' and add a
  red badge showing the missing input count at a glance
- Replace stacked card layout with horizontal pill strip in form editor toolbar
- Hover/click a pill to dim incompatible palette fields and ring compatible ones
  via injected <style> tag targeting .fjs-palette-field[data-field-type]
- Click to pin the highlight; click again to unpin
- Add ⓘ indicator and tooltip showing compatible component types
- Add full ARIA support: role=button, tabIndex, aria-pressed, aria-label,
  onKeyDown (Enter/Space), onFocus/onBlur for keyboard palette highlight,
  role=tooltip, group-focus-within tooltip visibility, focus-visible ring
- Remove stray console.log statements from customKeyDropdownProvider and pathOptionsService
- Fix extractInputPaths to skip "null" in JSON Schema union types before
  resolving the leaf type (e.g. ["null","string"] now correctly resolves to "string")

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@morissette morissette requested a review from prestoncabe as a code owner March 11, 2026 03:33
@morissette
Copy link
Contributor Author

Screenshot 2026-03-10 at 11 29 49 PM

@sierscse sierscse self-requested a review March 11, 2026 04:22
Copy link
Collaborator

@Justin-MacIntosh Justin-MacIntosh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really like the UX ideas here!! I personally was struggling to find a good place to put the form paths (and the save button), and the idea to highlight the relevant Form Components is really good. This needs a pass in terms of the functionality though, there are a few missing pieces before this is ready to go in.

{
// Place to display information about expected inputs for check
}
<Show when={extractInputPaths(checkConfig().inputDefinition).length > 0}>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: Unfortunately, this new function doesn't help when determining which FormPaths need to be filled out for a Selected EligibilityCheck. checkConfig().inputDefinition is the inputs "pre-transformation".

Right now the transformation logic for transforming the InputSchema of a Check to FormPath objects exists in the backend under FormDataTransformer and InputSchemaService. It's only accessible in the Frontend via the endpoint /screener/{screenerId}/form-paths, which returns FormPaths for a whole Screener, not individual checks. To do what you're doing here (which is a good idea for the UX!) we'll likely need another endpoint for returning the FormPaths of a CheckConfig object.

morissette and others added 7 commits March 12, 2026 08:36
Critical fixes:
- setFormSchema({ ...e.schema }) to force new reference and fix
  InputsPanel not updating when dropdown selections change
- extractInputPaths in SelectedEligibilityCheck now calls
  transformInputDefinitionSchema first so people/enrollments paths
  reflect parameter-keyed shape, matching what the backend returns
- Tooltips now render via Portal to escape overflow-x:auto clipping;
  position is computed from getBoundingClientRect on hover/focus

Nits:
- Extract updatePaletteHighlight as a module-level function for readability
- Extract shared InputPill component; red/green pills no longer duplicate
  markup
- Add py-1.5 px-0.5 to pill scroll container so pinned rings aren't
  clipped at edges

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The previous createEffect on (hoveredPath ?? pinnedPath) was indirect:
clicking a pill set pinnedPath, which triggered the effect, which called
setHighlightedTypes with TYPE_COMPATIBILITY[type] — often the same array
reference as the current value, so Solid.js { equals: === } would skip
the downstream FormEditorView effect and updatePaletteHighlight would
not re-run.

Fix:
- Remove the derived createEffect entirely
- Add a `highlight(path | null)` helper called directly from each event
  handler — hover, hover-end, and click each explicitly drive the palette
- Set equals: false on highlightedTypes so every setHighlightedTypes
  call always propagates to updatePaletteHighlight regardless of
  array reference equality

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pinned pills now show a colored box-shadow glow (red for missing,
green for mapped) alongside the existing ring, making it visually
clear which pill is active and that clicking again will deactivate it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pinned pills now use border-red-500/border-green-500 instead of the
default -300 shade, giving a stronger visual distinction alongside the
existing ring and glow.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
form-js loads Bootstrap CSS which has `.border { border: 1px solid #dee2e6 !important }`
overriding Tailwind's border-color utilities. Switch to darkening the
background (bg-red-100 / bg-green-100 vs bg-*-50) to distinguish the
pinned state, which is not affected by that rule.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Destructuring props in SolidJS evaluates each getter once at creation
time, producing a static snapshot. isPinned was destructured, so it
never updated after the initial render — clicking a pill set pinnedPath
in the parent but InputPill kept aria-pressed="false" and never applied
the ring/glow/bg classes.

Switch InputPill to the props-object pattern so every access of
props.isPinned reads the live reactive getter.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove ring and glow; pinned pills are now distinguished solely by
background darkening (bg-*-100 vs bg-*-50).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@morissette
Copy link
Contributor Author

Thanks for the thorough review! Here's what was addressed:

Issues

  • setFormSchema same-reference reactivity bug — changed to setFormSchema({ ...e.schema }) so every form edit produces a new object reference, fixing the InputsPanel not updating when dropdown selections change
  • extractInputPaths using pre-transformation schema in SelectedEligibilityCheck — added transformInputDefinitionSchema() to the frontend utils (mirrors the backend InputSchemaService people/enrollments logic) and applied it before path extraction, so displayed paths match what the backend returns
  • Tooltips not displaying — moved tooltip rendering to a Solid.js <Portal> with position: fixed coordinates from getBoundingClientRect, escaping the overflow-x: auto clipping context; works for hover and keyboard focus

Nits

  • Palette highlight readability — extracted updatePaletteHighlight() as a named module-level function
  • Duplicate pill markup — extracted a shared <InputPill status="missing"|"mapped"> component
  • Pill ring clipping — added py-1.5 px-0.5 to the scroll container

Additional fixes found during review

  • Click not driving palette highlight — the previous createEffect on hoveredPath ?? pinnedPath could silently no-op when TYPE_COMPATIBILITY[type] returned the same array reference. Replaced with an explicit highlight() helper called directly from each event handler (hover, hover-end, click). Added equals: false to the highlightedTypes signal so every update always propagates
  • Pinned pill state not renderingisPinned was destructured from SolidJS props, which evaluates the reactive getter once at creation time and captures a static false. Switched InputPill to the props-object pattern (props.isPinned) so it reads the live value on every render
  • Pinned pill visual — pinned pills now show a darker background (bg-*-100 vs bg-*-50) to distinguish the active state (border-color approach was blocked by Bootstrap's .border { border: 1px solid #dee2e6 !important } from form-js)

@sierscse
Copy link
Collaborator

@morissette Interesting ideas! I'm the representative Benefits Analyst who configures screeners as a non-coder/SE. For me, I prefer the hidden validation panel. As we add more checks and the ability to configure longer, more complicated screeners, my concern is that it will look cluttered at the top of the page with a long list of checks. I'm adding questions first, then using the key drop down to connect the check. It's a bit of a chicken-and-egg issue. I like highlighting the possible inputs you can add based on the check, but configuring the screener is about adding questions first then connecting the checks. It's tough for me to imagine it would be intuitive for a non-software engineer Benefits Analyst to do that in reverse. BTW we also have a Slack UX channel to discuss UX ideas.

@morissette
Copy link
Contributor Author

The biggest issue for me was when I added the inputs I didn't remember all the inputs and the less prominent validate button in the bottom right wasn't intuitively known to check. In addition I had to add like every input type to find the matching key to input type. Thanks for the slack mention; joined!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants