Skip to content

Commit ed3e38b

Browse files
author
Amelia Wattenberger
committed
feat: make headers editable
1 parent cb1cc7b commit ed3e38b

File tree

5 files changed

+131
-32
lines changed

5 files changed

+131
-32
lines changed

src/components/editable-cell.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useEffect, useRef } from 'react';
1+
import React, { useEffect } from 'react';
22
import { areEqual } from 'react-window';
33
import tw from 'twin.macro';
44

src/components/editable-header.tsx

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import React, { useEffect } from 'react';
2+
import { areEqual } from 'react-window';
3+
import tw from 'twin.macro';
4+
5+
interface EditableHeaderProps {
6+
value: string;
7+
isEditable: boolean;
8+
onChange?: (value: any) => void;
9+
children: any;
10+
}
11+
export const EditableHeader = React.memo(function (props: EditableHeaderProps) {
12+
const {
13+
value,
14+
isEditable,
15+
onChange,
16+
children,
17+
} = props;
18+
19+
const [isEditing, setIsEditing] = React.useState(false);
20+
const [editedValue, setEditedValue] = React.useState(value);
21+
22+
useEffect(() => {
23+
setEditedValue(value);
24+
}, [value]);
25+
26+
const onSubmit = () => {
27+
onChange?.(editedValue);
28+
setIsEditing(false);
29+
}
30+
31+
if (!isEditable) return children
32+
33+
return (
34+
<div
35+
css={[
36+
tw`h-full w-full max-w-full flex items-center cursor-cell`,
37+
]}
38+
onClick={() => setIsEditing(true)}
39+
>
40+
{isEditing ? (
41+
<input
42+
type="text"
43+
autoFocus
44+
onFocus={e => {
45+
e.target.select();
46+
}}
47+
css={[
48+
tw`w-full h-full py-2 px-2 font-mono text-black text-sm focus:outline-none bg-transparent bg-white`,
49+
]}
50+
style={{ fontSize: "0.875rem" }}
51+
value={editedValue}
52+
onChange={e => setEditedValue(e.target.value)}
53+
onKeyDown={e => {
54+
if (e.key === 'Enter') {
55+
onSubmit()
56+
} else if (e.key === 'Escape') {
57+
setIsEditing(false)
58+
setEditedValue(value)
59+
}
60+
}}
61+
onBlur={() => {
62+
setIsEditing(false)
63+
setEditedValue(value)
64+
}}
65+
/>
66+
) : (
67+
children
68+
)}
69+
</div>
70+
);
71+
}, areEqual);

src/components/grid.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -741,6 +741,8 @@ const HeaderWrapper = function (props: CellProps) {
741741
handleSortChange,
742742
focusedRowIndex,
743743
cellTypes,
744+
isEditable,
745+
onHeaderCellChange,
744746
} = useGridStore();
745747
const columnNameRef = React.useRef('');
746748

@@ -770,6 +772,10 @@ const HeaderWrapper = function (props: CellProps) {
770772
let possibleValues =
771773
cellType === 'category' ? categoryValues[columnName] : undefined;
772774

775+
const onHeaderCellChangeLocal = (value: any) => {
776+
onHeaderCellChange(columnName, value);
777+
}
778+
773779
return (
774780
<HeaderWrapperComputed
775781
style={style}
@@ -787,6 +793,8 @@ const HeaderWrapper = function (props: CellProps) {
787793
isSticky={isSticky}
788794
metadata={metadata[columnName]}
789795
isFirstColumn={columnIndex === 0}
796+
isEditable={isEditable}
797+
onChange={onHeaderCellChangeLocal}
790798
onSort={handleSortChange}
791799
onSticky={() => handleStickyColumnNameChange(columnName)}
792800
onFilterChange={(value: FilterValue) => {
@@ -812,6 +820,8 @@ interface HeaderComputedProps {
812820
showFilters: boolean;
813821
isFirstColumn: boolean;
814822
isSticky: boolean;
823+
isEditable: boolean;
824+
onChange: (value: any) => void;
815825
onFilterChange: Function;
816826
onSort: Function;
817827
onSticky: Function;
@@ -828,6 +838,7 @@ const HeaderWrapperComputed = React.memo(
828838
if (props.filter != newProps.filter) return false;
829839
if (props.width != newProps.width) return false;
830840
if (props.isSticky != newProps.isSticky) return false;
841+
if (props.isEditable != newProps.isEditable) return false;
831842
if (props.focusedValue != newProps.focusedValue) return false;
832843
if (props.style.width != newProps.style.width) return false;
833844
if (props.style.left != newProps.style.left) return false;

src/components/header.tsx

Lines changed: 34 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
PinIcon,
77
} from '@primer/octicons-react';
88
import { FilterValue, CategoryValue } from '../types';
9+
import { EditableHeader } from './editable-header';
910

1011
interface HeaderProps {
1112
style: object;
@@ -23,6 +24,8 @@ interface HeaderProps {
2324
showFilters: boolean;
2425
isFirstColumn: boolean;
2526
isSticky: boolean;
27+
isEditable: boolean;
28+
onChange?: (value: any) => void;
2629
onFilterChange: Function;
2730
onSort: Function;
2831
onSticky: Function;
@@ -44,6 +47,8 @@ export function Header(props: HeaderProps) {
4447
showFilters,
4548
isFirstColumn,
4649
isSticky,
50+
isEditable,
51+
onChange,
4752
onFilterChange,
4853
onSort,
4954
onSticky,
@@ -64,7 +69,7 @@ export function Header(props: HeaderProps) {
6469
className="header"
6570
tw="relative border-b border-gray-200 bg-white flex items-center flex-shrink-0"
6671
style={{ height: 37 }}
67-
// ref={popoverAnchorRef}
72+
// ref={popoverAnchorRef}
6873
>
6974
<div
7075
className="header__title group"
@@ -80,44 +85,50 @@ export function Header(props: HeaderProps) {
8085
>
8186
<PinIcon />
8287
</button>
83-
<button
88+
<div
8489
className="group"
85-
tw="flex justify-between items-center h-full p-2 border-white focus:bg-white hover:bg-white appearance-none flex-1 min-w-0"
86-
onClick={() =>
87-
onSort(columnName, activeSortDirection == 'asc' ? 'desc' : 'asc')
88-
}
90+
tw="flex justify-between items-center h-full border-white focus:bg-white hover:bg-white appearance-none flex-1 min-w-0"
8991
>
90-
<span
91-
css={[
92-
tw`text-sm font-medium truncate text-left`,
93-
['integer', 'number'].includes(cellType) && tw`text-right`,
94-
]}
95-
title={columnName}
96-
style={{ minWidth: 'calc(100% - 1.5em)' }}
92+
<EditableHeader
93+
value={columnName}
94+
isEditable={isEditable}
95+
onChange={onChange}
9796
>
98-
{columnName}
99-
{!!metadata && (
100-
<span tw="pl-2 inline-block text-gray-300">
101-
<InfoIcon />
102-
</span>
103-
)}
104-
</span>
105-
<div
97+
<span
98+
css={[
99+
tw`p-2 text-sm font-medium truncate text-left`,
100+
['integer', 'number'].includes(cellType) && tw`text-right`,
101+
]}
102+
title={columnName}
103+
style={{ minWidth: 'calc(100% - 1.5em)' }}
104+
>
105+
{columnName}
106+
{!!metadata && (
107+
<span tw="pl-2 inline-block text-gray-300">
108+
<InfoIcon />
109+
</span>
110+
)}
111+
</span>
112+
</EditableHeader>
113+
<button
106114
className="header__icon"
107115
css={[
108116
tw`flex items-center justify-center pl-1 pr-2 -mr-2`,
109117
activeSortDirection
110118
? tw`opacity-100`
111119
: tw`opacity-0 group-hover:opacity-40`,
112120
]}
121+
onClick={() =>
122+
onSort(columnName, activeSortDirection == 'asc' ? 'desc' : 'asc')
123+
}
113124
>
114125
{activeSortDirection == 'desc' ? (
115126
<ArrowDownIcon />
116127
) : (
117128
<ArrowUpIcon />
118129
)}
119-
</div>
120-
</button>
130+
</button>
131+
</div>
121132
{!!metadata && (
122133
<div tw="text-sm absolute bottom-0 bg-white p-4 text-indigo-500 transform translate-y-full border border-indigo-300 py-3 shadow-md left-0 right-0 pointer-events-none opacity-0 group-hover:opacity-100">
123134
<div tw="pr-2 inline-block text-indigo-200">

src/store.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -363,15 +363,21 @@ export const createGridStore = () =>
363363
draft.updatedData = newData;
364364
});
365365
},
366-
onHeaderCellChange: (oldColumnName: string, newColumnName: any) => {
366+
onHeaderCellChange: (oldColumnName: string, newColumnName: string) => {
367367
set((draft) => {
368-
const newData = [...draft.rawData];
369-
newData.forEach((row) => {
370-
const newRow = { ...row };
371-
const oldValue = newRow[oldColumnName];
372-
delete newRow[oldColumnName];
373-
newRow[newColumnName] = oldValue;
374-
return newRow;
368+
const columnKeys = Object.keys(draft.rawData[0]);
369+
const newData = [...draft.rawData].map((row) => {
370+
// keep same order of keys so it matches when the data updates
371+
return columnKeys.reduce((acc, columnKey) => {
372+
if (columnKey === oldColumnName) {
373+
// @ts-ignore
374+
acc[newColumnName] = row[oldColumnName];
375+
} else {
376+
// @ts-ignore
377+
acc[columnKey] = row[columnKey];
378+
}
379+
return acc;
380+
}, {});
375381
});
376382
draft.updatedData = newData;
377383
});

0 commit comments

Comments
 (0)