feat(react): add toolbarAlignment prop for formatting-toolbar alignment#1001
feat(react): add toolbarAlignment prop for formatting-toolbar alignment#1001aldrinjenson wants to merge 2 commits into
toolbarAlignment prop for formatting-toolbar alignment#1001Conversation
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
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
All contributors have signed the CLA ✍️ ✅ Posted by the CLA bot. |
Greptile SummaryAdds a
Confidence Score: 4/5Safe 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 No files require special attention; Toolbar.tsx and toolbarAlignment.test.tsx have the minor notes above but neither blocks the merge.
|
| 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
%%{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
Reviews (1): Last reviewed commit: "feat(react): add toolbarAlignment prop f..." | Re-trigger Greptile
| beforeAll(() => GlobalRegistrator.register()); | ||
| afterAll(() => GlobalRegistrator.unregister()); | ||
| import { afterEach, describe, expect, test } from 'bun:test'; |
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| const FORMATTING_BAR_ALIGNMENT_CLASS: Record< | ||
| NonNullable<ToolbarProps['toolbarAlignment']>, | ||
| string | ||
| > = { | ||
| start: '', | ||
| center: '[justify-content:safe_center]', | ||
| end: '[justify-content:safe_end]', | ||
| }; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
Summary
Adds a
toolbarAlignmentprop 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 underlyingToolbarProps, 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
'start'— no-op, preserves today's left-aligned layout. Existing callers are unaffected.'center'/'end'— applied to the formatting bar'sjustify-contentvia 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'soverflow-x-autocontent is wider than the viewport, it falls back to start alignment instead of clipping the leading controls (undo/redo) out of scroll reach.inlinemode — that mode rendersdisplay:contentsand has no flex container of its own.Wiring
DocxEditorProps.toolbarAlignment→DocxEditorToolbar→<EditorToolbar toolbarAlignment>(context) →Toolbar→ formatting-barjustify-content.Tests
toolbarAlignment.test.tsx: default omits anyjustify-contentoverride;center/endapply the safe-alignment classes.justify-content:safe center/safe end) are emitted into the builtpackages/react/dist/styles.css.Gates
typecheck(all 6 packages), fullbun test,api:check, andcheck:parity(all 5 sub-checks) pass.docs/api/docx-editor-react/{index,ui}.api.md).scripts/parity/parity.contract.json(deferredInVue) andscripts/check-editor-contract.mjs(REACT_PROPS_NOT_YET_IN_VUE).minor,@eigenpal/docx-editor-react).eslint.config.js: theDocxEditor.tsxmax-linesceiling 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.
Need help on this PR? Tag
/codesmithwith what you need. Autofix is disabled.