Skip to content

Commit 0ba622c

Browse files
committed
parent to child key down propagation
1 parent de418cb commit 0ba622c

File tree

12 files changed

+336
-171
lines changed

12 files changed

+336
-171
lines changed

src/components/BorderedApp/BorderedApp.tsx

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import React, { useRef } from "react";
1+
import React, {
2+
KeyboardEvent,
3+
RefObject,
4+
useCallback,
5+
useEffect,
6+
useRef,
7+
} from "react";
28
import useWindowManagerStore, {
39
BaseProps,
410
} from "../../stores/windowManagerStore";
@@ -23,7 +29,26 @@ import {
2329
StyledContentInner,
2430
} from "./styles";
2531

26-
interface BorderedAppProps extends BaseProps {
32+
/**
33+
* A type for each of the programs wrapped in a bordered app to forward a ref for,
34+
* allowing the bordered all to communicate with the inner app content.
35+
*/
36+
export interface BorderedAppContentHandles<T extends HTMLElement> {
37+
/**
38+
* Function for the app content (such as calculator, calendar etc) to implement
39+
* and handle keyDown events that fire from the bordered app.
40+
*/
41+
onParentKeyDown(e: React.KeyboardEvent): void;
42+
/**
43+
* A reference to the app content's main element.
44+
*/
45+
element?: T | null;
46+
}
47+
48+
interface BorderedAppProps<
49+
T extends BorderedAppContentHandles<E>,
50+
E extends HTMLElement = HTMLElement,
51+
> extends BaseProps {
2752
title: string;
2853
type: string;
2954
id: string;
@@ -32,9 +57,13 @@ interface BorderedAppProps extends BaseProps {
3257
maxDimensions?: Dimensions;
3358
minDimensions?: Dimensions;
3459
menus?: Array<MenuItemProps>;
60+
contentRef: RefObject<T>;
3561
}
3662

37-
function BorderedApp({
63+
function BorderedApp<
64+
E extends HTMLElement,
65+
T extends BorderedAppContentHandles<E>,
66+
>({
3867
title,
3968
type,
4069
id,
@@ -46,12 +75,26 @@ function BorderedApp({
4675
menus,
4776
zIndex,
4877
hidden,
49-
}: React.PropsWithChildren<BorderedAppProps>) {
78+
contentRef,
79+
}: React.PropsWithChildren<BorderedAppProps<T, E>>) {
5080
const winMan = useWindowManagerStore();
5181
const settings = useSystemSettings();
5282

83+
// Listen for keyDown events and send them down to the content rendered inside the bordered app
84+
const handleKeyDown = (e: KeyboardEvent) =>
85+
contentRef.current?.onParentKeyDown(e);
86+
5387
// Need a ref to point to the app for moving it around the screen
54-
const appRef = useRef<HTMLDivElement>(null);
88+
const appRef = useRef<HTMLDivElement | null>(null);
89+
90+
const onAppRef = useCallback((element: HTMLDivElement | null) => {
91+
if (element) {
92+
appRef.current = element;
93+
appRef.current.focus();
94+
} else {
95+
appRef.current = null;
96+
}
97+
}, []);
5598

5699
const {
57100
resizeHandleN,
@@ -80,13 +123,15 @@ function BorderedApp({
80123

81124
return (
82125
<StyledBorderedApp
83-
ref={appRef}
126+
ref={onAppRef}
84127
onMouseDown={() => winMan.focusWindow(type, id)}
85128
initialDimensions={initialDimensions}
86129
initialPosition={initialPosition}
87130
zIndex={zIndex}
88131
backgroundColor={settings.mainColor}
89132
display={hidden === true ? "none" : "grid"}
133+
tabIndex={0}
134+
onKeyDown={handleKeyDown}
90135
>
91136
<StyledCorner location="nw" ref={resizeHandleNW} />
92137
<StyledEdge location="n" ref={resizeHandleN} />

src/components/BottomBar/Launcher/Launcher.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useRef, useState } from "react";
1+
import { RefObject, useRef, useState } from "react";
22
import { v4 as uuid } from "uuid";
33

44
import useConditionalClick from "../../../hooks/useConditionalClick";
@@ -9,7 +9,8 @@ import { MenuItemProps } from "../../MenuItems";
99

1010
import { StyledIcon, StyledLauncher } from "./styles";
1111
import ContextMenu from "../../ContextMenu";
12-
interface LauncherProps {
12+
import { BorderedAppContentHandles } from "../../BorderedApp/BorderedApp";
13+
interface LauncherProps<T extends BorderedAppContentHandles> {
1314
windowType: string;
1415
WindowTitle: string;
1516
windowId?: string;
@@ -19,9 +20,10 @@ interface LauncherProps {
1920
menus?: Array<MenuItemProps>;
2021
appContent: JSX.Element;
2122
icon: string;
23+
contentRef: RefObject<T>;
2224
}
2325

24-
function Launcher({
26+
function Launcher<T extends BorderedAppContentHandles>({
2527
windowType,
2628
windowId,
2729
WindowTitle,
@@ -31,7 +33,8 @@ function Launcher({
3133
menus,
3234
appContent,
3335
icon,
34-
}: React.PropsWithChildren<LauncherProps>) {
36+
contentRef,
37+
}: React.PropsWithChildren<LauncherProps<T>>) {
3538
const winMan = useWindowManagerStore();
3639
const ref = useRef<HTMLDivElement>(null);
3740
const [contextOpen, setContextOpen] = useState(false);
@@ -62,6 +65,7 @@ function Launcher({
6265
y: getInitialPosition("y"),
6366
},
6467
menus,
68+
contentRef,
6569
},
6670
key: id,
6771
children: appContent,
Lines changed: 140 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import { useEffect, useState } from "react";
1+
import {
2+
forwardRef,
3+
useEffect,
4+
useImperativeHandle,
5+
useRef,
6+
useState,
7+
} from "react";
28

39
import {
410
StyledButton,
@@ -8,99 +14,145 @@ import {
814
StyledInputOutput,
915
StyledInputOutputContents,
1016
} from "./styles";
17+
import { BorderedAppContentHandles } from "../../components/BorderedApp/BorderedApp";
18+
19+
export type CalculatorHandles = BorderedAppContentHandles<HTMLDivElement>;
1120

1221
interface CalculatorProps {}
1322

14-
// eslint-disable-next-line no-empty-pattern
15-
function Calculator({}: CalculatorProps) {
16-
const [input, setInput] = useState<string>("");
17-
const [output, setOutput] = useState<string>("");
18-
19-
// Whenever the input changes we'll check if it forms a valid
20-
// calculation, and if so, add it to the output
21-
useEffect(() => {
22-
try {
23-
const res = eval(input);
24-
setOutput(res);
25-
} catch {
26-
/* empty */
27-
}
28-
}, [input]);
29-
30-
function Button({
31-
displayChar,
32-
mathematicalChar,
33-
action,
34-
}: {
35-
mathematicalChar: string | number;
36-
displayChar?: string;
37-
action: "append-to-end" | "remove-from-end" | "evaluate" | "clear";
38-
}) {
39-
if (displayChar === undefined) {
40-
displayChar = mathematicalChar.toString();
41-
}
23+
const isDigit = (key: string) => /^[0-9]$/.test(key);
24+
const operators = new Set(["+", "-", "*", "/", "%"]);
25+
26+
export const Calculator = forwardRef<CalculatorHandles, CalculatorProps>(
27+
(_props, ref) => {
28+
const elementRef = useRef<HTMLDivElement>(null);
29+
const [input, setInput] = useState<string>("");
30+
const [output, setOutput] = useState<string>("");
31+
const appendToInput = (value: string) =>
32+
setInput((current) => current + value);
33+
const removeFromEnd = (count: number = 1) =>
34+
setInput((current) => current.slice(0, -count));
35+
const clear = () => setInput("");
36+
const evaluate = () => {
37+
try {
38+
setInput(eval(input));
39+
setOutput("");
40+
} catch {
41+
setOutput("Invalid");
42+
}
43+
};
44+
45+
useImperativeHandle(ref, () => ({
46+
onParentKeyDown(e) {
47+
const { key } = e;
48+
49+
if (isDigit(key) || operators.has(key)) {
50+
appendToInput(key);
51+
return;
52+
}
53+
54+
switch (key) {
55+
case ".":
56+
case "(":
57+
case ")":
58+
appendToInput(key);
59+
break;
4260

43-
function handleClick() {
44-
switch (action) {
45-
case "append-to-end":
46-
setInput(input + mathematicalChar.toString());
47-
break;
48-
case "remove-from-end":
49-
setInput(String(input).slice(0, -1));
50-
break;
51-
case "evaluate":
52-
try {
53-
setInput(eval(input));
54-
setOutput("");
55-
} catch {
56-
setOutput("Invalid");
57-
}
58-
break;
59-
case "clear":
60-
setInput("");
61+
case "=":
62+
case "Enter":
63+
evaluate();
64+
break;
65+
66+
case "Backspace":
67+
removeFromEnd();
68+
break;
69+
70+
case "Escape":
71+
clear();
72+
break;
73+
}
74+
},
75+
element: elementRef.current,
76+
}));
77+
78+
// Whenever the input changes we'll check if it forms a valid
79+
// calculation, and if so, add it to the output
80+
useEffect(() => {
81+
try {
82+
const res = eval(input);
83+
setOutput(res);
84+
} catch {
85+
/* empty */
86+
}
87+
}, [input]);
88+
89+
function Button({
90+
displayChar,
91+
mathematicalChar,
92+
action,
93+
}: {
94+
mathematicalChar: string | number;
95+
displayChar?: string;
96+
action: "append-to-end" | "remove-from-end" | "evaluate" | "clear";
97+
}) {
98+
if (displayChar === undefined) {
99+
displayChar = mathematicalChar.toString();
61100
}
101+
102+
function handleClick() {
103+
switch (action) {
104+
case "append-to-end":
105+
return appendToInput(mathematicalChar.toString());
106+
break;
107+
case "remove-from-end":
108+
return removeFromEnd();
109+
break;
110+
case "evaluate":
111+
return evaluate();
112+
case "clear":
113+
return clear();
114+
}
115+
}
116+
117+
return (
118+
<StyledButton onClick={handleClick}>
119+
<StyledButtonContent>{displayChar}</StyledButtonContent>
120+
</StyledButton>
121+
);
62122
}
63123

64124
return (
65-
<StyledButton onClick={handleClick}>
66-
<StyledButtonContent>{displayChar}</StyledButtonContent>
67-
</StyledButton>
125+
<StyledCalc ref={elementRef}>
126+
<StyledInputOutput direction="input">
127+
<StyledInputOutputContents>{input}</StyledInputOutputContents>
128+
</StyledInputOutput>
129+
<StyledInputOutput direction="output">
130+
<StyledInputOutputContents>{output}</StyledInputOutputContents>
131+
</StyledInputOutput>
132+
<StyledButtons>
133+
<Button mathematicalChar={"AC"} action="clear" />
134+
<div />
135+
<div />
136+
<Button mathematicalChar="/" displayChar="÷" action="append-to-end" />
137+
138+
<Button mathematicalChar={7} action="append-to-end" />
139+
<Button mathematicalChar={8} action="append-to-end" />
140+
<Button mathematicalChar={9} action="append-to-end" />
141+
<Button mathematicalChar="*" displayChar="x" action="append-to-end" />
142+
<Button mathematicalChar={4} action="append-to-end" />
143+
<Button mathematicalChar={5} action="append-to-end" />
144+
<Button mathematicalChar={6} action="append-to-end" />
145+
<Button mathematicalChar="-" action="append-to-end" />
146+
<Button mathematicalChar={1} action="append-to-end" />
147+
<Button mathematicalChar={2} action="append-to-end" />
148+
<Button mathematicalChar={3} action="append-to-end" />
149+
<Button mathematicalChar="+" action="append-to-end" />
150+
<Button mathematicalChar={0} action="append-to-end" />
151+
<Button mathematicalChar="." action="append-to-end" />
152+
<Button mathematicalChar="←" action="remove-from-end" />
153+
<Button mathematicalChar="=" action="evaluate" />
154+
</StyledButtons>
155+
</StyledCalc>
68156
);
69-
}
70-
71-
return (
72-
<StyledCalc>
73-
<StyledInputOutput direction="input">
74-
<StyledInputOutputContents>{input}</StyledInputOutputContents>
75-
</StyledInputOutput>
76-
<StyledInputOutput direction="output">
77-
<StyledInputOutputContents>{output}</StyledInputOutputContents>
78-
</StyledInputOutput>
79-
<StyledButtons>
80-
<Button mathematicalChar={"AC"} action="clear" />
81-
<div />
82-
<div />
83-
<Button mathematicalChar="/" displayChar="÷" action="append-to-end" />
84-
85-
<Button mathematicalChar={7} action="append-to-end" />
86-
<Button mathematicalChar={8} action="append-to-end" />
87-
<Button mathematicalChar={9} action="append-to-end" />
88-
<Button mathematicalChar="*" displayChar="x" action="append-to-end" />
89-
<Button mathematicalChar={4} action="append-to-end" />
90-
<Button mathematicalChar={5} action="append-to-end" />
91-
<Button mathematicalChar={6} action="append-to-end" />
92-
<Button mathematicalChar="-" action="append-to-end" />
93-
<Button mathematicalChar={1} action="append-to-end" />
94-
<Button mathematicalChar={2} action="append-to-end" />
95-
<Button mathematicalChar={3} action="append-to-end" />
96-
<Button mathematicalChar="+" action="append-to-end" />
97-
<Button mathematicalChar={0} action="append-to-end" />
98-
<Button mathematicalChar="." action="append-to-end" />
99-
<Button mathematicalChar="←" action="remove-from-end" />
100-
<Button mathematicalChar="=" action="evaluate" />
101-
</StyledButtons>
102-
</StyledCalc>
103-
);
104-
}
105-
106-
export default Calculator;
157+
},
158+
);

0 commit comments

Comments
 (0)