smartrte-react 1.0.0-beta.8 → 1.0.0-beta.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.0-beta.9
4
+
5
+ - Fix toolbar dropdowns, the mobile "More tools" kebab menu, and the text/background colour and table-border popovers rendering far from their own trigger when the editor is embedded inside a host container that has its own CSS `transform` on an ancestor - e.g. a Radix/shadcn `Dialog`'s own centering transform. These overlays now correctly account for the resulting change of containing block instead of assuming it's always the viewport.
6
+
3
7
  ## 1.0.0-beta.8
4
8
 
5
9
  - Add a `tools` prop (`ClassicEditor`, `CanonicalAuthorityEditor`, and the standalone embed) letting a host hide any individual toolbar tool — Bold, Video, Version history, Review, and around 40 others — without wrapping or restyling the component. Every tool is visible by default; composes with existing gates (a required provider or capability must still be present). See the README's "Hiding individual toolbar tools" section for the full list and usage.
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useId, useLayoutEffect, useRef, useState } from "react";
3
+ import { getFixedPositioningOrigin } from "./fixedPositioning.js";
3
4
  const HEX_PATTERN = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i;
4
5
  const normalizeHex = (value) => {
5
6
  if (!HEX_PATTERN.test(value))
@@ -138,7 +139,8 @@ export function ColorPickerPopover({ x, y, label, initialValue = "#000000", rece
138
139
  const overflowsBottom = y + height > viewportHeight - margin;
139
140
  const top = overflowsBottom ? y - height : y;
140
141
  const clampedTop = Math.min(Math.max(margin, top), Math.max(margin, viewportHeight - margin));
141
- setPlacement({ left: clampedLeft, top: clampedTop });
142
+ const origin = getFixedPositioningOrigin(el);
143
+ setPlacement({ left: clampedLeft - origin.left, top: clampedTop - origin.top });
142
144
  }, [x, y]);
143
145
  const stage = (nextHsv) => {
144
146
  stagedRef.current = true;
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useLayoutEffect, useRef, useState } from "react";
3
3
  import { ColorPickerPopover } from "./ColorPickerPopover.js";
4
+ import { getFixedPositioningOrigin } from "./fixedPositioning.js";
4
5
  // Widths start at 2px, not 1px - table_cell's border-collapse table (see
5
6
  // theme.ts's th/td rule) already renders a 1px ambient default gridline on
6
7
  // every cell. A custom border tied at 1px loses CSS border-collapse's
@@ -75,7 +76,8 @@ export function TableBorderPopover({ x, y, initial, recentColors, onPreview, onA
75
76
  const overflowsBottom = y + height > viewportHeight - margin;
76
77
  const top = overflowsBottom ? y - height : y;
77
78
  const clampedTop = Math.min(Math.max(margin, top), Math.max(margin, viewportHeight - margin));
78
- setPlacement({ left: clampedLeft, top: clampedTop });
79
+ const origin = getFixedPositioningOrigin(el);
80
+ setPlacement({ left: clampedLeft - origin.left, top: clampedTop - origin.top });
79
81
  }, [x, y]);
80
82
  useEffect(() => {
81
83
  const dismissIfOutside = (event) => {
@@ -110,9 +110,12 @@ export declare function ToolbarMenuItem({ icon, label, ariaLabel, pressed, disab
110
110
  * anchored inside that container gets visually clipped the moment it needs
111
111
  * to extend past the container's own bounds, which is exactly when a
112
112
  * narrow host is most likely to need the dropdown in the first place.
113
- * `position: fixed`'s containing block is the viewport (barring an
114
- * ancestor with its own transform/filter, not the case here), so it always
115
- * escapes ANY ancestor's overflow clipping regardless of DOM depth.
113
+ * `position: fixed`'s containing block is normally the viewport, so this
114
+ * escapes ANY ancestor's overflow clipping regardless of DOM depth - unless
115
+ * an ancestor has its own transform/perspective/filter, which becomes the
116
+ * containing block instead (see fixedPositioning.ts, and
117
+ * docs/bugs/toolbar-menu-misplaced-inside-transformed-ancestor.md); the
118
+ * placement math below accounts for that case too.
116
119
  */
117
120
  export declare function ToolbarDropdown({ icon, label, priority, children }: {
118
121
  icon?: ToolbarIconKey;
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useLayoutEffect, useRef, useState } from "react";
3
3
  import { AlignCenter, AlignJustify, AlignLeft, AlignRight, ALargeSmall, Baseline, Bold, CheckSquare, ChevronDown, Code, Columns3, Download, Eye, FileJson, History, Image as ImageIcon, IndentDecrease, IndentIncrease, Italic, Link2, ListChecks, ListOrdered, ListRestart, List as ListIcon, MessageSquare, MessageSquarePlus, MoreHorizontal, MoveDown, MoveLeft, MoveRight, MoveUp, Music, Omega, PanelTop, PenLine, Pilcrow, Quote, Redo2, Rows3, Square, Sigma, Strikethrough, Subscript, Superscript, Table2, Trash2, Type, Underline, Undo2, Unlink2, Upload, Video, Pencil, ZoomIn, ZoomOut, } from "lucide-react";
4
+ import { getFixedPositioningOrigin } from "./fixedPositioning.js";
4
5
  /**
5
6
  * The toolbar's icon set (Phase: Direction B toolbar redesign, 2026-08-29).
6
7
  * Lucide, per the design proposal's §11 recommendation - MIT-licensed,
@@ -128,9 +129,12 @@ const useDismissDetailsOnOutsideClick = (ref) => {
128
129
  * anchored inside that container gets visually clipped the moment it needs
129
130
  * to extend past the container's own bounds, which is exactly when a
130
131
  * narrow host is most likely to need the dropdown in the first place.
131
- * `position: fixed`'s containing block is the viewport (barring an
132
- * ancestor with its own transform/filter, not the case here), so it always
133
- * escapes ANY ancestor's overflow clipping regardless of DOM depth.
132
+ * `position: fixed`'s containing block is normally the viewport, so this
133
+ * escapes ANY ancestor's overflow clipping regardless of DOM depth - unless
134
+ * an ancestor has its own transform/perspective/filter, which becomes the
135
+ * containing block instead (see fixedPositioning.ts, and
136
+ * docs/bugs/toolbar-menu-misplaced-inside-transformed-ancestor.md); the
137
+ * placement math below accounts for that case too.
134
138
  */
135
139
  export function ToolbarDropdown({ icon, label, priority, children }) {
136
140
  const detailsRef = useRef(null);
@@ -155,7 +159,8 @@ export function ToolbarDropdown({ icon, label, priority, children }) {
155
159
  const left = Math.min(Math.max(margin, triggerRect.left), Math.max(margin, viewportWidth - menuWidth - margin));
156
160
  const overflowsBottom = triggerRect.bottom + 6 + menuHeight > viewportHeight - margin;
157
161
  const top = overflowsBottom ? Math.max(margin, triggerRect.top - menuHeight - 6) : triggerRect.bottom + 6;
158
- setPlacement({ left, top });
162
+ const origin = getFixedPositioningOrigin(details);
163
+ setPlacement({ left: left - origin.left, top: top - origin.top });
159
164
  };
160
165
  recompute();
161
166
  details.addEventListener("toggle", recompute);
@@ -218,7 +223,8 @@ export function MobileMoreMenu({ children }) {
218
223
  const left = Math.min(Math.max(margin, preferredLeft), Math.max(margin, viewportWidth - menuWidth - margin));
219
224
  const overflowsBottom = triggerRect.bottom + 6 + menuHeight > viewportHeight - margin;
220
225
  const top = overflowsBottom ? Math.max(margin, triggerRect.top - menuHeight - 6) : triggerRect.bottom + 6;
221
- setPlacement({ left, top });
226
+ const origin = getFixedPositioningOrigin(details);
227
+ setPlacement({ left: left - origin.left, top: top - origin.top });
222
228
  };
223
229
  recompute();
224
230
  details.addEventListener("toggle", recompute);
@@ -0,0 +1,25 @@
1
+ /**
2
+ * A CSS `transform`, `perspective`, `filter`, `backdrop-filter`, or a
3
+ * `will-change` naming one of those, on an ancestor makes that ancestor the
4
+ * containing block for `position: fixed` descendants instead of the
5
+ * viewport (CSS Transforms spec's rendering model) - most commonly a
6
+ * Radix/shadcn `Dialog`'s centering transform (`translate-x-[-50%]
7
+ * translate-y-[-50%]`), which any host may legitimately wrap this editor
8
+ * in. When that happens, `left`/`top` set on a fixed-positioned menu/popover
9
+ * are resolved against that ancestor's border box (as rendered,
10
+ * post-transform) instead of the viewport, silently producing coordinates
11
+ * that are internally consistent but land far from the trigger with no
12
+ * error - confirmed live via a Sootr host rendering the editor inside such
13
+ * a dialog.
14
+ *
15
+ * `getBoundingClientRect()` always returns viewport-relative coordinates
16
+ * regardless of transforms, so every placement calculation in this codebase
17
+ * computes coordinates in viewport space first, then subtracts this
18
+ * origin - the real containing block's own viewport-relative top-left - so
19
+ * the applied `left`/`top` land correctly under either containing block.
20
+ */
21
+ export declare function findFixedPositioningContainer(el: HTMLElement): HTMLElement | null;
22
+ export declare function getFixedPositioningOrigin(el: HTMLElement): {
23
+ left: number;
24
+ top: number;
25
+ };
@@ -0,0 +1,42 @@
1
+ /**
2
+ * A CSS `transform`, `perspective`, `filter`, `backdrop-filter`, or a
3
+ * `will-change` naming one of those, on an ancestor makes that ancestor the
4
+ * containing block for `position: fixed` descendants instead of the
5
+ * viewport (CSS Transforms spec's rendering model) - most commonly a
6
+ * Radix/shadcn `Dialog`'s centering transform (`translate-x-[-50%]
7
+ * translate-y-[-50%]`), which any host may legitimately wrap this editor
8
+ * in. When that happens, `left`/`top` set on a fixed-positioned menu/popover
9
+ * are resolved against that ancestor's border box (as rendered,
10
+ * post-transform) instead of the viewport, silently producing coordinates
11
+ * that are internally consistent but land far from the trigger with no
12
+ * error - confirmed live via a Sootr host rendering the editor inside such
13
+ * a dialog.
14
+ *
15
+ * `getBoundingClientRect()` always returns viewport-relative coordinates
16
+ * regardless of transforms, so every placement calculation in this codebase
17
+ * computes coordinates in viewport space first, then subtracts this
18
+ * origin - the real containing block's own viewport-relative top-left - so
19
+ * the applied `left`/`top` land correctly under either containing block.
20
+ */
21
+ export function findFixedPositioningContainer(el) {
22
+ let node = el.parentElement;
23
+ while (node) {
24
+ const style = window.getComputedStyle(node);
25
+ if ((style.transform && style.transform !== "none") ||
26
+ (style.perspective && style.perspective !== "none") ||
27
+ (style.filter && style.filter !== "none") ||
28
+ (style.backdropFilter && style.backdropFilter !== "none") ||
29
+ (style.willChange && /transform|perspective|filter/.test(style.willChange))) {
30
+ return node;
31
+ }
32
+ node = node.parentElement;
33
+ }
34
+ return null;
35
+ }
36
+ export function getFixedPositioningOrigin(el) {
37
+ const container = findFixedPositioningContainer(el);
38
+ if (!container)
39
+ return { left: 0, top: 0 };
40
+ const rect = container.getBoundingClientRect();
41
+ return { left: rect.left, top: rect.top };
42
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "smartrte-react",
3
- "version": "1.0.0-beta.8",
3
+ "version": "1.0.0-beta.9",
4
4
  "description": "A powerful, feature-rich Rich Text Editor for React with support for tables, mathematical formulas (LaTeX/KaTeX), and media management",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",