Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -1,30 +1,39 @@
import { Grid } from '@mui/material';
import React, { LegacyRef, useEffect } from 'react';
import { StyleProp, StyleSheet, View, Text } from 'react-native';
import { useEffect, useLayoutEffect, useRef } from 'react';
import { StyleProp, StyleSheet, Text, View, ViewStyle } from 'react-native';
import ChartManager, { Item, WAVE_DELAY_MS } from './ChartManager';
import Animated, {
useAnimatedStyle,
useSharedValue,
withSpring,
} from 'react-native-reanimated';

export type Coordinate = {
x: number;
y: number;
};

interface ChartItemProps {
item: Item;
chartManager: ChartManager;
innerRef?: LegacyRef<View>;
style?: StyleProp<any>;
updateCoordinates?: (id: number, coordinate: Coordinate) => void;
style?: StyleProp<ViewStyle>;
}

const getCenter = (side: number, size: number) => side + size / 2;

export default function ChartItem({
item,
chartManager,
innerRef,
updateCoordinates,
style,
}: ChartItemProps) {
const ref = useRef<View>(null);

const progress = useSharedValue(0);

useEffect(() => {
if (item.id != ChartManager.EMPTY_SPACE_ID) {
if (item.id !== ChartManager.EMPTY_SPACE_ID) {
const listenerId = chartManager.addListener(item.id, (isActive) => {
progress.value = withSpring(isActive ? 1 : 0, {
duration: 2 * WAVE_DELAY_MS,
Expand All @@ -35,7 +44,7 @@ export default function ChartItem({
chartManager.removeListener(item.id, listenerId);
};
}
}, [chartManager]);
}, [chartManager, item.id, progress]);

const animatedStyle = useAnimatedStyle(() => {
return {
Expand All @@ -56,16 +65,31 @@ export default function ChartItem({
};
});

useLayoutEffect(() => {
const box = (
ref.current as unknown as HTMLElement
)?.getBoundingClientRect?.();

if (!box) {
return; // no-op on undefined view ref
}

updateCoordinates(item.id, {
x: getCenter(box.left, box.width),
y: getCenter(box.top, box.height),
});
}, [item, updateCoordinates]);

return (
<Grid item style={styles.box} xs={3}>
<Grid style={styles.box} size={3}>
<Animated.View
style={[
styles.item,
item.isVisible ? null : styles.hidden,
animatedStyle,
style,
]}
ref={innerRef}>
ref={ref}>
<Animated.Text style={[animatedTextStyle, styles.label, style]}>
{item.label}
</Animated.Text>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { useMemo } from 'react';
import {
TapGesture,
PanGesture,
Expand Down Expand Up @@ -92,7 +91,7 @@ export default class ChartManager {
private _connections: ChartConnection[] = [];
private _layout: number[][];
private _listeners: Map<number, Map<number, (isActive: boolean) => void>> =
useMemo(() => new Map(), []);
new Map();

public static EMPTY_SPACE_ID = 0;

Expand Down Expand Up @@ -120,7 +119,7 @@ export default class ChartManager {
itemId: number,
listener: (isActive: boolean) => void
): number {
const listenerId = this._listeners.get(itemId)?.size - 1 ?? 0;
const listenerId = this._listeners.get(itemId)?.size - 1;

// another map is used inside of _listeners to seamlessly remove listening functions from _listeners
if (this._listeners.has(itemId)) {
Expand Down Expand Up @@ -151,7 +150,7 @@ export default class ChartManager {
label = stateToName.get(label);
}

let highlightColor = labelColorMap.get(label) ?? Colors.YELLOW;
const highlightColor = labelColorMap.get(label) ?? Colors.YELLOW;

const newItem = {
id: newId,
Expand Down Expand Up @@ -205,12 +204,12 @@ export default class ChartManager {

undeterminedCallback(true);

const resetAllStates = (event: GestureStateChangeEvent<any>) => {
const resetAllStates = (event: GestureStateChangeEvent<unknown>) => {
undeterminedCallback(true);
if (event.state == State.FAILED) {
if (event.state === State.FAILED) {
failedCallback(true);
}
if (event.state == State.CANCELLED) {
if (event.state === State.CANCELLED) {
cancelledCallback(true);
}
setTimeout(() => {
Expand All @@ -236,7 +235,7 @@ export default class ChartManager {
.onEnd(() => {
endCallback(true);
})
.onFinalize((event: GestureStateChangeEvent<any>) => {
.onFinalize((event: GestureStateChangeEvent<unknown>) => {
resetAllStates(event);
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,66 +1,49 @@
import 'react-native-gesture-handler';
import React, { useEffect, useRef, useState } from 'react';
import { useCallback, useMemo, useReducer, useRef } from 'react';
import { StyleSheet, View } from 'react-native';
import ChartManager from './ChartManager';
import { Grid } from '@mui/material';
import ChartItem from './ChartItem';
import ChartItem, { Coordinate } from './ChartItem';
import Arrow from './Arrow';

type Coordinate = {
x: number;
y: number;
};

type FlowChartProps = {
chartManager: ChartManager;
};

export default function FlowChart({ chartManager }: FlowChartProps) {
const itemsRef = useRef([]);
const itemsCoordsRef = useRef([]);
const rootRef = useRef(null);

// there's a bug where arrows are not shown on the first render on production build
// i hate this but it forces a re-render after the component is mounted
// a man's gotta do what a man's gotta do
const [counter, setCounter] = useState(0);
useEffect(() => {
const timeout = setTimeout(() => {
setCounter(counter + 1);
}, 0);
return () => clearTimeout(timeout);
}, []);
const [, forceUpdate] = useReducer((x: number) => x + 1, 0);
const coordinates = useMemo<Map<number, Coordinate>>(() => new Map(), []);
const rootRef = useRef<View>(null);

const getCenter = (side: number, size: number) => side + size / 2;
const updateCoordinates = useCallback(
(id: number, coordinate: Coordinate) => {
const htmlRootElement = rootRef.current as unknown as HTMLElement;
const root = htmlRootElement.getBoundingClientRect();

itemsCoordsRef.current = itemsRef.current.map((element) => {
// during unloading or overresizing, item may reload itself, causing it to be undefined
if (!element) {
return {
x: 0,
y: 0,
} as Coordinate;
}
if (!root) {
return;
}

const box = element.getBoundingClientRect();
const root = rootRef.current.getBoundingClientRect();
return {
x: getCenter(box.left, box.width) - root.left,
y: getCenter(box.top, box.height) - root.top,
} as Coordinate;
});
// Adjust to root relative positioning
coordinates.set(id, {
x: coordinate.x - root.left,
y: coordinate.y - root.top,
});
forceUpdate();
},
[coordinates]
);

return (
<View style={styles.container} ref={rootRef}>
<Grid container rowGap={4}>
{chartManager.layout.map((row, index) => (
<Grid container spacing={4} key={index}>
{chartManager.layout?.map((row) => (
<Grid container width={'100%'} spacing={4} key={row.toString()}>
{row
.map((itemId) => chartManager.items[itemId])
.map((item, index) => (
.map((item) => (
<ChartItem
key={index}
innerRef={(el) => (itemsRef.current[item.id] = el)}
key={item.id}
updateCoordinates={updateCoordinates}
item={item}
chartManager={chartManager}
/>
Expand All @@ -72,21 +55,22 @@ export default function FlowChart({ chartManager }: FlowChartProps) {
// we have all the connections layed out,
// but the user may choose not to use some of the available items,
if (
!itemsCoordsRef.current[connection.from] ||
!itemsCoordsRef.current[connection.to]
!coordinates.get(connection.from) ||
!coordinates.get(connection.to)
) {
return <View key={connection.id} />;
}

return (
<Arrow
key={connection.id}
startPoint={{
x: itemsCoordsRef.current[connection.from].x,
y: itemsCoordsRef.current[connection.from].y,
x: coordinates.get(connection.from).x,
y: coordinates.get(connection.from).y,
}}
endPoint={{
x: itemsCoordsRef.current[connection.to].x,
y: itemsCoordsRef.current[connection.to].y,
x: coordinates.get(connection.to).x,
y: coordinates.get(connection.to).y,
}}
/>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import 'react-native-gesture-handler';
import React, { useEffect, useMemo, useRef } from 'react';
import { useEffect, useMemo, useReducer } from 'react';
import { StyleSheet, View, useWindowDimensions, Text } from 'react-native';
import Animated, {
useAnimatedStyle,
Expand All @@ -19,49 +18,59 @@ import FlowChart from './FlowChart';
const MIN_DESKTOP_WIDTH = 1298;

export default function App() {
const chartManager = useRef(new ChartManager());
const [, forceUpdate] = useReducer((x: number) => x + 1, 0);
const chartManager = useMemo(() => new ChartManager(), []);

const [panHandle, capturedPan, resetPan] = useMemo(
() => chartManager.current.newGesture(Gesture.Pan()),
[]
() => chartManager.newGesture(Gesture.Pan()),
[chartManager]
);

const [pressHandle, capturedPress, resetLongPress] = useMemo(
() => chartManager.current.newGesture(Gesture.LongPress()),
[]
() => chartManager.newGesture(Gesture.LongPress()),
[chartManager]
);

useEffect(() => {
resetPan();
resetLongPress();
}, []);

const panIds = panHandle.idObject;
const pressIds = pressHandle.idObject;

const dimensions = useWindowDimensions();
const isDesktopMode = dimensions.width > MIN_DESKTOP_WIDTH;

// prettier-ignore
const desktopLayout = [
[panIds.undetermined, ChartManager.EMPTY_SPACE_ID, pressIds.undetermined, ChartManager.EMPTY_SPACE_ID],
[panIds.began, panIds.failed, pressIds.began, pressIds.failed],
[panIds.active, panIds.cancelled, pressIds.active, pressIds.cancelled],
[panIds.end, ChartManager.EMPTY_SPACE_ID, pressIds.end, ChartManager.EMPTY_SPACE_ID],
];
useEffect(() => {
// Timing issue, neither useEffect, useLayoutEffect or requestAnimationFrame work
const timeout = setTimeout(() => {
resetPan();
resetLongPress();
}, 300);
Comment on lines +38 to +42
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😞


// prettier-ignore
const phoneLayout = [
[panIds.undetermined],
[panIds.began, panIds.failed],
[panIds.active, panIds.cancelled],
[panIds.end, ChartManager.EMPTY_SPACE_ID],
];
return () => {
clearTimeout(timeout);
};
}, [resetLongPress, resetPan]);

chartManager.current.layout = isDesktopMode ? desktopLayout : phoneLayout;
useEffect(() => {
const panIds = panHandle.idObject;
const pressIds = pressHandle.idObject;

// prettier-ignore
const desktopLayout = [
[panIds.undetermined, ChartManager.EMPTY_SPACE_ID, pressIds.undetermined, ChartManager.EMPTY_SPACE_ID],
[panIds.began, panIds.failed, pressIds.began, pressIds.failed],
[panIds.active, panIds.cancelled, pressIds.active, pressIds.cancelled],
[panIds.end, ChartManager.EMPTY_SPACE_ID, pressIds.end, ChartManager.EMPTY_SPACE_ID],
];

// prettier-ignore
const phoneLayout = [
[panIds.undetermined],
[panIds.began, panIds.failed],
[panIds.active, panIds.cancelled],
[panIds.end, ChartManager.EMPTY_SPACE_ID],
];

chartManager.layout = isDesktopMode ? desktopLayout : phoneLayout;
forceUpdate();
}, [chartManager, isDesktopMode, panHandle, pressHandle]);

const pressed = useSharedValue(false);

const offset = useSharedValue(0);
const scale = useSharedValue(1);

Expand Down Expand Up @@ -110,7 +119,7 @@ export default function App() {
<Text style={styles.label}>Gesture.LongPress()</Text>
)}
</View>
<FlowChart chartManager={chartManager.current} />
<FlowChart chartManager={chartManager} />
</View>
<GestureHandlerRootView style={styles.container}>
<View style={styles.container}>
Expand Down