Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
577746f
Color named expressions in the idle formula bar the same way as in th…
adrianspdev Oct 22, 2026
f0e03a6
Let FormulaBuilder.destroy() finish after the host destroyed the engine
adrianspdev Oct 22, 2026
673611a
Tokenize a long run of apostrophes in linear time
adrianspdev Oct 22, 2026
bf6dd66
Render a cell's error value and the error popup title in the same font
adrianspdev Oct 22, 2026
3e03752
Set color-scheme only on the package roots
adrianspdev Oct 22, 2026
943009a
Describe a cell's error to screen readers without the error popup open
adrianspdev Oct 22, 2026
963992f
Reject an address-box target the grid could not select
adrianspdev Oct 22, 2026
f0c617d
Fall back instead of throwing when a constructor container is missing
adrianspdev Oct 22, 2026
7be774f
Let a host commit to every selected cell with Ctrl/Cmd+Enter
adrianspdev Oct 22, 2026
081336d
Ship a reference palette that clears 4.5:1 contrast
adrianspdev Oct 22, 2026
46aed0a
Let a host choose the document and the parent of the popups
adrianspdev Oct 22, 2026
38f75bb
DEV-3554: Add the changelog entries
adrianspdev Oct 22, 2026
79f8031
Merge branch 'develop' into fix/dev-3554-hyperformula-ui-review-fixes
sequba Oct 22, 2026
e648f49
DEV-3554: Test the linear quoted-sheet scan without a time limit
adrianspdev Oct 22, 2026
eb38dbc
DEV-3554: Leave a cell's error description to the host
adrianspdev Oct 22, 2026
29cd4c1
DEV-3554: Resolve the error font where the error renders
adrianspdev Oct 22, 2026
3404585
DEV-3554: Keep every pair of reference colors visibly apart
adrianspdev Oct 22, 2026
fcd5f46
DEV-3554: Finish the editor teardown when the adapter throws
adrianspdev Oct 22, 2026
200767c
DEV-3554: Take the document from the portal parent by default
adrianspdev Oct 22, 2026
1fa057a
DEV-3554: Give the grid highlights the editor's color scheme
adrianspdev Oct 22, 2026
5a39347
DEV-3554: Listen to the window and document the editor is in
adrianspdev Oct 22, 2026
fd9edd4
DEV-3554: Refresh named-expression colors after undo, redo and sheet …
adrianspdev Oct 22, 2026
5febee3
DEV-3554: Let a host translate the editor's phrases
adrianspdev Oct 22, 2026
902b994
DEV-3554: Record that the popups stay over frozen panes
adrianspdev Oct 22, 2026
851ef03
DEV-3554: Complete the changelog and the API lists
adrianspdev Oct 22, 2026
09935da
DEV-3554: Treat an invalid container selector as a missing one
adrianspdev Oct 22, 2026
8d3076e
DEV-3554: Say how to keep each changed default
adrianspdev Oct 22, 2026
208c19a
DEV-3554: Look container selectors up in the grid's shadow root
adrianspdev Oct 22, 2026
bcaefc0
DEV-3554: Announce a cell the grid cannot select as unavailable, not …
adrianspdev Oct 22, 2026
2d3dbc1
DEV-3554: Translate the function help section labels and argument notes
adrianspdev Oct 23, 2026
814e282
DEV-3554: Let every package element inherit the host's color scheme
adrianspdev Oct 23, 2026
4d86213
DEV-3554: Follow scrolling inside the grid's shadow root
adrianspdev Oct 23, 2026
8149dd0
DEV-3554: List the address box outcome type in the API docs and chang…
adrianspdev Oct 23, 2026
96be8a3
DEV-3554: Keep editing when the engine has no sheet with the editor's id
adrianspdev Oct 23, 2026
1a3d539
DEV-3554: Replace the reference the caret sits in on a grid pick
adrianspdev Oct 26, 2026
8839a57
DEV-3554: Preview the slot of the reference a pick replaces
adrianspdev Oct 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion hyperformula-ui/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
- Added the `rootDocument` and `portalParent` options to `FormulaBuilder`, so the popups, the screen-reader announcements and the styles can live in the grid's iframe document or shadow root. Container selectors are also looked up in that shadow root. [#1839](https://github.com/handsontable/hyperformula/pull/1839)
- Added fill commit: Ctrl/Cmd+Enter commits the formula to every selected cell when the adapter implements the new optional `commitFill` method, or the `onCommitFill` option is set. Alt+Enter inserts a newline. [#1839](https://github.com/handsontable/hyperformula/pull/1839)
- Added the `--hfe-error-font-family` CSS variable, used by both a cell's error value and the error popup title. When it is not set, both use `--hfe-font-family`. [#1839](https://github.com/handsontable/hyperformula/pull/1839)
- Added smaller API pieces used by the fixes below: `FormulaBarCellOptions.tokenPostProcess` and `FormulaBarCell.refresh()`, a `root` argument (document or shadow root) on `StyleInjector.inject()` and `release()` with the `StyleRoot` type, a `ShadowRoot` container for `LiveRegion`, and, for the formula bar's address box, the `AddressBoxNavigation` outcome its navigation callbacks may return and the `onUnavailableTarget` option. [#1839](https://github.com/handsontable/hyperformula/pull/1839)
- Added the `phrases` option to `FormulaBuilder` and `FormulaEditor`, which translates every accessible name, hint and screen-reader announcement, with the English defaults exported as `DEFAULT_PHRASES`. [#1839](https://github.com/handsontable/hyperformula/pull/1839)

### Changed
- Changed the licence of the `hyperformula-ui` package from GPL-3.0-only to MIT. [#1840](https://github.com/handsontable/hyperformula/pull/1840)
- Changed the font of a cell's error value from the monospace formula font to the host's font (`--hfe-font-family`), to match the error popup title. To keep monospace, set `--hfe-error-font-family: var(--hfe-formula-font-family)`. [#1839](https://github.com/handsontable/hyperformula/pull/1839)
- Changed the default reference colors (`--hfe-ref-1` to `--hfe-ref-8`) so that each clears a 4.5:1 contrast ratio on the light and dark editor backgrounds and no two look alike. To keep the previous colors, set the eight properties on the class you pass as `className`. [#1839](https://github.com/handsontable/hyperformula/pull/1839)
- Changed the `FormulaBuilder` constructor to log a warning and use the default placement, instead of throwing, when a `popups.*.container` or `formulaBarContainer` selector matches nothing or is not valid CSS. To fail on a missing container, check it before constructing the builder, or attach it with `attachFormulaBar()`, `attachSuggestionsHost()` or `attachFnHelpHost()`, which still throw. [#1839](https://github.com/handsontable/hyperformula/pull/1839)
- Changed the address box to reject a cell or range when the adapter's `setActiveCell` or `setSelection` returns `false`, and to announce that it cannot be selected. Both methods now return `boolean | void`: an adapter that already returned a boolean should return `true` on success, and a wrapper declared as returning `void` that returns the wrapped adapter's result must declare `boolean | void`. [#1839](https://github.com/handsontable/hyperformula/pull/1839)
- Changed the stylesheet to set no `color-scheme`, so the editor, the formula bar, the popups and the grid highlights follow the scheme the host page sets, on `<html>` or on the class passed as `className`, instead of the operating system. A page that sets no scheme now gets the light colors; to follow the operating system, set `color-scheme: light dark` on `<html>`. [#1839](https://github.com/handsontable/hyperformula/pull/1839)

### Fixed
- Fixed a grid pick made with the caret inside an existing cell or range reference: it inserted the pick into the middle of the reference when the caret followed the colon of a range (`=SUM(C1:A1:B2C2)`), and otherwise counted as a click outside the editor, which committed the formula. The pick now replaces the whole reference the caret sits in, at either end included, and Ctrl/Cmd appends after it. [#1839](https://github.com/handsontable/hyperformula/pull/1839)
- Fixed the editor logging an error on every keystroke, and losing named-expression colors, when the HyperFormula instance has no sheet with the editor's `sheetId`. [#1839](https://github.com/handsontable/hyperformula/pull/1839)
- Fixed reference picking with `DomRangePicker` and the error popup in a grid inside a shadow root or an iframe: a drag picked only its first cell, the error popup did not open or close with Escape, and the popups did not follow a scroll inside the shadow root. [#1839](https://github.com/handsontable/hyperformula/pull/1839)
- Fixed a removed named expression staying colored as a reference in the editor and the formula bar after `undo()`, `redo()`, `changeNamedExpression()` or `removeSheet()`. [#1839](https://github.com/handsontable/hyperformula/pull/1839)
- Fixed the idle formula bar coloring references differently from the editor when the formula contains a named expression. [#1839](https://github.com/handsontable/hyperformula/pull/1839)
- Fixed `FormulaBuilder.destroy()` throwing, and leaving the popups and styles in the page, when the HyperFormula instance was destroyed first. [#1839](https://github.com/handsontable/hyperformula/pull/1839)
- Fixed slow tokenizing of a long run of apostrophes, which took about 600 ms for 40,000 characters. [#1839](https://github.com/handsontable/hyperformula/pull/1839)

## [0.3.5] - 2026-10-21

Expand Down
85 changes: 78 additions & 7 deletions hyperformula-ui/dev-docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,25 @@ reparents between two locations via `FormulaEditor.attachTo(mount)`:
address box), paired with `scrollCellIntoView`, and by `FormulaBuilder` after an
outside-click commit (see the outside-click commit policy below). Addresses are HF-space;
the external Handsontable plugin maps them back to visual indices before selecting.
Both `setActiveCell` and `setSelection` return `boolean | void`: `false` means the host could
not select the target (a filtered-out row, a hidden column), and the address box then rejects
the typed address — `FormulaBar` returns `"unavailable"` to it, so it reverts, announces
`Phrases.cellUnavailable` ("Cannot select B7") rather than `invalidCellAddress`, and skips
`scrollCellIntoView`. `void` counts as success, so adapters written before the result keep
working.
- `commit(value, direction?)` — controller calls to commit the current value; the optional
`Direction` drives the host's post-commit selection move (Enter → down, Tab → right, …).
`commit(value, direction)` must apply the value to the engine synchronously before
returning — commit-error announcements read the engine and capture `valuesUpdated`
inside the commit call. Applies equally to a host-side commit bridge (such as the plain-table
example's `PlainTableHost.commit`) and any consumer-supplied `onCommit`. A deferred write degrades announcements to silence
(no crash, no wrong message).
- `commitFill?(value)` — optional; Ctrl/Cmd+Enter calls it (through `FormulaBuilder`'s
commit wrapper, so commit-error announcements work the same) to write `value` into every
selected cell. A builder `onCommitFill` option takes precedence, as `onCommit` does over
`commit`. With neither, the `commitFill` command returns `false` and `ShortcutManager` falls
through to the next Ctrl/Cmd+Enter binding, `insertNewline`; Alt+Enter always inserts a
newline.
- `cancel()` — controller calls to cancel the current edit.
- `setInlineEditorVisible(visible)` — hides or shows the cell overlay mount while the editor
is hosted in the bar (the plain-table and TanStack example adapters toggle the container's CSS display;
Expand All @@ -78,7 +90,11 @@ calls `adapter.setActiveCell` + `scrollCellIntoView` for a single cell or `adapt
for an `A1:B5` range, and mirrors the current `GridSelection` via `setSelection()` — and `FormulaBarCell` (`formula-cell` scope) — the fx-icon +
formula area, owning both the idle read-only colored-formula view (its own `VisualOverlay`
seeded with `FormulaEditor.knownFunctionNames`, rendered via `renderValue()` through
`nToComments`) and the `overlayMount` the shared editor `attachTo`'s while bar-hosted.
`nToComments`) and the `overlayMount` the shared editor `attachTo`'s while bar-hosted. The idle
view's tokens go through the same `annotateNamedExprs` step as the editor's kernel
(`FormulaBarCellOptions.tokenPostProcess`), so a named expression takes a reference color slot
in both and every later reference keeps the same color; `FormulaBar` re-renders the idle view
through `FormulaBarCell.refresh()` when `EngineBridge.onNamedExpressionsChange` fires (on HyperFormula's named-expression, `valuesUpdated` and `sheetRemoved` events — see SPEC.md §10 for why the named-expression events alone are not enough).

While the editor is inline, `FormulaBarCell`'s idle view shows a live colored mirror of the
current text. Because the wrap styles are shared, a bar-hosted editor soft-wraps at the bar
Expand Down Expand Up @@ -155,6 +171,21 @@ adapters may remove the inline mount on close (the TanStack example adapter dele
without the reattach the live editor DOM would be torn out with the mount. With a bar
attached, `FormulaBar` owns the same reattachment instead.

### Phrases

No user-visible or spoken English is hard-coded in a component. Every accessible name, hint
and announcement is a member of `Phrases` (`ui/types.ts`), with English defaults in
`DEFAULT_PHRASES` (`ui/phrases.ts`); phrases that take values (a function name, a cell address,
an error count) are functions, so a translation can reorder words and handle plurals.
`FormulaEditor` resolves the `phrases` option once and exposes the result as
`FormulaEditor.phrases`; the popups receive it through `BasePopupOptions.phrases` (read in
`createRoot`, which runs inside the base constructor), and `FormulaBar` and `FormulaBuilder` read
it from the editor. A new string goes into `Phrases`, not into the component. The one exception is a key
name shown as a key chip (`Tab`, `↑`, `↓`): it names the key on the keyboard, as Excel and Google Sheets
show it in every language. Phrases that end up inside brackets or after a key chip carry no brackets
or surrounding spaces; the component adds them, so a host dictionary that trims whitespace or treats
`[name]` as a variable (Handsontable's) can hold them.

## Component map

```
Expand Down Expand Up @@ -481,6 +512,37 @@ All popup-style overlays share a common base class `BasePopup`
(`popupCtx.portal`) is the shared DOM parent all three popups append to. All three popups
take a plain `container: HTMLElement`, not the full `PopupContext` — `FormulaBuilder` passes
`popupCtx.portal` to each; `popupCtx.destroy()` runs in `FormulaBuilder.destroy()`.
- **Document and portal parent:** `FormulaBuilder`'s `portalParent` option (default
`rootDocument.body`) is where the portal and the `LiveRegion` go; both are created in the
parent's own document. A host inside a shadow root passes that shadow root, so the
textarea's `aria-controls`/`aria-activedescendant` and a cell's `aria-errormessage` resolve
in one tree, and an axe scan of that root sees the popups. `rootDocument` (default the portal
parent's document, else the global `document`, so passing only an iframe's `body` is enough) is
the document selector options are resolved against, the detached mount is created in, and the
stylesheet goes into. Every other element (editor, popups, icons, highlights) is created in the
global `document` and adopted when it is appended. Styles are injected per root
(`createStyleInjector(...).inject(root)`, ref-counted per document or shadow root): into
`rootDocument`, plus into the portal parent's shadow root when it is in one. Container selectors
(`formulaBarContainer`, `popups.*.container`, the `attach*` methods) are looked up in that shadow
root first, then in `rootDocument`. No other shadow root receives the styles, so a popup docked
in a third tree renders unstyled; hosts dock popups in the grid's own tree. Focus checks
never read `document.activeElement`: they call `deepActiveElement(element.ownerDocument)`
(`ui/dom.ts`), which follows open shadow roots down to the focused element and reads the
document the element is actually in. Window and document listeners follow the element too:
`viewOf(node)` gives the window of the document a node is in, and the error popup, popup
viewport sizes, `PopupController`'s fallback clip, and `DomRangePicker` read it instead of the
global `window`/`document`. The editor's ambient scroll/resize listeners (`ScrollAnchor`,
`EditorSizeController`) live in an `editor-view` scope that `FormulaEditor.attachTo` rebinds when
the editor moves into another document or shadow root (`listenToView`). `scroll` is not composed,
so a scroller inside a shadow root never reaches a window listener: the editor's controllers, the
error popup and `DomRangePicker` also listen for scroll (capture) on the shadow root their element
is in. Node checks never use `instanceof`:
an element from an iframe belongs to another realm and fails `instanceof Element`, so
`isElement`/`isHTMLElement`/`isNode` (`ui/dom.ts`) test `nodeType`. `DomRangePicker` reads a
document-level event's cell from `eventOrigin(event)` (the first node of `composedPath()`),
because a shadow root retargets `event.target` to its host, also listens for scroll on its
shadow root (scroll is not composed), and hit-tests through the shadow root's own
`elementFromPoint` (`elementAt`).
- `FormulaBar` constructor signature:
`(container: HTMLElement, options: { adapter: IGridAdapter; editor: FormulaEditor; knownFunctions: ReadonlySet<string>; className?: string })`.
It receives the pre-built `FormulaEditor` from `FormulaBuilder` — it does not construct
Expand Down Expand Up @@ -604,9 +666,12 @@ All popup-style overlays share a common base class `BasePopup`
string` (selector via `document.querySelector`, throwing on a miss) plus
`popups.suggestions.container`/`popups.fnHelp.container` constructor sugar
(`PopupHostOption`) that delegates to attach after construction — mirroring
`formulaBarContainer`/`attachFormulaBar`. All three constructor selectors resolve
first thing in the constructor, before styles/portal/editor allocate, so a bad
selector throws without leaking resources.
`formulaBarContainer`/`attachFormulaBar`. The constructor never throws on a selector:
one that matches nothing, or is not valid CSS, logs a `console.warn` once per message and falls back to the
default placement (a floating popup, no bar), because a host that rebuilds the builder
from an event handler may pass a container its framework unmounted in between, and a
throw there would leave the host without a builder. The `attach*` methods still throw on
a miss: the host calls them directly and can handle the error.

### BasePopup lifecycle invariants

Expand Down Expand Up @@ -699,7 +764,7 @@ inside their host-specific renderers, because each host renders cells differentl
The error tooltip itself is Esc-dismissible: `ErrorPopup` owns a window-level `keydown`
(capture) listener, registered while attached; it consumes Escape only while the tooltip is
visible AND no builder surface holds focus (`builderSurfaceHasFocus()` —
`document.activeElement` inside a `.hfe-editor`, `.hfe-fn-help`, or `.hfe-formula-bar` root;
the deep active element inside a `.hfe-editor`, `.hfe-fn-help`, or `.hfe-formula-bar` root;
the fn-help dialog's own Escape handler and the address box's Escape-revert must win the
same way the editor's ladder does), acted on in `attach()`'s
scope alongside its existing scroll listener — no adapter or
Expand Down Expand Up @@ -728,6 +793,11 @@ re-render clears that identity, so a re-render after Esc-dismiss can make the to
even though pointer/focus never logically left the cell. Accepted as a documented limitation;
re-keying by logical cell would need a host render hook — future work if it bites.

`markCell` does not write `aria-description`: the host owns the cell's accessible
description and its accessibility settings, so it adds the error message itself (the external
Handsontable plugin writes it from `data-hfe-error`, only under its `ariaTags` setting, and keeps
an application's own description). A package-side write would override both.

While a hovered/focused error cell surfaces its message via `ErrorPopup` and
`aria-invalid`/`aria-errormessage` (the popup root carries a generated id, associated onto the
cell for the screen reader to resolve on demand), committing a formula moves focus away before
Expand All @@ -747,8 +817,9 @@ contract this relies on.

While the editor is open, a mousedown on a grid cell either picks a reference or commits —
decided by `FormulaEditor.isRefSelectionActive()` (ref insertion not disabled via
`setRefActive(false)`, and caret after a ref opener or a pending ref at the caret — a
deactivated editor treats every cell click as a commit, never a dead pick). `FormulaEditor.enterRefSelectionMode()` is the single wiring point: it
`setRefActive(false)`, and caret after a ref opener, a pending ref at the caret, or the caret
inside a cell or range reference, which the pick then replaces — a deactivated editor treats
every cell click as a commit, never a dead pick). `FormulaEditor.enterRefSelectionMode()` is the single wiring point: it
calls `adapter.enterRefSelectionMode(resolveColor, isPickEligible)` with both callbacks
derived from the editor, and every entry path (`FormulaBuilder.openEditor`,
`FormulaBar.handleInlineEditStart`) goes through it. When the predicate is false,
Expand Down
Loading
Loading