Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[lexical-list] Bullet item color matches text color #7024

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion packages/lexical-list/src/LexicalListItemNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,6 @@ export class ListItemNode extends ElementNode {
// @ts-expect-error - this is always HTMLListItemElement
dom.value = this.__value;
$setListItemThemeClassNames(dom, config.theme, this);

return false;
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I see, could you explain to me the general rule here. Why are we avoiding returning 'true' in updateDOM with something like (this.style != prevNode.style), but we are always ok on doing the comparison and changing this in here and returning false. Does updateDOM returning true, trigger a waterfall of updates that is unnecessary?

Copy link
Collaborator

Choose a reason for hiding this comment

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

The check here is for performance and to avoid having style=“” show up in the DOM, it could be unconditionally set.

returning true should be avoided whenever possible, it creates a lot more work on the browser and can lose state if there’s anything ephemeral in the DOM. Same reason why react does DOM diffing instead of rendering everything from scratch, just more manual here.

}

Expand Down
49 changes: 48 additions & 1 deletion packages/lexical-list/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,17 @@

import type {SerializedListItemNode} from './LexicalListItemNode';
import type {ListType, SerializedListNode} from './LexicalListNode';
import type {LexicalCommand, LexicalEditor} from 'lexical';
import type {LexicalCommand, LexicalEditor, LexicalNode} from 'lexical';

import {mergeRegister} from '@lexical/utils';
import {
$getNodeByKey,
$getSelection,
$isRangeSelection,
COMMAND_PRIORITY_LOW,
createCommand,
INSERT_PARAGRAPH_COMMAND,
TextNode,
} from 'lexical';

import {
Expand Down Expand Up @@ -97,6 +101,49 @@ export function registerList(editor: LexicalEditor): () => void {
},
COMMAND_PRIORITY_LOW,
),
editor.registerMutationListener(
Copy link
Collaborator

Choose a reason for hiding this comment

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

Using an editor.update in a mutation listener causes an update cascade, if this were directly updating the DOM style and not the node's state it wouldn't be a problem but this will cause a second reconciliation as implemented. Is there a reason why this can't work as a transform? I think it should be fine so long as you check to make sure that the style actually changed before setting it (and thus marking the node dirty again, causing a second iteration of the transform)

Copy link
Collaborator Author

@ivailop7 ivailop7 Jan 20, 2025

Choose a reason for hiding this comment

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

Hmm, I did remove it, but it was throwing me the error that it couldn't find an activeEditor, I'll try again, in case it was an HMR thing, but I tried a few times. OK, will look into making this a transform vs mutationListener.

Copy link
Collaborator

Choose a reason for hiding this comment

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

well if you are reading or updating the editor from a mutation listener then you do need a read or update call, it doesn't put you in a specific editor state because there are two to choose from. Updating is something that shouldn't happen in a mutation listener for the same reason that it shouldn't happen in an update listener (an update listener is basically a mutation listener for the whole editor state)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ok, revisiting this again. Your suggestion is to have 1 node transform on TextNode-level and have the whole logic in there or just convert the current mutationlistener to a nodetransform. I did the convert of this method, but it doesn't reflect correctly if I already have the style applied and then create a bullet, it only updates after the first character is typed. I've pushed, despite not working to get a bit better clarity

Copy link
Collaborator

Choose a reason for hiding this comment

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

This implementation is not what I meant, a node transform shouldn't do anything with the DOM, it can just modify the lexical nodes which will reflect to the DOM with updateDOM and/or createDOM. It's possible I misread the previous commit, I'll take a look at it locally in the next day or so.

ListItemNode,
(mutations) => {
editor.update(() => {
for (const [key, type] of mutations) {
if (type !== 'destroyed') {
const node = $getNodeByKey<ListItemNode>(key);
const listItemElement = editor.getElementByKey(key);
if (node && listItemElement) {
const firstChild = node.getFirstChild<LexicalNode>();
if (firstChild) {
const textElement = editor.getElementByKey(
firstChild.getKey(),
);
if (textElement && textElement.style.cssText) {
listItemElement.setAttribute(
'style',
textElement.style.cssText,
);
}
} else {
const selection = $getSelection();
if (
$isRangeSelection(selection) &&
selection.isCollapsed() &&
selection.style
) {
listItemElement.setAttribute('style', selection.style);
}
}
}
}
}
});
},
{skipInitialization: false},
),
editor.registerNodeTransform(TextNode, (node) => {
const listItemParentNode = node.getParent();
if ($isListItemNode(listItemParentNode)) {
listItemParentNode.markDirty();
}
}),
);
return removeListener;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/lexical-playground/__tests__/e2e/List.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ test.describe.parallel('Nested List', () => {

await assertHTML(
page,
'<ul class="PlaygroundEditorTheme__ul"><li value="1" class="PlaygroundEditorTheme__listItem PlaygroundEditorTheme__ltr" dir="ltr"><strong class="PlaygroundEditorTheme__textBold" style="color: rgb(208, 2, 27)" data-lexical-text="true">Hello</strong></li></ul><p class="PlaygroundEditorTheme__paragraph PlaygroundEditorTheme__ltr" dir="ltr"><strong class="PlaygroundEditorTheme__textBold" style="color: rgb(208, 2, 27)" data-lexical-text="true">World</strong></p>',
'<ul class="PlaygroundEditorTheme__ul"><li value="1" class="PlaygroundEditorTheme__listItem PlaygroundEditorTheme__ltr" dir="ltr" style="color: rgb(208, 2, 27)"><strong class="PlaygroundEditorTheme__textBold" style="color: rgb(208, 2, 27)" data-lexical-text="true">Hello</strong></li></ul><p class="PlaygroundEditorTheme__paragraph PlaygroundEditorTheme__ltr" dir="ltr"><strong class="PlaygroundEditorTheme__textBold" style="color: rgb(208, 2, 27)" data-lexical-text="true">World</strong></p>',
);
});

Expand Down
Loading