Skip to content

Commit

Permalink
Table Cell Background Color (#4306)
Browse files Browse the repository at this point in the history
  • Loading branch information
zurfyx authored Apr 17, 2023
1 parent 1270716 commit 6a239b6
Show file tree
Hide file tree
Showing 11 changed files with 307 additions and 114 deletions.
36 changes: 33 additions & 3 deletions packages/lexical-playground/__tests__/e2e/Tables.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
SAMPLE_IMAGE_URL,
selectCellsFromTableCords,
selectFromAdditionalStylesDropdown,
setBackgroundColor,
test,
unmergeTableCell,
} from '../utils/index.mjs';
Expand Down Expand Up @@ -1484,7 +1485,6 @@ test.describe('Tables', () => {
);
await mergeTableCells(page);
await insertTableColumnBefore(page);
await page.pause();

await assertHTML(
page,
Expand Down Expand Up @@ -1646,9 +1646,7 @@ test.describe('Tables', () => {
await insertTable(page, 1, 1);
await selectAll(page);

await page.pause();
await click(page, 'div[contenteditable="true"] p:first-of-type');
await page.pause();

await assertSelection(page, {
anchorOffset: 3,
Expand All @@ -1657,4 +1655,36 @@ test.describe('Tables', () => {
focusPath: [0, 0, 0],
});
});

test('Background color to cell', async ({page, isPlainText}) => {
test.skip(isPlainText);
if (IS_COLLAB) {
// The contextual menu positioning needs fixing (it's hardcoded to show on the right side)
page.setViewportSize({height: 1000, width: 3000});
}

await focusEditor(page);

await insertTable(page, 1, 1);
await setBackgroundColor(page);
await click(page, '.color-picker-basic-color button');
await click(page, '.Modal__closeButton');

await assertHTML(
page,
html`
<p class="PlaygroundEditorTheme__paragraph"><br /></p>
<table class="PlaygroundEditorTheme__table">
<tr>
<th
class="PlaygroundEditorTheme__tableCell PlaygroundEditorTheme__tableCellHeader"
style="background-color: rgb(208, 2, 27)">
<p class="PlaygroundEditorTheme__paragraph"><br /></p>
</th>
</tr>
</table>
<p class="PlaygroundEditorTheme__paragraph"><br /></p>
`,
);
});
});
5 changes: 5 additions & 0 deletions packages/lexical-playground/__tests__/utils/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -826,6 +826,11 @@ export async function deleteTable(page) {
await click(page, '.item[data-test-id="table-delete"]');
}

export async function setBackgroundColor(page) {
await click(page, '.table-cell-action-button-container');
await click(page, '.item[data-test-id="table-background-color"]');
}

export async function enableCompositionKeyEvents(page) {
const targetPage = IS_COLLAB ? await page.frame('left') : page;
await targetPage.evaluate(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@
*
*/

import type {DEPRECATED_GridCellNode, ElementNode} from 'lexical';
import type {
DEPRECATED_GridCellNode,
ElementNode,
LexicalEditor,
} from 'lexical';

import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import useLexicalEditable from '@lexical/react/useLexicalEditable';
Expand Down Expand Up @@ -45,6 +49,9 @@ import {ReactPortal, useCallback, useEffect, useRef, useState} from 'react';
import {createPortal} from 'react-dom';
import invariant from 'shared/invariant';

import useModal from '../../hooks/useModal';
import ColorPicker from '../../ui/ColorPicker';

function computeSelectionCount(selection: GridSelection): {
columns: number;
rows: number;
Expand Down Expand Up @@ -134,10 +141,30 @@ function $selectLastDescendant(node: ElementNode): void {
}
}

function currentCellBackgroundColor(editor: LexicalEditor): null | string {
return editor.getEditorState().read(() => {
const selection = $getSelection();
if (
$isRangeSelection(selection) ||
DEPRECATED_$isGridSelection(selection)
) {
const [cell] = DEPRECATED_$getNodeTriplet(selection.anchor);
if ($isTableCellNode(cell)) {
return cell.getBackgroundColor();
}
}
return null;
});
}

type TableCellActionMenuProps = Readonly<{
contextRef: {current: null | HTMLElement};
onClose: () => void;
setIsMenuOpen: (isOpen: boolean) => void;
showColorPickerModal: (
title: string,
showModal: (onClose: () => void) => JSX.Element,
) => void;
tableCellNode: TableCellNode;
cellMerge: boolean;
}>;
Expand All @@ -148,6 +175,7 @@ function TableActionMenu({
setIsMenuOpen,
contextRef,
cellMerge,
showColorPickerModal,
}: TableCellActionMenuProps) {
const [editor] = useLexicalComposerContext();
const dropDownRef = useRef<HTMLDivElement | null>(null);
Expand All @@ -158,6 +186,9 @@ function TableActionMenu({
});
const [canMergeCells, setCanMergeCells] = useState(false);
const [canUnmergeCell, setCanUnmergeCell] = useState(false);
const [backgroundColor, setBackgroundColor] = useState(
() => currentCellBackgroundColor(editor) || '',
);

useEffect(() => {
return editor.registerMutationListener(TableCellNode, (nodeMutations) => {
Expand All @@ -168,6 +199,7 @@ function TableActionMenu({
editor.getEditorState().read(() => {
updateTableCellNode(tableCellNode.getLatest());
});
setBackgroundColor(currentCellBackgroundColor(editor) || '');
}
});
}, [editor, tableCellNode]);
Expand Down Expand Up @@ -410,6 +442,24 @@ function TableActionMenu({
});
}, [editor, tableCellNode, clearTableSelection, onClose]);

const handleCellBackgroundColor = useCallback(
(value: string) => {
editor.update(() => {
const selection = $getSelection();
if (
$isRangeSelection(selection) ||
DEPRECATED_$isGridSelection(selection)
) {
const [cell] = DEPRECATED_$getNodeTriplet(selection.anchor);
if ($isTableCellNode(cell)) {
cell.setBackgroundColor(value);
}
}
});
},
[editor],
);

let mergeCellButton: null | JSX.Element = null;
if (cellMerge) {
if (canMergeCells) {
Expand Down Expand Up @@ -441,12 +491,21 @@ function TableActionMenu({
onClick={(e) => {
e.stopPropagation();
}}>
{mergeCellButton !== null && (
<>
{mergeCellButton}
<hr />
</>
)}
{mergeCellButton}
<button
className="item"
onClick={() =>
showColorPickerModal('Cell background color', () => (
<ColorPicker
color={backgroundColor}
onChange={handleCellBackgroundColor}
/>
))
}
data-test-id="table-background-color">
<span className="text">Background color</span>
</button>
<hr />
<button
className="item"
onClick={() => insertTableRowAtSelection(false)}
Expand Down Expand Up @@ -552,6 +611,8 @@ function TableCellActionMenuContainer({
null,
);

const [colorPickerModal, showColorPickerModal] = useModal();

const moveMenu = useCallback(() => {
const menu = menuButtonRef.current;
const selection = $getSelection();
Expand Down Expand Up @@ -650,13 +711,15 @@ function TableCellActionMenuContainer({
ref={menuRootRef}>
<i className="chevron-down" />
</button>
{colorPickerModal}
{isMenuOpen && (
<TableActionMenu
contextRef={menuRootRef}
setIsMenuOpen={setIsMenuOpen}
onClose={() => setIsMenuOpen(false)}
tableCellNode={tableCellNode}
cellMerge={cellMerge}
showColorPickerModal={showColorPickerModal}
/>
)}
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,8 @@ import {IS_APPLE} from 'shared/environment';
import useModal from '../../hooks/useModal';
import catTypingGif from '../../images/cat-typing.gif';
import {$createStickyNode} from '../../nodes/StickyNode';
import ColorPicker from '../../ui/ColorPicker';
import DropDown, {DropDownItem} from '../../ui/DropDown';
import DropdownColorPicker from '../../ui/DropdownColorPicker';
import {getSelectedNode} from '../../utils/getSelectedNode';
import {sanitizeUrl} from '../../utils/url';
import {EmbedConfigs} from '../AutoEmbedPlugin';
Expand Down Expand Up @@ -759,7 +759,7 @@ export default function ToolbarPlugin(): JSX.Element {
type="button">
<i className="format link" />
</button>
<ColorPicker
<DropdownColorPicker
disabled={!isEditable}
buttonClassName="toolbar-item color-picker"
buttonAriaLabel="Formatting text color"
Expand All @@ -768,7 +768,7 @@ export default function ToolbarPlugin(): JSX.Element {
onChange={onFontColorSelect}
title="text color"
/>
<ColorPicker
<DropdownColorPicker
disabled={!isEditable}
buttonClassName="toolbar-item color-picker"
buttonAriaLabel="Formatting background color"
Expand Down
103 changes: 42 additions & 61 deletions packages/lexical-playground/src/ui/ColorPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,14 @@

import './ColorPicker.css';

import {ReactNode, useEffect, useMemo, useRef, useState} from 'react';
import {useEffect, useMemo, useRef, useState} from 'react';
import * as React from 'react';

import DropDown from './DropDown';
import TextInput from './TextInput';

interface ColorPickerProps {
disabled?: boolean;
buttonAriaLabel?: string;
buttonClassName: string;
buttonIconClassName?: string;
buttonLabel?: string;
color: string;
children?: ReactNode;
onChange?: (color: string) => void;
stopCloseOnClickSelf?: boolean;
title?: string;
}

const basicColors = [
Expand All @@ -50,11 +41,7 @@ const HEIGHT = 150;

export default function ColorPicker({
color,
children,
onChange,
disabled = false,
stopCloseOnClickSelf = true,
...rest
}: Readonly<ColorPickerProps>): JSX.Element {
const [selfColor, setSelfColor] = useState(transformColor('hex', color));
const [inputColor, setInputColor] = useState(color);
Expand Down Expand Up @@ -118,57 +105,51 @@ export default function ColorPicker({
}, [color]);

return (
<DropDown
{...rest}
disabled={disabled}
stopCloseOnClickSelf={stopCloseOnClickSelf}>
<div
className="color-picker-wrapper"
style={{width: WIDTH}}
ref={innerDivRef}>
<TextInput label="Hex" onChange={onSetHex} value={inputColor} />
<div className="color-picker-basic-color">
{basicColors.map((basicColor) => (
<button
className={basicColor === selfColor.hex ? ' active' : ''}
key={basicColor}
style={{backgroundColor: basicColor}}
onClick={() => {
setInputColor(basicColor);
setSelfColor(transformColor('hex', basicColor));
}}
/>
))}
</div>
<MoveWrapper
className="color-picker-saturation"
style={{backgroundColor: `hsl(${selfColor.hsv.h}, 100%, 50%)`}}
onChange={onMoveSaturation}>
<div
className="color-picker-saturation_cursor"
style={{
backgroundColor: selfColor.hex,
left: saturationPosition.x,
top: saturationPosition.y,
}}
/>
</MoveWrapper>
<MoveWrapper className="color-picker-hue" onChange={onMoveHue}>
<div
className="color-picker-hue_cursor"
style={{
backgroundColor: `hsl(${selfColor.hsv.h}, 100%, 50%)`,
left: huePosition.x,
<div
className="color-picker-wrapper"
style={{width: WIDTH}}
ref={innerDivRef}>
<TextInput label="Hex" onChange={onSetHex} value={inputColor} />
<div className="color-picker-basic-color">
{basicColors.map((basicColor) => (
<button
className={basicColor === selfColor.hex ? ' active' : ''}
key={basicColor}
style={{backgroundColor: basicColor}}
onClick={() => {
setInputColor(basicColor);
setSelfColor(transformColor('hex', basicColor));
}}
/>
</MoveWrapper>
))}
</div>
<MoveWrapper
className="color-picker-saturation"
style={{backgroundColor: `hsl(${selfColor.hsv.h}, 100%, 50%)`}}
onChange={onMoveSaturation}>
<div
className="color-picker-color"
style={{backgroundColor: selfColor.hex}}
className="color-picker-saturation_cursor"
style={{
backgroundColor: selfColor.hex,
left: saturationPosition.x,
top: saturationPosition.y,
}}
/>
</div>
{children}
</DropDown>
</MoveWrapper>
<MoveWrapper className="color-picker-hue" onChange={onMoveHue}>
<div
className="color-picker-hue_cursor"
style={{
backgroundColor: `hsl(${selfColor.hsv.h}, 100%, 50%)`,
left: huePosition.x,
}}
/>
</MoveWrapper>
<div
className="color-picker-color"
style={{backgroundColor: selfColor.hex}}
/>
</div>
);
}

Expand Down
Loading

2 comments on commit 6a239b6

@vercel
Copy link

@vercel vercel bot commented on 6a239b6 Apr 17, 2023

Choose a reason for hiding this comment

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

Successfully deployed to the following URLs:

lexical – ./packages/lexical-website

lexical-git-main-fbopensource.vercel.app
lexicaljs.com
lexicaljs.org
lexical.dev
lexical-fbopensource.vercel.app
www.lexical.dev

@vercel
Copy link

@vercel vercel bot commented on 6a239b6 Apr 17, 2023

Choose a reason for hiding this comment

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

Successfully deployed to the following URLs:

lexical-playground – ./packages/lexical-playground

lexical-playground-git-main-fbopensource.vercel.app
lexical-playground.vercel.app
lexical-playground-fbopensource.vercel.app
playground.lexical.dev

Please sign in to comment.