event handle rework + firefox support

This commit is contained in:
unknown 2020-08-09 10:51:25 +03:00
parent 39f2188c76
commit 7833e05da5
6 changed files with 259 additions and 312 deletions

View File

@ -57,8 +57,8 @@ const App = () => {
}, },
{ {
start: new Date(currentDate.getFullYear(), currentDate.getMonth(), 15), start: new Date(currentDate.getFullYear(), currentDate.getMonth(), 15),
end: new Date(currentDate.getFullYear(), currentDate.getMonth(), 26), end: new Date(currentDate.getFullYear(), currentDate.getMonth(), 16),
name: "Release", name: "Release & Eat Burgers",
id: "Task 6", id: "Task 6",
progress: currentDate.getMonth(), progress: currentDate.getMonth(),
dependencies: ["Task 4"], dependencies: ["Task 4"],

View File

@ -4,12 +4,12 @@ import { BarProgressHandle } from "./bar-progress-handle";
import { BarDateHandle } from "./bar-date-handle"; import { BarDateHandle } from "./bar-date-handle";
import { BarDisplay } from "./bar-display"; import { BarDisplay } from "./bar-display";
import { BarTask } from "../../types/bar-task"; import { BarTask } from "../../types/bar-task";
import { BarAction } from "../Gantt/gantt-content";
import { import {
progressWithByParams, progressWithByParams,
getProgressPoint, getProgressPoint,
} from "../../helpers/bar-helper"; } from "../../helpers/bar-helper";
import styles from "./bar.module.css"; import styles from "./bar.module.css";
import { GanttContentMoveAction } from "../Gantt/gantt-content";
export type BarProps = { export type BarProps = {
task: BarTask; task: BarTask;
@ -17,18 +17,12 @@ export type BarProps = {
onDoubleClick?: (task: Task) => any; onDoubleClick?: (task: Task) => any;
isProgressChangeable: boolean; isProgressChangeable: boolean;
isDateChangeable: boolean; isDateChangeable: boolean;
handleMouseEvents: ( isDelete: boolean;
event: onEventStart: (
| React.MouseEvent<SVGPolygonElement, MouseEvent> event: React.MouseEvent | React.KeyboardEvent,
| React.MouseEvent<SVGGElement, MouseEvent> action: GanttContentMoveAction,
| React.MouseEvent<SVGRectElement, MouseEvent>, selectedTask: BarTask
eventType: BarAction, ) => any;
task: BarTask
) => void;
handleButtonSVGEvents: (
event: React.KeyboardEvent<SVGGElement>,
task: BarTask
) => void;
}; };
export const Bar: React.FC<BarProps> = ({ export const Bar: React.FC<BarProps> = ({
@ -37,8 +31,8 @@ export const Bar: React.FC<BarProps> = ({
onDoubleClick, onDoubleClick,
isProgressChangeable, isProgressChangeable,
isDateChangeable, isDateChangeable,
handleMouseEvents, onEventStart,
handleButtonSVGEvents, isDelete,
}) => { }) => {
const [isSelected, setIsSelected] = useState(false); const [isSelected, setIsSelected] = useState(false);
@ -57,13 +51,18 @@ export const Bar: React.FC<BarProps> = ({
}} }}
tabIndex={0} tabIndex={0}
onKeyDown={e => { onKeyDown={e => {
handleButtonSVGEvents(e, task); switch (e.key) {
case "Delete": {
if (isDelete) onEventStart(e, "delete", task);
break;
}
}
}} }}
onMouseEnter={e => { onMouseEnter={e => {
handleMouseEvents(e, "mouseenter", task); onEventStart(e, "mouseenter", task);
}} }}
onMouseLeave={e => { onMouseLeave={e => {
handleMouseEvents(e, "mouseleave", task); onEventStart(e, "mouseleave", task);
}} }}
onFocus={() => setIsSelected(true)} onFocus={() => setIsSelected(true)}
onBlur={() => setIsSelected(false)} onBlur={() => setIsSelected(false)}
@ -81,7 +80,7 @@ export const Bar: React.FC<BarProps> = ({
styles={task.styles} styles={task.styles}
isSelected={isSelected} isSelected={isSelected}
onMouseDown={e => { onMouseDown={e => {
isDateChangeable && handleMouseEvents(e, "move", task); isDateChangeable && onEventStart(e, "move", task);
}} }}
/> />
<g className="handleGroup"> <g className="handleGroup">
@ -95,7 +94,7 @@ export const Bar: React.FC<BarProps> = ({
height={task.height - 2} height={task.height - 2}
barCornerRadius={task.barCornerRadius} barCornerRadius={task.barCornerRadius}
onMouseDown={e => { onMouseDown={e => {
handleMouseEvents(e, "start", task); onEventStart(e, "start", task);
}} }}
/> />
{/* right */} {/* right */}
@ -106,7 +105,7 @@ export const Bar: React.FC<BarProps> = ({
height={task.height - 2} height={task.height - 2}
barCornerRadius={task.barCornerRadius} barCornerRadius={task.barCornerRadius}
onMouseDown={e => { onMouseDown={e => {
handleMouseEvents(e, "end", task); onEventStart(e, "end", task);
}} }}
/> />
</g> </g>
@ -115,7 +114,7 @@ export const Bar: React.FC<BarProps> = ({
<BarProgressHandle <BarProgressHandle
progressPoint={progressPoint} progressPoint={progressPoint}
onMouseDown={e => { onMouseDown={e => {
handleMouseEvents(e, "progress", task); onEventStart(e, "progress", task);
}} }}
/> />
)} )}

View File

@ -1,25 +1,25 @@
import React, { useState, useEffect } from "react"; import React, { useEffect, useState } from "react";
import { Task, EventOption } from "../../types/public-types"; import { Task, EventOption } from "../../types/public-types";
import { Bar } from "../Bar/bar"; import { Bar } from "../Bar/bar";
import { BarTask } from "../../types/bar-task"; import { BarTask } from "../../types/bar-task";
import { Arrow } from "../Other/arrow"; import { Arrow } from "../Other/arrow";
import { import {
convertToBarTasks, convertToBarTasks,
progressByX, handleTaskBySVGMouseEvent,
startByX, BarMoveAction,
endByX,
moveByX,
dateByX,
} from "../../helpers/bar-helper"; } from "../../helpers/bar-helper";
import { Tooltip } from "../Other/tooltip"; import { Tooltip } from "../Other/tooltip";
import { isKeyboardEvent } from "../../helpers/other-helper";
export interface GanttTask extends Task { export type GanttContentMoveAction =
x1: number; | "mouseenter"
x2: number; | "mouseleave"
y: number; | "delete"
width: number; | BarMoveAction;
height: number; export type BarEvent = {
} selectedTask?: BarTask;
action: GanttContentMoveAction;
};
export type GanttContentProps = { export type GanttContentProps = {
tasks: Task[]; tasks: Task[];
dates: Date[]; dates: Date[];
@ -33,8 +33,8 @@ export type GanttContentProps = {
barBackgroundSelectedColor: string; barBackgroundSelectedColor: string;
headerHeight: number; headerHeight: number;
handleWidth: number; handleWidth: number;
svg: React.MutableRefObject<SVGSVGElement | null>;
timeStep: number; timeStep: number;
svg: React.RefObject<SVGSVGElement>;
arrowColor: string; arrowColor: string;
arrowIndent: number; arrowIndent: number;
fontSize: string; fontSize: string;
@ -46,19 +46,6 @@ export type GanttContentProps = {
) => JSX.Element; ) => JSX.Element;
} & EventOption; } & EventOption;
export type BarAction =
| "progress"
| "end"
| "start"
| "move"
| "mouseenter"
| "mouseleave"
| "";
type BarEvent = {
action: BarAction;
selectedTask: BarTask | null;
};
export const GanttContent: React.FC<GanttContentProps> = ({ export const GanttContent: React.FC<GanttContentProps> = ({
tasks, tasks,
rowHeight, rowHeight,
@ -73,26 +60,26 @@ export const GanttContent: React.FC<GanttContentProps> = ({
headerHeight, headerHeight,
handleWidth, handleWidth,
arrowColor, arrowColor,
svg,
timeStep, timeStep,
fontFamily, fontFamily,
fontSize, fontSize,
arrowIndent, arrowIndent,
svg,
onDateChange, onDateChange,
onProgressChange, onProgressChange,
onDoubleClick, onDoubleClick,
onTaskDelete, onTaskDelete,
getTooltipContent, getTooltipContent,
}) => { }) => {
const point = svg.current?.createSVGPoint();
const [barEvent, setBarEvent] = useState<BarEvent>({ const [barEvent, setBarEvent] = useState<BarEvent>({
action: "", action: "",
selectedTask: null,
}); });
const [isSVGListen, setIsSVGListen] = useState(false);
const [barTasks, setBarTasks] = useState<BarTask[]>([]); const [barTasks, setBarTasks] = useState<BarTask[]>([]);
const [xStep, setXStep] = useState(0); const [xStep, setXStep] = useState(0);
const [initEventX1Delta, setInitEventX1Delta] = useState(0); const [initEventX1Delta, setInitEventX1Delta] = useState(0);
const [isMoving, setIsMoving] = useState(false);
// create xStep
useEffect(() => { useEffect(() => {
const dateDelta = const dateDelta =
dates[1].getTime() - dates[1].getTime() -
@ -103,8 +90,9 @@ export const GanttContent: React.FC<GanttContentProps> = ({
if (newXStep !== xStep) { if (newXStep !== xStep) {
setXStep(newXStep); setXStep(newXStep);
} }
}, [tasks, rowHeight, barCornerRadius, columnWidth, dates, timeStep, xStep]); }, [columnWidth, dates, timeStep, xStep]);
// generate tasks
useEffect(() => { useEffect(() => {
const dateDelta = const dateDelta =
dates[1].getTime() - dates[1].getTime() -
@ -136,257 +124,133 @@ export const GanttContent: React.FC<GanttContentProps> = ({
barCornerRadius, barCornerRadius,
columnWidth, columnWidth,
dates, dates,
timeStep,
barFill, barFill,
handleWidth, handleWidth,
headerHeight, headerHeight,
]); barProgressColor,
barProgressSelectedColor,
useEffect(() => { barBackgroundColor,
/** barBackgroundSelectedColor,
* Method handles event in real time(mousemove) and on finish(mouseup)
*/
const handleMouseSVGChangeEventsSubscribe = async (event: MouseEvent) => {
if (!barEvent.selectedTask || !barEvent.action || !svg || !svg.current)
return;
const selectedTask = barEvent.selectedTask;
const changedTask = { ...selectedTask } as BarTask;
switch (event.type) {
// On Event changing
case "mousemove": {
switch (barEvent.action) {
case "progress":
changedTask.progress = progressByX(event.offsetX, selectedTask);
break;
case "start": {
const newX1 = startByX(event.offsetX, xStep, selectedTask);
changedTask.x1 = newX1;
changedTask.start = dateByX(
newX1,
selectedTask.x1,
selectedTask.start,
xStep,
timeStep
);
break;
}
case "end": {
const newX2 = endByX(event.offsetX, xStep, selectedTask);
changedTask.x2 = newX2;
changedTask.end = dateByX(
newX2,
selectedTask.x2,
selectedTask.end,
xStep,
timeStep
);
break;
}
case "move": {
const [newMoveX1, newMoveX2] = moveByX(
event.offsetX - initEventX1Delta,
xStep,
selectedTask
);
changedTask.start = dateByX(
newMoveX1,
selectedTask.x1,
selectedTask.start,
xStep,
timeStep
);
changedTask.end = dateByX(
newMoveX2,
selectedTask.x2,
selectedTask.end,
xStep,
timeStep
);
changedTask.x1 = newMoveX1;
changedTask.x2 = newMoveX2;
break;
}
}
// Update internal state
setBarTasks(
barTasks.map(t => (t.id === changedTask.id ? changedTask : t))
);
setBarEvent({ ...barEvent, selectedTask: changedTask });
break;
}
// On finish Event
case "mouseup": {
let eventForExecution: (
task: Task
) => void | Promise<void> = () => {};
switch (barEvent.action) {
case "progress":
changedTask.progress = progressByX(event.offsetX, selectedTask);
if (onProgressChange) {
eventForExecution = onProgressChange;
}
break;
case "start": {
const newX1 = startByX(event.offsetX, xStep, selectedTask);
changedTask.start = dateByX(
newX1,
selectedTask.x1,
selectedTask.start,
xStep,
timeStep
);
if (onDateChange && newX1 !== selectedTask.x1) {
eventForExecution = onDateChange;
}
break;
}
case "end": {
const newX2 = endByX(event.offsetX, xStep, selectedTask);
changedTask.end = dateByX(
newX2,
selectedTask.x2,
selectedTask.end,
xStep,
timeStep
);
if (onDateChange && newX2 !== selectedTask.x2) {
eventForExecution = onDateChange;
}
break;
}
case "move": {
const [newMoveX1, newMoveX2] = moveByX(
event.offsetX - initEventX1Delta,
xStep,
selectedTask
);
changedTask.start = dateByX(
newMoveX1,
selectedTask.x1,
selectedTask.start,
xStep,
timeStep
);
changedTask.end = dateByX(
newMoveX2,
selectedTask.x2,
selectedTask.end,
xStep,
timeStep
);
if (
onDateChange &&
newMoveX1 !== selectedTask.x1 &&
newMoveX2 !== selectedTask.x2
) {
eventForExecution = onDateChange;
}
break;
}
}
setBarEvent({ action: "", selectedTask: null });
setIsSVGListen(false);
svg.current.removeEventListener(
"mousemove",
handleMouseSVGChangeEventsSubscribe
);
svg.current.removeEventListener(
"mouseup",
handleMouseSVGChangeEventsSubscribe
);
// If update successful - update Gantt state, otherwise we shell back old Bar state
await eventForExecution(changedTask);
break;
}
}
};
if (
barEvent.selectedTask &&
barEvent.action &&
!isSVGListen &&
svg &&
svg.current
) {
setIsSVGListen(true);
svg.current.addEventListener(
"mousemove",
handleMouseSVGChangeEventsSubscribe
);
svg.current.addEventListener(
"mouseup",
handleMouseSVGChangeEventsSubscribe
);
}
}, [
barEvent,
isSVGListen,
xStep,
svg,
initEventX1Delta,
barTasks,
onProgressChange,
timeStep,
onDateChange,
]); ]);
/** /**
* Method is Start point of task change * Method is Start point of task change
* @param event init mouse event
* @param eventType
* @param task events task
*/ */
const handleMouseEvents = ( const handleBarEventStart = (
event: event: React.MouseEvent | React.KeyboardEvent,
| React.MouseEvent<SVGPolygonElement, MouseEvent> action: GanttContentMoveAction,
| React.MouseEvent<SVGRectElement, MouseEvent> selectedTask: BarTask
| React.MouseEvent<SVGGElement, MouseEvent>,
eventType: BarAction,
task: BarTask
) => { ) => {
switch (event.type) { if (isKeyboardEvent(event)) {
case "mousedown": if (action === "delete") {
setBarEvent({ ...barEvent, selectedTask: task, action: eventType }); setBarTasks(barTasks.filter(t => t.id !== barEvent.selectedTask?.id));
setInitEventX1Delta(event.nativeEvent.offsetX - task.x1); }
event.stopPropagation(); } else if (action === "mouseenter") {
break; if (!barEvent.action) {
case "mouseleave": setBarEvent({ action, selectedTask });
if (!barEvent.action) }
setBarEvent({ ...barEvent, selectedTask: null, action: "" }); } else if (action === "mouseleave") {
break; if (barEvent.action === "mouseenter") {
case "mouseenter": setBarEvent({ action: "" });
if (!barEvent.selectedTask) { }
setBarEvent({ ...barEvent, selectedTask: task, action: "" }); } else if (action === "move") {
} if (!svg.current || !point) return;
break; point.x = event.clientX;
const cursor = point.matrixTransform(
svg.current.getScreenCTM()?.inverse()
);
setInitEventX1Delta(cursor.x - selectedTask.x1);
setBarEvent({ action, selectedTask });
} else {
setBarEvent({
action,
selectedTask,
});
} }
}; };
/** useEffect(() => {
* Method handles Bar keyboard events const handleMouseMove = async (event: MouseEvent) => {
* @param event if (!barEvent.selectedTask || !point || !svg.current) return;
* @param task event.preventDefault();
*/
const handleButtonSVGEvents = async ( point.x = event.clientX;
event: React.KeyboardEvent<SVGGElement>, const cursor = point.matrixTransform(
task: BarTask svg.current.getScreenCTM()?.inverse()
) => { );
if (task.isDisabled) return;
switch (event.key) { const { isChanged, changedTask } = handleTaskBySVGMouseEvent(
case "Delete": { cursor.x,
if (onTaskDelete) { barEvent.action as BarMoveAction,
onTaskDelete(task); barEvent.selectedTask,
} xStep,
break; timeStep,
initEventX1Delta
);
if (isChanged) {
setBarTasks(
barTasks.map(t => (t.id === changedTask.id ? changedTask : t))
);
setBarEvent({ ...barEvent, selectedTask: changedTask });
} }
};
const handleMouseUp = async (event: MouseEvent) => {
const { selectedTask, action } = barEvent;
if (!selectedTask || !point || !svg.current) return;
event.preventDefault();
point.x = event.clientX;
const cursor = point.matrixTransform(
svg.current.getScreenCTM()?.inverse()
);
const { changedTask } = handleTaskBySVGMouseEvent(
cursor.x,
action as BarMoveAction,
selectedTask,
xStep,
timeStep,
initEventX1Delta
);
if (
(action === "move" || action === "end" || action === "start") &&
onDateChange
) {
onDateChange(changedTask);
} else if (onProgressChange) {
onProgressChange(changedTask);
}
svg.current.removeEventListener("mousemove", handleMouseMove);
svg.current.removeEventListener("mouseup", handleMouseUp);
console.log(`Start: ${changedTask.start} End: ${changedTask.end}`);
setBarEvent({ action: "" });
setIsMoving(false);
};
if (
!isMoving &&
(barEvent.action === "move" ||
barEvent.action === "end" ||
barEvent.action === "start" ||
barEvent.action === "progress") &&
svg.current
) {
svg.current.addEventListener("mousemove", handleMouseMove);
svg.current.addEventListener("mouseup", handleMouseUp);
setIsMoving(true);
} }
}; }, [
barTasks,
barEvent,
xStep,
initEventX1Delta,
onProgressChange,
timeStep,
onDateChange,
svg,
isMoving,
]);
return ( return (
<g className="content"> <g className="content">
@ -395,7 +259,7 @@ export const GanttContent: React.FC<GanttContentProps> = ({
return task.barChildren.map(child => { return task.barChildren.map(child => {
return ( return (
<Arrow <Arrow
key={`Arrow from ${task.id} to ${barTasks[child].id}`} key={`Arrow from ${task.id} to ${tasks[child].id}`}
taskFrom={task} taskFrom={task}
taskTo={barTasks[child]} taskTo={barTasks[child]}
rowHeight={rowHeight} rowHeight={rowHeight}
@ -414,26 +278,24 @@ export const GanttContent: React.FC<GanttContentProps> = ({
isProgressChangeable={!!onProgressChange && !task.isDisabled} isProgressChangeable={!!onProgressChange && !task.isDisabled}
onDoubleClick={onDoubleClick} onDoubleClick={onDoubleClick}
isDateChangeable={!!onDateChange && !task.isDisabled} isDateChangeable={!!onDateChange && !task.isDisabled}
handleMouseEvents={handleMouseEvents} isDelete={!!onTaskDelete && !task.isDisabled}
handleButtonSVGEvents={handleButtonSVGEvents} onEventStart={handleBarEventStart}
key={task.id} key={task.id}
/> />
); );
})} })}
</g> </g>
<g className="toolTip"> <g className="toolTip">
{barEvent.selectedTask && {barEvent.selectedTask && (
barEvent.action !== "end" && <Tooltip
barEvent.action !== "start" && ( x={barEvent.selectedTask.x2 + arrowIndent + arrowIndent * 0.5}
<Tooltip y={barEvent.selectedTask.y + rowHeight}
x={barEvent.selectedTask.x2 + columnWidth + arrowIndent} task={barEvent.selectedTask}
y={barEvent.selectedTask.y + rowHeight} fontFamily={fontFamily}
task={barEvent.selectedTask} fontSize={fontSize}
fontFamily={fontFamily} getTooltipContent={getTooltipContent}
fontSize={fontSize} />
getTooltipContent={getTooltipContent} )}
/>
)}
</g> </g>
</g> </g>
); );

View File

@ -33,7 +33,7 @@ export const Gantt: React.SFC<GanttProps> = ({
}) => { }) => {
const [startDate, endDate] = ganttDateRange(tasks, viewMode); const [startDate, endDate] = ganttDateRange(tasks, viewMode);
const dates = seedDates(startDate, endDate, viewMode); const dates = seedDates(startDate, endDate, viewMode);
const svg = useRef<SVGSVGElement | null>(null); const svg = useRef<SVGSVGElement>(null);
const gridProps: GridProps = { const gridProps: GridProps = {
columnWidth, columnWidth,
@ -68,24 +68,23 @@ export const Gantt: React.SFC<GanttProps> = ({
handleWidth, handleWidth,
timeStep, timeStep,
arrowColor, arrowColor,
svg,
fontFamily, fontFamily,
fontSize, fontSize,
arrowIndent, arrowIndent,
svg,
onDateChange, onDateChange,
onProgressChange, onProgressChange,
onDoubleClick, onDoubleClick,
onTaskDelete, onTaskDelete,
getTooltipContent, getTooltipContent,
}; };
return ( return (
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
width={columnWidth * dates.length} width={columnWidth * dates.length}
height={headerHeight + rowHeight * tasks.length} height={headerHeight + rowHeight * tasks.length}
ref={svg}
fontFamily={fontFamily} fontFamily={fontFamily}
ref={svg}
> >
<Grid {...gridProps} /> <Grid {...gridProps} />
<Calendar {...calendarProps} /> <Calendar {...calendarProps} />

View File

@ -68,6 +68,7 @@ export const convertToBarTask = (
const x1 = taskXCoordinate(task.start, dates, dateDelta, columnWidth); const x1 = taskXCoordinate(task.start, dates, dateDelta, columnWidth);
const x2 = taskXCoordinate(task.end, dates, dateDelta, columnWidth); const x2 = taskXCoordinate(task.end, dates, dateDelta, columnWidth);
const y = taskYCoordinate(index, rowHeight, taskHeight, headerHeight); const y = taskYCoordinate(index, rowHeight, taskHeight, headerHeight);
const styles = { const styles = {
backgroundColor: barBackgroundColor, backgroundColor: barBackgroundColor,
backgroundSelectedColor: barBackgroundSelectedColor, backgroundSelectedColor: barBackgroundSelectedColor,
@ -213,3 +214,84 @@ export const dateByX = (
); );
return newDate; return newDate;
}; };
export type BarMoveAction = "progress" | "end" | "start" | "move" | "";
/**
* Method handles event in real time(mousemove) and on finish(mouseup)
*/
export const handleTaskBySVGMouseEvent = (
svgX: number,
action: BarMoveAction,
selectedTask: BarTask,
xStep: number,
timeStep: number,
initEventX1Delta: number
) => {
const changedTask: BarTask = { ...selectedTask };
let isChanged = false;
switch (action) {
case "progress":
changedTask.progress = progressByX(svgX, selectedTask);
isChanged = changedTask.progress !== selectedTask.progress;
break;
case "start": {
const newX1 = startByX(svgX, xStep, selectedTask);
changedTask.x1 = newX1;
isChanged = changedTask.x1 !== selectedTask.x1;
if (isChanged) {
changedTask.start = dateByX(
newX1,
selectedTask.x1,
selectedTask.start,
xStep,
timeStep
);
}
break;
}
case "end": {
const newX2 = endByX(svgX, xStep, selectedTask);
changedTask.x2 = newX2;
isChanged = changedTask.x2 !== selectedTask.x2;
if (isChanged) {
changedTask.end = dateByX(
newX2,
selectedTask.x2,
selectedTask.end,
xStep,
timeStep
);
}
break;
}
case "move": {
const [newMoveX1, newMoveX2] = moveByX(
svgX - initEventX1Delta,
xStep,
selectedTask
);
isChanged = newMoveX1 !== selectedTask.x1;
if (isChanged) {
changedTask.start = dateByX(
newMoveX1,
selectedTask.x1,
selectedTask.start,
xStep,
timeStep
);
changedTask.end = dateByX(
newMoveX2,
selectedTask.x2,
selectedTask.end,
xStep,
timeStep
);
changedTask.x1 = newMoveX1;
changedTask.x2 = newMoveX2;
}
break;
}
}
return { isChanged, changedTask };
};

View File

@ -0,0 +1,5 @@
export function isKeyboardEvent(
event: React.MouseEvent | React.KeyboardEvent
): event is React.KeyboardEvent {
return (event as React.KeyboardEvent).key !== undefined;
}