Skip to content

Conversation

@lsinger
Copy link

@lsinger lsinger commented Sep 18, 2025

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

  • 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

    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 accurately summarizes the main change: implementing list-item-level TK detection and positioning in the TKPlugin, which is directly supported by the code changes.
    Description check ✅ Passed The description is directly related to the changeset and provides clear context about the problem, solution, and benefits of the changes made to TK detection for list items.
    ✨ Finishing touches
    • 📝 Generate docstrings
    🧪 Generate unit tests (beta)
    • Create PR with unit tests
    • Post copyable unit tests in a comment
    • Commit unit tests in branch issue/PROD-2389

    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.

    2 participants