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

Table Cell Background Color #4306

Merged
merged 5 commits into from
Apr 17, 2023
Merged
Show file tree
Hide file tree
Changes from 3 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
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);
Copy link
Contributor

Choose a reason for hiding this comment

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

I assume there isn't a non-deprecated alternative here?

Copy link
Member Author

Choose a reason for hiding this comment

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

The idea is to kill GridSelection eventually by moving it into the Table package itself. That's why it remains as deprecated while still actively using it

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}
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we no longer need to nil check the mergeCellButton?

Copy link
Member Author

Choose a reason for hiding this comment

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

Rendering a nullish component in React will not render it. It's a shorthand for foo != null ? foo : null

<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%)`}}
Copy link
Contributor

Choose a reason for hiding this comment

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

you've done this logic (modify an HSV colour to have 100% sat, 50% lightness) a few times; maybe factor into its own function?

Copy link
Member Author

Choose a reason for hiding this comment

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

Didn't write this file, just moved the files around. Sorry if the blame is confusing

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