Skip to content

DEV-3554: Fix the hyperformula-ui issues found in the Handsontable formula builder review - #1839

Open
adrianspdev wants to merge 34 commits into
developfrom
fix/dev-3554-hyperformula-ui-review-fixes
Open

adrianspdev wants to merge 34 commits into
developfrom
fix/dev-3554-hyperformula-ui-review-fixes

Conversation

@adrianspdev

@adrianspdev adrianspdev commented Oct 22, 2026 •

Copy link
Copy Markdown

Context

The PR fixes the hyperformula-ui issues found while reviewing the Handsontable formula builder (handsontable/handsontable#13172).

  • Why: each one leaves a visible defect in a host grid or forces the host into a workaround.
  • What changed: one commit per fix, all in hyperformula-ui/.
Issue Now
Idle formula bar colors references differently from the editor after a named expression Same named-expression step as the editor; redraws when names change
A removed named expression stays colored after undo(), redo(), changeNamedExpression(), removeSheet() Cache also cleared on valuesUpdated, sheetRemoved and every setValue
destroy() throws after the host destroyed the engine, leaking DOM and styles Builder and editor run every teardown step; first error rethrown
No way to place popups in a shadow root or iframe rootDocument and portalParent options; listeners (scroll in a shadow root included), hit tests and focus checks follow the element's own document
A missing or invalid container selector throws from the constructor; selectors miss a shadow-root grid Warns once and uses the default placement; selectors also resolve in the grid's shadow root
Address box reports success when the grid cannot select the cell setActiveCell/setSelection may return false; announced as "Cannot select B7", not as invalid
A long run of ' tokenizes in quadratic time One linear pass, same tokens
An engine without the editor's sheetId makes every keystroke throw Treated as a sheet with no local names and no dimensions
Error value and error popup title use different fonts Both read --hfe-error-font-family, falling back to --hfe-font-family where the error renders
Ctrl/Cmd+Enter only inserts a newline Calls optional adapter.commitFill or onCommitFill; Alt+Enter inserts a newline
Reference colors fail 4.5:1, and some pairs look alike All clear 4.5:1; every pair at least 10 apart (OKLab ΔE×100)
color-scheme set on every element through [class*=hfe-] Not set at all: every element inherits the host page's scheme
Labels and announcements are English only phrases option, English defaults in DEFAULT_PHRASES
  • Result: the Handsontable plugin drops its style overrides and gains translations, shadow-root support and fill commit (paired commits on #13172).

How did you test your changes?

Every fix has a test that fails without it.

Command Result
npm run build:ui Builds
npm run typecheck (after npm run bundle-all) Passes
npm run lint 0 errors
npm run test:ui 1147 tests passed
npm run test:e2e 1 passed
Handsontable plugin against this build: unit tests, Playwright formula-builder specs, axe scan 486 (with i18n), 53 and 9 passed

Checked in Chromium on the Handsontable demo against 0.3.5: error-title font, color-scheme with the OS in the other scheme, Ctrl/Cmd+Enter fill, undo of a named expression, a reference drag in a shadow root, the error popup in an iframe.

Compatibility with Excel and Google Sheets

Not applicable. The change is in the formula editor UI and cannot affect calculation results.

Public API changes

All in hyperformula-ui:

  • FormulaBuilderOptions: rootDocument, portalParent.
  • FormulaEditorOptions (and FormulaBuilderOptions): onCommitFill(value), phrases; FormulaEditor.phrases.
  • IGridAdapter: optional commitFill(value); setActiveCell and setSelection return boolean | void.
  • HyperFormulaLike: on/off for sheetRemoved.
  • New exports: Phrases (includes cellUnavailable), DEFAULT_PHRASES, StyleRoot, AddressBoxNavigation.
  • FormulaBarCellOptions.tokenPostProcess and phrases, FormulaBarCell.refresh(), FormulaBarAddressBoxOptions.phrases and onUnavailableTarget; its navigation callbacks may return "unavailable".
  • StyleInjector.inject(root?) / release(root?); LiveRegion accepts a ShadowRoot.
  • CSS: --hfe-error-font-family; new --hfe-ref-1..8 defaults.

Not breaking, though some defaults change (reference colors, the error cell's font, the color-scheme scope, no constructor throw). The package is 0.x and no released consumer depends on these defaults. The changelog says how to keep each old default.

Out of scope and follow-ups

None. Keeping all fixes in one PR was agreed by the PR author. The popups staying over frozen panes is recorded as the intended behavior in dev-docs/SPEC.md.

The License Compliance (FOSSA) check also fails on #1838 and #1841; it is not related to this change.

Types of changes

  • Breaking change (existing functionality no longer works as before) – agreed with the maintainers
  • New feature or improvement (a non-breaking change that adds functionality)
  • Bug fix (a non-breaking change that fixes an issue)
  • Additional language file, or a change to an existing language file (translations)
  • Change to the documentation

Related issues

  1. DEV-3554

Checklist


Note

Medium Risk
Touches the public IGridAdapter contract, destroy/teardown paths, and event/focus behavior in shadow roots and iframes—important for host integrations though calculation logic is unchanged.

Overview
hyperformula-ui gains host-friendly embedding, i18n, and several Handsontable review fixes in one release-sized change set.

Embedding & DOM: FormulaBuilder adds rootDocument and portalParent so popups, live regions, and styles can live in an iframe or shadow root; listeners, hit-testing, and focus use the element’s document instead of the global document. Style injection is ref-counted per StyleRoot (document or shadow root). Invalid or missing constructor container selectors warn once and fall back; attach* methods still throw.

Grid adapter & keyboard: Optional commitFill / onCommitFill makes Ctrl/Cmd+Enter fill all selected cells; Alt+Enter inserts a newline when fill isn’t supported. setActiveCell / setSelection may return false; the address box surfaces "unavailable" with translated announcements via new phrases / DEFAULT_PHRASES.

Named expressions & teardown: Named-expression coloring stays in sync between editor and idle formula bar (tokenPostProcess, refresh()); caches invalidate on valuesUpdated, sheetRemoved, and setValue. destroy() runs all teardown steps via runAll even if the engine was destroyed first.

Other fixes: Lexer uses a linear pass for quoted sheet prefixes (long ' runs). Theming updates reference contrast defaults, --hfe-error-font-family, and removes package-level color-scheme so the host controls light/dark.

Reviewed by Cursor Bugbot for commit 96be8a3. Bugbot is set up for automated code reviews on this repo. Configure here.

…e editor

The idle formula bar tokenized without resolving named expressions, so a
named expression did not take a reference color slot there while it did in
the cell editor, and every later reference was shown in a different color.
The idle view now runs the same named-expression annotation as the editor and
re-renders when a named expression is added or removed.
Unsubscribing from a destroyed HyperFormula instance throws, and the first
throw in FormulaBuilder.destroy() skipped the rest of the teardown, leaving
the popup portal, the live region and the injected stylesheet in the page.
The named-expression unsubscribe now tolerates a destroyed engine, and every
teardown step runs even when one throws; the first error is rethrown at the
end.
The quoted sheet-name pattern was retried at every position of a run of
apostrophes that no '! closes, so a long run took quadratic time on every
tokenize (about 600 ms for 40,000 characters). The closing quote of every
possible quoted sheet name is now found in one right-to-left pass per
tokenize, with the same matches as the pattern it replaces.
The error value in a cell used the monospace formula font while the error
popup title used the UI font, so the same #NAME? appeared in two fonts side
by side. Both now read the new --hfe-error-font-family custom property, which
defaults to --hfe-font-family, the host's font.
The stylesheet set color-scheme: light dark on every package element through
a class-attribute substring selector, so light-dark() followed the operating
system instead of the host theme: a dark grid on a light system showed light
popups, a host could undo it only element by element, and the selector was
tested against every element of the page. It is now set once per root (the
editor, the formula bar, the popup portal and a hosted popup), with no
specificity, so a host theme class on a root overrides it and the rest
inherits.
A cell's error message was associated with the cell only while the error
popup was open, through aria-errormessage, which some screen readers ignore,
so a screen-reader user landing on the cell heard nothing. markCell() now also
sets the message as the cell's aria-description, and removes it again when the
error clears.
The address box checked only the grid bounds and reported success even when
the host could not select the cell, for example a filtered-out row or a
hidden column, so it showed the typed address while the selection stayed
put. IGridAdapter.setActiveCell() and setSelection() may now return false;
the address box then reverts, announces the invalid address and does not
scroll. Adapters that return nothing keep working as before.
A popups.*.container or formulaBarContainer selector that matched nothing
made the FormulaBuilder constructor throw. A host that rebuilds the builder
from an event handler may pass a container its framework has just unmounted,
and the throw left it without a builder. The constructor now logs a warning
once per selector and uses the default placement: a floating popup, or no
formula bar. The attach methods still throw on a missing container.
Ctrl/Cmd+Enter was bound to inserting a newline, and the adapter had no way
to commit to the whole selection, so a spreadsheet host could not offer the
usual fill-selection shortcut. Ctrl/Cmd+Enter now calls the new optional
IGridAdapter.commitFill(value), or the onCommitFill option, when the host
provides one, and inserts a newline otherwise. Alt+Enter always inserts a
newline.
Most default reference colors fell below 4.5:1 against the editor background
in light or dark mode (orange and green were near 2:1 on white), so reference
text and highlight borders failed WCAG 1.4.3 and 1.4.11 unless the host
restated the whole palette. The colors that failed are darkened in light mode
or lightened in dark mode, keeping their hue, and the README documents
--hfe-ref-1 to --hfe-ref-8 as the way to override them.
The popup portal, the live region and the stylesheet always went into the
global document's body and head, and focus checks read the global
document.activeElement. Inside a shadow root the editor's ARIA references
pointed across the boundary and resolved to nothing; in an iframe the popups
landed in the parent page without its styles; and focus checks were wrong in
both. FormulaBuilder now takes rootDocument and portalParent options, creates
its elements and resolves selectors in that document, appends the portal and
live region to the portal parent, injects the styles into a shadow root when
the portal parent is in one, and reads focus through open shadow roots.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
hyperformula-docs 96be8a3 Commit Preview URL

Branch Preview URL
Oct 23 2026, 12:22 PM

@github-actions

github-actions Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Performance comparison of head (96be8a3) vs base (d143456)

                                     testName |    base |    head |  change
---------------------------------------------------------------------------
                                      Sheet A |  488.15 |  489.67 |  +0.31%
                                      Sheet B |  157.21 |  159.11 |  +1.21%
                                      Sheet T |   138.1 |  140.72 |  +1.90%
                                Column ranges |  468.53 |  473.75 |  +1.11%
                                Sorted lookup | 14005.2 | 16088.1 | +14.87%
Sheet A:  change value, add/remove row/column |   15.89 |   16.95 |  +6.67%
 Sheet B: change value, add/remove row/column |  146.71 |  145.63 |  -0.74%
                   Column ranges - add column |   149.7 |  156.33 |  +4.43%
                Column ranges - without batch |  454.85 |  476.72 |  +4.81%
                        Column ranges - batch |  118.27 |  123.42 |  +4.35%

@adrianspdev adrianspdev self-assigned this Oct 22, 2026
@adrianspdev
adrianspdev marked this pull request as ready for review October 22, 2026 10:57

@Haxikowy Haxikowy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Findings with no line in the diff:

  • adapter/domRangePicker.ts:159-162,386 binds mousemove/mouseup/scroll to the global document and calls document.elementFromPoint, so drag-to-extend a range ref does not work in a shadow root (the hit is the retargeted host) or an iframe. Either use the root's document there or list it next to the window listeners under out of scope.
  • DEFINITION-OF-DONE.md:78 asks for one change per pull request, and :84 for no new public option on its own; this one carries 11 fixes and adds rootDocument, portalParent, onCommitFill, commitFill and --hfe-error-font-family. Please split it along the existing commits, or record the maintainers' agreement in the description.
  • DEFINITION-OF-DONE.md:105-111 counts a changed default and removed documented behavior as breaking: the ref colors, the error cell font, the color-scheme scope and the constructor no longer throwing. Tick the box or say why not, and give CHANGELOG.md:17 the "what to do instead" line.
  • CHANGELOG has no Added entry for FormulaBarCell.refresh(), tokenPostProcess, StyleRoot, inject(root)/release(root) or LiveRegion taking a ShadowRoot. SPEC.md's types list lacks StyleRoot, and KEYBOARD-SHORTCUTS.md's behavior table has no Alt+Enter row.
  • The three out-of-scope items need follow-up task IDs (DEFINITION-OF-DONE.md:86), and the red FOSSA check needs a line in the description (:39); it is unrelated, and the same failure is on #1835-#1838.

Comment thread hyperformula-ui/packages/core/src/styles/tokens.scss Outdated
Comment thread hyperformula-ui/packages/core/src/formulaBuilder.ts
Comment thread hyperformula-ui/packages/core/tests/lexer/lexer.test.ts Outdated
Comment thread hyperformula-ui/packages/core/src/styles/tokens.scss Outdated
Comment thread hyperformula-ui/packages/core/src/styles/error.scss Outdated
Comment thread hyperformula-ui/packages/core/src/styles/tokens.scss Outdated
Comment thread hyperformula-ui/packages/core/README.md Outdated
Comment thread hyperformula-ui/packages/core/src/formulaBuilder.ts
Comment thread hyperformula-ui/packages/core/src/formulaBar/formulaBar.ts
Comment thread hyperformula-ui/packages/core/src/adapter/types.ts

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 79f8031. Configure here.

Comment thread hyperformula-ui/CHANGELOG.md Outdated
@adrianspdev

Copy link
Copy Markdown
Author

On the findings in the review body:

  • DomRangePicker in a shadow root or an iframe: fixed in 5a39347. Its listeners, its hit test (elementFromPoint on the shadow root) and the cell lookup (composedPath()[0] instead of the retargeted event.target) follow the root element's own document. Node checks no longer use instanceof, which fails for iframe elements. The error popup, the popup viewport and the editor's scroll/resize listeners follow the same rule. Checked in Chromium: a drag in a shadow root now picks the range (0.3.5 collapsed it to one cell), and the error popup in an iframe opens and closes on Escape.
  • One change per PR: keeping the fixes together was the PR author's decision, recorded in the description. Each fix is its own commit, so the branch still splits cleanly if needed.
  • Changed defaults: I left "Breaking change" unticked and say why in the description: the package is 0.x and no released consumer depends on these defaults. Every changed default in the changelog now says how to keep the old behavior (8d3076e adds the missing ones).
  • Missing changelog entries, StyleRoot in SPEC.md, the Alt+Enter row: added in 851ef03.
  • Follow-ups: none left. The three out-of-scope items are now in this PR: the window and document listeners (5a39347), stale named-expression colors (fd9edd4) and translatable phrases (5febee3). The popups staying over frozen panes is recorded as the intended behavior in SPEC.md (902b994).
  • FOSSA: noted in the description. The check also fails on HF-150: run the performance workflows against 100k-row scale scenarios #1838 and Exclude the hyperformula-ui demo and e2e workspaces from FOSSA #1841.

Comment thread hyperformula-ui/packages/core/src/ui/types.ts
Comment thread hyperformula-ui/packages/core/src/styles/tokens.scss Outdated
Comment thread hyperformula-ui/packages/core/src/ui/popup/errorPopup.ts
Comment thread hyperformula-ui/packages/core/src/index.ts
@codecov

codecov Bot commented Oct 23, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.28%. Comparing base (d143456) to head (96be8a3).

Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff            @@
##           develop    #1839   +/-   ##
========================================
  Coverage    97.28%   97.28%           
========================================
  Files          207      207           
  Lines        16218    16218           
  Branches      3556     3588   +32     
========================================
  Hits         15777    15777           
  Misses         433      433           
  Partials         8        8           
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

This branch has not been deployed

No deployments
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.

3 participants