Skip to content

feat(react): add toolbarAlignment prop for formatting-toolbar alignment#1001

Open
aldrinjenson wants to merge 2 commits into
eigenpal:mainfrom
aldrinjenson:feat/toolbar-alignment
Open

feat(react): add toolbarAlignment prop for formatting-toolbar alignment#1001
aldrinjenson wants to merge 2 commits into
eigenpal:mainfrom
aldrinjenson:feat/toolbar-alignment

Conversation

@aldrinjenson

@aldrinjenson aldrinjenson commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a toolbarAlignment prop to <DocxEditor> ('start' | 'center' | 'end', default 'start') that controls the horizontal alignment of the formatting toolbar's contents. The same prop is added to the underlying ToolbarProps, so the standalone <Toolbar> / <EditorToolbar> honor it too.

Previously the formatting bar was always left-aligned with no consumer control, forcing brittle CSS overrides on [data-testid="formatting-bar"]. Closes #1000.

Behavior

  • Default 'start' — no-op, preserves today's left-aligned layout. Existing callers are unaffected.
  • 'center' / 'end' — applied to the formatting bar's justify-content via a small class map. They use safe alignment (justify-content: safe center / safe end, expressed as a Tailwind arbitrary property) so that when the bar's overflow-x-auto content is wider than the viewport, it falls back to start alignment instead of clipping the leading controls (undo/redo) out of scroll reach.
  • Ignored in inline mode — that mode renders display:contents and has no flex container of its own.

Wiring

DocxEditorProps.toolbarAlignmentDocxEditorToolbar<EditorToolbar toolbarAlignment> (context) → Toolbar → formatting-bar justify-content.

Tests

  • New toolbarAlignment.test.tsx: default omits any justify-content override; center / end apply the safe-alignment classes.
  • Verified the arbitrary classes (justify-content:safe center / safe end) are emitted into the built packages/react/dist/styles.css.

Gates

  • typecheck (all 6 packages), full bun test, api:check, and check:parity (all 5 sub-checks) pass.
  • API snapshots regenerated (docs/api/docx-editor-react/{index,ui}.api.md).
  • Parity: registered as a React-only staged divergence in both scripts/parity/parity.contract.json (deferredInVue) and scripts/check-editor-contract.mjs (REACT_PROPS_NOT_YET_IN_VUE).
  • Changeset added (minor, @eigenpal/docx-editor-react).
  • eslint.config.js: the DocxEditor.tsx max-lines ceiling was bumped modestly (2000 → 2010) to fit the additive prop, matching the existing "bumped modestly to accommodate new fields" pattern used elsewhere in the config (the file already sits exactly at its prior cap).

Notes

The Vue adapter is intentionally untouched; this lands as a documented React-only divergence pending a matching Vue toolbar option.


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.

The formatting toolbar was always left-aligned with no consumer control.
Add a toolbarAlignment prop ('start' | 'center' | 'end', default 'start')
to <DocxEditor>, threaded through to the formatting bar's justify-content.
The same prop is added to ToolbarProps so the standalone Toolbar/EditorToolbar
honor it too. 'center'/'end' use safe alignment so the leading controls stay
scroll-reachable when the bar overflows. Ignored in inline mode.

Closes eigenpal#1000
@vercel

vercel Bot commented Jun 23, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
docx-editor Ready Ready Preview, Comment Jun 23, 2026 7:29pm

Request Review

@eigenpal-release-pal

Copy link
Copy Markdown
Contributor

All contributors have signed the CLA ✍️ ✅

Posted by the CLA bot.

@greptile-apps

greptile-apps Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Adds a toolbarAlignment prop ('start' | 'center' | 'end', default 'start') to <DocxEditor>, <Toolbar>, and the internal DocxEditorToolbar, letting consumers control the horizontal alignment of the formatting bar without brittle CSS overrides. The default preserves existing left-aligned layout so no callers are affected.

  • Prop wiring: threaded from DocxEditorPropsDocxEditorToolbar (required, no default) → Toolbar (default 'start'), which maps each value to a Tailwind arbitrary justify-content class; 'start' maps to an empty string (no class added) and is correctly ignored by cn().
  • Safe alignment: center and end use justify-content: safe center/end to prevent leading controls being clipped on overflow; note the safe keyword is not implemented in Safari, so the prop silently has no effect on that engine.
  • Parity: documented as a React-only divergence in both parity.contract.json and check-editor-contract.mjs; changeset, API snapshots, and ESLint line-cap bump included.

Confidence Score: 4/5

Safe to merge; the change is purely additive and the default 'start' value is a no-op for all existing callers.

The prop wiring and class-map logic are correct end-to-end, existing callers are unaffected, and the parity/API/changeset bookkeeping is thorough. The two minor points worth knowing before shipping: the safe alignment keyword is unimplemented in Safari so center/end will silently have no effect on that browser, and the new test file places one import after callable statements (unusual but harmless at runtime).

No files require special attention; Toolbar.tsx and toolbarAlignment.test.tsx have the minor notes above but neither blocks the merge.

Important Files Changed

Filename Overview
packages/react/src/components/Toolbar.tsx Adds toolbarAlignment prop and FORMATTING_BAR_ALIGNMENT_CLASS map; class applied via cn() only on non-inline bar. Logic is correct; safe alignment keyword has no effect in Safari, causing silent degradation for center/end on that browser.
packages/react/src/components/DocxEditor.tsx Adds toolbarAlignment prop with default 'start' and threads it down to DocxEditorToolbar. Interface ordering, JSDoc, and API snapshot are all consistent.
packages/react/src/components/DocxEditor/DocxEditorToolbar.tsx Wires the new required toolbarAlignment prop through destructuring and passes it to <EditorToolbar>. Clean pass-through with no logic concerns.
packages/react/src/components/DocxEditor/toolbarAlignment.test.tsx New unit tests cover all three alignment values via class-name assertions. A static import appears after beforeAll/afterAll calls, which works at runtime (ESM hoisting) but is an unusual placement that may trigger linting rules.
scripts/check-editor-contract.mjs Registers toolbarAlignment in the React-only prop exclusion set, correctly documenting the Vue parity gap.
scripts/parity/parity.contract.json Adds toolbarAlignment to deferredInVue with a clear rationale string; matches the convention used by other deferred props.
eslint.config.js Bumps DocxEditor.tsx max-lines cap from 2000 to 2010 to accommodate the additive prop; follows the existing incremental-bump pattern documented in the comment above the rule.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["DocxEditorProps\ntoolbarAlignment?: 'start'|'center'|'end'\n(default: 'start')"] --> B["DocxEditor\ndestructure + default 'start'"]
    B --> C["DocxEditorToolbar\ntoolbarAlignment: required"]
    C --> D["Toolbar\n(toolbarAlignment = 'start')"]
    D --> E{toolbarAlignment}
    E -->|start| F["FORMATTING_BAR_ALIGNMENT_CLASS\nstart → '' (no class)"]
    E -->|center| G["FORMATTING_BAR_ALIGNMENT_CLASS\ncenter → '[justify-content:safe_center]'"]
    E -->|end| H["FORMATTING_BAR_ALIGNMENT_CLASS\nend → '[justify-content:safe_end]'"]
    F --> I["cn() applied to formatting-bar div\n(inline mode skipped)"]
    G --> I
    H --> I
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A["DocxEditorProps\ntoolbarAlignment?: 'start'|'center'|'end'\n(default: 'start')"] --> B["DocxEditor\ndestructure + default 'start'"]
    B --> C["DocxEditorToolbar\ntoolbarAlignment: required"]
    C --> D["Toolbar\n(toolbarAlignment = 'start')"]
    D --> E{toolbarAlignment}
    E -->|start| F["FORMATTING_BAR_ALIGNMENT_CLASS\nstart → '' (no class)"]
    E -->|center| G["FORMATTING_BAR_ALIGNMENT_CLASS\ncenter → '[justify-content:safe_center]'"]
    E -->|end| H["FORMATTING_BAR_ALIGNMENT_CLASS\nend → '[justify-content:safe_end]'"]
    F --> I["cn() applied to formatting-bar div\n(inline mode skipped)"]
    G --> I
    H --> I
Loading

Reviews (1): Last reviewed commit: "feat(react): add toolbarAlignment prop f..." | Re-trigger Greptile

Comment on lines +7 to +9
beforeAll(() => GlobalRegistrator.register());
afterAll(() => GlobalRegistrator.unregister());
import { afterEach, describe, expect, test } from 'bun:test';

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.

P2 Import placed after executable statements

import { afterEach, describe, expect, test } on line 9 appears after the two beforeAll/afterAll call expressions on lines 7–8. In ESM all static import declarations are hoisted before any statement evaluates, so this works at runtime, but TypeScript's no-restricted-syntax/ESLint import/first rules will flag it, and it misleads readers into thinking the import order affects when @testing-library/react loads (it doesn't — happy-dom is registered in the beforeAll callback, not at module-evaluation time). Moving the import to the top alongside the other two imports removes the confusion without changing behaviour.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This harness (happy-dom register/unregister between the two bun:test imports) is copied verbatim from the sibling helpMenu.test.tsx and fileOpen.test.tsx in this directory, so I kept it identical for consistency. You're right that the imports hoist and the textual order is cosmetic — but reordering only this file would make it the odd one out of the three. Happy to do a separate PR normalizing the import order across all three test files if you'd prefer.

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.

Fair point — keeping the harness identical across helpMenu.test.tsx, fileOpen.test.tsx, and this file is a better reason to leave it as-is than to make this one the odd one out. A separate normalization PR touching all three at once makes much more sense. No changes needed here.

Tip: You can customize Greptile's behavior for this repo with .greptile/rules.md and .greptile/config.json.

Comment on lines +409 to +416
const FORMATTING_BAR_ALIGNMENT_CLASS: Record<
NonNullable<ToolbarProps['toolbarAlignment']>,
string
> = {
start: '',
center: '[justify-content:safe_center]',
end: '[justify-content:safe_end]',
};

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.

P2 safe alignment keyword silently no-ops in Safari

The Tailwind arbitrary classes [justify-content:safe_center] and [justify-content:safe_end] emit justify-content: safe center / justify-content: safe end in CSS. The safe overflow keyword is not implemented in WebKit/Safari (it is supported in Chromium 94+ and Firefox 63+). When the full declaration is unrecognised, Safari drops it entirely, so any consumer who sets toolbarAlignment="center" or "end" will see no change on Safari — the bar stays left-aligned regardless. Since this is a new opt-in prop the default behaviour is unchanged, but the silent no-op on Safari is worth surfacing in the prop's JSDoc so callers aren't surprised.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Great catch — justify-content: safe center/safe end does silently no-op in WebKit/Safari. Fixed in 7e91a2f by switching center/end to auto margins on the formatting bar's first/last child ([&>*:first-child]:ms-auto / [&>*:last-child]:me-auto). That centers/end-aligns in every browser and still collapses to start alignment when the row overflows, so the leading controls stay scroll-reachable.

Greptile flagged that justify-content: safe center/end silently no-ops in
WebKit/Safari (the safe overflow keyword is unimplemented there), leaving the
toolbar left-aligned. Switch to auto margins on the formatting bar's first/last
child: this centers/end-aligns in every browser and still collapses to start
alignment (keeping leading controls scroll-reachable) when the row overflows.
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.

Add a toolbarAlignment prop to control formatting-toolbar alignment

1 participant