Repository navigation
EPBDS-16211 E2: the leaf components the first batch still cast, as TypeScript - #134
Merged
Merged
Conversation
…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
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)
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
Eight leaf components become TypeScript:
View,Text,Row,Icon,Button,Loading,Checkbox;Spinner, which onlyLoadingrenders, so thatLoadingneeds no new cast.All eight are renamed with
git mv. Git detects every rename (52–65% similarity), so their history follows.View.tsxViewProps: the props it reads, plus an open rest bag for the<div>Row.tsxRowPropsisViewProps;RowRefis typed as taking a callback ref only (below)Text.tsxTextProps; the config context is read through a commented castIcon.tsxIconProps, withnamerequired, as itspropTypessayButton.tsxButtonProps;typeis the DOM's'button' | 'submit' | 'reset'Loading.tsx,Spinner.tsxLoadingProps,SpinnerProps; size and color are thepropTypeslistsCheckbox.tsxCheckboxProps,CheckboxChangeHandlerThe casts that stood in for these components are deleted:
InputNumber;Upload;LocalDraftTableRow;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:
Iconwithoutname;typeoutside the DOM's three;RowRef;onChange;className.The first batch's conventions apply:
unknown, because it is forwarded and never inspected;propTypeskept on the inner function.Runtime: one literal changes
A readonly
Checkboxnow passesundefinedinstead ofnullas its input'sonChange. The DOM typings rejectnull, 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:
Textstill throws outside aConfigContextprovider.Checkboxstill throws on a change when it has noonChange.Two comments were wrong
I checked both with a probe on all four fixture legs, before and after the change; the results were identical.
RowRefattaches a callback ref only. An object ref fromcreateRef()staysnullon every React, although the comment said it works.Rowattaches its second argument only if it is a function. Through the default export'sReact.memo, that argument holds React's legacy context instead of a ref: a frozen{}on 16–18,undefinedon 19.refreaches the element.View's note said arefcan never arrive.refis an ordinary prop. It rides the rest bag onto the<div>, and the same holds forRow,ButtonandIcon.I also removed an "Enter press" that the
onClickdocs ofView,Text,RowandIconpromised; none of them implements it.Measured
translatecalls 1013, and 27 / 64 on an edit. The DOM is identical after mount and after an edit.src/core+src/libraryhold 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%.dist/index.jswent from 327,475 to 327,405 bytes.Housekeeping
Checkboxcoverage threshold injest.config.jsfollows the renamed file.Text.jsnow namesText.tsx.InputNumber.tsx, whose cast block is gone.docs/SUPPORTED-PROPS.mdneeded no change, asdocs:props:checkconfirms.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;Buttonis 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;In the demo on the dev server, the toggle example flips on a click and logs no new console error.
🤖 Generated with Claude Code