Commit fd1a740
feat(super-editor): native LTR/RTL paragraph direction toolbar (#3226)
* feat(super-editor): native LTR/RTL paragraph direction toolbar
Add ProseMirror commands `setParagraphDirection` / `clearParagraphDirection`,
toolbar buttons next to the indent controls, and `Mod-Alt-Shift-L/R` keymaps.
Buttons highlight to reflect the current paragraph's direction via the
headless toolbar registry.
Closes #3219.
* chore(super-editor): drop LTR/RTL keymap, ship buttons only
Matches Google Docs behavior — no default keyboard shortcut for paragraph
direction. The 4-key Mod-Alt-Shift-L/R chord (added to avoid colliding
with the existing Mod-Shift-L/R alignment binding) was awkward enough
that any future shortcut should be a deliberate single-key toggle.
* fix(super-editor): direction-rtl was silently applying LTR via no-payload path
The `direction-ltr` / `direction-rtl` registry entries delegated to
`createDirectCommandExecute('setParagraphDirection')`. Headless callers
that invoke `controller.execute('direction-rtl')` without a payload
ended up calling `editor.commands.setParagraphDirection()` with no
arguments — which defaulted `direction` to `undefined`, evaluated
`direction === 'rtl'` as false, and wrote `rightToLeft: false`. The RTL
toolbar id silently performed an LTR write.
Fix in two layers:
- A dedicated `createParagraphDirectionExecute(direction)` helper that
bakes the fixed `{ direction, alignmentPolicy: 'matchDirection' }`
payload into the registry execute so callers don't need to know the
direction is encoded in the command id.
- Guard the command itself so a missing direction is a no-op rather
than a silent LTR write — defense in depth for any other generic
command-by-name pathway.
* fix(super-editor): ltr direction deletes pPr.rightToLeft instead of writing false
Writing `rightToLeft: false` round-trips as `<w:bidi w:val="0"/>` on export
(direct formatting that overrides any inherited style direction), so clicking
LTR on a vanilla paragraph injected `<w:bidi w:val="0"/>` into the DOCX even
though nothing semantically changed. Now LTR deletes the property, matching
`clearParagraphDirection` and leaving vanilla paragraphs untouched.
Replaces a previous test that locked in the buggy behavior; adds a regression
test for the vanilla-paragraph no-op case.
* fix(super-editor): address PR #3226 review feedback
Three issues caught in review:
1. Clicking LTR on a paragraph that inherits `rightToLeft: true` from its
style was a silent no-op. The previous fix only deleted the inline
override; with no inline value to delete, `shallowEqual` saw no change
and skipped dispatch. LTR now re-resolves the cascade with the would-be
inline state and writes an explicit `rightToLeft: false` when the
style would still resolve to RTL. Vanilla paragraphs (no style-driven
RTL) keep the delete-only behavior so they round-trip clean.
2. `defaultItems.test.js` was checking that the legacy XL_ITEMS list
stays in overflow at narrow widths, but did not extend the list to
the new `directionLtr` / `directionRtl` items even though they were
added to `itemsToHideXL`. The test silently underverified the new
items; now exercises them too.
3. Active-state highlight was never applied to items in the overflow
popup (pre-existing bug in `super-toolbar.js`, surfaced by this
feature because RTL/LTR's highlight is the only signal the buttons
give). The active-state loop now iterates `toolbarItems` AND
`overflowItems`.
* fix(super-editor): direction-ltr/direction-rtl payload type to `never`
The headless toolbar payload types for `direction-ltr` and `direction-rtl`
declared a `{ direction, alignmentPolicy? }` shape, but
`createParagraphDirectionExecute` bakes both fields in (direction from
the closure, alignmentPolicy hardcoded to `'matchDirection'`) and
ignores any payload.
A TS consumer doing `controller.execute('direction-ltr', { direction: 'rtl' })`
got LTR regardless of payload. Set both to `never` so the type matches
the runtime: this command takes no payload.
* test(super-editor): pin direction-ltr/direction-rtl execute contract
Adds 4 unit tests proving the runtime contract that justifies the
`never` payload type:
1. controller.execute('direction-ltr') -> setParagraphDirection(
{ direction: 'ltr', alignmentPolicy: 'matchDirection' })
2. controller.execute('direction-rtl') -> setParagraphDirection(
{ direction: 'rtl', alignmentPolicy: 'matchDirection' })
3. payload is ignored if passed (direction comes from the command id)
4. execute returns false when the editor command is unavailable
These pin the property that makes ToolbarPayloadMap['direction-*'] = never
a faithful declaration of the runtime, not a workaround.
* test(super-editor): comprehensive coverage for direction buttons
Adds regression coverage across the toolbar click path so the next time
someone touches `useToolbarItem({argument})` forwarding, the direction
buttons' state, or the `setParagraphDirection` payload shape, tests fail
loudly instead of silently breaking the buttons.
Unit tests (13 new):
- defaultItems.test.js: pin directionLtr/directionRtl item config
(command, argument, aria-label) so config drift fails fast.
- ButtonGroup.test.js: pin item.argument.value forwarding through the
click handler. Plain button click emits the static argument; explicit
caller arg wins; disabled item skips emission. Catches the opus review
concern about the new fallback affecting all buttons.
- toolbar-registry.test.ts: state-deriver tests for direction-ltr /
direction-rtl across all three paragraphProperties.rightToLeft states
(true / false / undefined). Pins active/value derivation.
Playwright behavior tests (5 new) in tests/behavior/tests/toolbar/
paragraph-direction.spec.ts: click the actual button users see, read
the ProseMirror doc, assert attrs change. Covers:
- RTL click sets paragraphProperties.rightToLeft = true
- LTR click on vanilla paragraph deletes the rightToLeft key
- RTL → LTR is one undo step (atomic transaction)
- Multi-paragraph selection applies to every paragraph
- Direction buttons reachable in overflow at narrow viewports
All 12751 super-editor unit tests pass.
* test(behavior): make paragraph-direction spec use correct fixture API
Two real bugs in the spec, not workarounds:
- Pin viewport to 1600x1200 in test.use. The Playwright config spreads
`devices['Desktop Chrome']` (viewport 1280x720) which silently overrode
the global 1600 default, putting the direction buttons in overflow.
Below the XL cutoff (~1494px container width), the spec couldn't click
the buttons directly. Wider viewport keeps them in the main toolbar.
- Fix multi-paragraph selection: setTextSelection takes positional
(from, to) args, not an object; findTextPos returns a single number
(the start position), not {from,to}. Earlier draft passed an object
to setTextSelection so the selection collapsed to undefined/object
and only the cursor's paragraph got RTL.
The narrow-viewport test deliberately resizes below XL and opens the
overflow popup explicitly, so the direct-click helper stays clean.
All 15 tests pass across chromium/firefox/webkit.
* test(behavior): expand paragraph-direction spec - alignment + active state
Adds two end-to-end coverage areas that the unit tests can't reach, and
fixes the undo-step test name to match what it actually verifies.
- alignmentPolicy plumbing: paragraph has explicit `justification: 'left'`,
user clicks Right-to-left button, assert paragraph ends up with
`rightToLeft: true` AND `justification: 'right'`. Proves the UI ->
command path forwards `alignmentPolicy: 'matchDirection'`, not just the
unit-level command logic.
- overflow active-state regression for 151cff8: at narrow viewport,
click RTL via overflow popup, close, reopen, assert the RTL overflow
button has the .active class (and LTR doesn't). Pre-fix this would
silently miss because the active-state loop only iterated toolbarItems,
not overflowItems.
- Rename "Right-to-left then Left-to-right is one undo step" to
"Right-to-left click is one undo step" - the test only verified the
RTL single-click case. A separate "LTR after RTL is its own undo step"
was attempted but PM history coalesces rapid clicks into one step,
so that's testing PM behavior, not the PR's contract.
All 21 tests pass across chromium/firefox/webkit.
* refactor(super-editor): remove direction buttons from default toolbar
Per review: directionLtr / directionRtl shouldn't ship in the default
toolbar config - they crowd the toolbar for every consumer (including
the majority who never touch RTL), and the XL_OVERFLOW_SAFETY_BUFFER had
to bump from 20 -> 84 specifically to accommodate them.
This commit:
- Removes the directionLtr / directionRtl `useToolbarItem` blocks and
drops them from `toolbarItems` and `itemsToHideXL`.
- Reverts XL_OVERFLOW_SAFETY_BUFFER to 20 (was 84 with a 32px-each-for-
direction comment).
- Updates defaultItems.test.js: XL_ITEMS no longer includes direction;
the 3 direction-config tests are replaced with 2 "not in default
toolbar" guard tests so a future re-add will fail loudly.
- Deletes tests/behavior/tests/toolbar/paragraph-direction.spec.ts
(assumed buttons were in the default toolbar).
What stays:
- `setParagraphDirection` command + its unit tests
- Headless toolbar `direction-ltr` / `direction-rtl` ids + tests
(typed payload still `never`; execute/state tests still pin contract)
- Icons (`paragraph-ltr-solid.svg`, `paragraph-rtl-solid.svg`)
- ButtonGroup argument forwarding tests (generic, not direction-specific)
Customer-side: wire the buttons into your own toolbar via the headless
toolbar API, or call `editor.commands.setParagraphDirection({ direction })`
directly.
* test(super-editor): reword ButtonGroup forwarding comment
Direction buttons are no longer default toolbar items (see 32153f8),
so the test comment shouldn't reference them as the example consumer of
this forwarding behavior. Rewords to "custom buttons" since the test
itself uses a direction-shaped payload only as an illustrative example.
---------
Co-authored-by: Caio Pizzol <97641911+caio-pizzol@users.noreply.github.com>
Co-authored-by: Caio Pizzol <caio@superdoc.dev>1 parent c446999 commit fd1a740
18 files changed
Lines changed: 676 additions & 2 deletions
File tree
- packages/super-editor/src
- editors/v1
- components/toolbar
- core/commands
- extensions
- paragraph
- types
- headless-toolbar
- helpers
Lines changed: 98 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
56 | 56 | | |
57 | 57 | | |
58 | 58 | | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
Lines changed: 5 additions & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
114 | 114 | | |
115 | 115 | | |
116 | 116 | | |
117 | | - | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
118 | 122 | | |
119 | 123 | | |
120 | 124 | | |
| |||
Lines changed: 2 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
88 | 88 | | |
89 | 89 | | |
90 | 90 | | |
| 91 | + | |
| 92 | + | |
91 | 93 | | |
92 | 94 | | |
93 | 95 | | |
| |||
Lines changed: 22 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
95 | 95 | | |
96 | 96 | | |
97 | 97 | | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
Lines changed: 5 additions & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
712 | 712 | | |
713 | 713 | | |
714 | 714 | | |
715 | | - | |
| 715 | + | |
| 716 | + | |
| 717 | + | |
| 718 | + | |
| 719 | + | |
716 | 720 | | |
717 | 721 | | |
718 | 722 | | |
| |||
Lines changed: 4 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
60 | 60 | | |
61 | 61 | | |
62 | 62 | | |
| 63 | + | |
| 64 | + | |
63 | 65 | | |
64 | 66 | | |
65 | 67 | | |
| |||
89 | 91 | | |
90 | 92 | | |
91 | 93 | | |
| 94 | + | |
| 95 | + | |
92 | 96 | | |
93 | 97 | | |
94 | 98 | | |
| |||
Lines changed: 2 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
29 | 29 | | |
30 | 30 | | |
31 | 31 | | |
| 32 | + | |
| 33 | + | |
32 | 34 | | |
33 | 35 | | |
34 | 36 | | |
| |||
Lines changed: 1 addition & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
44 | 44 | | |
45 | 45 | | |
46 | 46 | | |
| 47 | + | |
47 | 48 | | |
48 | 49 | | |
49 | 50 | | |
| |||
Lines changed: 97 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
0 commit comments