Skip to content

Conversation

@lsinger
Copy link

@lsinger lsinger commented Sep 18, 2025

Previously, TK monikers in bulleted lists were tracked at the

    level,
    making it difficult for authors to identify which specific list items
    contained incomplete content. This change implements list-aware TK detection
    that treats each
  • element as an individual container.

    • Added getEffectiveTopLevelElement() to recognize list items as containers
    • Updated mutation listener to use list-aware detection logic
    • Enhanced positioning calculations for precise list item indicator placement
    • Added comprehensive test coverage for list-specific TK behavior

    This provides authors with granular visibility into which list items need
    completion, consistent with how TK tracking works for paragraph elements.

    Fixes PROD-2389

…ning

  Previously, TK monikers in bulleted lists were tracked at the <ul> level,
  making it difficult for authors to identify which specific list items
  contained incomplete content. This change implements list-aware TK detection
  that treats each <li> element as an individual container.

  - Added getEffectiveTopLevelElement() to recognize list items as containers
  - Updated mutation listener to use list-aware detection logic
  - Enhanced positioning calculations for precise list item indicator placement
  - Added comprehensive test coverage for list-specific TK behavior

  This provides authors with granular visibility into which list items need
  completion, consistent with how TK tracking works for paragraph elements.

  Fixes PROD-2389
@coderabbitai
Copy link

coderabbitai bot commented Sep 18, 2025

Warning

Rate limit exceeded

@lsinger has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 0 minutes and 0 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between bd7544d and 432e5f0.

📒 Files selected for processing (1)
  • packages/koenig-lexical/src/plugins/TKPlugin.jsx (6 hunks)

Walkthrough

Treats list items (LI) as the effective top-level container for TK nodes by adding getEffectiveTopLevelElement(node). Introduces indicator positioning constants and updates TKIndicator.calculatePosition to prefer an LI or a card element as the positioning target, with early returns when no container exists. Per-key element lookups are cached and missing elements are skipped. TKPlugin mutation handling derives parentNodeKey from the effective top-level element. TKIndicators rendering returns null for missing parentContainer. Adds two e2e tests verifying per-list-item indicator creation and distinct vertical positioning.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The title succinctly and accurately describes the primary change—implementing list-item-level TK detection and positioning in TKPlugin. It follows conventional commit format and highlights the most important change without file lists or noise. It directly reflects the modifications in the PR (list-item containment handling, positioning logic, and tests).
Description Check ✅ Passed The description clearly explains the problem, lists the implemented changes (getEffectiveTopLevelElement, updated mutation listener, positioning refinements, and added tests), and references the tracking ticket PROD-2389, all of which align with the file summaries and PR objectives. It is directly related to the changeset and provides sufficient context for reviewers to understand intent.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (4)
packages/koenig-lexical/test/e2e/plugins/TKPlugin.test.js (2)

118-131: Good coverage; add a quick guard that we indeed created 4 list items

This avoids false positives if list auto‑continuation ever changes.

Apply this diff:

         await page.keyboard.type('Fourth item with TK');

-        // Should have 3 separate TK indicators, one for each list item containing TK
+        // Sanity check: we created a 4-item list
+        await expect(page.getByRole('listitem')).toHaveCount(4);
+
+        // Should have 3 separate TK indicators, one for each list item containing TK
         await expect(page.getByTestId('tk-indicator')).toHaveCount(3);

133-152: Stabilize position assertions to avoid flake from null bounding boxes

Ensure indicators are visible and add a small guard before reading positions.

Apply this diff:

         const indicators = page.getByTestId('tk-indicator');
         await expect(indicators).toHaveCount(2);

+        // Ensure they are visible and we're indeed in a 2-item list
+        await expect(page.getByRole('listitem')).toHaveCount(2);
+
         // Get positions to verify they're different (one per list item)
         const firstIndicator = indicators.first();
         const secondIndicator = indicators.last();

-        const firstBox = await firstIndicator.boundingBox();
-        const secondBox = await secondIndicator.boundingBox();
+        await expect(firstIndicator).toBeVisible();
+        await expect(secondIndicator).toBeVisible();
+        const firstBox = await firstIndicator.boundingBox();
+        const secondBox = await secondIndicator.boundingBox();
+        expect(firstBox).not.toBeNull();
+        expect(secondBox).not.toBeNull();

         // The indicators should have different vertical positions
         expect(Math.abs(firstBox.y - secondBox.y)).toBeGreaterThan(10);
packages/koenig-lexical/src/plugins/TKPlugin.jsx (2)

53-59: Nice LI anchoring; also observe the card element to reduce reposition lag

When anchoring to a card within a non‑LI container, observe that card so indicator updates on card resizes.

Apply this diff in the resize observer effect:

 useEffect(() => {
     const observer = new ResizeObserver(() => (setPosition(calculatePosition())));

     observer.observe(rootElement);
     observer.observe(containingElement);
+    // If positioned against a nested card, observe it too
+    const cardEl = containingElement.nodeName !== 'LI'
+        ? containingElement.querySelector('[data-kg-card]')
+        : null;
+    if (cardEl) {
+        observer.observe(cardEl);
+    }

     return () => {
         observer.disconnect();
     };
 }, [rootElement, containingElement, calculatePosition]);

192-196: Guard against missing parent key to avoid “undefined” buckets

If effectiveTopLevel is null or DOM not yet attached, parentNodeKey can be undefined; skip until next mutation to avoid polluting tkNodeMap.

Apply this diff:

-const effectiveTopLevel = getEffectiveTopLevelElement($getNodeByKey(tkNodeKey));
-const parentNodeKey = effectiveTopLevel?.getKey();
-const topLevelNodeKey = parentEditorNodeKey || parentNodeKey;
-addEditorTkNode(editor.getKey(), topLevelNodeKey, tkNodeKey);
+const effectiveTopLevel = getEffectiveTopLevelElement($getNodeByKey(tkNodeKey));
+const parentNodeKey = effectiveTopLevel?.getKey();
+if (!parentEditorNodeKey && !parentNodeKey) {
+    // Node not yet attached to a top-level container; try on next mutation
+    continue;
+}
+const topLevelNodeKey = parentEditorNodeKey || parentNodeKey;
+addEditorTkNode(editor.getKey(), topLevelNodeKey, tkNodeKey);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ac61b07 and 631f87a.

📒 Files selected for processing (2)
  • packages/koenig-lexical/src/plugins/TKPlugin.jsx (3 hunks)
  • packages/koenig-lexical/test/e2e/plugins/TKPlugin.test.js (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
packages/koenig-lexical/test/e2e/plugins/TKPlugin.test.js (1)
packages/koenig-lexical/test/utils/e2e.js (1)
  • focusEditor (58-61)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Node 22.13.1
  • GitHub Check: Node 20.11.1

…ction

  Fixed TypeError in getEffectiveTopLevelElement where currentNode.isRoot()
  method doesn't exist. Replaced with proper Lexical node parent checking
  using currentNode.getParent() !== null to traverse the node hierarchy
  correctly when identifying list items as TK containers.

  Fixes PROD-2389
- Add INDICATOR_OFFSET_RIGHT and INDICATOR_OFFSET_TOP constants
- Add NODE_TYPES constant for element type checking
- Improves code readability and maintainability
- Use early return for non-list elements for better readability
- Use optional chaining for safer null checking in while loop
- Add explicit fallback comment for clarity
- Restructure logic flow to be more maintainable
- Replace conditional assignment with ternary operator
- Use NODE_TYPES constant for element type checking
- Eliminate redundant element queries
- More concise and readable code
- Add null check for containingElement in TKIndicator
- Add element validation in toggleHighlightClasses forEach loop
- Return null instead of false for consistency in map filter
- Prevents potential runtime errors when DOM elements are unavailable
- Move null check for containingElement after all hooks
- Add proper curly braces for if statement return
- Ensures hooks are called in consistent order per React rules
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (1)
packages/koenig-lexical/src/plugins/TKPlugin.jsx (1)

4-4: Good call switching to Lexical list helpers

Using $isListItemNode/$isListNode avoids brittle DOM/nodeName checks and aligns with Lexical’s model.

🧹 Nitpick comments (3)
packages/koenig-lexical/src/plugins/TKPlugin.jsx (3)

15-18: Hard-coded offsets: consider theming or CSS vars

If these need tweaking across themes, prefer deriving from editor theme or CSS variables to avoid magic numbers in JS.


19-23: Tiny: drop NODE_TYPES indirection

Comparing containingElement.tagName === 'LI' is sufficiently clear; the extra constant adds indirection without payoff.

Apply this small refactor:

-// Node type constants
-const NODE_TYPES = {
-    LIST_ITEM: 'LI'
-};

And where used:

-const positioningElement = containingElement.nodeName === NODE_TYPES.LIST_ITEM
+const positioningElement = containingElement.tagName === 'LI'
     ? containingElement
     : containingElement.querySelector('[data-kg-card]') || containingElement;

24-49: List-aware top-level resolution looks right

Logic is correct and guarded for null topLevel. Minor robustness: stop at root explicitly in the climb.

Apply:

-while (currentNode?.getParent()) {
+while (currentNode && !currentNode.isRoot()) {
     if ($isListItemNode(currentNode)) {
         return currentNode;
     }
     currentNode = currentNode.getParent();
}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 91e62ec and c3aec84.

📒 Files selected for processing (1)
  • packages/koenig-lexical/src/plugins/TKPlugin.jsx (5 hunks)
🧰 Additional context used
🪛 Biome (2.1.2)
packages/koenig-lexical/src/plugins/TKPlugin.jsx

[error] 62-62: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

Hooks should not be called after an early return.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Node 20.11.1
  • GitHub Check: Node 22.13.1
🔇 Additional comments (3)
packages/koenig-lexical/src/plugins/TKPlugin.jsx (3)

68-72: Positioning target selection is sensible

Preferring LI as anchor and otherwise falling back to [data-kg-card] or the container should give stable placement in lists and cards.


75-76: Offset math reads well

Using INDICATOR_OFFSET_TOP atop the container rect delta keeps vertical alignment predictable.


295-312: Null filtering on missing parent containers is fine

Returning null and filtering via .filter(Boolean) keeps the render list clean.

Comment on lines 56 to 60
// Early return if containing element is not available
if (!containingElement) {
return null;
}

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

React hooks called after an early return (violates Hooks rules)

TKIndicator returns null before declaring hooks, then declares useCallback/useState/useEffect in other renders. This breaks hook ordering and can crash at runtime.

Apply this fix: remove the early return and guard inside hooks; only return null just before rendering.

-// Early return if containing element is not available
-if (!containingElement) {
-    return null;
-}
+// No early return here; guard inside hooks and at render time

 // position element relative to the TK Node containing element
 const calculatePosition = useCallback(() => {
-    let top = 0;
-    let right = INDICATOR_OFFSET_RIGHT;
+    // Default position when container is missing
+    if (!containingElement) {
+        return {top: 0, right: INDICATOR_OFFSET_RIGHT};
+    }
+    let top = 0;
+    let right = INDICATOR_OFFSET_RIGHT;

     const rootElementRect = rootElement.getBoundingClientRect();

     // Determine positioning element based on container type
-    const positioningElement = containingElement.nodeName === NODE_TYPES.LIST_ITEM
+    const positioningElement = containingElement.tagName === 'LI'
         ? containingElement
         : containingElement.querySelector('[data-kg-card]') || containingElement;

     const positioningElementRect = positioningElement.getBoundingClientRect();

     top = positioningElementRect.top - rootElementRect.top + INDICATOR_OFFSET_TOP;

     if (positioningElementRect.right > rootElementRect.right) {
         right = right - (positioningElementRect.right - rootElementRect.right);
     }

     return {top, right};
 }, [rootElement, containingElement]);

 const [position, setPosition] = useState(calculatePosition());

 // set up an observer to reposition the indicator when the TK node containing
 // element moves relative to the root element
 useEffect(() => {
     const observer = new ResizeObserver(() => (setPosition(calculatePosition())));

     observer.observe(rootElement);
-    observer.observe(containingElement);
+    if (containingElement) {
+        observer.observe(containingElement);
+    }

     return () => {
         observer.disconnect();
     };
 }, [rootElement, containingElement, calculatePosition]);

 const style = {
     top: `${position.top}px`,
     right: `${position.right}px`
 };

 return (
+    // Render nothing when container isn't available
+    !containingElement ? null :
     <div
         className="absolute cursor-pointer p-1 text-2xs font-medium text-grey-600"
         data-testid="tk-indicator"
         style={style}
         onClick={onClick}
         onMouseEnter={onMouseEnter}
         onMouseLeave={onMouseLeave}
     >TK</div>
 );

Also applies to: 62-86, 153-165, 171-181

🤖 Prompt for AI Agents
In packages/koenig-lexical/src/plugins/TKPlugin.jsx around lines 56-60 (and
similarly for ranges 62-86, 153-165, 171-181), the component returns null before
any hook declarations which breaks the Rules of Hooks; remove the early return
so hooks (useState/useCallback/useEffect) are always declared in the same order,
add conditional guards inside the hook bodies or their callbacks to no-op when
containingElement is null/undefined, and only perform a final conditional return
null immediately before the JSX render so rendering still short-circuits when
the containing element is missing.

Comment on lines 131 to 140
const element = editor.getElementByKey(key);
if (!element) return;

if (isHighlighted) {
editor.getElementByKey(key).classList.remove(...tkClasses);
editor.getElementByKey(key).classList.add(...tkHighlightClasses);
element.classList.remove(...tkClasses);
element.classList.add(...tkHighlightClasses);
} else {
editor.getElementByKey(key).classList.add(...tkClasses);
editor.getElementByKey(key).classList.remove(...tkHighlightClasses);
element.classList.add(...tkClasses);
element.classList.remove(...tkHighlightClasses);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Potential TypeError: classList.add/remove called with 0 args or empty tokens

If theme.tk or theme.tkHighlighted is undefined or contains extra spaces, tkClasses/tkHighlightClasses can be [], leading to DOMTokenList.add()/remove() with no args (TypeError) or empty token '' (also TypeError).

Apply guards and sanitize tokens:

-        nodeKeys.forEach((key) => {
-            const element = editor.getElementByKey(key);
-            if (!element) return;
-
-            if (isHighlighted) {
-                element.classList.remove(...tkClasses);
-                element.classList.add(...tkHighlightClasses);
-            } else {
-                element.classList.add(...tkClasses);
-                element.classList.remove(...tkHighlightClasses);
-            }
-        });
+        const baseClasses = tkClasses.filter(Boolean);
+        const highlightClasses = tkHighlightClasses.filter(Boolean);
+
+        nodeKeys.forEach((key) => {
+            const element = editor.getElementByKey(key);
+            if (!element) return;
+
+            if (isHighlighted) {
+                if (baseClasses.length) element.classList.remove(...baseClasses);
+                if (highlightClasses.length) element.classList.add(...highlightClasses);
+            } else {
+                if (baseClasses.length) element.classList.add(...baseClasses);
+                if (highlightClasses.length) element.classList.remove(...highlightClasses);
+            }
+        });

Optional: also sanitize at source:

-const tkClasses = editor._config.theme.tk?.split(' ') || [];
-const tkHighlightClasses = editor._config.theme.tkHighlighted?.split(' ') || [];
+const tkClasses = (editor._config.theme.tk || '').split(/\s+/).filter(Boolean);
+const tkHighlightClasses = (editor._config.theme.tkHighlighted || '').split(/\s+/).filter(Boolean);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const element = editor.getElementByKey(key);
if (!element) return;
if (isHighlighted) {
editor.getElementByKey(key).classList.remove(...tkClasses);
editor.getElementByKey(key).classList.add(...tkHighlightClasses);
element.classList.remove(...tkClasses);
element.classList.add(...tkHighlightClasses);
} else {
editor.getElementByKey(key).classList.add(...tkClasses);
editor.getElementByKey(key).classList.remove(...tkHighlightClasses);
element.classList.add(...tkClasses);
element.classList.remove(...tkHighlightClasses);
}
const baseClasses = tkClasses.filter(Boolean);
const highlightClasses = tkHighlightClasses.filter(Boolean);
nodeKeys.forEach((key) => {
const element = editor.getElementByKey(key);
if (!element) return;
if (isHighlighted) {
if (baseClasses.length) element.classList.remove(...baseClasses);
if (highlightClasses.length) element.classList.add(...highlightClasses);
} else {
if (baseClasses.length) element.classList.add(...baseClasses);
if (highlightClasses.length) element.classList.remove(...highlightClasses);
}
});
🤖 Prompt for AI Agents
In packages/koenig-lexical/src/plugins/TKPlugin.jsx around lines 131 to 140,
tkClasses and tkHighlightClasses may be empty or contain empty tokens which
causes classList.add/remove to throw; sanitize the token arrays (trim strings,
filter out empty strings) when building tkClasses/tkHighlightClasses and only
call element.classList.add(...) or .remove(...) if the resulting array length >
0 (i.e. guard the calls), and optionally ensure the theme.tk and
theme.tkHighlighted source values are normalized (split on whitespace, trim,
filter) to prevent empty tokens upstream.

Comment on lines +208 to 210
const effectiveTopLevel = getEffectiveTopLevelElement($getNodeByKey(tkNodeKey));
const parentNodeKey = effectiveTopLevel?.getKey();
const topLevelNodeKey = parentEditorNodeKey || parentNodeKey;
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

🧩 Analysis chain

Guard addEditorTkNode when no container key is resolved

Effective container can be null in rare cases; avoid inserting undefined keys into tkNodeMap.

Apply:

-const topLevelNodeKey = parentEditorNodeKey || parentNodeKey;
-addEditorTkNode(editor.getKey(), topLevelNodeKey, tkNodeKey);
+const topLevelNodeKey = parentEditorNodeKey || parentNodeKey;
+if (topLevelNodeKey) {
+    addEditorTkNode(editor.getKey(), topLevelNodeKey, tkNodeKey);
+}

If you suspect this never happens in practice, quick check:


🏁 Script executed:

#!/bin/bash
# Look for any safeguards already in addEditorTkNode implementation
rg -nC2 "function\s+addEditorTkNode|addEditorTkNode\s*\(" --type=js --type=jsx

Length of output: 27


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "PWD: $(pwd)"
echo "Git branch: $(git rev-parse --abbrev-ref HEAD || true)"
echo "--- Search for addEditorTkNode occurrences ---"
rg -nC3 "addEditorTkNode" || true
echo "--- Search for possible definitions ---"
rg -nC3 "function\s+addEditorTkNode" || true
rg -nC3 "const\s+addEditorTkNode\s*=" || true
rg -nC3 "export\s+function\s+addEditorTkNode" || true
echo "--- Search for tkNodeMap occurrences ---"
rg -nC3 "tkNodeMap" || true
echo "--- Show packages/koenig-lexical/src/plugins/TKPlugin.jsx (first 300 lines) ---"
if [ -f "packages/koenig-lexical/src/plugins/TKPlugin.jsx" ]; then
  sed -n '1,300p' packages/koenig-lexical/src/plugins/TKPlugin.jsx
else
  echo "TKPlugin.jsx not found at expected path; listing directory:"
  ls -la packages/koenig-lexical/src/plugins || true
fi

Length of output: 18029


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "PWD: $(pwd)"
echo "---- show addEditorTkNode occurrences ----"
rg -nC3 "addEditorTkNode" packages/koenig-lexical || true
echo "---- print TKContext.jsx ----"
if [ -f "packages/koenig-lexical/src/context/TKContext.jsx" ]; then
  sed -n '1,260p' packages/koenig-lexical/src/context/TKContext.jsx
  echo "---- remainder ----"
  sed -n '261,520p' packages/koenig-lexical/src/context/TKContext.jsx || true
else
  echo "packages/koenig-lexical/src/context/TKContext.jsx not found"
  ls -la packages/koenig-lexical/src || true
fi

Length of output: 6996


Guard addEditorTkNode when no container key is resolved

Effective container can be null; avoid inserting undefined keys into tkNodeMap.

Apply:

-const topLevelNodeKey = parentEditorNodeKey || parentNodeKey;
-addEditorTkNode(editor.getKey(), topLevelNodeKey, tkNodeKey);
+const topLevelNodeKey = parentEditorNodeKey || parentNodeKey;
+if (topLevelNodeKey) {
+    addEditorTkNode(editor.getKey(), topLevelNodeKey, tkNodeKey);
+}

File: packages/koenig-lexical/src/plugins/TKPlugin.jsx (around lines 208-211)

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const effectiveTopLevel = getEffectiveTopLevelElement($getNodeByKey(tkNodeKey));
const parentNodeKey = effectiveTopLevel?.getKey();
const topLevelNodeKey = parentEditorNodeKey || parentNodeKey;
const effectiveTopLevel = getEffectiveTopLevelElement($getNodeByKey(tkNodeKey));
const parentNodeKey = effectiveTopLevel?.getKey();
const topLevelNodeKey = parentEditorNodeKey || parentNodeKey;
if (topLevelNodeKey) {
addEditorTkNode(editor.getKey(), topLevelNodeKey, tkNodeKey);
}
🤖 Prompt for AI Agents
In packages/koenig-lexical/src/plugins/TKPlugin.jsx around lines 208-210, the
code computes topLevelNodeKey from an effective top-level element which can be
null and may lead to inserting undefined into tkNodeMap; before calling
addEditorTkNode (or otherwise adding entries keyed by topLevelNodeKey), guard by
checking that topLevelNodeKey is truthy (e.g., if (!topLevelNodeKey)
return/continue or skip the add) so no undefined keys are inserted into
tkNodeMap.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (2)
packages/koenig-lexical/src/plugins/TKPlugin.jsx (2)

50-53: Sanitize class tokens; guard classList.add/remove to avoid TypeError

Empty/undefined theme tokens lead to classList.add/remove being called with 0 args.

Apply:

-    const tkClasses = editor._config.theme.tk?.split(' ') || [];
-    const tkHighlightClasses = editor._config.theme.tkHighlighted?.split(' ') || [];
+    const tkClasses = (editor._config.theme.tk || '').split(/\s+/).filter(Boolean);
+    const tkHighlightClasses = (editor._config.theme.tkHighlighted || '').split(/\s+/).filter(Boolean);
@@
-            if (isHighlighted) {
-                element.classList.remove(...tkClasses);
-                element.classList.add(...tkHighlightClasses);
-            } else {
-                element.classList.add(...tkClasses);
-                element.classList.remove(...tkHighlightClasses);
-            }
+            if (isHighlighted) {
+                if (tkClasses.length) element.classList.remove(...tkClasses);
+                if (tkHighlightClasses.length) element.classList.add(...tkHighlightClasses);
+            } else {
+                if (tkClasses.length) element.classList.add(...tkClasses);
+                if (tkHighlightClasses.length) element.classList.remove(...tkHighlightClasses);
+            }

Also applies to: 131-137


210-214: Guard addEditorTkNode when no container key is resolved

Avoid inserting undefined keys into tkNodeMap.

-                        const topLevelNodeKey = parentEditorNodeKey || parentNodeKey;
-                        addEditorTkNode(editor.getKey(), topLevelNodeKey, tkNodeKey);
+                        const topLevelNodeKey = parentEditorNodeKey || parentNodeKey;
+                        if (topLevelNodeKey) {
+                            addEditorTkNode(editor.getKey(), topLevelNodeKey, tkNodeKey);
+                        }
🧹 Nitpick comments (2)
packages/koenig-lexical/src/plugins/TKPlugin.jsx (2)

15-18: Optional: make horizontal positioning RTL-aware

Right-only offset misplaces indicators in RTL. Compute side from root direction.

-const INDICATOR_OFFSET_RIGHT = -56;
+const INDICATOR_OFFSET_RIGHT = -56; // default inline-end offset for LTR
@@
-    const style = {
-        top: `${position.top}px`,
-        right: `${position.right}px`
-    };
+    const direction = getComputedStyle(rootElement).direction;
+    const style = direction === 'rtl'
+        ? {top: `${position.top}px`, left: `${position.right}px`}
+        : {top: `${position.top}px`, right: `${position.right}px`};

Also applies to: 168-171


19-23: Nit: drop NODE_TYPES indirection; use tagName directly

Simplify by removing NODE_TYPES and checking tagName.

-// Node type constants
-const NODE_TYPES = {
-    LIST_ITEM: 'LI'
-};
@@
-        const positioningElement = containingElement.nodeName === NODE_TYPES.LIST_ITEM
+        const positioningElement = containingElement.tagName === 'LI'
             ? containingElement
             : containingElement.querySelector('[data-kg-card]') || containingElement;

Also applies to: 63-67

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c3aec84 and bd7544d.

📒 Files selected for processing (1)
  • packages/koenig-lexical/src/plugins/TKPlugin.jsx (7 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
packages/koenig-lexical/**/*.{tsx,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

packages/koenig-lexical/**/*.{tsx,jsx}: Use Tailwind CSS utility classes where possible in React components
Import SVGs as React components instead of using them as image sources

Files:

  • packages/koenig-lexical/src/plugins/TKPlugin.jsx
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Node 22.13.1
  • GitHub Check: Node 20.11.1
🔇 Additional comments (4)
packages/koenig-lexical/src/plugins/TKPlugin.jsx (4)

4-4: Good: list-aware imports added

Using $isListNode/$isListItemNode is the right approach for robust list detection.


24-49: Effective container resolution: LGTM (please verify nested lists)

Logic correctly prefers the nearest LI when top-level is a list. Consider adding a test for nested lists (LI > UL/OL > LI) to ensure the nearest LI is used.


163-167: Hooks order fix: LGTM

Early return moved after hooks; complies with Rules of Hooks.


297-303: TKIndicators: null-guard render is correct

Returning null (then filtered) avoids rendering indicators for missing containers.

Comment on lines 56 to 67
// position element relative to the TK Node containing element
const calculatePosition = useCallback(() => {
let top = 0;
let right = -56;
let right = INDICATOR_OFFSET_RIGHT;

const rootElementRect = rootElement.getBoundingClientRect();

const positioningElement = containingElement.querySelector('[data-kg-card]') || containingElement;
// Determine positioning element based on container type
const positioningElement = containingElement.nodeName === NODE_TYPES.LIST_ITEM
? containingElement
: containingElement.querySelector('[data-kg-card]') || containingElement;

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix crash when containingElement is null (initial render + observer)

calculatePosition dereferences containingElement before the post-hook early return; useEffect also observes a null element. Both can throw.

Apply:

-    const calculatePosition = useCallback(() => {
-        let top = 0;
-        let right = INDICATOR_OFFSET_RIGHT;
+    const calculatePosition = useCallback(() => {
+        // Guard: container may be missing during initial renders/mutations
+        if (!containingElement) {
+            return {top: 0, right: INDICATOR_OFFSET_RIGHT};
+        }
+        let top = 0;
+        let right = INDICATOR_OFFSET_RIGHT;
@@
-        const positioningElement = containingElement.nodeName === NODE_TYPES.LIST_ITEM
+        const positioningElement = (containingElement.tagName === 'LI')
             ? containingElement
             : containingElement.querySelector('[data-kg-card]') || containingElement;
@@
-    const [position, setPosition] = useState(calculatePosition());
+    const [position, setPosition] = useState(calculatePosition());
@@
-        observer.observe(rootElement);
-        observer.observe(containingElement);
+        observer.observe(rootElement);
+        if (containingElement) {
+            observer.observe(containingElement);
+        }

Also applies to: 79-79, 150-161

🤖 Prompt for AI Agents
In packages/koenig-lexical/src/plugins/TKPlugin.jsx around lines 56-67 (and
additionally at 79 and 150-161), calculatePosition and the useEffect observer
assume containingElement (and rootElement) are non-null and call methods on them
causing crashes on initial render or when the observer supplies null; add
null/undefined guards at the start of calculatePosition and in the useEffect
callbacks so they return early if containingElement or rootElement is falsy, and
when determining positioningElement only access containingElement.nodeName or
querySelector after confirming containingElement exists; ensure the callback
dependencies remain correct and that any observers only attach when the element
references are present.

- Extract positioning logic into dedicated helper functions
- Consolidate getPositioningElement and calculateRightOffset functions
- Simplify calculatePosition callback with cleaner, more readable flow
- Improve separation of concerns and maintainability
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.

1 participant