Skip to content

EPBDS-16211 E2: the leaf components the first batch still cast, as TypeScript - #134

Merged
AlexSamBY merged 1 commit into
masterfrom
e2-ts-leaf-seven
Sep 30, 2026
Merged

AlexSamBY merged 1 commit into
masterfrom
e2-ts-leaf-seven

Conversation

@AlexSamBY

Copy link
Copy Markdown
Member

What changes

Eight leaf components become TypeScript:

All eight are renamed with git mv. Git detects every rename (52–65% similarity), so their history follows.

file notes
View.tsx ViewProps: the props it reads, plus an open rest bag for the <div>
Row.tsx RowProps is ViewProps; RowRef is typed as taking a callback ref only (below)
Text.tsx TextProps; the config context is read through a commented cast
Icon.tsx IconProps, with name required, as its propTypes say
Button.tsx ButtonProps; type is the DOM's 'button' | 'submit' | 'reset'
Loading.tsx, Spinner.tsx LoadingProps, SpinnerProps; size and color are the propTypes lists
Checkbox.tsx CheckboxProps, CheckboxChangeHandler

The casts that stood in for these components are deleted:

  • five in InputNumber;
  • five in Upload;
  • one in LocalDraftTableRow;
  • one in ToggleField.

Those call sites are now checked against the real prop types. A throwaway probe of six wrong call sites failed on all six, and its six correct counterparts compiled:

  • an Icon without name;
  • a button type outside the DOM's three;
  • a spinner size outside its list;
  • an object ref on RowRef;
  • a mistyped onChange;
  • a numeric className.

The first batch's conventions apply:

  • exported prop types;
  • the rest bag typed unknown, because it is forwarded and never inspected;
  • propTypes kept on the inner function.

Runtime: one literal changes

A readonly Checkbox now passes undefined instead of null as its input's onChange. The DOM typings reject null, and React attaches no listener for either value. The two readonly tests pass on 16, 17, 18 and 19.

Everything else is types and casts, which Babel strips. Two casts keep a JavaScript behaviour on purpose, and their comments say so:

  • Text still throws outside a ConfigContext provider.
  • Checkbox still throws on a change when it has no onChange.

Two comments were wrong

I checked both with a probe on all four fixture legs, before and after the change; the results were identical.

  • RowRef attaches a callback ref only. An object ref from createRef() stays null on every React, although the comment said it works.
    • The cause: Row attaches its second argument only if it is a function. Through the default export's React.memo, that argument holds React's legacy context instead of a ref: a frozen {} on 16–18, undefined on 19.
    • A new test pins the real behaviour, and the type now states it.
  • On React 19, a ref reaches the element. View's note said a ref can never arrive.
    • On 16–18 the ref is dropped, with React's warning.
    • On 19, ref is an ordinary prop. It rides the rest bag onto the <div>, and the same holds for Row, Button and Icon.
    • No caller passes one.

I also removed an "Enter press" that the onClick docs of View, Text, Row and Icon promised; none of them implements it.

Measured

  • The corpus is identical to master. Over the 38 examples through the published entry: commits at mount 107, translate calls 1013, and 27 / 64 on an edit. The DOM is identical after mount and after an edit.
  • The TypeScript share grew. src/core + src/library hold 43 TypeScript files, 8,090 lines (was 35 and 7,586), against 114 JavaScript files, 11,628 lines (was 122 and 12,052). That is 41.0% of the lines, up from 38.6%.
  • The bundle shrank by 70 bytes. dist/index.js went from 327,475 to 327,405 bytes.

Housekeeping

  • The Checkbox coverage threshold in jest.config.js follows the renamed file.
  • A test comment that named Text.js now names Text.tsx.
  • The three remaining cast comments no longer point at InputNumber.tsx, whose cast block is gone.
  • docs/SUPPORTED-PROPS.md needed no change, as docs:props:check confirms.
  • docs/UPGRADE-PLAN.md §9.6-E2 records the batch.

Gates

All exited 0:

  • lint:js, lint:css, typecheck, typecheck:contract;
  • docs:props:check, docs:views:check, css:fixture:check;
  • test:env-flags, build;
  • test:coverage, thresholds included. The eight files' coverage is unchanged; Button is still at 90% of branches, because no test renders a loading button;
  • test:pack, test:pack:peers, test:types;
  • test:react16, test:react17, test:react19, 2908 tests each (one of them new);
  • test:e2e, 41 tests;
  • the manifest contract.

In the demo on the dev server, the toggle example flips on a click and logs no new console error.

🤖 Generated with Claude Code

…peScript

View, Text, Row, Icon, Button, Loading and Checkbox, plus Spinner, which
only Loading renders, become .tsx with exported prop types (git mv, so
their history follows). The casts that stood in for them in InputNumber,
Upload, LocalDraftTableRow and ToggleField are deleted, so those call
sites are checked now.

Runtime: one literal. A readonly Checkbox passes `undefined` rather than
`null` as its input's onChange; the DOM typings reject null, and React
attaches no listener for either.

Two comments were wrong, both measured on all four fixture legs: RowRef
attaches a callback ref only (an object ref stays null, and a new test
pins it), and on React 19 a ref given to View, Row, Button or Icon does
reach the element. The onClick docs also promised an Enter press that
none of these components implements.

Measured: the 38-example corpus is identical to master (107 / 1013 /
27 / 64, same DOM); TypeScript is 41.0% of src/core + src/library lines,
was 38.6%; dist/index.js 327,475 -> 327,405 bytes.
@AlexSamBY
AlexSamBY merged commit 4369f4d into master Sep 30, 2026
5 checks passed
@AlexSamBY
AlexSamBY deleted the e2-ts-leaf-seven branch September 30, 2026 14:06
AlexSamBY added a commit that referenced this pull request Oct 1, 2026
## What changes

Seven stateful components become TypeScript:
- `ProgressBar`, `ProgressSteps` and `Counter`;
- `Pagination`, `Select` and `Dropzone`;
- `StandaloneTabs`.

All seven are renamed with `git mv`. Git detects every rename (61–75%
similarity), so their history follows.

The §9.3 step 6 hooks conversions among them are typed as they are:
- their state objects get types;
- the refs their timers read go through non-null assertions, since every
render assigns the ref before any handler runs;
- the `props !== propsSeen` derivations are untouched.

| file | notes |
|---|---|
| `ProgressBar.tsx` | `ProgressBarProps`; the value may be a fraction or
NaN, undefined or null, as documented |
| `ProgressSteps.tsx` | `ProgressStepsProps`, `ProgressStep` |
| `Counter.tsx` | `CounterProps`, `CounterRender`, and `CounterFrame`
for the exported `nextFrame` |
| `Pagination.tsx` | `PaginationProps`, `PageChange` |
| `Select.tsx` | `SelectProps`, `SelectOption` |
| `Dropzone.tsx` | `DropzoneProps`, and `DropzoneHandle`, the real
handle with `fileInputEl` included |
| `StandaloneTabs.tsx` | `StandaloneTabsProps`, `StandaloneTab`, and
`StandaloneTabsHandle` for function content |

**`Upload`'s last component cast, `Dropzone`'s, is deleted.** Its
`DropzoneHandle` is re-exported from `Dropzone`.

A throwaway probe of seven wrong call sites failed on all seven, and its
correct counterparts compiled:
- a string `value` on `ProgressBar`;
- a numeric `step`;
- a `Counter` without `end`;
- a `Pagination` without `totalPages`;
- a `Select` without `options`;
- an `onDrop` taking a string;
- a tab without `content`.

## What the checker surfaced

- **`forwardRef`'s own type erases named props.** It runs them through
`Omit<…, 'ref'>`, which keeps nothing but the index signature of a type
that has one. `RowRef` hit the same trap in #134. `Dropzone` is
therefore cast to the same component with its props kept.
- **`InputNative`'s `select` branch hands `Select` the engine
`onChange`, `(value, name, event)`.** That is the signature `Select`
reports with. `ForwardedProps` said every branch overwrote the handler.
The comment is corrected, and the call is a commented cast.
- **`StandaloneTabs` renders an object tab without an `icon` as it is.**
Its `propTypes` accept `{text}` alone, and React rejects rendering it.
The component is demo-only, and `NavTabs` passes strings, so this is
recorded and not fixed. The cast that keeps the JavaScript's assumption
says so.
- **Two smaller casts:**
- `renderFloat` is still JavaScript, and its JSDoc types the element it
returns as `Object`, so `Counter`'s default `render` is cast.
- `Counter`'s `className` and `style` are declared by its `propTypes`
and read by nothing. The type says so.

## No runtime change

Every edit is a type, a cast or a comment.

`dist/index.js` shrank by 12 bytes, from 327,399 to 327,387, because
`Upload`'s `const Dropzone = DropzoneJs` alias is gone.

## Measured

- **The corpus is identical to master.** Over the 38 examples through
the published entry: commits at mount 107, `translate` calls 1013, and
27 / 64 on an edit. The DOM is identical after mount and after an edit.
- **The TypeScript share grew.** `src/core` + `src/library` hold 64
TypeScript files, 9,989 lines (was 57 and 8,830), against 93 JavaScript
files, 10,111 lines (was 100 and 11,051). That is 49.7% of the lines, up
from 44.4%.

## Housekeeping

- **Six coverage thresholds follow the renamed files.** They are in
`jest.config.js`.
- **Two comments that named `Pagination.js` now name `Pagination.tsx`.**
One of them is in `_semantic.less` and survives compilation, so
`public/static/ui-render.built.css` is rebuilt; it differs by that one
line.
- **`docs/SUPPORTED-PROPS.md` needed no change**, as `docs:props:check`
confirms.
- **The plan records the batch.** `docs/UPGRADE-PLAN.md` §9.6-E2 has the
note.

## Gates

All exited 0:
- `lint:js`, `lint:css`, `typecheck`, `typecheck:contract`;
- `docs:props:check`, `docs:views:check`, `css:fixture:check`;
- `test:env-flags`, `build`;
- `test:coverage`, thresholds included. Every touched file's coverage is
unchanged, and the partial ones (`Pagination`, `StandaloneTabs`,
`InputNative`, `Upload`) are partial on the same expressions as on
master;
- `test:pack`, `test:pack:peers`, `test:types`;
- `test:react16`, `test:react19`, 2908 tests each;
- `test:e2e`, 41 tests;
- the manifest contract.

**`test:react17` took three runs, and none of them failed a test.** Two
runs lost a jest worker to `SIGSEGV`, the V8 crash of nodejs/node#62393
that this machine's Node 24.14 is known to hit:
1. **First run:** the worker died in `manifest.contract.test.js`, and
2900 tests passed.
2. **Second run:** the worker died in `Dropdown.gate.test.js`, a file
this PR does not touch, and 2901 tests passed.
3. **Third run:** clean, 2908 of 2908.

Both crashed suites also passed alone on the React 17 leg, 15 of 15. The
three earlier React 17 runs this session, one per batch, were all clean.

In the demo on the dev server:
- **`Pagination`:** a click on page 2 moved the current page and the
table to rows 6 onwards.
- **`Upload`:** it rendered its three `Dropzone`s, each with a hidden
file input carrying the right `accept` and `multiple`.
- **`StandaloneTabs`:** the navigation switched to "Styles", and one
Back returned to the examples page, so there is one history entry per
click.
- **Console:** no error.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
AlexSamBY added a commit that referenced this pull request Oct 1, 2026
…drifted (#139)

## What changes

This PR covers the bugs found during the modernization that were
recorded and left open, and the documentation that had drifted. **Every
behaviour change has a test that fails on the previous code.** I checked
each one by running its test against master's version of the file; the
two deletions have nothing to fail.

### Bugs

1. **`validate: 'maxLength'` failed every value.**
- `FIELD.VALIDATION` registered the length *factory* as the validator.
The draft row registered the same factory (#133 recorded it there).
- So the field's validator returned a function, which counts as an
error. A form field declaring it could never be submitted, and a draft
row could never be added.
- Both now register `maxLength()`, the validator at the factory's
default of 100 characters.
- Tests: `UIRender.form-flows.test.js` and
`LocalDraftTableRow.append-validation.test.js`.
2. **The per-form registries lasted one render.**
- react-final-form 6.5.9 hands its render prop a new `{...form, reset}`
on every render. The registries in `state/formRegistry` (touched fields,
errors, baseline) were keyed by that object.
- That defeated the touched registry, which exists to remember a field
that unmounted. Measured: on a pristine form, touch a required field so
that "Required" shows, switch tabs and come back. The error had
vanished.
- The wrapper now keeps **one object per final-form instance**, so the
registries and the subscription outlive a render. All those objects
share the instance's methods.
- **Why the instance is told apart by its methods.** My first version
kept the very first object seen. `examples.strict-mode.test.js` failed
on `nestedDataKind`, because StrictMode's discarded first render builds
an instance of its own. The document was then bound to that discarded
instance, and an edit did not show.
   - The corpus measured identical, commits included.
3. **A popup item bound by `name` did not show its row.** Popup content
renders with `relativeData: false`, so the item's data is never resolved
by name. The mapper's `Text`/`Title` view rendered whatever context it
had:
- **nothing**, in a popup declared inside a nested row document, where
`_data` is unset and the row is in `data`;
- **the whole row object**, which React rejects, when the popup resolved
its row into `_data`.

It now reads the field it names from that row, as `config.md` promises.
Test: `rules.popup-content.test.js`.
4. **Two keys leaked to the DOM.** Both are in `ENGINE_PROPS` now.
   - `meta`: every popup item carried it as `meta="[object Object]"`.
- `@class`: OpenL's serializer puts this tag on every meta node, and
React reported it as an invalid attribute name.

   The per-component DOM-boundary tests pick both up from the list.
5. **`StandaloneTabs` rendered a `{text}` tab without an icon as the
object itself.** Its `propTypes` accept such a tab, and React rejects
rendering it. It now renders the text. The component is demo-only.
6. **The demo's percent slider showed "5,000 %" for 50.**
- The `Percent` renderer takes a fraction and multiplies it by 100,
while the example's marks treat the values as percents.
- The example now uses `unit: '%'` and shows "50%". The renderer is
unchanged, since other views rely on its fraction semantics.
   - The DOM snapshot changed by exactly that tooltip.
7. **The corpus logged five warnings, and now logs none.**
   - Two metas put `buttoned`, a `Tabs` prop, on a `VerticalLayout`.
- `InputNumber`'s `propTypes` rejected the `error={false}` every field
is handed before the user interacts with it.
- The form layer marked `formProps` and `instance` as required. A nested
`Data` document does not pass them, because it shares its parent's form,
which the layer's own `form` getter already supports.
   - `@class` reached a `<span>` (item 4).

**A new contract test, `examples.console-clean.test.js`, renders every
example under StrictMode and expects no console output.** With one stray
`buttoned` restored, it fails.
8. **Dead code removed.**
- `popupScope`'s search for a row in the form values' keys could never
succeed, because final-form nests a bracketed name into a real array.
Doing what it meant, reading a row from field names, would be the guess
§2.6-16 removed, so only its live half is kept: the values.
- `components/utils/layouts.js`, imported by nothing, is deleted, and so
is `utils/SizeMe.js`, imported only by it.

### Documentation

- **`docs/UPGRADE-PLAN.md`**:
- roadmap row 4-S is marked done (H3, H4 and H5 all landed 2026-09-23),
and row 4 records H1 as done;
- **risk R14 is retired after a re-check.** Every global it names is
closed or was measured not to leak, and it is pinned by the two-instance
suites;
  - the §9.3 and E2 findings above are marked fixed.
- **`CLAUDE.md`** no longer says the source is still JavaScript.
- **`TooltipPop.tsx`**'s note that a ref cannot reach those components
now says that this held on React 16–18. On 19 a `ref` is an ordinary
prop and reaches the element; this was measured in #134.
- **`changelog.md`** records the consumer-visible fixes under Unreleased
→ Fixes.
- **`docs/SUPPORTED-PROPS.md`** is regenerated: the boundary lists and
one line count.

## Measured

- **The corpus is identical to master except the slider example.** That
example's DOM changed by its tooltip, and it makes one fewer `translate`
call, because the `Percent` renderer's "%" text is gone: 1013 → 1012 in
total.
- **Commits are unchanged:** 107 at mount and 27 on an edit.

## Gates

All exited 0:
- `lint:js`, `lint:css`, `typecheck`, `typecheck:contract`;
- `docs:props:check`, `docs:views:check`, `css:fixture:check`;
- `test:env-flags`, `build`;
- `test:coverage`, thresholds included;
- `test:pack`, `test:pack:peers`, `test:types`;
- `test:react16`, `test:react17`, `test:react19`, 2914 tests each, with
no worker crash;
- `test:e2e`, 41 tests;
- the manifest contract.

In the demo on the dev server:
- **Slider example:** its five tooltips read 35%, 120$, 760$, 500 and
50%.
- **Console:** no error in the three examples that used to log one,
`dropdownExperience`, `ratingDetails` and `tableForm`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
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.

1 participant