Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
18c94a7
add Box for row labels
michaelkirschbaum Dec 7, 2025
708543c
add Box for column labels
michaelkirschbaum Dec 7, 2025
4ee4592
add corner Box for labels
michaelkirschbaum Dec 7, 2025
0ba7a48
add formatToCurrency helper method
michaelkirschbaum Dec 7, 2025
a4db342
convert input to number and center row labels
michaelkirschbaum Dec 7, 2025
1d47581
wrap data cells in Box for flex alignment
michaelkirschbaum Dec 7, 2025
862d9c8
get raw value in onChangeHandler for reformatting
michaelkirschbaum Dec 7, 2025
bae8e2e
allow decimals in formatting
michaelkirschbaum Dec 7, 2025
c01bd9a
change value to displayValue and add onFocus and onBlur events
michaelkirschbaum Dec 7, 2025
dfff586
add state to track which box is selected
michaelkirschbaum Dec 7, 2025
6aa8c4c
onClick set selected box to row and columns ids
michaelkirschbaum Dec 7, 2025
7e98470
if selected change background color of cell
michaelkirschbaum Dec 7, 2025
2b5493a
add keyboard event listener
michaelkirschbaum Dec 7, 2025
02da270
get selected get column and row from state in keyboard event handler
michaelkirschbaum Dec 7, 2025
c1341cb
implement logic for keyboard arrow click events in handler
michaelkirschbaum Dec 7, 2025
0dab45e
add focused prop
michaelkirschbaum Dec 7, 2025
2953c12
pass focused to cell prop in spreadsheet
michaelkirschbaum Dec 7, 2025
5da6879
base display value on focused prop
michaelkirschbaum Dec 7, 2025
cb55617
add useeffects to focus on inputRef
michaelkirschbaum Dec 7, 2025
6d9c196
clean up input style
michaelkirschbaum Dec 7, 2025
b946e40
align input cursor
michaelkirschbaum Dec 7, 2025
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
51 changes: 46 additions & 5 deletions src/components/Cell.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,63 @@
import { Input, Box } from '@chakra-ui/react';
import React, { useCallback } from 'react';
import React, { useCallback, useRef, useEffect } from 'react';

interface Props {
value: string;
onChange: (newValue: string) => void;
focused: boolean;
}

const Cell: React.FC<Props> = ({ value, onChange }) => {
export function formatToCurrency(value: string): string {
if (value === '') return '';

const num = Number(value.replace(/[^0-9.-]/g, ''));
if (isNaN(num)) return value;

return num.toLocaleString("en-US", {
style: "currency",
currency: "USD",
maximumFractionDigits: 2,
minimumFractionDigits: 0,
});
}

const Cell: React.FC<Props> = ({ value, onChange, focused }) => {
const inputRef = useRef<HTMLInputElement>(null);

useEffect(() => {
if (focused) {
inputRef.current?.focus();
}
}, [focused]);

// when focused, show unfromatted value
// when not focused, show currency format
const displayValue = focused ? value : formatToCurrency(value);

const onChangeHandler = useCallback<React.ChangeEventHandler<HTMLInputElement>>(
(ev) => {
onChange(ev.target.value);
const raw = ev.target.value.replace(/[^0-9.-]/g, "");
onChange(raw);
},
[onChange],
);

return (
<Box>
<Input value={value} borderRadius={0} width="full" onChange={onChangeHandler} />
<Box width="100%" height="100%">
<Input
ref={inputRef}
value={displayValue}
onChange={onChangeHandler}
borderRadius={0}
width="100%"
height="100%"
padding="0"
border="none"
outline="none"
fontSize="14px"
textAlign="right"
_focus={{ outline: "none", boxShadow: "none" }}
/>
</Box>
);
};
Expand Down
103 changes: 88 additions & 15 deletions src/components/Spreadsheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,29 +11,102 @@ const Spreadsheet: React.FC = () => {
const [spreadsheetState, setSpreadsheetState] = useState(
_.times(NUM_ROWS, () => _.times(NUM_COLUMNS, _.constant(''))),
);
const [selected, setSelected] = useState<{ row: number; col: number } | null>(null);

React.useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (!selected) return;

const { row, col } = selected;

if (e.key === "ArrowUp" && row > 0) {
setSelected({ row: row - 1, col });
} else if (e.key === "ArrowDown" && row < NUM_ROWS - 1) {
setSelected({ row: row + 1, col });
} else if (e.key === "ArrowLeft" && col > 0) {
setSelected({ row, col: col - 1 });
} else if (e.key === "ArrowRight" && col < NUM_COLUMNS - 1) {
setSelected({ row, col: col + 1 });
}
};

window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [selected]);

return (
<Box width="full">
<Flex>
{/* corner cell */}
<Box
width="40px"
height="32px"
border="1px solid #ddd"
background="#fafafa"
/>
{/* header labels */}
{_.times(NUM_COLUMNS, (colIdx) => (
<Box
key={`header-${colIdx}`}
flex="1"
display="flex"
alignItems="center"
justifyContent="center"
height="32px"
border="1px solid #ddd"
fontWeight="bold"
background="#fafafa"
>
{String.fromCharCode(65 + colIdx)}
</Box>
))}
</Flex>
{spreadsheetState.map((row, rowIdx) => {
return (
<Flex key={String(rowIdx)}>
<Box
width="40px"
border="1px solid #ddd"
display="flex"
alignItems="center"
justifyContent="center"
fontWeight="bold"
background="#fafafa"
>
{rowIdx + 1}
</Box>
{/* row labels */}
{row.map((cellValue, columnIdx) => (
<Cell
<Box
key={`${rowIdx}/${columnIdx}`}
value={cellValue}
onChange={(newValue: string) => {
const newRow = [
...spreadsheetState[rowIdx].slice(0, columnIdx),
newValue,
...spreadsheetState[rowIdx].slice(columnIdx + 1),
];
setSpreadsheetState([
...spreadsheetState.slice(0, rowIdx),
newRow,
...spreadsheetState.slice(rowIdx + 1),
]);
}}
/>
flex="1"
height="32px"
border="1px solid #ddd"
onClick={() => setSelected({ row: rowIdx, col: columnIdx })}
background={
selected?.row === rowIdx && selected?.col === columnIdx
? "#d0e7ff"
: "white"
}
>
<Cell
key={`${rowIdx}/${columnIdx}`}
value={cellValue}
onChange={(newValue: string) => {
const newRow = [
...spreadsheetState[rowIdx].slice(0, columnIdx),
newValue,
...spreadsheetState[rowIdx].slice(columnIdx + 1),
];
setSpreadsheetState([
...spreadsheetState.slice(0, rowIdx),
newRow,
...spreadsheetState.slice(rowIdx + 1),
]);
}}
focused={selected?.row === rowIdx && selected?.col === columnIdx}
/>
</Box>
))}
</Flex>
);
Expand Down