diff --git a/packages/lexical-playground/src/Editor.tsx b/packages/lexical-playground/src/Editor.tsx
index c8eef486825..1284efb2da1 100644
--- a/packages/lexical-playground/src/Editor.tsx
+++ b/packages/lexical-playground/src/Editor.tsx
@@ -59,7 +59,6 @@ import EquationsPlugin from './plugins/EquationsPlugin';
import ExcalidrawPlugin from './plugins/ExcalidrawPlugin';
import FigmaPlugin from './plugins/FigmaPlugin';
import FloatingLinkEditorPlugin from './plugins/FloatingLinkEditorPlugin';
-import TableHoverActionsV2Plugin from './plugins/FloatingTableTopBorderPlugin';
import FloatingTextFormatToolbarPlugin from './plugins/FloatingTextFormatToolbarPlugin';
import ImagesPlugin from './plugins/ImagesPlugin';
import KeywordsPlugin from './plugins/KeywordsPlugin';
@@ -76,6 +75,7 @@ import SpeechToTextPlugin from './plugins/SpeechToTextPlugin';
import TabFocusPlugin from './plugins/TabFocusPlugin';
import TableCellActionMenuPlugin from './plugins/TableActionMenuPlugin';
import TableCellResizer from './plugins/TableCellResizer';
+import TableHoverActionsV2Plugin from './plugins/TableHoverActionsV2Plugin';
import TableOfContentsPlugin from './plugins/TableOfContentsPlugin';
import TableScrollShadowPlugin from './plugins/TableScrollShadowPlugin';
import ToolbarPlugin from './plugins/ToolbarPlugin';
diff --git a/packages/lexical-playground/src/images/icons/filter-left.svg b/packages/lexical-playground/src/images/icons/filter-left.svg
new file mode 100644
index 00000000000..9049b0f9ea6
--- /dev/null
+++ b/packages/lexical-playground/src/images/icons/filter-left.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/packages/lexical-playground/src/plugins/FloatingTableTopBorderPlugin/index.css b/packages/lexical-playground/src/plugins/FloatingTableTopBorderPlugin/index.css
deleted file mode 100644
index 6083a17df05..00000000000
--- a/packages/lexical-playground/src/plugins/FloatingTableTopBorderPlugin/index.css
+++ /dev/null
@@ -1,29 +0,0 @@
-/**
- * Copyright (c) Meta Platforms, Inc. and affiliates.
- *
- * This source code is licensed under the MIT license found in the
- * LICENSE file in the root directory of this source tree.
- *
- */
-
-.floating-add-indicator {
- background-image: url(../../images/icons/plus.svg);
- background-color: #ffffff;
- background-position: center;
- background-repeat: no-repeat;
- background-size: 12px 12px;
- border: 1px solid #d0d0d0;
- border-radius: 2px;
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
- box-sizing: border-box;
- height: 18px;
- pointer-events: auto;
- transition: opacity 80ms ease;
- width: 18px;
- z-index: 10;
- cursor: pointer;
-}
-
-.floating-add-indicator:hover {
- background-color: #f3f3f3;
-}
diff --git a/packages/lexical-playground/src/plugins/TableHoverActionsV2Plugin/index.css b/packages/lexical-playground/src/plugins/TableHoverActionsV2Plugin/index.css
new file mode 100644
index 00000000000..52f488eb5c4
--- /dev/null
+++ b/packages/lexical-playground/src/plugins/TableHoverActionsV2Plugin/index.css
@@ -0,0 +1,77 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ */
+
+.floating-add-indicator {
+ background-image: url(../../images/icons/plus.svg);
+ background-color: #ffffff;
+ background-position: center;
+ background-repeat: no-repeat;
+ background-size: 12px 12px;
+ border: 1px solid #d0d0d0;
+ border-radius: 2px;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
+ box-sizing: border-box;
+ height: 18px;
+ pointer-events: auto;
+ transition: opacity 80ms ease;
+ width: 18px;
+ z-index: 10;
+ cursor: pointer;
+}
+
+.floating-filter-indicator {
+ background-image: url(../../images/icons/filter-left.svg);
+ background-color: #ffffff;
+ background-position: center;
+ background-repeat: no-repeat;
+ background-size: 12px 12px;
+ border: 1px solid #d0d0d0;
+ border-radius: 2px;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
+ box-sizing: border-box;
+ height: 18px;
+ pointer-events: auto;
+ transition: opacity 80ms ease;
+ width: 18px;
+ z-index: 10;
+ cursor: pointer;
+}
+
+.floating-filter-indicator:hover {
+ background-color: #f3f3f3;
+}
+
+.floating-add-indicator:hover {
+ background-color: #f3f3f3;
+}
+
+.floating-top-actions {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ position: relative;
+}
+
+.floating-filter-container {
+ position: relative;
+}
+
+.floating-sort-menu__item {
+ width: 100%;
+ padding: 6px 12px;
+ background: transparent;
+ border: none;
+ text-align: left;
+ font-size: 12px;
+ color: #1f1f1f;
+ cursor: pointer;
+}
+
+.floating-sort-menu__item:hover {
+ background-color: #f3f3f3;
+}
diff --git a/packages/lexical-playground/src/plugins/FloatingTableTopBorderPlugin/index.tsx b/packages/lexical-playground/src/plugins/TableHoverActionsV2Plugin/index.tsx
similarity index 69%
rename from packages/lexical-playground/src/plugins/FloatingTableTopBorderPlugin/index.tsx
rename to packages/lexical-playground/src/plugins/TableHoverActionsV2Plugin/index.tsx
index 1022ffbbac2..b6122f10699 100644
--- a/packages/lexical-playground/src/plugins/FloatingTableTopBorderPlugin/index.tsx
+++ b/packages/lexical-playground/src/plugins/TableHoverActionsV2Plugin/index.tsx
@@ -5,28 +5,39 @@
* LICENSE file in the root directory of this source tree.
*
*/
-
-import type {VirtualElement} from '@floating-ui/react';
import type {JSX} from 'react';
import './index.css';
-import {autoUpdate, offset, shift, useFloating} from '@floating-ui/react';
+import {
+ autoUpdate,
+ offset,
+ shift,
+ useFloating,
+ type VirtualElement,
+} from '@floating-ui/react';
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import {useLexicalEditable} from '@lexical/react/useLexicalEditable';
import {
+ $computeTableMapSkipCellCheck,
$insertTableColumnAtSelection,
$insertTableRowAtSelection,
$isTableCellNode,
+ $isTableNode,
+ $isTableRowNode,
+ type TableNode,
} from '@lexical/table';
import {
+ $getChildCaret,
$getNearestNodeFromDOMNode,
- EditorThemeClasses,
+ $getSiblingCaret,
+ type EditorThemeClasses,
isHTMLElement,
} from 'lexical';
import {useEffect, useRef, useState} from 'react';
import {createPortal} from 'react-dom';
+import DropDown, {DropDownItem} from '../../ui/DropDown';
import {getThemeSelector} from '../../utils/getThemeSelector';
const INDICATOR_SIZE_PX = 18;
@@ -34,6 +45,39 @@ const SIDE_INDICATOR_SIZE_PX = 18;
const TOP_BUTTON_OVERHANG = INDICATOR_SIZE_PX / 2;
const LEFT_BUTTON_OVERHANG = SIDE_INDICATOR_SIZE_PX / 2;
+/**
+ * Checks if the table does not have any merged cells.
+ *
+ * @param table Table to check for if it has any merged cells.
+ * @returns True if the table does not have any merged cells, false otherwise.
+ */
+function $isSimpleTable(table: TableNode): boolean {
+ const rows = table.getChildren();
+ let columns: null | number = null;
+ for (const row of rows) {
+ if (!$isTableRowNode(row)) {
+ return false;
+ }
+ if (columns === null) {
+ columns = row.getChildrenSize();
+ }
+ if (row.getChildrenSize() !== columns) {
+ return false;
+ }
+ const cells = row.getChildren();
+ for (const cell of cells) {
+ if (
+ !$isTableCellNode(cell) ||
+ cell.getRowSpan() !== 1 ||
+ cell.getColSpan() !== 1
+ ) {
+ return false;
+ }
+ }
+ }
+ return (columns || 0) > 0;
+}
+
function getTableFromMouseEvent(
event: MouseEvent,
getTheme: () => EditorThemeClasses | null | undefined,
@@ -148,8 +192,12 @@ function TableHoverActionsV2({
const handleMouseMove = (event: MouseEvent) => {
if (
- event.target === floatingElemRef.current ||
- event.target === leftFloatingElemRef.current
+ (floatingElemRef.current &&
+ event.target instanceof Node &&
+ floatingElemRef.current.contains(event.target)) ||
+ (leftFloatingElemRef.current &&
+ event.target instanceof Node &&
+ leftFloatingElemRef.current.contains(event.target))
) {
return;
}
@@ -294,9 +342,73 @@ function TableHoverActionsV2({
});
};
+ const handleSortColumn = (direction: 'asc' | 'desc') => {
+ const targetCell = hoveredTopCellRef.current;
+ if (!targetCell) {
+ return;
+ }
+
+ editor.update(() => {
+ const cellNode = $getNearestNodeFromDOMNode(targetCell);
+ if (!$isTableCellNode(cellNode)) {
+ return;
+ }
+
+ const rowNode = cellNode.getParent();
+ if (!rowNode || !$isTableRowNode(rowNode)) {
+ return;
+ }
+
+ const tableNode = rowNode.getParent();
+ if (!$isTableNode(tableNode) || !$isSimpleTable(tableNode)) {
+ return;
+ }
+
+ const colIndex = cellNode.getIndexWithinParent();
+ const rows = tableNode.getChildren().filter($isTableRowNode);
+
+ const [tableMap] = $computeTableMapSkipCellCheck(
+ tableNode,
+ cellNode,
+ cellNode,
+ );
+
+ const headerCell = tableMap[0]?.[colIndex]?.cell;
+ const shouldSkipTopRow = headerCell?.hasHeader() ?? false;
+
+ const sortableRows = shouldSkipTopRow ? rows.slice(1) : rows;
+
+ if (sortableRows.length <= 1) {
+ return;
+ }
+
+ sortableRows.sort((a, b) => {
+ const aRowIndex = rows.indexOf(a);
+ const bRowIndex = rows.indexOf(b);
+
+ const aMapRow = tableMap[aRowIndex] ?? [];
+ const bMapRow = tableMap[bRowIndex] ?? [];
+
+ const aCellValue = aMapRow[colIndex];
+ const bCellValue = bMapRow[colIndex];
+
+ const aText = aCellValue?.cell.getTextContent() ?? '';
+ const bText = bCellValue?.cell.getTextContent() ?? '';
+ const result = aText.localeCompare(bText, undefined, {numeric: true});
+ return direction === 'asc' ? -result : result;
+ });
+
+ const insertionCaret = shouldSkipTopRow
+ ? $getSiblingCaret(rows[0], 'next')
+ : $getChildCaret(tableNode, 'next');
+
+ insertionCaret?.splice(0, sortableRows);
+ });
+ };
+
return (
<>
-