Skip to content

Add plain-language check explanations and accessible result tooltips - #582

Merged
prestoncabe merged 31 commits into
mainfrom
561-plain-language-check-explanations
Nov 2, 2026
Merged

prestoncabe merged 31 commits into
mainfrom
561-plain-language-check-explanations

Conversation

@prestoncabe

@prestoncabe prestoncabe commented Oct 29, 2026 •

Copy link
Copy Markdown
Collaborator

Analysts can explain each configured eligibility check in plain language. Published and preview results show the text in an accessible tooltip; failed checks use it in Requirements not met, including print summaries and CRM Next steps. Checks without explanations keep their alias/name fallback.

Closes #561. Follow-up to #552 after #580.

  • Store optional CheckConfig.explanation per configured check, separate from technical descriptions and aliases. Saves and imports enforce a 2,000-character limit; blank/null clears it. Benefit storage, copying, transfer, publication, and evaluation preserve it.
  • Provide Edit explanation beside the alias. Failed saves retain and refocus the draft with actionable errors. Refetches preserve open drafts and newer saved explanations; dismissal cannot accidentally discard edits.
  • Share accessible tooltips across results and existing help controls. Support focus, hover, tap, Escape and outside-press dismissal, screen-reader descriptions, viewport positioning, scrolling long content, and following the trigger during layout/scroll changes.
  • Reconcile result rows in place so evaluations preserve keyboard focus and open tooltips. Preserve authored check wording consistently across results, unmet requirements, print and CRM; retain explanation line breaks.
  • Apply check edits in Firestore transactions, writing only the checks to preserve concurrent benefit and explanation changes. Parameter edits cannot retain generated aliases for different rule values, and manual aliases remain intact.

Validation on the reviewed branch:

  • 389 frontend tests across 54 files and the production frontend build passed.
  • 248 builder API tests passed, including regressions for concurrent parameter/alias edits.
  • 12 public screener and CRM browser scenarios passed, including tooltip descriptions, keyboard focus/Escape, scroll clipping, printed explanations, and stale-result suppression.
  • git diff --check passed. All six GitHub Actions checks passed on reviewed head 0e168ac8 before merge, including the full end-to-end suite.

prestoncabe and others added 30 commits October 29, 2026 19:53
CheckExplanation had its own copy of the hover, focus and viewport
handling in Tooltip, so fixes would have to be made twice. Tooltip now
owns that behavior, including a real button trigger, an associated
tooltip element and Escape to dismiss, which every tooltip gains.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Tooltips were positioned inside their trigger, so the published results
section, which scrolls on its own, cut them off and gained a scrollbar.
Fixed positioning takes them out of that section, and they now track
the trigger while the page or a section scrolls.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Escape only worked while the trigger had focus, so a tooltip opened by
hover stayed up, and a tap on iOS, which doesn't focus a button, left
touch users with no way to close it. Escape now works from anywhere
while a tooltip is open, and a second tap or a tap elsewhere closes it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Explanations can run to 2,000 characters, which pushed tooltips past
the viewport with no way to reach the rest, and long URLs overflowed the
box. Tooltips now open upward when that gives them more room, limit
their height to the space available, scroll when needed, focusably so
keyboard users can too, and break long words.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The reload shared a promise with the save, so if it failed after the
save succeeded, the author was told the save had failed and the card
kept the old text. It also cost a full read and rebuilt every check row
for one string we already knew. The saved value now goes straight into
the store, as next steps does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Modal doesn't restore focus, so closing it left keyboard users at the
top of the page. Now that saving keeps the row in place, the button that
opened the modal is still there to go back to.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Authors write explanations in a multi-line field and the tooltip kept
their line breaks, but Requirements not met, its print summary and the
editor card ran the lines together.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Its alias and parameters siblings declare @consumes, so a non-JSON
request is turned away at routing. This one didn't.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
unmetRequirements was still described as the failed checks' display
names, but it now carries the explanation when a check has one, and
check_results gained an undocumented field. Hosts matching on those
names would have stopped matching once an author added an explanation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The alias-or-name expression was repeated for the visible label and the
tooltip button's accessible name in both results views, so a naming
change could have left "About ..." out of step with the text beside it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The endpoint updated only the first match, while the client store and
the alias and parameter endpoints update them all. With duplicate IDs,
the second row showed the new text until a reload and then lost it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every check endpoint read the whole benefit and wrote all of it back,
so a concurrent edit could be reverted. A parameter update also spent
seconds generating an alias between its read and write, which could
erase an explanation the client had been told was saved.

Each endpoint now reads and writes only the checks in one transaction.
Aliases are generated first, because a transaction must stay short and
can rerun, and an alias written by hand meanwhile is kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An in-flight refetch could put the old explanation back in the store,
and the next save from the reopened modal could then overwrite the new
text. A refetch also gave every check a new reference, which remounted
its row and closed any open modal.

Saved explanations now win over a refetch that started first, the way
next steps do, and reconciling by check ID keeps rows in place.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Focus stayed on the button behind the backdrop, so Tab walked the
obscured page before reaching the dialog, which is portaled to the end
of the body, and a screen reader announced nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Dragging a text selection past the dialog edge fires a click on the
backdrop, which closed the modal and discarded up to 2,000 characters
without asking. With unsaved edits only Cancel closes it now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every failure said to try again, even a 404 for a check removed in
another tab, so an author could retry forever with nothing logged.
Rejected requests now show the server's reason, and every failure is
logged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each evaluation produced new objects, so For remounted every row. A
keyboard user who tabbed to an explanation while results were updating
lost the open tooltip, and focus dropped to the body.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The fixed tooltip kept tracking a trigger clipped by its scrolling
ancestor, so it floated over other sections with nothing to point at.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each scroll event measured the page and set a new position object,
including scrolls of the tooltip's own content, which don't move the
trigger.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An alias was title-cased in results but raw in Requirements not met,
CRM unmetRequirements and the explanation modal's label. CRM hosts now
receive the same display name the crm-integrations docs describe.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Title-casing an alias is meant for identifiers such as
PrimaryResidenceRequirement, but it also rewrote prose: "Age 18-64"
became "Age 18 64" in results, Next steps and CRM unmetRequirements, and
an alias of only punctuation became empty, which dropped the failed check
from Requirements not met. The editor's card now names a check the same
way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rows now stay mounted across evaluations, so a tooltip can outlive a
change that moves its trigger or replaces its text. Neither fires a
scroll or resize, which left it pointing at the wrong row with a stale
height.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Published results already kept their rows, but preview unmounted them
whenever an evaluation started and rebuilt them from new objects, which
closed an open explanation and dropped focus to the body. Both views now
share the reconciled store. Preview's loading message sits beside the
heading once there are rows, so they don't shift on every evaluation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Saving disabled the textarea and the focused Save button, so a keyboard
user was left on the body when the failure said their edits were still
there. The textarea is now read-only during a save, which lets it take
focus back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The check only looked at ancestors with auto or scroll overflow, so it
missed the window, which is what scrolls on a phone, and ancestors that
clip with hidden or clip overflow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
updateCustomBenefitChecks returns false for either a missing benefit or
a missing check, so removing or editing a check on a deleted benefit
said "Check not found in benefit", which the explanation modal shows
to the author.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Alias and explanation updates matched checks null-safely, but parameter
updates, removals and alias generation called equals on the stored ID,
so one config without an ID made them return 500.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@prestoncabe
prestoncabe marked this pull request as ready for review November 2, 2026 03:12
@prestoncabe
prestoncabe merged commit 6189bf0 into main Nov 2, 2026
6 checks passed
@prestoncabe
prestoncabe deleted the 561-plain-language-check-explanations branch November 2, 2026 03:18
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.

Plain-language check explanations shown as screener tooltips

1 participant