-
Notifications
You must be signed in to change notification settings - Fork 0
Review 5204
#5204 — fix(richtext): close all 8 RichTextEditor audit BLOCKs
AKnassa · open · view on GitHub
Verdict: REQUEST CHANGES (posted)
HEAD REVIEWED: 8f470c9ed65060ab087dab292fb6ccd73c48eeb2 — the head was re-fetched before posting and had not moved.
Epic coordination. The original stable RichTextArea proposal #981 remains closed. The later #4678 decision created the narrower, canary-only @astryxdesign/richtext package. This review treats #5204 as hardening that canary surface, not as reopening the stable package/API proposal; the link was recorded on #981.
#5204 fix(richtext): close all 8 RichTextEditor audit BLOCKs by AKnassa (bucket: contributor)
8f470c9ed65060ab087dab292fb6ccd73c48eeb2
LOOP VERSION: 1.5.0 AUDIT RUBRIC: 1.12
LANE: full WHY: 31 files and 3,021 changed lines; public core API, behavior, Effect, theming, accessibility, and intended visual changes all promote this out of fast review.
WHY 1: The audited editor left visible/AT strings untranslated, a read-only textbox unnamed and unreachable, disabled/read-only semantics identical, theme icon overrides ineffective, and RTL arrows wrong. WHY 2: Keyboard and screen-reader users could not reliably reach or understand persisted content, while builders could not theme the toolbar through the supported path. WHY 3: A rich-text system has to preserve authored content and its controls across input modality, locale, direction, and theme; otherwise people cannot safely read or edit the document. USER-FACING PROBLEM: People reading or editing rich text through keyboard, assistive technology, another locale, RTL, or a custom theme get an inaccessible or misleading editor. PROBLEM SEVERITY: broken task — the audited read-only path could not be reached by keyboard and exposed no accessible name.
VERDICT: clear
The editor now translates its own language, separates read-only from disabled behavior, synchronizes Lexical when those props change, and routes toolbar visuals through theme and direction owners. It also widens core icon theming, changes the shared Selector and isRenderable, adds a render error boundary, and extends lint/tests so these contracts stay checked. Those extra owners make this a bundle of independent changes rather than one audit fix.
SOLUTION (15 decisions · ~754 runtime-line churn of 3,021 changed lines)
- Extend strict/type-aware Astryx lint coverage to
packages/richtext— root cause. - Route 31 toolbar/editor strings through the catalog — audit i18n problem.
- Add
RichTextView.labeland split read-only/disabled semantics — audit a11y problem. - Synchronize Lexical editability after prop changes — state/ARIA consistency.
- Move Selector trigger padding and add 44px coarse toolbar targets — target-size finding.
- Widen
defineTheme({icons})to namespaced extension keys — toolbar theme problem. - Resolve toolbar icons through the active theme and mirror undo/redo — theme/RTL problems.
- Tokenize toolbar glyph and editor heading typography — audit token debt.
- Add a richtext
themePropsdoc/source guard — lint-coverage root cause. - Add BaseProps/ref/style merging on toolbar/view — audit public-contract problem.
- Catch unusable serialized editor states and recover on value change — [NO STATED PROBLEM].
- Make character counts grapheme-safe and merge computed/consumer ARIA — [NO STATED PROBLEM].
- Change global
isRenderablearray behavior and icon own-property lookup — [NO STATED PROBLEM]. - Publicly export test-only
__resetDevWarnings— [NO STATED PROBLEM]. - Rename
linkOpensInNewTaband convert refs/defaults/casts — audit FIXes, independently landable.
BURDEN: high — 15 decisions, 31 files, 754 runtime-line churn, shared core behavior/API, one new Effect, four changesets, and 345 targeted tests. BURDEN MATCH: disproportionate — the eight serious audit findings justify substantial work, but unrelated core utilities and public test surface make settled fixes wait on independent decisions.
VERDICT: BLOCKS — independent fixes and unrecorded core changes need a concrete split.
OWNER: RichTextEditor/RichTextView own editor semantics; IconRegistry/defineTheme own icon extension; Selector owns its trigger; isRenderable owns slot presence.
TIER 1: Field/input anatomy, useTranslator, IconRegistry, themeProps, and React/Lexical synchronization are reused.
TIER 2: shared size tokens and semantic icon extension apply.
SEAMS: toolbar slot, theme object/name lookup, RichTextView value/fallback transition, Selector InputGroup, RTL, and coarse pointer.
BEHAVIOR UNIT: inline — the one new [editor, editable] Effect is a narrow one-way synchronization with the external Lexical editor; deterministic presence logic remains a pure utility.
| seam | driven result |
|---|---|
| RichTextEditor toolbar slot | theme icon, RTL wrapper, and 44px coarse controls resolved in Chromium |
| RichTextView value update | valid → unusable value rendered the fallback in the review probe |
| Selector default trigger | 200×20 before → 200×30 after; pixels remained byte-identical |
| theme inheritance/name lookup | targeted registry tests passed |
The individual owner placements are coherent. Their number is evidence for splitting, not for inventing a new cross-system owner.
VERDICT: note — sound owners, bundled across four independently reviewable systems.
Richtext is private/canary-only, so its rename has no stable installed consumer; its editor fixes reach canary users. The core Selector, isRenderable, icon types, and /utils barrel reach released core builders: a legal unlabeled RichTextView remains unnamed, an array of only empty React children still draws guarded wrappers, and a test-only reset becomes public API.
VERDICT: BLOCKS — two user-visible holes remain and one unsupported public surface is added.
<RichTextView value={json} /> // still compiles; renders an unnamed role="textbox"
defineTheme({icons: {'richtext:bold': <MyBold />}})| change | public? | class | doc'd? | verdict | |
|---|---|---|---|---|---|
+ |
RichTextView.label?: string |
private/canary package | required input labels across Field components | guidance added; JSDoc example still omits it | finding — legal call remains unnamed |
+ |
ExtendedIconRegistry and widened defineTheme.icons
|
core barrels | existing getExtendedIcon namespaced-key contract |
theme doc + template + changeset | ok; split for focused review |
~ |
RichTextEditorToolbarProps extends BaseProps, ref?
|
private/canary package | every component BaseProps/ref contract | source/tests | ok |
~ |
linkOpensInNewTab? → hasNewTabLinks?
|
private/canary package | boolean naming | docs/tests | canary-only break; split |
+ |
__resetDevWarnings |
released @astryxdesign/core/utils
|
none — test-only implementation control | missing body rationale and changeset | finding |
~ |
isRenderable(ReactNode) now special-cases arrays |
released @astryxdesign/core/utils
|
global slot-presence helper | changeset + test | finding — nonempty all-empty arrays still return true |
OSSIFICATION: label belongs to an established required-label class, so make it required before this canary API ships. Namespaced theme icons extend an already-landed getExtendedIcon class. The test reset has no user class and would cost a deprecation cycle to remove; it should not enter the public barrel. isRenderable is already public, so its new array promise must cover the reachable array class it names.
VERDICT: BLOCKS — optional label, incomplete array semantics, and a test-only core export.
No theme target is added or removed: the existing astryx-rich-text-editor target now documents size and status. Toolbar glyphs resolve through the active theme, heading/glyph typography uses role tokens, and namespaced icon typing extends the existing registry concept. The pixel-changing heading and read-only treatments require visual inspection before approval.
VERDICT: note — source/theming contracts pass; rendered treatment was not visually inspectable in this session.
BEHAVIOR: yes — read-only/disabled, Selector hit area, empty-array slot guards, invalid-state fallback, character counting, and icon lookup change; the empty-child array class remains broken.
API: yes for private/canary richtext (linkOpensInNewTab rename); released core additions are additive, including the unwanted test reset.
VISUAL: yes — read-only opacity, 44px coarse controls, RTL glyph direction, and h3 typography change; the default Selector screenshot is byte-identical despite a 20→30px button box.
THEME: no existing target/token is removed; namespaced icon overrides become reachable through defineTheme.
VERDICT: BLOCKS — the new isRenderable behavior does not cover its reachable class.
EFFECTS: + useEffect [editor, editable] at RichTextEditor.tsx:836 — one-way synchronization to Lexical's external editor state.
| Effect + deps | external system | why render/handler cannot do it | measured render cost | lifetime + cleanup | focused test |
|---|---|---|---|---|---|
useEffect [editor, editable] |
Lexical editor | props can change without a user event; initialConfig.editable is mount-only |
N=1/4/10: one React update commit in every arm; 1/4/10 setEditable calls; no extra React commit |
no listener/layout read; one call per mounted editor per editability change | local profiler probe + PR toggle tests |
RENDER: flat one React commit at N=1/4/10; the Effect adds linear Lexical calls but no state-driven React pass. Repo stories render at most one editor together; N=10 was the stress bound.
LISTENERS/OBSERVERS: none added by the Effect.
LAYOUT: no layout/style read or listener; setEditable updates Lexical's external editable state.
BUNDLE: no dependency change.
VERDICT: clear
VISUAL CHECK: manual frames required WHY: the PR intentionally changes opacity, target geometry, type size, and RTL glyph paint while claiming the Selector pixels remain unchanged.
Four matched parent/head pairs were captured in Chromium on port 6524 with passing receipts: neutral/light, 900×500@1, exact story/state, zero page errors, same sensors except Build. The client has the shown artifacts PR #5204 — Selector before/after, Read-only before/after, RTL toolbar before/after, and Touch toolbar before/after.
| pair | computed delta | geometry/state receipt |
|---|---|---|
| Selector default | 0 / 450,000 pixels | button 200×20 → 200×30 |
| seeded read-only | 3,421 / 450,000 pixels | opacity .5 → 1; tabIndex -1 → 0 |
| RTL toolbar | 669 / 450,000 pixels | undo/redo wrapper transform none → scaleX(-1)
|
| coarse toolbar | 8,451 / 450,000 pixels | eight controls 28×28 → 44×44 |
The image reader returned “model protocol cannot carry media,” and private attachment upload was denied; therefore the reviewer could not perform the required by-eye judgement or embed the pixels in this markdown block.
VERDICT: not looked at — receipts and pixel deltas are banked, but the frames were not visible to the reviewer.
Exact-head pr-a11y and pr-rtl ran green; the baseline shrinks by 13 and 345 targeted PR tests pass. Chromium proved read-only enters the tab order with aria-readonly, full opacity, and contenteditable=false; disabled stays skipped with aria-disabled, opacity .5; RTL glyphs flip once; coarse toolbar controls measure 44×44.
A public call <RichTextView value={value} /> still compiles and the review test fails toHaveAccessibleName(); the PR's own test expects the textbox to remain unnamed after only warning. Current rubric 1.12 also reclassifies the original toolbar target: before this PR it measured 68×20 fine / 77×20 coarse, while the nearest target began 71px / 75px from its center — well outside the 12px radius — so WCAG 2.5.8's spacing exception passes; 44px coarse expansion remains a FIX-level house improvement, not one of eight BLOCKs.
VERDICT: BLOCKS — the A1 accessible-name defect remains reachable through the legal API and its JSDoc example.
| slot | verdict |
|---|---|
| PROBLEM | clear |
| SOLUTION | BLOCKS — independent/unrecorded decisions need a split |
| ARCHITECTURE | note — sound owners, four systems |
| IMPACT | BLOCKS — reachable holes + public test surface |
| API | BLOCKS — optional label, incomplete utility, test export |
| THEMING | note — rendered treatment uninspected |
| BREAKING | BLOCKS — incomplete array behavior |
| PERFORMANCE | clear |
| VISUAL | not looked at — image transport prevented inspection |
| A11Y & I18N | BLOCKS — unnamed textbox remains legal |
PRIOR REVIEW: gonzoblasco called the close-out strong and highlighted the state split and lint root cause; this extends rather than contradicts that review by exercising legal public calls and the current rubric.
GOAL: partly met — six live audit BLOCK classes are fixed; A1 remains, and the former A10 block now passes rubric 1.12's spacing exception.
DISPOSITION: unnamed legal RichTextView → blocks now; all-empty nonempty array → blocks now; public test reset → blocks now; 15-decision bundle/stale A10 claim → split before review; missing by-eye visual inspection → blocks any later approval, not this request-changes verdict; en.json merge conflict → merge main.
ADVICE: proven remedy — require the established label contract and update the JSDoc call site; recurse array children in isRenderable until one is renderable; keep warning-reset control out of the public barrel; split into richtext a11y/i18n, icon theming, Selector sizing, and core utility changes, starting with the first.
AUTHOR CAN PROCEED: yes — each block has outcome-based acceptance criteria and no unresolved owner/API/design choice is being handed to the contributor.
WORST OUTCOME: “a public call <RichTextView value={value} /> still compiles and the review test fails toHaveAccessibleName()” → request changes.
JUDGEMENT NEEDED: none — existing accessibility, utility, and public-surface contracts decide these findings.
request changes
-
RichTextView.labelis optional and the JSDoc still omits it → a builder follows a legal documented call and ships an unnamed textbox ·RichTextView.tsx:66,158 -
isRenderabletreats every nonempty array as visible content → a conditional mapped slot containing onlynull/falsestill draws an empty divider or wrapper ·isRenderable.ts:33 -
Fifteen decisions include a public test reset and an obsolete A10 premise → core builders inherit unsupported API while the six ready editor fixes stay blocked behind unrelated review ·
utils/index.ts:106,Selector.test.tsx:745
Thanks — the lint root cause and read-only/disabled split are strong. This still bundles about fifteen independent decisions; __resetDevWarnings is one test-only public export with no product case.
Two correctness holes remain: <RichTextView value={json} /> still compiles to an unnamed textbox, and isRenderable([null, false, '']) returns true although React renders nothing. This also needs a merge with main.
Could you stack this as (1) required label + read-only/disabled, (2) i18n, (3) icon theming, (4) Selector sizing, and (5) one core utility per PR, with lint coverage last once the package is clean? Start with 1. If you'd rather talk it through with someone, we're in Discord.
-
packages/richtext/src/RichTextView.tsx:66— This needs to be required; the optional path still ships an unnamed textbox. -
packages/core/src/utils/isRenderable.ts:33— A nonempty array can still contain only empty React children. -
packages/core/src/utils/index.ts:106— This test-only reset should stay out of the public package barrel.
- Exact-head CI ran 18 checks; all executable jobs passed and only
review-requiredremains pending. - Targeted changed suites passed: 8 files, 345 tests; the two independent review probes fail exactly on the unnamed textbox and all-empty array.
- The head conflicts with current main only in
packages/core/locales/en.json; current main also has five later Selector commits.
TIME total 30m
setup 6m dedicated worktree + fast install + one build + Storybook 6524 (warm main reused: yes)
reading 9m full brief/critic, PR body, 4,107-line diff, current rubric/conventions, prior review/main drift
measuring 10m 345 tests, 6 review probes, 4 parent/head frame pairs, 4 Chromium arms; 2 re-measures after sensor/class checks
writing 5m three presentations + two critic passes
waste 5m $KIT was unset; first Storybook start lacked a log; three sensor retries; one intercepted browser click; two blocked image-upload routes
- By-eye pixel judgement: captures and receipts exist, but this session's image renderer returned no pixels and private upload was denied.
- Safari/Firefox behavior: managed-Mac policy supports Chromium only.