Skip to content

Commit d1fb6d0

Browse files
[REFACTOR] Use Command Pattern for Indoor Next/Prev Buttons (#216)
* refactor indoor buttons to command pattern * format * comment
1 parent 8ba60a2 commit d1fb6d0

4 files changed

Lines changed: 208 additions & 101 deletions

File tree

__tests__/indoor-map-step-navigation.test.tsx

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -155,8 +155,10 @@ describe("IndoorMap step-driven navigation", () => {
155155
expect(controlsProps.mode).toBe("step");
156156
expect(controlsProps.currentStep).toBe(1);
157157
expect(controlsProps.totalSteps).toBe(2);
158-
expect(controlsProps.canGoPrevious).toBe(false);
159-
expect(controlsProps.canGoNext).toBe(true);
158+
expect(controlsProps.previousCommand.canExecute).toBe(false);
159+
expect(controlsProps.previousCommand.label).toBe("Prev Step");
160+
expect(controlsProps.nextCommand.canExecute).toBe(true);
161+
expect(controlsProps.nextCommand.label).toBe("Next Step");
160162
expect(controlsProps.stepInstruction).toContain("Start");
161163
});
162164
});
@@ -455,7 +457,7 @@ describe("IndoorMap step-driven navigation", () => {
455457

456458
await act(async () => {
457459
const controlsProps = mockIndoorNavigationControls.mock.calls.at(-1)?.[0] as any;
458-
controlsProps.onNext();
460+
controlsProps.nextCommand.execute();
459461
});
460462

461463
await waitFor(() => {
@@ -468,7 +470,7 @@ describe("IndoorMap step-driven navigation", () => {
468470

469471
await act(async () => {
470472
const controlsProps = mockIndoorNavigationControls.mock.calls.at(-1)?.[0] as any;
471-
controlsProps.onNext();
473+
controlsProps.nextCommand.execute();
472474
});
473475

474476
await waitFor(() => {
@@ -481,7 +483,7 @@ describe("IndoorMap step-driven navigation", () => {
481483

482484
await act(async () => {
483485
const controlsProps = mockIndoorNavigationControls.mock.calls.at(-1)?.[0] as any;
484-
controlsProps.onPrevious();
486+
controlsProps.previousCommand.execute();
485487
});
486488

487489
await waitFor(() => {
@@ -563,26 +565,26 @@ describe("IndoorMap step-driven navigation", () => {
563565

564566
await waitFor(() => {
565567
const controlsProps = mockIndoorNavigationControls.mock.calls.at(-1)?.[0] as any;
566-
expect(controlsProps.canGoNext).toBe(true);
568+
expect(controlsProps.nextCommand.canExecute).toBe(true);
567569
});
568570

569571
await act(async () => {
570572
const controlsProps = mockIndoorNavigationControls.mock.calls.at(-1)?.[0] as any;
571-
controlsProps.onNext();
573+
controlsProps.nextCommand.execute();
572574
});
573575

574576
await waitFor(() => {
575577
const controlsProps = mockIndoorNavigationControls.mock.calls.at(-1)?.[0] as any;
576578
expect(controlsProps.currentStep).toBe(2);
577-
expect(controlsProps.canGoNext).toBe(true);
579+
expect(controlsProps.nextCommand.canExecute).toBe(true);
578580
expect(controlsProps.stepInstruction).toContain(
579581
"Continue to the outdoor route on the next step.",
580582
);
581583
});
582584

583585
await act(async () => {
584586
const controlsProps = mockIndoorNavigationControls.mock.calls.at(-1)?.[0] as any;
585-
controlsProps.onNext();
587+
controlsProps.nextCommand.execute();
586588
});
587589

588590
expect(router.back).toHaveBeenCalled();
@@ -1084,28 +1086,30 @@ describe("IndoorMap step-driven navigation", () => {
10841086
render(<IndoorMap />);
10851087

10861088
expect(getLatestControlProps().mode).toBe("floor");
1089+
expect(getLatestControlProps().previousCommand.label).toBe("Prev Floor");
1090+
expect(getLatestControlProps().nextCommand.label).toBe("Next Floor");
10871091
expect(getLatestFloorProps().floor).toBe(1);
10881092

10891093
await act(async () => {
1090-
getLatestControlProps().onNext();
1094+
getLatestControlProps().nextCommand.execute();
10911095
});
10921096

10931097
await waitFor(() => {
10941098
expect(getLatestFloorProps().floor).toBe(2);
1095-
expect(getLatestControlProps().canGoPrevious).toBe(true);
1099+
expect(getLatestControlProps().previousCommand.canExecute).toBe(true);
10961100
});
10971101

10981102
await act(async () => {
1099-
getLatestControlProps().onNext();
1103+
getLatestControlProps().nextCommand.execute();
11001104
});
11011105

11021106
await waitFor(() => {
11031107
expect(getLatestFloorProps().floor).toBe(3);
1104-
expect(getLatestControlProps().canGoNext).toBe(false);
1108+
expect(getLatestControlProps().nextCommand.canExecute).toBe(false);
11051109
});
11061110

11071111
await act(async () => {
1108-
getLatestControlProps().onPrevious();
1112+
getLatestControlProps().previousCommand.execute();
11091113
});
11101114

11111115
await waitFor(() => {

app/(tabs)/(map)/[buildingCode].tsx

Lines changed: 97 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ import {
1111
FloorCheckpointsGraph,
1212
IndoorNavigationPath,
1313
} from "@/types/mapTypes";
14+
import {
15+
createFloorNavigationCommandSet,
16+
createStepNavigationCommandSet,
17+
} from "@/utils/indoorNavigationCommands";
1418
import { findIndoorPath } from "@/utils/indoorNavigation";
1519
import { describeIndoorStep } from "@/utils/indoorStepInstructions";
1620
import {
@@ -215,52 +219,105 @@ export default function IndoorMap() {
215219
}, [endRoom, startRoom, validEndRoom, validStartRoom]);
216220

217221
type NavigationDirection = "next" | "prev";
218-
const handleFloorNavigation = (direction: NavigationDirection) => {
219-
if (currentFloorIndex < 0) {
220-
return;
221-
}
222-
const delta = direction === "next" ? 1 : -1;
223-
const newFloorIndex = Math.min(
224-
Math.max(currentFloorIndex + delta, 0),
225-
availableFloors.length - 1,
226-
);
227-
setFloor(availableFloors[newFloorIndex]);
228-
};
229-
const handleStepNavigation = (direction: NavigationDirection) => {
230-
if (!hasStepNavigation || !navigationPath || !floorInfo) {
231-
return;
232-
}
222+
const handleFloorNavigation = useCallback(
223+
(direction: NavigationDirection) => {
224+
if (currentFloorIndex < 0) {
225+
return;
226+
}
227+
const delta = direction === "next" ? 1 : -1;
228+
const newFloorIndex = Math.min(
229+
Math.max(currentFloorIndex + delta, 0),
230+
availableFloors.length - 1,
231+
);
232+
setFloor(availableFloors[newFloorIndex]);
233+
},
234+
[availableFloors, currentFloorIndex],
235+
);
236+
const handleStepNavigation = useCallback(
237+
(direction: NavigationDirection) => {
238+
if (!hasStepNavigation || !navigationPath || !floorInfo) {
239+
return;
240+
}
233241

234-
if (direction === "next" && isOnLastIndoorStep) {
235-
if (outdoorStepResume) {
236-
OutdoorStepResume.setPendingStep(outdoorStepResume);
237-
if (autoResumeContinuationId) {
238-
OutdoorStepResume.clearContinuation(autoResumeContinuationId);
242+
if (direction === "next" && isOnLastIndoorStep) {
243+
if (outdoorStepResume) {
244+
OutdoorStepResume.setPendingStep(outdoorStepResume);
245+
if (autoResumeContinuationId) {
246+
OutdoorStepResume.clearContinuation(autoResumeContinuationId);
247+
}
248+
router.back();
239249
}
240-
router.back();
250+
return;
241251
}
242-
return;
243-
}
244252

245-
const delta = direction === "next" ? 1 : -1;
246-
const nextStepPointer = Math.min(
247-
Math.max(boundedStepPointer + delta, 0),
248-
totalPathSteps - 1,
249-
);
250-
if (nextStepPointer === boundedStepPointer) {
251-
return;
252-
}
253+
const delta = direction === "next" ? 1 : -1;
254+
const nextStepPointer = Math.min(
255+
Math.max(boundedStepPointer + delta, 0),
256+
totalPathSteps - 1,
257+
);
258+
if (nextStepPointer === boundedStepPointer) {
259+
return;
260+
}
253261

254-
const nextPathIndex = stepStopIndices[nextStepPointer];
255-
const nextStepCheckpoint =
256-
floorInfo.graphData.checkpoints[navigationPath[nextPathIndex]];
257-
if (!nextStepCheckpoint) {
258-
return;
262+
const nextPathIndex = stepStopIndices[nextStepPointer];
263+
const nextStepCheckpoint =
264+
floorInfo.graphData.checkpoints[navigationPath[nextPathIndex]];
265+
if (!nextStepCheckpoint) {
266+
return;
267+
}
268+
269+
setCurrentPathStepIndex(nextStepPointer);
270+
setFloor(nextStepCheckpoint.floor);
271+
},
272+
[
273+
autoResumeContinuationId,
274+
boundedStepPointer,
275+
floorInfo,
276+
hasStepNavigation,
277+
isOnLastIndoorStep,
278+
navigationPath,
279+
outdoorStepResume,
280+
stepStopIndices,
281+
totalPathSteps,
282+
],
283+
);
284+
const navigationControls = useMemo(() => {
285+
const currentFloor = defaultFloor ?? firstFloor ?? 0;
286+
287+
if (hasStepNavigation) {
288+
return createStepNavigationCommandSet({
289+
currentFloor,
290+
currentStep: boundedStepPointer + 1,
291+
totalSteps: Math.max(totalPathSteps, 1),
292+
stepInstruction: currentStepInstruction,
293+
canGoNext: canGoNextStep,
294+
canGoPrevious: canGoPreviousStep,
295+
onNext: () => handleStepNavigation("next"),
296+
onPrevious: () => handleStepNavigation("prev"),
297+
});
259298
}
260299

261-
setCurrentPathStepIndex(nextStepPointer);
262-
setFloor(nextStepCheckpoint.floor);
263-
};
300+
return createFloorNavigationCommandSet({
301+
currentFloor,
302+
canGoNext: canGoNextFloor,
303+
canGoPrevious: canGoPreviousFloor,
304+
onNext: () => handleFloorNavigation("next"),
305+
onPrevious: () => handleFloorNavigation("prev"),
306+
});
307+
}, [
308+
boundedStepPointer,
309+
canGoNextFloor,
310+
canGoNextStep,
311+
canGoPreviousFloor,
312+
canGoPreviousStep,
313+
currentStepInstruction,
314+
defaultFloor,
315+
firstFloor,
316+
handleFloorNavigation,
317+
handleStepNavigation,
318+
hasStepNavigation,
319+
totalPathSteps,
320+
]);
264321

265322
const createPathFromRooms = useCallback(() => {
266323
setHasCheckpointDrivenRoute(false);
@@ -648,29 +705,7 @@ export default function IndoorMap() {
648705
poiFilters={poiFilters}
649706
setPoiFilters={setPoiFilters}
650707
/>
651-
<IndoorNavigationControls
652-
onNext={() => {
653-
if (hasStepNavigation) {
654-
handleStepNavigation("next");
655-
return;
656-
}
657-
handleFloorNavigation("next");
658-
}}
659-
onPrevious={() => {
660-
if (hasStepNavigation) {
661-
handleStepNavigation("prev");
662-
return;
663-
}
664-
handleFloorNavigation("prev");
665-
}}
666-
currentFloor={defaultFloor}
667-
canGoNext={hasStepNavigation ? canGoNextStep : canGoNextFloor}
668-
canGoPrevious={hasStepNavigation ? canGoPreviousStep : canGoPreviousFloor}
669-
mode={hasStepNavigation ? "step" : "floor"}
670-
currentStep={boundedStepPointer + 1}
671-
totalSteps={Math.max(totalPathSteps, 1)}
672-
stepInstruction={currentStepInstruction}
673-
/>
708+
<IndoorNavigationControls {...navigationControls} />
674709
<BuildingFloor
675710
info={floorInfo}
676711
poiFilters={poiFilters}

components/map/indoor-navigation-controls.tsx

Lines changed: 12 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,19 @@
11
import { Colors } from "@/constants/theme";
22
import { useColorScheme } from "@/hooks/use-color-scheme";
3+
import type { IndoorNavigationControlsState } from "@/utils/indoorNavigationCommands";
34
import { Ionicons } from "@expo/vector-icons";
45
import React from "react";
56
import { View, Text, TouchableOpacity, StyleSheet } from "react-native";
67

7-
interface IndoorNavigationControlsProps {
8-
onNext: () => void;
9-
onPrevious: () => void;
10-
currentFloor: number;
11-
canGoNext?: boolean;
12-
canGoPrevious?: boolean;
13-
mode?: "floor" | "step";
14-
currentStep?: number;
15-
totalSteps?: number;
16-
stepInstruction?: string;
17-
}
18-
198
export default function IndoorNavigationControls({
20-
onNext,
21-
onPrevious,
9+
nextCommand,
10+
previousCommand,
2211
currentFloor,
23-
canGoNext = true,
24-
canGoPrevious = true,
2512
mode = "floor",
2613
currentStep = 1,
2714
totalSteps = 1,
2815
stepInstruction,
29-
}: Readonly<IndoorNavigationControlsProps>) {
16+
}: Readonly<IndoorNavigationControlsState>) {
3017
const colorScheme = useColorScheme();
3118
const theme = Colors[colorScheme];
3219
const styles = makeStyles(theme);
@@ -42,12 +29,12 @@ export default function IndoorNavigationControls({
4229
return (
4330
<View style={styles.container}>
4431
<TouchableOpacity
45-
style={[styles.sideButton, !canGoPrevious && styles.disabled]}
46-
onPress={onPrevious}
47-
disabled={!canGoPrevious}
32+
style={[styles.sideButton, !previousCommand.canExecute && styles.disabled]}
33+
onPress={previousCommand.execute}
34+
disabled={!previousCommand.canExecute}
4835
>
4936
<Ionicons name="arrow-back" size={18} color={theme.mapSettings.fabIcon} />
50-
<Text style={styles.sideText}>{isStepMode ? "Prev Step" : "Prev Floor"}</Text>
37+
<Text style={styles.sideText}>{previousCommand.label}</Text>
5138
</TouchableOpacity>
5239

5340
<View style={styles.centerCard}>
@@ -60,11 +47,11 @@ export default function IndoorNavigationControls({
6047
</View>
6148

6249
<TouchableOpacity
63-
style={[styles.sideButton, !canGoNext && styles.disabled]}
64-
onPress={onNext}
65-
disabled={!canGoNext}
50+
style={[styles.sideButton, !nextCommand.canExecute && styles.disabled]}
51+
onPress={nextCommand.execute}
52+
disabled={!nextCommand.canExecute}
6653
>
67-
<Text style={styles.sideText}>{isStepMode ? "Next Step" : "Next Floor"}</Text>
54+
<Text style={styles.sideText}>{nextCommand.label}</Text>
6855
<Ionicons name="arrow-forward" size={18} color={theme.mapSettings.fabIcon} />
6956
</TouchableOpacity>
7057
</View>

0 commit comments

Comments
 (0)