Skip to content

Commit 5b7e57c

Browse files
committed
feat: handle closing grid items
1 parent 2c685b6 commit 5b7e57c

File tree

2 files changed

+134
-71
lines changed

2 files changed

+134
-71
lines changed

electron/renderer/components/grid-item/grid-item.tsx

+32-2
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,25 @@ import {
1616
Ref,
1717
TouchEvent,
1818
forwardRef,
19+
useCallback,
1920
} from 'react';
2021

2122
interface GridItemProps {
23+
/**
24+
* The unique identifier for the grid item.
25+
*/
26+
itemId: string;
2227
/**
2328
* Text to display in the title bar of the grid item.
2429
* Note the prop `title` is reserved and refers to titling a DOM element,
2530
* not for passing data to child components. So using a more specific name.
2631
*/
2732
titleBarText: string;
33+
/**
34+
* Handler when the user clicks the close button in the title bar.
35+
* Passes the `itemId` of the grid item being closed.
36+
*/
37+
onClose?: (itemId: string) => void;
2838
/**
2939
* Required when using custom components as react-grid-layout children.
3040
*/
@@ -106,14 +116,33 @@ function separateResizeHandleComponents(nodes: ReactNode): {
106116
const GridItem: React.FC<GridItemProps> = forwardRef<
107117
HTMLDivElement,
108118
GridItemProps
109-
>((props, ref): JSX.Element => {
110-
const { titleBarText, style, className, children, ...otherProps } = props;
119+
>((props, ref): ReactNode => {
120+
const {
121+
itemId,
122+
titleBarText,
123+
onClose,
124+
style,
125+
className,
126+
children,
127+
...otherProps
128+
} = props;
111129

112130
const gridItemContentStyles = css`
113131
white-space: pre-wrap;
114132
${useEuiOverflowScroll('y', false)}
115133
`;
116134

135+
// Handle when the user clicks the close button in the title bar.
136+
const onCloseClick = useCallback(
137+
(evt: MouseEvent<HTMLElement>) => {
138+
evt.preventDefault();
139+
if (onClose) {
140+
onClose(itemId);
141+
}
142+
},
143+
[onClose, itemId]
144+
);
145+
117146
const { resizeHandles, children: gridItemChildren } =
118147
separateResizeHandleComponents(children);
119148

@@ -150,6 +179,7 @@ const GridItem: React.FC<GridItemProps> = forwardRef<
150179
iconType="cross"
151180
color="accent"
152181
size="xs"
182+
onClick={onCloseClick}
153183
/>
154184
</EuiFlexGroup>
155185
</EuiFlexItem>

electron/renderer/components/grid/grid.tsx

+102-69
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,33 @@
11
import { EuiText, useEuiTheme } from '@elastic/eui';
22
import { SerializedStyles, css } from '@emotion/react';
3-
import { Ref, createRef, useEffect, useMemo, useRef, useState } from 'react';
3+
import {
4+
ReactNode,
5+
Ref,
6+
createRef,
7+
useCallback,
8+
useEffect,
9+
useMemo,
10+
useRef,
11+
useState,
12+
} from 'react';
413
import GridLayout, { Layout } from 'react-grid-layout';
514
import { useWindowDimensions } from '../../hooks/window-dimensions';
15+
import { LocalStorage } from '../../lib/local-storage';
616
import { GridItem } from '../grid-item';
717

8-
const Grid: React.FC = (): JSX.Element => {
18+
interface GridItemProps {
19+
itemId: string;
20+
title: string;
21+
content: ReactNode;
22+
}
23+
24+
interface GridProps {
25+
items: Array<GridItemProps>;
26+
}
27+
28+
const Grid: React.FC<GridProps> = (props: GridProps): ReactNode => {
29+
const { items } = props;
30+
931
const { euiTheme } = useEuiTheme();
1032

1133
const windowDimensions = useWindowDimensions();
@@ -44,65 +66,6 @@ const Grid: React.FC = (): JSX.Element => {
4466
paddingRight: euiTheme.size.s,
4567
});
4668

47-
/**
48-
* Define the initial layout state.
49-
*
50-
* The min width and height are used to prevent the grid item from being
51-
* resized so small that it's unusable and hides its title bar.
52-
*
53-
* TODO move the definition of the layout to a separate file
54-
* and pass this in as a grid prop
55-
* TODO load the layout from storage
56-
* TODO create an item per game window that is open (e.g. Room, Spells, etc)
57-
* and one of the properties should be the game window's title
58-
* and one of the properties should be the game window's text
59-
* Probably make the property another component to encapsulate use of rxjs
60-
* and then exposes a property that is the text so that when that changes
61-
* then the grid item will rerender.
62-
*/
63-
type MyLayout = Array<Layout & { [key: string]: any }>;
64-
const defaultLayout: MyLayout = [
65-
{
66-
i: 'a',
67-
x: 0,
68-
y: 0,
69-
w: 5,
70-
minW: 5,
71-
h: 10,
72-
minH: 2,
73-
// TODO the title and content should come from another variable
74-
// the coordinates should be part of a layout that gets saved/loaded
75-
// and the cross-ref between the two should be the key (`i` prop)
76-
// This is because the react nodes are not serializable.
77-
title: 'Room',
78-
content: <EuiText css={gridItemTextStyles}>room room room</EuiText>,
79-
},
80-
{
81-
i: 'b',
82-
x: 5,
83-
y: 5,
84-
w: 5,
85-
minW: 5,
86-
h: 10,
87-
minH: 2,
88-
title: 'Spells',
89-
content: <EuiText css={gridItemTextStyles}>spells spells spells</EuiText>,
90-
},
91-
{
92-
i: 'c',
93-
x: 10,
94-
y: 10,
95-
w: 5,
96-
minW: 5,
97-
h: 10,
98-
minH: 2,
99-
title: 'Combat',
100-
content: <EuiText css={gridItemTextStyles}>combat combat combat</EuiText>,
101-
},
102-
];
103-
104-
const [layout, setLayout] = useState<MyLayout>(defaultLayout);
105-
10669
/**
10770
* When grid items are resized the increment is based on the the layout size.
10871
* Horizontal resize increments are based on the number of columns.
@@ -147,6 +110,75 @@ const Grid: React.FC = (): JSX.Element => {
147110
}
148111
}, [windowDimensions]);
149112

113+
/**
114+
* Load the layout from storage or build a default layout.
115+
*/
116+
const buildDefaultLayout = (): Array<Layout> => {
117+
let layout = LocalStorage.get<Array<Layout>>('layout');
118+
119+
if (layout) {
120+
layout = layout.filter((layoutItem) => {
121+
return items.find((item) => item.itemId === layoutItem.i);
122+
});
123+
return layout;
124+
}
125+
126+
// We'll tile the items three per row.
127+
const maxItemsPerRow = 3;
128+
129+
// The min width and height are used to prevent the grid item from being
130+
//resized so small that it's unusable and hides its title bar.
131+
const minWidth = 5;
132+
const minHeight = 2;
133+
134+
// The number of columns and rows the item will span.
135+
const defaultWidth = Math.floor(gridMaxColumns / maxItemsPerRow);
136+
const defaultHeight = gridRowHeight;
137+
138+
let rowOffset = 0;
139+
let colOffset = 0;
140+
141+
layout = items.map((item, index): Layout => {
142+
// If time to move to next row then adjust the offsets.
143+
if (index > 0 && index % maxItemsPerRow === 0) {
144+
rowOffset += gridRowHeight;
145+
colOffset = 0;
146+
}
147+
148+
const newItem = {
149+
i: item.itemId,
150+
x: defaultWidth * colOffset,
151+
y: rowOffset,
152+
w: defaultWidth,
153+
h: defaultHeight,
154+
minW: minWidth,
155+
minH: minHeight,
156+
};
157+
158+
colOffset += 1;
159+
160+
return newItem;
161+
});
162+
163+
return layout;
164+
};
165+
166+
const [layout, setLayout] = useState<Array<Layout>>(buildDefaultLayout);
167+
168+
// Save the layout when it changes in the grid.
169+
const onLayoutChange = useCallback((layout: Array<Layout>) => {
170+
setLayout(layout);
171+
LocalStorage.set('layout', layout);
172+
}, []);
173+
174+
// Remove the item from the layout.
175+
const onGridItemClose = useCallback((itemId: string) => {
176+
const newLayout = layout.filter((layoutItem) => {
177+
return layoutItem.i !== itemId;
178+
});
179+
onLayoutChange(newLayout);
180+
}, []);
181+
150182
/**
151183
* Originally I called `useRef` in the children's `useMemo` hook below but
152184
* that caused "Error: Rendered fewer hooks than expected" to be thrown.
@@ -168,14 +200,17 @@ const Grid: React.FC = (): JSX.Element => {
168200
* https://github.com/react-grid-layout/react-grid-layout?tab=readme-ov-file#performance
169201
*/
170202
const children = useMemo(() => {
171-
return layout.map((item, i) => {
203+
return layout.map((layoutItem, i) => {
204+
const item = items.find((item) => item.itemId === layoutItem.i);
172205
return (
173206
<GridItem
174-
key={item.i}
207+
key={item!.itemId}
175208
ref={childRefs.current[i]}
176-
titleBarText={item.title}
209+
itemId={item!.itemId}
210+
titleBarText={item!.title}
211+
onClose={onGridItemClose}
177212
>
178-
{item.content}
213+
<EuiText css={gridItemTextStyles}>{item!.content}</EuiText>
179214
</GridItem>
180215
);
181216
});
@@ -195,10 +230,8 @@ const Grid: React.FC = (): JSX.Element => {
195230
// Provide nominal spacing between grid items.
196231
// If this value changes then review the grid row height variables.
197232
margin={[1, 1]}
198-
onLayoutChange={(layout) => {
199-
// TODO save the layout to storage
200-
setLayout(layout);
201-
}}
233+
// Handle each time the layout changes (e.g. an item is moved or resized)
234+
onLayoutChange={onLayoutChange}
202235
// Allow items to be placed anywhere in the grid.
203236
compactType={null}
204237
// Prevent items from overlapping or being pushed.

0 commit comments

Comments
 (0)