Skip to content

Commit adaaf21

Browse files
authored
[lexical-table] Freeze top row using pure CSS (#7190)
1 parent 0e98db1 commit adaaf21

File tree

5 files changed

+86
-0
lines changed

5 files changed

+86
-0
lines changed

packages/lexical-playground/src/plugins/TableActionMenuPlugin/index.tsx

+20
Original file line numberDiff line numberDiff line change
@@ -516,6 +516,19 @@ function TableActionMenu({
516516
});
517517
}, [editor, tableCellNode, clearTableSelection, onClose]);
518518

519+
const toggleFirstRowFreeze = useCallback(() => {
520+
editor.update(() => {
521+
if (tableCellNode.isAttached()) {
522+
const tableNode = $getTableNodeFromLexicalNodeOrThrow(tableCellNode);
523+
if (tableNode) {
524+
tableNode.setFrozenRows(tableNode.getFrozenRows() === 0 ? 1 : 0);
525+
}
526+
}
527+
clearTableSelection();
528+
onClose();
529+
});
530+
}, [editor, tableCellNode, clearTableSelection, onClose]);
531+
519532
const toggleFirstColumnFreeze = useCallback(() => {
520533
editor.update(() => {
521534
if (tableCellNode.isAttached()) {
@@ -670,6 +683,13 @@ function TableActionMenu({
670683
</div>
671684
</DropDownItem>
672685
</DropDown>
686+
<button
687+
type="button"
688+
className="item"
689+
onClick={() => toggleFirstRowFreeze()}
690+
data-test-id="table-freeze-first-row">
691+
<span className="text">Toggle First Row Freeze</span>
692+
</button>
673693
<button
674694
type="button"
675695
className="item"

packages/lexical-playground/src/themes/PlaygroundEditorTheme.css

+28
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,34 @@
221221
margin-top: 25px;
222222
margin-bottom: 30px;
223223
}
224+
.PlaygroundEditorTheme__tableFrozenRow {
225+
/* position:sticky needs overflow:clip or visible
226+
https://github.com/w3c/csswg-drafts/issues/865#issuecomment-350585274 */
227+
overflow-x: clip;
228+
}
229+
.PlaygroundEditorTheme__tableFrozenRow tr:nth-of-type(1) > td {
230+
overflow: clip;
231+
background-color: #ffffff;
232+
position: sticky;
233+
z-index: 2;
234+
top: 44px;
235+
}
236+
.PlaygroundEditorTheme__tableFrozenRow tr:nth-of-type(1) > th {
237+
overflow: clip;
238+
background-color: #f2f3f5;
239+
position: sticky;
240+
z-index: 2;
241+
top: 44px;
242+
}
243+
.PlaygroundEditorTheme__tableFrozenRow tr:nth-of-type(1) > th:after,
244+
.PlaygroundEditorTheme__tableFrozenRow tr:nth-of-type(1) > td:after {
245+
content: '';
246+
position: absolute;
247+
left: 0;
248+
bottom: 0;
249+
width: 100%;
250+
border-bottom: 1px solid #bbb;
251+
}
224252
.PlaygroundEditorTheme__tableFrozenColumn tr > td:first-child {
225253
background-color: #ffffff;
226254
position: sticky;

packages/lexical-playground/src/themes/PlaygroundEditorTheme.ts

+1
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ const theme: EditorThemeClasses = {
105105
tableCellResizer: 'PlaygroundEditorTheme__tableCellResizer',
106106
tableCellSelected: 'PlaygroundEditorTheme__tableCellSelected',
107107
tableFrozenColumn: 'PlaygroundEditorTheme__tableFrozenColumn',
108+
tableFrozenRow: 'PlaygroundEditorTheme__tableFrozenRow',
108109
tableRowStriping: 'PlaygroundEditorTheme__tableRowStriping',
109110
tableScrollableWrapper: 'PlaygroundEditorTheme__tableScrollableWrapper',
110111
tableSelected: 'PlaygroundEditorTheme__tableSelected',

packages/lexical-table/src/LexicalTableNode.ts

+36
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export type SerializedTableNode = Spread<
4949
colWidths?: readonly number[];
5050
rowStriping?: boolean;
5151
frozenColumnCount?: number;
52+
frozenRowCount?: number;
5253
},
5354
SerializedElementNode
5455
>;
@@ -103,6 +104,20 @@ function setFrozenColumns(
103104
}
104105
}
105106

107+
function setFrozenRows(
108+
dom: HTMLElement,
109+
config: EditorConfig,
110+
frozenRowCount: number,
111+
): void {
112+
if (frozenRowCount > 0) {
113+
addClassNamesToElement(dom, config.theme.tableFrozenRow);
114+
dom.setAttribute('data-lexical-frozen-row', 'true');
115+
} else {
116+
removeClassNamesFromElement(dom, config.theme.tableFrozenRow);
117+
dom.removeAttribute('data-lexical-frozen-row');
118+
}
119+
}
120+
106121
function alignTableElement(
107122
dom: HTMLElement,
108123
config: EditorConfig,
@@ -153,6 +168,7 @@ export class TableNode extends ElementNode {
153168
/** @internal */
154169
__rowStriping: boolean;
155170
__frozenColumnCount: number;
171+
__frozenRowCount: number;
156172
__colWidths?: readonly number[];
157173

158174
static getType(): string {
@@ -181,6 +197,7 @@ export class TableNode extends ElementNode {
181197
this.__colWidths = prevNode.__colWidths;
182198
this.__rowStriping = prevNode.__rowStriping;
183199
this.__frozenColumnCount = prevNode.__frozenColumnCount;
200+
this.__frozenRowCount = prevNode.__frozenRowCount;
184201
}
185202

186203
static importDOM(): DOMConversionMap | null {
@@ -201,13 +218,15 @@ export class TableNode extends ElementNode {
201218
.updateFromJSON(serializedNode)
202219
.setRowStriping(serializedNode.rowStriping || false)
203220
.setFrozenColumns(serializedNode.frozenColumnCount || 0)
221+
.setFrozenRows(serializedNode.frozenRowCount || 0)
204222
.setColWidths(serializedNode.colWidths);
205223
}
206224

207225
constructor(key?: NodeKey) {
208226
super(key);
209227
this.__rowStriping = false;
210228
this.__frozenColumnCount = 0;
229+
this.__frozenRowCount = 0;
211230
}
212231

213232
exportJSON(): SerializedTableNode {
@@ -217,6 +236,7 @@ export class TableNode extends ElementNode {
217236
frozenColumnCount: this.__frozenColumnCount
218237
? this.__frozenColumnCount
219238
: undefined,
239+
frozenRowCount: this.__frozenRowCount ? this.__frozenRowCount : undefined,
220240
rowStriping: this.__rowStriping ? this.__rowStriping : undefined,
221241
};
222242
}
@@ -259,6 +279,9 @@ export class TableNode extends ElementNode {
259279
if (this.__frozenColumnCount) {
260280
setFrozenColumns(tableElement, config, this.__frozenColumnCount);
261281
}
282+
if (this.__frozenRowCount) {
283+
setFrozenRows(tableElement, config, this.__frozenRowCount);
284+
}
262285
if (this.__rowStriping) {
263286
setRowStriping(tableElement, config, true);
264287
}
@@ -284,6 +307,9 @@ export class TableNode extends ElementNode {
284307
if (prevNode.__frozenColumnCount !== this.__frozenColumnCount) {
285308
setFrozenColumns(dom, config, this.__frozenColumnCount);
286309
}
310+
if (prevNode.__frozenRowCount !== this.__frozenRowCount) {
311+
setFrozenRows(dom, config, this.__frozenRowCount);
312+
}
287313
updateColgroup(dom, config, this.getColumnCount(), this.getColWidths());
288314
alignTableElement(
289315
this.getDOMSlot(dom).element,
@@ -510,6 +536,16 @@ export class TableNode extends ElementNode {
510536
return this.getLatest().__frozenColumnCount;
511537
}
512538

539+
setFrozenRows(rowCount: number): this {
540+
const self = this.getWritable();
541+
self.__frozenRowCount = rowCount;
542+
return self;
543+
}
544+
545+
getFrozenRows(): number {
546+
return this.getLatest().__frozenRowCount;
547+
}
548+
513549
canSelectBefore(): true {
514550
return true;
515551
}

packages/lexical-table/src/__tests__/unit/LexicalTableNode.test.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ const editorConfig = Object.freeze({
7878
right: 'test-table-alignment-right',
7979
},
8080
tableFrozenColumn: 'test-table-frozen-column-class',
81+
tableFrozenRow: 'test-table-frozen-row-class',
8182
tableRowStriping: 'test-table-row-striping-class',
8283
tableScrollableWrapper: 'table-scrollable-wrapper',
8384
},

0 commit comments

Comments
 (0)