diff --git a/hyperformula-ui/CHANGELOG.md b/hyperformula-ui/CHANGELOG.md index 6adc5310f..38f2558c1 100644 --- a/hyperformula-ui/CHANGELOG.md +++ b/hyperformula-ui/CHANGELOG.md @@ -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 `` 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 ``. [#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 diff --git a/hyperformula-ui/dev-docs/ARCHITECTURE.md b/hyperformula-ui/dev-docs/ARCHITECTURE.md index e7cd6337f..7042d4c84 100644 --- a/hyperformula-ui/dev-docs/ARCHITECTURE.md +++ b/hyperformula-ui/dev-docs/ARCHITECTURE.md @@ -60,6 +60,12 @@ 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 @@ -67,6 +73,12 @@ reparents between two locations via `FormulaEditor.attachTo(mount)`: 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; @@ -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 @@ -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 ``` @@ -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; className?: string })`. It receives the pre-built `FormulaEditor` from `FormulaBuilder` — it does not construct @@ -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 @@ -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 @@ -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 @@ -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, diff --git a/hyperformula-ui/dev-docs/KEYBOARD-SHORTCUTS.md b/hyperformula-ui/dev-docs/KEYBOARD-SHORTCUTS.md index 34d91a065..a481dca03 100644 --- a/hyperformula-ui/dev-docs/KEYBOARD-SHORTCUTS.md +++ b/hyperformula-ui/dev-docs/KEYBOARD-SHORTCUTS.md @@ -19,7 +19,8 @@ sync with `keymap.ts`, `KeyboardController`, and their tests (unit matrix in - **Ref insertion:** `RefController.tryInsertRef(refStr, force?, append?)` — `force` bypasses only the `canInsertRef()` value-position guard (used by explicit point mode); the mouse-pick path never forces. `append` (set by a Cmd/Ctrl-modified pick, delivered via - `CellPick`/`RangePick.append`) joins the new ref after the pending one with the locale + `CellPick`/`RangePick.append`) joins the new ref after the pending one, or after the + reference the caret sits in (`RefController.refTokenAtCaret()`), with the locale `argSeparator` instead of replacing it. - **Tests:** unit — `packages/core/tests/formulaEditor/keyboard-controller.test.ts` (the key matrix), `point-mode-controller.test.ts`, `point-mode-keys.test.ts`, `shortcut-manager.test.ts`, @@ -60,7 +61,8 @@ the formula). `rangeMode ∈ { auto, on, off }`, held in `KeyboardController`, d | Shift + Enter | Commit, move **up** | same | | Tab | Commit, move **right** | same | | Shift + Tab | Commit, move **left** | same | -| Ctrl/Cmd + Enter | Insert a newline in the formula | same | +| Ctrl/Cmd + Enter | Commit to every selected cell when the host supports it (`IGridAdapter.commitFill` or the `onCommitFill` option); otherwise insert a newline in the formula | same | +| Alt + Enter | Insert a newline in the formula | same | | Escape | Ladder first (see below); if neither popup is open, cancels the pending point ref and stays editing — a 2nd Escape cancels the edit | Ladder: close suggestions popup if open, else close fn-help popup if open (suppressing it for the edit session), else cancel the edit | | `(` | Auto-insert the matching `)`, caret inside | same | | `)` | Skip over an existing `)` (else insert) | same | @@ -132,10 +134,11 @@ resting on an error cell. No adapter involvement. | 13 | Ctrl/Cmd + E | Identical to F2 | | 14 | Enter / Tab / Shift+Tab commit | Commit value, move active cell down / right / left | | 15 | Shift+Enter commit | Commit value, move active cell up one cell | -| 16 | Ctrl/Cmd+Enter | Insert a newline instead of committing | -| 17 | Escape while pointing | First press removes the pending ref (stay editing); second cancels the edit | -| 18 | `(` typed in a formula | Auto-insert `)` and place the caret inside | -| 19 | `)` typed before an existing `)` | Skip over it instead of inserting a duplicate | +| 16 | Ctrl/Cmd+Enter | Commit to every selected cell when the host fills; else insert a newline | +| 17 | Alt+Enter | Insert a newline instead of committing | +| 18 | Escape while pointing | First press removes the pending ref (stay editing); second cancels the edit | +| 19 | `(` typed in a formula | Auto-insert `)` and place the caret inside | +| 20 | `)` typed before an existing `)` | Skip over it instead of inserting a duplicate | ## Host scope diff --git a/hyperformula-ui/dev-docs/SPEC.md b/hyperformula-ui/dev-docs/SPEC.md index 69996589e..e6d311545 100644 --- a/hyperformula-ui/dev-docs/SPEC.md +++ b/hyperformula-ui/dev-docs/SPEC.md @@ -137,9 +137,9 @@ single-editor host-switch model): [`ARCHITECTURE.md`](ARCHITECTURE.md). ## 3. Public API (`packages/core/src/index.ts`) -Types: `CellAddress`, `CellRange`, `GridSelection`, `CellPick`, `RangePick`, `CellPickListener`, `RangePickListener`, `OutsideCellClickListener`, `HeaderAxis`, `HeaderPick`, `HeaderPickListener`, `GridHeaderPickSurface`, `Direction`, `DomRangePickerOptions`, `EditStartIntent`, `EditStartKeyEvent`, `RangeDragTrackerOptions`, `HeaderDragTrackerOptions`, `EdgeAutoScrollerOptions`, `EditorGridAdapter`, `EphemeralHighlightSurface`, `GridEditLifecycle`, `GridGeometry`, `GridNavigation`, `GridPickSurface`, `GridSize`, `IGridAdapter`, `OutsideClickCommitMode`, `RangeHighlight`, `RangeRect`, `RefSelectionColorResolver`, `Unsubscribe`, `CellTextSource`, `EngineCellAddress`, `EngineValueChange`, `FunctionCatalogEntry`, `FunctionParam`, `FunctionSignature`, `HyperFormulaLike`, `AddressBoxTarget`, `AddressBoxRange`, `FormulaBarAddressBoxOptions`, `FormulaBarCellOptions`, `FormulaBarOptions`, `FormulaBuilderOptions`, `FormulaEditor` (type only), `FormulaEditorEvents`, `FormulaEditorOptions`, `EditorHost`, `HostToggleDelegate`, `LayoutDirection`, `StyleInjector`, `ErrorOptions`, `FunctionHelpOptions`, `HighlightLayerOptions`, `CellRect`, `CellRun`, `FrozenPredicate`, `HighlightLayerKind`, `IconName`, `IconsOptions`, `PopupsOptions`, `PopupHostOption`, `SuggestionItem`, `SuggestionsAriaState`, `SuggestionsOptions`, `SuggestionsHandlers`, plus the `events/` types (`Disposable`, `DomEventOptions`, `ScopePolicy`). +Types: `CellAddress`, `CellRange`, `GridSelection`, `CellPick`, `RangePick`, `CellPickListener`, `RangePickListener`, `OutsideCellClickListener`, `HeaderAxis`, `HeaderPick`, `HeaderPickListener`, `GridHeaderPickSurface`, `Direction`, `DomRangePickerOptions`, `EditStartIntent`, `EditStartKeyEvent`, `RangeDragTrackerOptions`, `HeaderDragTrackerOptions`, `EdgeAutoScrollerOptions`, `EditorGridAdapter`, `EphemeralHighlightSurface`, `GridEditLifecycle`, `GridGeometry`, `GridNavigation`, `GridPickSurface`, `GridSize`, `IGridAdapter`, `OutsideClickCommitMode`, `RangeHighlight`, `RangeRect`, `RefSelectionColorResolver`, `Unsubscribe`, `CellTextSource`, `EngineCellAddress`, `EngineValueChange`, `FunctionCatalogEntry`, `FunctionParam`, `FunctionSignature`, `HyperFormulaLike`, `AddressBoxTarget`, `AddressBoxRange`, `FormulaBarAddressBoxOptions`, `FormulaBarCellOptions`, `FormulaBarOptions`, `FormulaBuilderOptions`, `FormulaEditor` (type only), `FormulaEditorEvents`, `FormulaEditorOptions`, `EditorHost`, `HostToggleDelegate`, `LayoutDirection`, `StyleInjector`, `StyleRoot`, `Phrases`, `AddressBoxNavigation`, `ErrorOptions`, `FunctionHelpOptions`, `HighlightLayerOptions`, `CellRect`, `CellRun`, `FrozenPredicate`, `HighlightLayerKind`, `IconName`, `IconsOptions`, `PopupsOptions`, `PopupHostOption`, `SuggestionItem`, `SuggestionsAriaState`, `SuggestionsOptions`, `SuggestionsHandlers`, plus the `events/` types (`Disposable`, `DomEventOptions`, `ScopePolicy`). -Values: `FormulaBuilder`, `FormulaBar`, `FormulaBarAddressBox`, `FormulaBarCell`, `DomRangePicker`, `RangeDragTracker`, `HeaderDragTracker`, `EdgeAutoScroller`, `PickEmitter`, `NullAdapter`, `formatRef`, `formatWholeRef`, `formatCellValue`, `parseCellInput`, `normalizeRange`, `expandHeaderSpan`, `armTrailingClickSwallow`, `resolveEditStartKey`, `splitRangeByFrozen`, `stepCell`, `findDataEdge`, `unionRectRelativeTo`, `readCellText`, `indexToColLetters`, `CN`, `createStyleInjector`, `HighlightLayer`, `LiveRegion`, `EPHEMERAL_COLOR`, `ensureRelativePosition`, `cellErrorMessage`, `classifyCellLayer`, `fillColor`, `groupCellsIntoRuns`, `setCellError`, plus the `events/` classes (`EventManager`, `EventScope`, `ListenerSet`). +Values: `FormulaBuilder`, `FormulaBar`, `FormulaBarAddressBox`, `FormulaBarCell`, `DomRangePicker`, `RangeDragTracker`, `HeaderDragTracker`, `EdgeAutoScroller`, `PickEmitter`, `NullAdapter`, `formatRef`, `formatWholeRef`, `formatCellValue`, `parseCellInput`, `normalizeRange`, `expandHeaderSpan`, `armTrailingClickSwallow`, `resolveEditStartKey`, `splitRangeByFrozen`, `stepCell`, `findDataEdge`, `unionRectRelativeTo`, `readCellText`, `indexToColLetters`, `CN`, `createStyleInjector`, `DEFAULT_PHRASES`, `HighlightLayer`, `LiveRegion`, `EPHEMERAL_COLOR`, `ensureRelativePosition`, `cellErrorMessage`, `classifyCellLayer`, `fillColor`, `groupCellsIntoRuns`, `setCellError`, plus the `events/` classes (`EventManager`, `EventScope`, `ListenerSet`). Internal classes (`Kernel`, `Lexer`, `EngineBridge`, `RichInput`, `VisualOverlay`, popup classes, `PopupContext`) are exported from their subdirectory barrels for in-repo use but are **not** part of the top-level public surface. @@ -147,9 +147,9 @@ Internal classes (`Kernel`, `Lexer`, `EngineBridge`, `RichInput`, `VisualOverlay One `FormulaBuilder` per grid instance. It owns the `PopupContext` (shared popup portal), the `ErrorPopup`, the single `FormulaEditor`, and — when `formulaBarContainer` is given — a `FormulaBar`. -Constructed as `new FormulaBuilder(options: FormulaBuilderOptions)`; the options interface lives in `packages/core/src/formulaBuilder.ts`. Fields: `rootEl` (required — grid root, error-popup hover/focus host), `adapter` (defaults to `NullAdapter`), `hyperformula` (instance — functions, validation, named exprs), `className` (single theming entry point: portal, bar, editor, overlay), `formulaBarContainer` (wires the formula bar — element or CSS selector), `popups` (`PopupsOptions` — shared `showClose` default + per-popup overrides), `icons` (`IconsOptions` — host factories that replace the built-in glyphs: `close`, `externalLink`, `namedExpression`, `function`; forwarded to the editor's popups, the error popup and the bar's fx icon), `initialValue` / `language` / `sheetId` (forwarded to the editor), `direction` (`LayoutDirection` — `"ltr" | "rtl"`, default `"ltr"`; construction-only — layout mirrors, formula text stays LTR), `onCommit(value, direction?)` (default: `adapter.commit`), `onCancel()` (default: `adapter.cancel`). +Constructed as `new FormulaBuilder(options: FormulaBuilderOptions)`; the options interface lives in `packages/core/src/formulaBuilder.ts`. Fields: `rootEl` (required — grid root, error-popup hover/focus host), `adapter` (defaults to `NullAdapter`), `hyperformula` (instance — functions, validation, named exprs), `className` (single theming entry point: portal, bar, editor, overlay), `formulaBarContainer` (wires the formula bar — element or CSS selector), `popups` (`PopupsOptions` — shared `showClose` default + per-popup overrides), `icons` (`IconsOptions` — host factories that replace the built-in glyphs: `close`, `externalLink`, `namedExpression`, `function`; forwarded to the editor's popups, the error popup and the bar's fx icon), `initialValue` / `language` / `sheetId` (forwarded to the editor), `direction` (`LayoutDirection` — `"ltr" | "rtl"`, default `"ltr"`; construction-only — layout mirrors, formula text stays LTR), `onCommit(value, direction?)` (default: `adapter.commit`), `onCommitFill(value)` (default: `adapter.commitFill` when present), `onCancel()` (default: `adapter.cancel`), `phrases` (`Partial` — translations of every accessible name, hint and screen-reader announcement, merged over `DEFAULT_PHRASES`; also a `FormulaEditor` option, read back through `FormulaEditor.phrases`), `rootDocument` (the grid's document, e.g. an iframe's — selector lookup, styles and the detached mount; default `portalParent`'s document, else `document`), `portalParent` (`HTMLElement | ShadowRoot` — parent of the popup portal and the live region, and a shadow root that receives the styles; default `rootDocument.body`). -Methods: `openEditor(mount, seed)`, `closeEditor()`, `markCell(cellEl, value)`, `attachFormulaBar(container)`, `detachFormulaBar()`, `attachSuggestionsHost(container: HTMLElement | string)`, `detachSuggestionsHost()`, `attachFnHelpHost(container: HTMLElement | string)`, `detachFnHelpHost()`, `destroy()`. Getters: `editor` (the `FormulaEditor`), `hasFormulaBar`. The `formulaBarContainer` option is sugar for `attachFormulaBar`; detaching restores the barless inline-edit subscription. `popups.suggestions.container` / `popups.fnHelp.container` (`PopupHostOption`, `HTMLElement | string`) are constructor sugar mirroring `formulaBarContainer`: stripped from the popup options and delegated to `attachSuggestionsHost`/`attachFnHelpHost` after editor construction. A selector that resolves to nothing throws `FormulaBuilder: host "…" not found`; all four attach/detach methods are no-ops after `destroy()`. +Methods: `openEditor(mount, seed)`, `closeEditor()`, `markCell(cellEl, value)`, `attachFormulaBar(container)`, `detachFormulaBar()`, `attachSuggestionsHost(container: HTMLElement | string)`, `detachSuggestionsHost()`, `attachFnHelpHost(container: HTMLElement | string)`, `detachFnHelpHost()`, `destroy()`. Getters: `editor` (the `FormulaEditor`), `hasFormulaBar`. The `formulaBarContainer` option is sugar for `attachFormulaBar`; detaching restores the barless inline-edit subscription. `popups.suggestions.container` / `popups.fnHelp.container` (`PopupHostOption`, `HTMLElement | string`) are constructor sugar mirroring `formulaBarContainer`: stripped from the popup options and delegated to `attachSuggestionsHost`/`attachFnHelpHost` after editor construction. A selector that resolves to nothing makes the `attach*` methods throw `FormulaBuilder: host "…" not found`; in the constructor options it logs that message once as a warning and falls back to the default placement; all four attach/detach methods are no-ops after `destroy()`. `PopupsOptions`: shared `showClose` default plus per-popup overrides — `suggestions` (`showClose`, `showKeyboardHelp`, `showNamedExpressions` — all default `true`, plus `container?: HTMLElement | string` to dock it into a host element instead of floating), `fnHelp` (`showClose`, plus `container?: HTMLElement | string`), `error` (`showClose`, default `false`). @@ -170,7 +170,7 @@ There is exactly **one** `FormulaEditor` per grid; it reparents between the inli ## 4. Layered Text Editing — DOM & CSS -DOM structure (built by `packages/core/src/ui/text/richInput.ts` + `ui/text/visualOverlay.ts`; class names from `theme/classNames.ts`): a root `div.hfe-editor` wraps `div.hfe-editor__layers`, which stacks `div.hfe-overlay` (`aria-hidden` — token spans + ref marks) beneath a `textarea.hfe-editor__input` (`spellcheck=false`). Popups render into the shared `PopupContext` portal appended to `document.body`. +DOM structure (built by `packages/core/src/ui/text/richInput.ts` + `ui/text/visualOverlay.ts`; class names from `theme/classNames.ts`): a root `div.hfe-editor` wraps `div.hfe-editor__layers`, which stacks `div.hfe-overlay` (`aria-hidden` — token spans + ref marks) beneath a `textarea.hfe-editor__input` (`spellcheck=false`). Popups render into the shared `PopupContext` portal appended to `FormulaBuilder`'s `portalParent` (default `document.body`). **Pixel-alignment-critical props** (must match on both layers): `font-family`, `font-size`, `font-weight`, `letter-spacing`, `line-height` (unitless), `padding`, `tab-size`, `white-space: pre-wrap`, `word-break`, `box-sizing: border-box`. Textarea: `position:absolute; inset:0; background:transparent; color:transparent; caret-color: var(--hfe-color-caret); resize:none; border:0`. Overlay: `position:absolute; inset:0; pointer-events:none`. Default font: `ui-monospace, SFMono-Regular, Menlo, Consolas, monospace`; `font-variant-ligatures: none`. @@ -216,7 +216,9 @@ Keybindings are declarative: `formulaEditor/keymap.ts` (`BINDINGS`, first match - **Auto-close parens**: typing `(` while editing a formula inserts `()` and places the caret between. If a selection exists, it wraps the selection. - **Smart `)`**: typing `)` when one already sits at the caret moves the caret past it instead of inserting. -- **Newline**: `Cmd/Ctrl+Enter` inserts `\n`. +- **Newline**: `Alt+Enter` inserts `\n`; so does `Cmd/Ctrl+Enter` when the host has no fill commit. +- **Fill commit**: `Cmd/Ctrl+Enter` commits the value to every selected cell through `onCommitFill` or `IGridAdapter.commitFill`, when either is provided. +- **Popups over frozen panes**: the suggestions and function-help popups anchor to the editor and paint over a grid's frozen rows and columns, as autocomplete does in Excel and Google Sheets. This is the decided behaviour, not a gap: clipping them to the scrollable pane would hide them for an edit near a frozen edge, and flipping them away from the panes would move them off the caret. - **Directional commit**: plain `Enter` commits down, `Shift+Enter` up, `Tab` right, `Shift+Tab` left — after `closeUnbalancedParens` tops off missing `)`. The direction reaches the host via `onCommit(value, direction?)` → `adapter.commit(value, direction?)`; the host owns the selection move. - **Escape**: a ladder — first closes the suggestions popup if open (`closeSuggestions`), else closes the function-help popup if open (`closeFnHelp`, suppressing it for the rest of the edit session), else falls to `cancel`: while pointing, a first `cancel` press cancels the pending point ref; a second (or plain) Escape cancels the edit via `onCancel`. Each press eats one layer (`keymap.ts` `BINDINGS`, first match wins). - **Point mode (keyboard range selection)**: at a value position, arrows move a grid marquee one visible cell (`adapter.stepCell`) and insert/replace the ref at the caret; `Shift+Arrow` extends the range; `Ctrl/Cmd+Arrow` jumps to the data-block edge (`adapter.getDataEdge`); `Ctrl/Cmd+Shift+Arrow` extends the range to the data-block edge (anchor kept). Both map visual↔engine indices in the adapter, so a step lands on the visually-adjacent cell even when rows/cols are trimmed or reordered. Each step draws an ephemeral preview highlight (`adapter.highlightRange`, cleared on exit/cancel) and scrolls the cursor cell into view. `F2` / `Ctrl/Cmd+E` toggles pointing on/off — toggling arms point state without inserting (the first arrow inserts); at a non-value position it force-inserts a ref. State machine: `pointModeController.ts`. @@ -224,9 +226,9 @@ Keybindings are declarative: `formulaEditor/keymap.ts` (`BINDINGS`, first match - **Popup**: function name suggestions for documented built-in functions (with short descriptions) plus matching named expressions; `↑/↓` moves, `Enter`/`Tab` selects (a function inserts `NAME()` with the caret inside; a named expression inserts the bare name), `Esc` closes. Items order prefix-matches before substring-matches and highlight the matched prefix; an optional keyboard-help footer is gated by `SuggestionsOptions.showKeyboardHelp`. Popups position via viewport-anchored strategies (flip/clamp + max-height) and hide when the anchor scrolls out of the clip rect; `PopupController` owns the ARIA wiring (`role="listbox"`/`option` + `aria-activedescendant` from `SuggestionsAriaState`; fn-help is a non-modal `role="dialog"` labeled `" function help"` + `aria-describedby`; error stays `role="tooltip"` + `aria-describedby`). - **Function help**: when the caret sits inside a known call's argument list, `FunctionHelpPopup` shows the signature with the active argument highlighted. `F6`/`Shift+F6` move focus into the popup via the reusable `FocusTrap` primitive (`ui/focusTrap.ts`; first focusable element in DOM order via a generic `button, a[href], input, …` selector — normally the scrollable body, which carries `tabindex="0"` whenever it has content; falls through to the editor-host toggle below only when a content-free body leaves nothing focusable); the popup root advertises the exit via `aria-keyshortcuts="F6"`, and its keydown listener stops propagation of every key while the popup holds focus (host JS shortcuts stay quiet; a popup docked via `attachToHost` does not trap Tab — native tab order moves focus out); inside it, `Tab`/`Shift+Tab` wrap through the focusables (trap `exitKeys` is `["F6"]` only — key-name matching ignores shift, so Shift+F6 exits too), and `F6`/`Shift+F6` return focus to the editor without closing the popup, while `Escape` inside the popup closes it (same effect as the close button) and returns focus to the editor. From the editor, plain `Escape` closes fn-help via the ladder above instead of returning focus. Shift+F6 binds to dedicated `focusFnHelpBack`/`toggleEditorHostBack` commands that delegate to the forward `focusFnHelp`/`toggleEditorHost` logic (`ShortcutManager` itself stays no-arg; the reverse commands carry the direction). Closing the popup while it holds focus (e.g. `Enter` on the close button, or Escape) also returns focus to the editor. When the popup is closed, `F6`/`Shift+F6` instead toggle the editor host inline cell ↔ formula bar (active bar edit session only) via `FormulaEditor.setHostToggleDelegate` — popup mode always wins, so the host never switches while the popup is open. See [`KEYBOARD-SHORTCUTS.md`](KEYBOARD-SHORTCUTS.md) for the full matrix. - **Error tooltip**: shown on hover/focus of an error cell while the grid (not the editor) owns focus. `Escape`, captured by a window-level keydown listener owned by `ErrorPopup`, dismisses it and suppresses that cell's tooltip until both pointer and focus leave the cell — required for WCAG 1.4.13 (content shown on hover/focus must be dismissible via Escape without moving pointer or focus). The listener stands down while focus is inside an editor, fn-help, or formula-bar root, so the focused surface's own Escape handling (including the address box's Escape-revert) always wins over a hover-raised tooltip; suppression survives unrelated focus movement while the pointer stays on the cell, and re-entering the suppressed cell while the tooltip shows another cell's error closes the stale tooltip. No adapter or `IGridAdapter` involvement; self-contained in `ErrorPopup`. -- **Ref insertion**: a single grid pick replaces the previously inserted ref if the caret hasn't moved past it (`lastRef` tracking) — picking again "edits" the ref instead of appending another. -- **Mod+click multi-select**: holding Cmd/Ctrl while picking (click or drag) appends the new range as a comma-joined ref after the previously-picked one instead of replacing it, as long as that previous ref hasn't been edited away and the caret still sits at its end (the same pending-ref condition that gates a plain replace). A mod+click with no prior ref, or after the prior ref was edited away or the caret moved off it, falls back to a plain fresh insert (same `canInsertRef` gating as a normal click). `setValue` resets the tracked ref, so a re-seeded editor never appends after a ref from a previous edit session. Picks travel as `CellPick`/`RangePick` (`{ ref, append }`) through `CellPickListener`/`RangePickListener`. -- **canInsertRef gate**: refs only insert if the text starts with `=` and the caret follows whitespace + an opener. Openers are `= + - * / ^ & < > ( :` plus the active locale's `argSeparator` and `arrayRowSeparator` (`refOpeners`), so no orphan refs glue to identifiers. +- **Ref insertion**: a single grid pick replaces the previously inserted ref if the caret hasn't moved past it (`lastRef` tracking) — picking again "edits" the ref instead of appending another. With no pending ref, a pick replaces the cell or range reference the caret sits in (`refTokenAtCaret()`: a `CELL_REF`/`RANGE_REF` token with `start <= caret <= end`, the same token F4 cycles), so a caret placed anywhere in `C1:C2`, after its colon included, swaps the whole range for the pick instead of splicing the pick into the token or committing the formula. +- **Mod+click multi-select**: holding Cmd/Ctrl while picking (click or drag) appends the new range as a comma-joined ref after the previously-picked one instead of replacing it, as long as that previous ref hasn't been edited away and the caret still sits at its end (the same pending-ref condition that gates a plain replace). A mod+click with no prior ref, or after the prior ref was edited away or the caret moved off it, appends after the reference the caret sits in when there is one, and otherwise falls back to a plain fresh insert (same `canInsertRef` gating as a normal click). `setValue` resets the tracked ref, so a re-seeded editor never appends after a ref from a previous edit session. Picks travel as `CellPick`/`RangePick` (`{ ref, append }`) through `CellPickListener`/`RangePickListener`. +- **canInsertRef gate**: a fresh ref only inserts if the text starts with `=` and the caret follows whitespace + an opener (the replace paths above run first). Openers are `= + - * / ^ & < > ( :` plus the active locale's `argSeparator` and `arrayRowSeparator` (`refOpeners`), so no orphan refs glue to identifiers. - **Header click/drag whole-ref insertion**: clicking or dragging a grid row/column header while ref-eligible inserts a whole-row/whole-column ref (`A:A`, `3:3`) via the optional `GridHeaderPickSurface.onHeaderPick` (§8); implemented by the Handsontable plugin and the four example adapters maintained outside this repository — plain-table and TanStack cover both row and column axes, AG Grid and MUI are column-axis only (their hosts don't expose row headers as a pickable surface). ### 7a. Formula bar @@ -247,7 +249,7 @@ Role interfaces and their members: |---|---| | `GridPickSurface` | `outsideClickCommitMode` (readonly), `getCellAddressAt(x, y)`, `formatRef(ref)`, `setHighlights(items)`, `highlightRange(range, color)`, `clearEphemeralHighlight()`, `clearHighlights()`, `setOverlayClassName(className)`, `onCellPick(callback)`, `onRangePick(callback)`, `onOutsideCellClick(callback)`, `enterRefSelectionMode(resolveColor?, isPickEligible?)`, `exitRefSelectionMode()` | | `GridNavigation` | `getGridSize()`, `getDataEdge(from, direction)`, `stepCell(from, direction)`, `scrollCellIntoView(address)` | -| `GridEditLifecycle` | `getActiveCell()`, `onSelectionChange(callback)`, `getCellFormula(address)`, `beginEdit(address, seed?)`, `selectFirstCell()`, `setActiveCell(address)`, `setSelection(range)`, `onInlineEditStart(callback)`, `onInlineEditClose(callback)`, `onSwitchToInline(callback)`, `commit(value, direction?)` (must write to the engine synchronously before returning — commit-error announcements read the engine and capture `valuesUpdated` inside the commit call; a deferred write degrades announcements to silence, never a crash), `cancel()`, `setInlineEditorVisible(visible)` | +| `GridEditLifecycle` | `getActiveCell()`, `onSelectionChange(callback)`, `getCellFormula(address)`, `beginEdit(address, seed?)`, `selectFirstCell()`, `setActiveCell(address)` and `setSelection(range)` (return `false` when the host cannot select the target, so the address box rejects it; `void` counts as success), `onInlineEditStart(callback)`, `onInlineEditClose(callback)`, `onSwitchToInline(callback)`, `commit(value, direction?)` (must write to the engine synchronously before returning — commit-error announcements read the engine and capture `valuesUpdated` inside the commit call; a deferred write degrades announcements to silence, never a crash), optional `commitFill(value)` (Ctrl/Cmd+Enter: write `value` into every selected cell, synchronously like `commit`; when absent Ctrl/Cmd+Enter inserts a newline, and Alt+Enter always does), `cancel()`, `setInlineEditorVisible(visible)` | | `GridHeaderPickSurface` (optional) | `onHeaderPick(callback)` — emits `HeaderPick` on row/column header click/drag; mixed into `IGridAdapter`/`EditorGridAdapter` as `Partial`, so adapters without headers implement nothing (no `NullAdapter` no-op) | | `IGridAdapter` | extends the three required role interfaces plus `Partial`; adds `destroy()` | | `GridGeometry` | adapter-side geometry contract consumed by `HighlightLayer`: `getOverlayHost()`, `getRangeRects(range)`, `getViewportClip?()`, `onViewportChange(callback)`, `getColHeaderEl?(col)`, `getRowHeaderEl?(row)` | @@ -273,9 +275,10 @@ No adapter ships in this repository; see §1. An adapter implements `IGridAdapte ## 10. HyperFormula Integration - **Optional** peer dep. `EngineBridge` accepts the HF instance via `hyperformula`. Degrades to a no-op when no instance is supplied. -- Calls used (all optional, duck-typed, feature-detected): instance `getAvailableFunctions()` / `getFunctionDetails(canonicalName)` (HF instance-metadata API, 3.4.0+) with `getRegisteredFunctionNames()` / `getFunctionPlugin(functionId)` fallbacks (public instance methods, available in HF 3.3.0+) — no language argument; HF translates names via the instance's configured language (§5); `getConfig()` for language + separators; `simpleCellAddressFromString` / `simpleCellRangeFromString`; `getSheetId(name)` / `getSheetName(id)`; `getCellValue(address)`; `listNamedExpressions(scope?)`, `getNamedExpressionFormula(name, scope?)`, and `getSheetDimensions(sheetId)` for named-expression resolution (§5a); `on` / `off` for `valuesUpdated` and named-expression add/remove events (cache invalidation + commit-error capture). +- Calls used (all optional, duck-typed, feature-detected): instance `getAvailableFunctions()` / `getFunctionDetails(canonicalName)` (HF instance-metadata API, 3.4.0+) with `getRegisteredFunctionNames()` / `getFunctionPlugin(functionId)` fallbacks (public instance methods, available in HF 3.3.0+) — no language argument; HF translates names via the instance's configured language (§5); `getConfig()` for language + separators; `simpleCellAddressFromString` / `simpleCellRangeFromString`; `getSheetId(name)` / `getSheetName(id)`; `getCellValue(address)`; `listNamedExpressions(scope?)`, `getNamedExpressionFormula(name, scope?)`, and `getSheetDimensions(sheetId)` for named-expression resolution (§5a); `on` / `off` for `valuesUpdated`, `sheetRemoved` and named-expression add/remove events (cache invalidation + commit-error capture). HyperFormula emits the named-expression events only from `addNamedExpression`/`removeNamedExpression`, not from `undo`/`redo`, `changeNamedExpression` or `removeSheet`, so the named-expression cache is also dropped on `valuesUpdated` and `sheetRemoved`, and on every `FormulaEditor.setValue` (each edit start and selection change): a cached resolution never outlives the text it colors. - Against HyperFormula 3.0.0–3.3.0 (no instance-metadata API): the editor degrades to a names-only catalog (`getRegisteredFunctionNames()` fallback) and plugin-shaped signatures (`getFunctionPlugin(functionId)` fallback). -- The caller owns the HF instance; the editor never destroys it. +- The editor's `sheetId` may name no sheet (a host mistake, or a sheet removed while the editor is open). HyperFormula throws `NoSheetWithIdError` from `listNamedExpressions(sheetId)` and `getSheetDimensions(sheetId)` then, so the engine bridge treats such a sheet as having no local names and no dimensions: global names still color, whole-column and whole-row ranges just do not expand. +- The caller owns the HF instance; the editor never destroys it. The caller may destroy it before the builder: unsubscribing from a destroyed instance throws, so the engine bridge swallows that error, and `FormulaBuilder.destroy()` and `FormulaEditor.destroy()` run every teardown step even when one throws (an adapter whose grid is already gone, for example), rethrowing the first error at the end (`events/runAll.ts`), so no popup portal, live region or injected stylesheet is left behind. - The engine type in the public surface is duck-typed (`HyperFormulaLike` in `engine/types.ts`), not imported from `hyperformula` — keeps the editor decoupled from the engine version. - `EngineBridge` is the only HF access point: raw `HyperFormulaLike` is never touched outside `src/engine/`. @@ -291,8 +294,9 @@ No adapter ships in this repository; see §1. An adapter implements `IGridAdapte `createStyleInjector(marker, cssText)` for their own generated stylesheets. - BEM blocks: `hfe-editor`, `hfe-overlay`, `hfe-ref-mark`, `hfe-popup`, `hfe-popup-portal`, `hfe-suggestions`, `hfe-fn-help`, `hfe-highlight`, `hfe-token`, `hfe-error`, `hfe-cell--error`, `hfe-formula-bar` (`__cell`, `__cell--address`, `__cell--formula`, `__address-input`, `__fx-icon`, `__body`, `__idle`, `__overlay`). Decoration modifiers: `hfe-token--paren-match`, `hfe-token--paren-err`, `hfe-token--dim`, `hfe-token--arg-active`; error/status modifiers `hfe-token--{comment-err,fn-unknown,ref-invalid}`; per-token-type classes via `TOKEN_CN`. Full element/modifier inventory: `theme/classNames.ts`. - Every class name string lives in `theme/classNames.ts` (`CN`, `TOKEN_CN`) — no string literals scattered in components. -- `color-scheme` is set on the editor root in `tokens.scss`: `light` under `[data-theme="light"]`, `dark` under `[data-theme="dark"]`. All themed colors via `light-dark(lightVal, darkVal)`; auto when no `data-theme` override. -- Variable namespace `--hfe-*`. Ref colors from the fixed `REF_PALETTE` (`theme/refPalette.ts`). Formula-bar tokens: the color/border ones (`--hfe-formula-bar-{background,foreground,border,border-width,border-radius}`) alias global tokens — no bar-specific colors; the sizing ones (`--hfe-formula-bar-{padding-vertical,padding-horizontal,gap-address,gap-formula,address-width,scrollbar-width}`, `--hfe-z-index-formula-bar`) carry literal defaults. The vertical padding sits on the bar's overlay and textarea, not on the formula cell, and the cell has no end padding. +- The stylesheet sets no `color-scheme`. Every package element inherits the host's: from ``, or from the host theme class, which the `className` option puts on every root (editor, formula bar, popup portal, hosted popups, the adapter's highlight layer). A page that sets none renders `light-dark()` colors light, like the page itself; a page that follows the operating system sets `color-scheme: light dark` on ``. Setting it on package elements would override the host: `light dark` on a root follows the operating system even when `` is forced to `dark`. Never select package elements through a class-attribute substring selector such as `[class*="hfe-"]`: the browser tests it against every element of the page on each style recalc. +- Variable namespace `--hfe-*`. Ref colors from the fixed `REF_PALETTE` (`theme/refPalette.ts`), which points at `--hfe-ref-1..8`. Those eight properties are a public override surface, documented in the package README; every default clears 4.5:1 against `--hfe-color-background` in both schemes (WCAG 1.4.3 for reference text, which also covers 1.4.11's 3:1 for highlight borders), and every pair stays at least 10 apart in OKLab (ΔE × 100) within each scheme, so no two references read as the same color; `tests/theme/stylesheet.test.ts` checks both. The contrast holds at full opacity only: `--hfe-dim-opacity` dims the references of the enclosing calls (`hfe-token--dim`, §6a) below it. Formula-bar tokens: the color/border ones (`--hfe-formula-bar-{background,foreground,border,border-width,border-radius}`) alias global tokens — no bar-specific colors; the sizing ones (`--hfe-formula-bar-{padding-vertical,padding-horizontal,gap-address,gap-formula,address-width,scrollbar-width}`, `--hfe-z-index-formula-bar`) carry literal defaults. The vertical padding sits on the bar's overlay and textarea, not on the formula cell, and the cell has no end padding. +- Fonts: `--hfe-font-family` (default `inherit`, the host's font) for UI text, `--hfe-formula-font-family` (monospace) for formula text, and `--hfe-error-font-family`, read by both `hfe-cell--error` and `hfe-error__title` as `var(--hfe-error-font-family, var(--hfe-font-family))`, so an error value in a cell and the error popup title always render in the same font. The fallback sits at the use site on purpose: a custom property that refers to another one resolves where it is declared, so declaring `--hfe-error-font-family: var(--hfe-font-family)` on `:root` would freeze the root value and ignore a host's `--hfe-font-family` on its theme class. - Scrollbars: `--hfe-scrollbar-width` (default `thin`) sets `scrollbar-width` on a height-capped editor and the two popup lists; the bar-hosted editor takes `--hfe-formula-bar-scrollbar-width` (default `auto`) instead, because a thin bar in a one-line scroll port is hard to grab. - Editor sizing is host-configurable via `--hfe-editor-min-lines` / `--hfe-editor-max-lines` (line-unit height clamps, read by `EditorSizeController`), `--hfe-editor-resize` (`none`/`vertical`/`horizontal`/`both` for the native resize grip), `--hfe-editor-max-width` (px width cap, applied with or without a clipping ancestor), and `--hfe-editor-min-width` (width floor for the resize grip; defaults to `100%` of the mount). Set them on the editor root or an ancestor. - `FormulaBuilder`'s `className` option is the single theming entry point — it lands on the popup portal, formula bar container, editor root, and (via `adapter.setOverlayClassName`) the adapter's highlight overlays. diff --git a/hyperformula-ui/packages/core/DOCS.md b/hyperformula-ui/packages/core/DOCS.md index 471fdaf3c..aa09d7606 100644 --- a/hyperformula-ui/packages/core/DOCS.md +++ b/hyperformula-ui/packages/core/DOCS.md @@ -78,6 +78,7 @@ const builder = new FormulaBuilder({ rootEl: gridRoot, adapter, formulaBarContai | Export | Kind | Summary | |---|---|---| +| `AddressBoxNavigation` | type | Outcome of an address-box navigation: `true` accepted, `false` not a target in the grid, `"unavailable"` a valid target the grid could not select (a hidden or filtered-out cell). | | `AddressBoxRange` | interface | Normalized coordinates a typed `A1:B5` range resolves to. | | `AddressBoxTarget` | interface | Sheet-less cell coordinates a typed `A1` address resolves to. | | `FormulaBar` ★ | class | Excel-style formula bar composing `FormulaBarAddressBox` and `FormulaBarCell`. | @@ -191,15 +192,17 @@ const builder = new FormulaBuilder({ rootEl: gridRoot, adapter, formulaBarContai | Export | Kind | Summary | |---|---|---| | `CN` | const | The `hfe-` BEM class-name table shared by core, adapters, and consumer CSS overrides. | -| `createStyleInjector` | function | Creates a ref-counted, SSR-guarded injector that maintains one `