Two-box currency control · searchable ISO-4217 picker · canonical amount normalization · nested response · state variants · Storybook & visual-editor authoring
The <currency-entry> widget renders as the two-box currency control on the live DEV renderer, the searchable ISO-4217 picker works, the amount normalizes to a canonical period-decimal string, and submissions return the nested map { "Type":"CURRENCYENTRY", "Value":{ "StandardCode","Value" }, "Label","State" } with no ResolvedValue. All four states render and submit their State. Storybook coverage is complete and matches the mobile reference, and the widget is authorable end-to-end in the visual editor.
Three failures: (1) autofocus='true' does not focus the amount field on load (Low, renderer); (2) the picker does not place keyboard focus in its search field on open, so arrow-key navigation is inert until the field is clicked (Medium, renderer); (3) the visual editor's Show SFML emits style="0" where the test expected style="LARGE" (Low, editor — intended numeric serialization that round-trips through the renderer, likely a stale expectation).
Method note: TC1-TC4 driven on DEV via the OrgApp GraphQL (sessionStart + anonymous sessionShareIssue) same-origin from the authenticated browser, opened in the ViewerApp; shadow-DOM values read via getComputedStyle / native input value; submissions via session.stage.submittedValues. TC5/TC6 ran locally (Storybook :6006, editor-demo :4200). Node v22.11 sat one patch below the Angular CLI v22.12 floor, so the two local dev servers needed a version-gate shim + --experimental-require-module + a missing oxc-parser binding to start — none of which affect the widget under test.
| # | Check | Result | Evidence |
|---|---|---|---|
| 1–2 | Two-box control renders (not unsupported fallback) | PASS | button "Currency" (USD) + textbox "Amount amount" |
| 3–4 | Label above, caption below | PASS | "Amount" above, "Total due" below the control |
| 5 | Empty caption → none shown | PASS | caption='' → no caption node |
| 6–7 | Seeded currency USD / amount 19.99 | PASS | currency "USD", amount value "19.99" |
| 8 | No amount → "0.00" placeholder | PASS | initvalue='' → placeholder "0.00", empty value |
| 9 | No currency → em-dash | PASS | currency box "—" |
| 10 | autofocus focuses the amount field | FAIL | after load and reload, deepest activeElement=BODY (hasFocus true); component has no .focus() wiring |
| 11 | Long label/caption — row stays aligned | PASS | both boxes top=499 (5px gap); label wrapped to 2 lines above |
| 12 | SMALL identical to LARGE | PASS | SMALL 70x43 / 429x43 = LARGE exactly |
| # | Check | Result | Evidence |
|---|---|---|---|
| 1 | Empty currency box | PASS | currency box "—" |
| 2 | Click → searchable list opens | PASS | textbox "Search currency" + listbox below the [expanded] button |
| 3 | Option shows "(CODE) Name" | PASS | e.g. "(USD) US Dollar" |
| 4 | "gbp" case-insensitive filter | PASS | filtered to one option "(GBP) British Pound" |
| 5 | "zzzz" → No matches | PASS | listitem "No matches" |
| 6–7 | Click "(EUR) Euro" → box EUR, list closes | PASS | currency box "EUR"; button no longer [expanded] |
| 8 | Reopen + Down arrow moves highlight | FAIL | focus stays on the trigger button; highlight did not move. Only moves once the search field is focused (EUR→FJD) |
| 9–10 | Enter selects / Escape closes (unchanged) | PASS* | work once the search field is focused (same focus gap as #8) |
| 11 | Click outside closes | PASS | listbox present → absent after outside click |
| 12 | Preselected EUR highlighted as selected | PASS | EUR is aria-activedescendant and the only aria-selected option |
| 13 | Distinct accessible names | PASS | "Currency" vs "Amount amount" |
| 14 | Non-standard code XBT shown unchanged | PASS | currency box "XBT" |
*Enter and Escape are handled on the search input and work correctly once it has focus; they are inert immediately after opening because the picker leaves focus on the trigger button (see the three failures).
| # | Input / Check | Result | Evidence |
|---|---|---|---|
| 1 | Empty amount + Submit | PASS | placeholder "0.00" + Submit button |
| 2 | 19,99 | PASS | → 19.99 |
| 3 | 42.50 | PASS | kept 42.50 (not 42.5) |
| 4 | 1.234,56 | PASS | → 1234.56 |
| 5 | 1e3 | PASS | → 13, "e" rejected |
| 6 | +1,000 | PASS | → 1.000; "+" and "," absent (lone comma read as the decimal mark per the documented rightmost-separator rule) |
| 7 | -50.00 | PASS | → -50.00, leading "-" accepted |
| 8 | 30-digit input | PASS | capped at 22 chars |
| 9–12 | Submit EUR / 19,99 | PASS | Value:{"StandardCode":"EUR","Value":"19.99"}, Type "CURRENCYENTRY", no ResolvedValue |
| 13 | Submit 42.50 | PASS | Value 42.50 (no precision loss) |
| 14 | Seeded USD/19.99, submit untouched | PASS | {"USD","19.99"} (not nulls) |
| 15 | Bare form, submit untouched | PASS | {"StandardCode":"","Value":""} (empty strings, not nulls) |
| State | Render / behaviour | Submitted State | Result |
|---|---|---|---|
| DEFAULT | Enabled, editable | "DEFAULT" (25.00) | PASS |
| READ_ONLY | Seeded "1234.56"; both boxes disabled + greyed (bg rgb(245,245,245)); rejects typing; picker won't open | "READ_ONLY" | PASS |
| WARNING | Amber border both boxes rgb(255,193,7) + amber "?" icon (icon_144.svg); editable | "WARNING" | PASS |
| ERROR | Red border both boxes rgb(239,68,68) + red "!" icon (icon_150.svg); amount input aria-invalid | "ERROR" | PASS |
Sidebar lists Widgets / CurrencyEntry / Docs; the Docs page renders all 9 stories. Each story inspected in its standalone iframe.
| Story | Expected | Result |
|---|---|---|
| Amount | two-box, "0.00" placeholder | PASS |
| Prefilled | currency EUR, amount 42.50 | PASS |
| NoCurrency | empty currency box | PASS |
| Small | identical to LARGE (70x43 / 861x43) | PASS |
| LongLabel | row stays aligned | PASS* |
| Required | "Required" marker shown | PASS |
| Warning | amber borders + amber "?" icon (144) | PASS |
| Error | red borders + red "!" icon (150), aria-invalid | PASS |
| ReadOnly | greyed/disabled boxes, fixed amount 1234.56 | PASS |
| Corpus / RealWorld | two-box control, no fallback | PASS |
| Mobile reference | two-box proportions + amber/red treatment match | PASS |
*LongLabel: the two-box row stays aligned; in Storybook's ~968px container the long label fits on one line rather than wrapping (wrapping confirmed live in TC1).
| # | Check | Result | Evidence |
|---|---|---|---|
| 1 | Editor loads with palette + canvas | PASS | "SFML Form Editor Demo", Widgets palette, canvas |
| 2 | Input palette lists "Currency Entry" | PASS | described "Currency code + amount input" |
| 3 | Drag → seeded Amount / USD / 0.00 | PASS | editorCells 0→1, dropZones 1→0 |
| 4–5 | Property panel: Common + Currency Entry sections/fields | PASS | Initial Currency (ISO code), Initial Amount, Placeholder |
| 6 | Set Initial Currency EUR → canvas EUR | PASS | canvas currency box "EUR" |
| 7 | Set Initial Amount 42.50 → canvas 42.50 | PASS | canvas amount box "42.50" |
| 8 | Show SFML exactly matches expected | FAIL | emits style="0" vs expected style="LARGE" (also attribute order + html/body wrapper) |
| 9 | Markup includes device-required attrs | PASS | flow, style, label, caption, inittext, autofocus present |
| 10 | Clear currency/amount → attrs omitted | PASS | initcurrency / initvalue omitted from cleared SFML |
autofocus='true' not honored (web renderer)With the widget's autofocus='true', the amount field does not receive focus on load. After load and reload the deepest document.activeElement is BODY with document.hasFocus()===true; the component has no .focus() call though autofocus is parsed and modeled. Same gap logged in the 2026-06-19 report for ENG-5373. Convenience feature, no data impact.
Opening the picker leaves keyboard focus on the currency trigger button, whose keydown handler only opens the popup and is a no-op while open. The Down/Up/Enter/Escape handlers live on the search input, which is never auto-focused — so keyboard navigation is inert until the user clicks into the search field. Once focused, the handlers are correct (Down moved EUR→FJD, Enter selected, Escape closed). Accessibility-relevant.
style="0" not style="LARGE" (visual editor)The editor serializes currency-entry style numerically ('1' for SMALL, else '0'), consistent with how flow is emitted ('0'). The test (and the 2026-07-01 runbook) expected the enum name style="LARGE". The editor's output round-trips cleanly through the live renderer (parsed with no errors, rendered as the two-box control with EUR/42.50), so this is a serialization-format difference rather than a functional break — likely a stale expectation. Because SFML is a frozen-once-prod contract, the canonical form (numeric "0" vs string "LARGE") is worth a conscious decision.












