Repository navigation
EPBDS-16211 E2: the five components #132 converted, as TypeScript - #133
Merged
Merged
Conversation
Data, PopupContent, LocalDraftTableRow, Upload and ToggleField become .tsx, renamed with git mv so their history follows. mapper.js's own PopupContent waits for mapper.js (E3), and the demo's Examples stays JSX, as §9.6's definition of done allows. The pilot's conventions: - exported prop types; - unconverted components cast to open prop bags; - propTypes kept, on the inner function, which React checks through `memo`. E5 removes them separately. Two defects of my own were caught on the way and fixed: - Data cast its import of rules.js in a module-level const. rules.js -> mapper.js -> Data -> rules.js is an import cycle, so the const captured the still-undefined export for good, and every nested form-backed document rendered nothing. Six engine tests failed on it. The cast now happens at render, as does PopupContent's cast of Render, and CLAUDE.md states the rule. - Expressions restructured for the checker changed behaviour. A type guard on the add button's `onClick` dropped the function the engine resolves the action to. Splitting `e && e.target ? ... : e` added a branch the tests never take, which LocalDraftTableRow's 93% branch floor caught. Both are the original expressions again, with casts. Measured on the corpus (38 examples, published entry), identical to master: - commits at mount 107 and `translate` calls 1013; - on an edit, 27 commits and 64 `translate` calls; - the same DOM after mount and after an edit. TypeScript is now 38.6% of the lines in src/core + src/library (was 34.4%). Found while typing and left as it was: LocalDraftTableRow treats `maxLength`, a validator factory, as a validator. A draft whose meta says `validate: 'maxLength'` can never be added. The cast there says so. The coverage thresholds follow the renamed files, SUPPORTED-PROPS.md is regenerated for the new paths, and comments that named the old .js paths name the new ones.
AlexSamBY
added a commit
that referenced
this pull request
Sep 30, 2026
…peScript (#134) ## What changes Eight leaf components become TypeScript: - the seven the first batch (#133) still had to cast: `View`, `Text`, `Row`, `Icon`, `Button`, `Loading`, `Checkbox`; - `Spinner`, which only `Loading` renders, so that `Loading` needs no new cast. 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](https://claude.com/claude-code)
AlexSamBY
added a commit
that referenced
this pull request
Oct 1, 2026
…#138) ## What changes The field components and the helpers the converted files already leaned on become TypeScript. - **Components, as `.tsx`:** `Input`, `InputDate` and `Table`. - **Modules, as `.ts`:** - `domProps`, with a typed `omitProps`; - `inputs/validationRules`; - `inputs/normalizers`; - `utils/timers`, which holds `useTimers`. All seven are renamed with `git mv`. Git detects every rename (53–96% similarity), so their history follows. | file | notes | |---|---| | `Input.tsx` | `InputProps` | | `InputDate.tsx` | `InputDateProps`, over rc-picker's own `PickerProps` | | `Table.tsx` | `TableProps` and `TablePartProps`; the family's structure is unchanged | | `domProps.ts` | `omitProps` returns `Record<string, unknown>`, not the `Object` its JSDoc said | | `validationRules.ts` | `ValidationResult` | | `normalizers.ts` | parameter types from what each body needs | | `timers.ts` | `Timers`, with `setTimeout`'s extra arguments typed through to the callback | **`LocalDraftTableRow`'s last three component casts go, so no `.tsx` file casts a component any more.** What is left of `UnconvertedComponent` types the registry slots (`Active.Field`, `Active.UIRender`) and the engine's import cycle. `Row`'s cast of the `omitProps` result is gone too. A throwaway probe of seven wrong uses failed on all seven, and its correct counterparts compiled: - an `onRemove` taking a number; - an `InputDate` `onChange` taking a number; - a string `striped`; - `omitProps` on a string; - `maxLength('5')`; - `phone(5)`; - a timer callback whose arguments do not match. ## What the checker found - **The draft row hands its fields an `unknown` error.** `fieldErrors[name]` reaches `Input`/`InputDate` as `error`. It is normally a message. Under `validate: 'maxLength'` it is the function the factory returns, the bug #133 recorded, and React does not render it. A commented cast where it is read says so. - **`InputDate` passes rc-picker a `resize` that rc-picker does not declare.** A probe showed that rc-picker drops it: no element it renders carries the attribute, the open panel included. It is typed rather than removed, so the runtime is unchanged. - **`password` keeps its last value on itself for `password.confirm`.** TypeScript recognises a function's own properties only when they are assigned at the top level, so the assignment inside the function is a cast. ## No runtime change Every edit is a type, a cast or a comment. `dist/index.js` went from 327,387 to 327,369 bytes, 18 smaller, because three cast aliases are gone and one is added. ## The prop-reference generator It statically parses `domProps` and `Table`. - It finds them by extension, because `pack()` already looked for `.ts`. - It matches `export const ENGINE_PROPS = [` literally, so the two lists stay unannotated. - `docs/SUPPORTED-PROPS.md` is regenerated. Only the paths and `Table`'s line count changed. ## 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. - **TypeScript is now the larger half.** `src/core` + `src/library` hold 71 TypeScript files, 10,806 lines (was 64 and 9,989), against 86 JavaScript files, 9,396 lines (was 93 and 10,111). That is 53.5% of the lines, up from 49.7%. ## Housekeeping - **Every mention of `domProps.js` names `domProps.ts`.** There were 22 in 17 files: component and engine comments, tests, the generator, the curation, and a comment in the CI workflow. - **The other renamed files' mentions follow too.** That covers the three coverage thresholds in `jest.config.js` and nine comments. Two of the comments are in LESS (`_semantic.less`, `tooltip.less`) that survives compilation, so `public/static/ui-render.built.css` is rebuilt. A comment that told history ("`Table.js` used to be a 24-line wrapper") now names the component without an extension. - **The plan records the batch.** `docs/UPGRADE-PLAN.md` §9.6-E2 has the note. ## A miss of my own, which the gates caught The first run of the gate set failed `test:coverage`: "Coverage data for `./src/core/components/Table.js` was not found", and the same for `Input.js` and `InputDate.js`. The three thresholds still named the old files. My stale-path check had passed, but vacuously. On this machine `git grep -E` with `\b` matches nothing: 0 hits where the same search without `\b` finds 9. Every check in this PR was rerun word-bounded with `git grep -P`, next to a control search for a known hit. That rerun also found older stale mentions, which are fixed here as well: - **six comments still named `Data.js`.** #133 said it had renamed all of them, and its search missed these. - **five named `TooltipPop.js`.** ## Gates The final run, after the fixes above, exited 0 on every gate: - `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 `validationRules`' and `LocalDraftTableRow`'s partial branches are the same expressions as on master; - `test:pack`, `test:pack:peers`, `test:types`; - `test:react16`, `test:react17`, `test:react19`, 2908 tests each, with no worker crash; - `test:e2e`, 41 tests; - the manifest contract. In the demo on the dev server: - **`input` example:** typing kept the value, and the wrapper took the `done` class. That is the path the `done` cast covers. - **`nestedDataKind` example:** it renders four `ui table`s. None of their 58 cells carries an empty `class`, the cells hold 44 fields, and the draft row's "Add line" button is there. - **Console:** no error. The demo has no date field, since the one date-typed meta is an orphan, so `InputDate` is covered by its suites. 🤖 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)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changes
The five library components #132 converted to hooks become TypeScript. All five are renamed with
git mv, so their history follows.engine/Data.tsxDataProps; itsinstancetyped as what the draft row reads off itengine/components/PopupContent.tsxPopupContentProps; the memoized function is still created per callengine/components/LocalDraftTableRow.tsxDraftItem,LocalDraftTableRowProps, typed state and validatorsmodules/upload/views/Upload.tsxUploadProps, and aDropzoneHandlefor the refmodules/form/inputs/ToggleField.tsxToggleFieldProps, and a typed context for the inner componentOut of this batch, on purpose:
mapper.js's ownPopupContentwaits formapper.jsitself (E3).Examplesstays JSX, which the definition of done allows.The pilot's conventions apply (§9.6 governance):
propTypeskept, on the inner function, which React checks throughmemo. E5 removes them separately.Two defects of my own, caught on the way and fixed
A module-level cast of a cyclic import.
Datafirst cast its import ofrules.jsin a top-levelconst. Butrules.js→mapper.js→Data→rules.jsis an import cycle, so whileDatais evaluated that export is still undefined, and theconstkept the undefined for good. Every nested form-backed document then rendered nothing, and six engine tests failed on it. The cast now happens at render, which sees the live binding as the.jsdid; so doesPopupContent's cast ofRender.CLAUDE.mdstates the rule for the rest of the TypeScript tail. Components can be cast at the top of a file: none imports the engine (checked).Expressions restructured for the checker.
typeof oc === 'object', dropped the case where the engine hands the action as a function namedaddData.e && e.target ? … : einto two conditionals added a branch the tests never take, andLocalDraftTableRow's 93% branch floor caught it.Both are the original expressions again, with casts.
Measured
translatecalls 1013, and 27 / 64 on an edit. The DOM is identical after mount and after an edit.src/core+src/libraryhold 35 TypeScript files, 7,586 lines (was 30 and 6,677), against 122 JavaScript files, 12,052 lines. That is 38.6% of the lines, up from 34.4%.dist/index.jsis 327,402 → 327,475 bytes: the render-time casts and one restated guard.Found, and left for a change of its own
LocalDraftTableRowtreatsmaxLength, a validator factory (maxLength(length)returns the validator), as a validator. A draft whose meta saysvalidate: 'maxLength'therefore records a function as the field's error and can never be added. No tracked example does it. The typed cast there says so rather than hiding it.Housekeeping
jest.config.jsfollow the renamed files.docs/SUPPORTED-PROPS.mdis regenerated; only the paths changed..jspaths name the new ones.Gates
All exited 0:
lint:js,lint:css,typecheck,typecheck:contract;docs:props:check,docs:views:check,css:fixture:check;test:env-flags,test:coverage(thresholds included),build;test:pack,test:pack:peers,test:types;test:react16,test:react17,test:react19, 2907 tests each;test:e2e, 41 tests;🤖 Generated with Claude Code