Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mobile device accessibility #121

Merged
merged 37 commits into from
Oct 8, 2024
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
2e2ba8f
on mobile device, only third selection mode is allowed
TeunHuijben Sep 30, 2024
3559298
typo
TeunHuijben Sep 30, 2024
8532e3d
added alert to check if mobile is detected
TeunHuijben Sep 30, 2024
bea014c
iPad wasnt detected
TeunHuijben Sep 30, 2024
9191f98
iPad stil not detected
TeunHuijben Sep 30, 2024
d5b5541
initialize sphere selector in middle of point cloud
TeunHuijben Sep 30, 2024
9852c66
added select-cells button when on Mobile device, to place shift+click"
TeunHuijben Oct 1, 2024
ee02efd
changed sections in leftSideBar
TeunHuijben Oct 1, 2024
183cee8
renamed tooltips + cleanup
TeunHuijben Oct 1, 2024
9cf0e05
detect desktop/tablet/phone
TeunHuijben Oct 1, 2024
4610393
removed device alert
TeunHuijben Oct 1, 2024
7bf8710
scale canvas to screen (tablet/phone/desktop) to prevent the playBack…
TeunHuijben Oct 1, 2024
d1bce74
added margins for phone/tablet
TeunHuijben Oct 1, 2024
d286d81
corrected typos
TeunHuijben Oct 1, 2024
06df6d1
clean up
TeunHuijben Oct 1, 2024
3381a08
don't reduce point brightness when no cells are selected
TeunHuijben Oct 1, 2024
074ec8a
remove spherical selector when no mode is selected
TeunHuijben Oct 1, 2024
ac990b0
disable any selector on phone
TeunHuijben Oct 1, 2024
d98919e
on iPad: only show the select cells button if the selection mode is a…
TeunHuijben Oct 1, 2024
27e8988
selection info box disappears when no mode is selected
TeunHuijben Oct 1, 2024
8b08ed4
center the select cells button on
TeunHuijben Oct 1, 2024
4224bb0
disable timeStamp on Canvas, since it is already on top of the slider
TeunHuijben Oct 1, 2024
3943c50
lint fixes
TeunHuijben Oct 1, 2024
19dd67b
added slider to iPad configuration, to allow changing selector size w…
TeunHuijben Oct 2, 2024
f47a0be
fixed bug in selector scale slider
TeunHuijben Oct 2, 2024
7ccee15
updated ControlInstructions syntax
TeunHuijben Oct 2, 2024
1f1d5eb
added conditional iPad statement to the ConrolInstructions
TeunHuijben Oct 2, 2024
5d184aa
minor textual change
TeunHuijben Oct 2, 2024
d835f95
fixed lint issue
TeunHuijben Oct 2, 2024
47a3af0
popup on phone
TeunHuijben Oct 2, 2024
9a76187
changed phone popup from alert to confirm
TeunHuijben Oct 2, 2024
2242ed7
change to popup
TeunHuijben Oct 2, 2024
d0fd8b7
changed data path
TeunHuijben Oct 8, 2024
e294474
merge with main + fixes
TeunHuijben Oct 8, 2024
ef61364
keep preview when setting brightness or changing the selector size on…
TeunHuijben Oct 8, 2024
edbe9bf
fixed that DataControls stay on pays and dont scroll
TeunHuijben Oct 8, 2024
c91e10e
Merge branch 'main' into mobile
TeunHuijben Oct 8, 2024
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
246 changes: 164 additions & 82 deletions src/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
import { TrackManager, loadTrackManager } from "@/lib/TrackManager";
import { PointSelectionMode } from "@/lib/PointSelector";
import LeftSidebarWrapper from "./leftSidebar/LeftSidebarWrapper";
import { TimestampOverlay } from "./overlays/TimestampOverlay";
// import { TimestampOverlay } from "./overlays/TimestampOverlay";
import { ColorMap } from "./overlays/ColorMap";
import { TrackDownloadData } from "./DownloadButton";

Expand All @@ -27,6 +27,51 @@
console.log("initial viewer state: ", initialViewerState);
clearUrlHash();

function detectDeviceType(): { isPhone: boolean; isTablet: boolean; isMobile: boolean } {
const ua = navigator.userAgent || navigator.vendor;

// Detect iPads, iPhones, and iPods based on the user agent string
const isiPad =
/iPad/.test(ua) || (navigator.maxTouchPoints && navigator.maxTouchPoints > 1 && /Macintosh/.test(ua));
const isiPhoneOrIPod = /iPhone|iPod/.test(ua);
TeunHuijben marked this conversation as resolved.
Show resolved Hide resolved

// Detect Android phones and tablets
const isAndroidPhone = /Android/.test(ua) && /Mobile/.test(ua);
const isAndroidTablet = /Android/.test(ua) && !/Mobile/.test(ua);

// Screen size check (tablets typically have a wider screen)
const isSmallScreen = window.screen.width <= 768;

// Determine if it's a phone, tablet, or desktop
const isPhone = isiPhoneOrIPod || isAndroidPhone || isSmallScreen;
TeunHuijben marked this conversation as resolved.
Show resolved Hide resolved
const isTablet = isiPad || isAndroidTablet || (!isPhone && isSmallScreen && window.screen.width > 600); // Optional: Add a threshold for larger screens
TeunHuijben marked this conversation as resolved.
Show resolved Hide resolved
const isDesktop = !isPhone && !isTablet; // It's a desktop if it's neither a phone nor a tablet

// manually asign labels for debugging
// const isPhone = false;
// const isTablet = true;
// const isDesktop = false;

return {
isPhone: isPhone,
isTablet: isTablet && !isPhone, // To avoid small phones being miscategorized as tablets
isMobile: !isDesktop,
};
}

export const detectedDevice = detectDeviceType();
console.log("detectDeviceType: ", detectDeviceType());
// for debugging: show the detected device type in an alert
// window.alert(
// "detected device type (desktop | tablet | phone) = (" +
// !detectDeviceType().isMobile +
// " | " +
// detectDeviceType().isTablet +
// " | " +
// detectDeviceType().isPhone +
// ")",
// );

const drawerWidth = 256;
const playbackFPS = 16;
const playbackIntervalMs = 1000 / playbackFPS;
Expand Down Expand Up @@ -183,7 +228,7 @@
};
updateTracks();
// TODO: add missing dependencies
}, [trackManager, dispatchCanvas, canvas.selectedPointIds]);

Check warning on line 231 in src/components/App.tsx

View workflow job for this annotation

GitHub Actions / lint-and-test

React Hook useEffect has a missing dependency: 'canvas'. Either include it or remove the dependency array

// playback time points
// TODO: this is basic and may drop frames
Expand Down Expand Up @@ -251,109 +296,146 @@
};

return (
<Box sx={{ display: "flex", width: "100%", height: "100%" }}>
<Box sx={{ display: "flex", flexDirection: "column", width: "100%", height: "100%", overflow: "hidden" }}>
{/* TODO: components *could* go deeper still for organization */}
<Drawer
anchor="left"
variant="permanent"
sx={{
"width": drawerWidth,
"flexShrink": 0,
"& .MuiDrawer-paper": { width: drawerWidth, boxSizing: "border-box" },
}}
>
<Box
{!detectedDevice.isPhone && (
<Drawer
anchor="left"
variant="permanent"
sx={{
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
width: "100%",
height: "100%",
"width": drawerWidth,
"flexShrink": 0,
"& .MuiDrawer-paper": { width: drawerWidth, boxSizing: "border-box" },
}}
>
<Box
sx={{
flexGrow: 0,
padding: "1em 1.5em",
display: "flex",
flexDirection: "row",
alignItems: "center",
flexDirection: "column",
justifyContent: "space-between",
width: "100%",
height: "100%",
}}
>
{brandingLogoPath && <img src={brandingLogoPath} alt="" />}
{brandingLogoPath && brandingName && <Divider orientation="vertical" flexItem />}
{brandingName && <h2>{brandingName}</h2>}{" "}
</Box>
<Box flexGrow={0} padding="2em">
<CellControls
clearTracks={() => {
dispatchCanvas({ type: ActionType.REMOVE_ALL_TRACKS });
}}
getTrackDownloadData={getTrackDownloadData}
numSelectedCells={numSelectedCells}
numSelectedTracks={numSelectedTracks}
trackManager={trackManager}
pointBrightness={canvas.pointBrightness}
setPointBrightness={(brightness: number) => {
dispatchCanvas({ type: ActionType.POINT_BRIGHTNESS, brightness });
}}
selectionMode={canvas.selector.selectionMode}
setSelectionMode={(value: PointSelectionMode) => {
dispatchCanvas({ type: ActionType.SELECTION_MODE, selectionMode: value });
<Box
sx={{
flexGrow: 0,
padding: "1em 1.5em",
display: "flex",
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
}}
/>
>
{brandingLogoPath && <img src={brandingLogoPath} alt="" />}
{brandingLogoPath && brandingName && <Divider orientation="vertical" flexItem />}
{brandingName && <h2>{brandingName}</h2>}{" "}
</Box>
<Box flexGrow={0} padding="2em">
<CellControls
clearTracks={() => {
dispatchCanvas({ type: ActionType.REMOVE_ALL_TRACKS });
}}
getTrackDownloadData={getTrackDownloadData}
numSelectedCells={numSelectedCells}
numSelectedTracks={numSelectedTracks}
trackManager={trackManager}
pointBrightness={canvas.pointBrightness}
setPointBrightness={(brightness: number) => {
dispatchCanvas({ type: ActionType.POINT_BRIGHTNESS, brightness });
}}
selectionMode={canvas.selector.selectionMode}
setSelectionMode={(value: PointSelectionMode) => {
dispatchCanvas({ type: ActionType.SELECTION_MODE, selectionMode: value });
}}
isTablet={detectedDevice.isTablet}
MobileSelectCells={() => {
dispatchCanvas({ type: ActionType.MOBILE_SELECT_CELLS });
}}
setSelectorScale={(scale: number) => {
dispatchCanvas({ type: ActionType.SELECTOR_SCALE, scale });
}}
selectorScale={canvas.selector.sphereSelector.cursor.scale.x}
/>
</Box>
<Divider />
<Box flexGrow={4} padding="2em">
<LeftSidebarWrapper
hasTracks={numSelectedCells > 0}
trackManager={trackManager}
trackHighlightLength={trackHighlightLength}
selectionMode={canvas.selector.selectionMode}
showTracks={canvas.showTracks}
setShowTracks={(show: boolean) => {
dispatchCanvas({ type: ActionType.SHOW_TRACKS, showTracks: show });
}}
showTrackHighlights={canvas.showTrackHighlights}
setShowTrackHighlights={(show: boolean) => {
dispatchCanvas({
type: ActionType.SHOW_TRACK_HIGHLIGHTS,
showTrackHighlights: show,
});
}}
setTrackHighlightLength={(length: number) => {
dispatchCanvas({
type: ActionType.MIN_MAX_TIME,
minTime: canvas.curTime - length / 2,
maxTime: canvas.curTime + length / 2,
});
}}
isTablet={detectedDevice.isTablet}
/>
</Box>
<Divider />
<Box flexGrow={0} padding="1em">
<DataControls
dataUrl={dataUrl}
initialDataUrl={initialViewerState.dataUrl}
setDataUrl={setDataUrl}
removeTracksUponNewData={removeTracksUponNewData}
copyShareableUrlToClipboard={copyShareableUrlToClipboard}
trackManager={trackManager}
/>
</Box>
</Box>
<Divider />
<Box flexGrow={4} padding="2em">
<LeftSidebarWrapper
hasTracks={numSelectedCells > 0}
trackManager={trackManager}
trackHighlightLength={trackHighlightLength}
selectionMode={canvas.selector.selectionMode}
showTracks={canvas.showTracks}
setShowTracks={(show: boolean) => {
dispatchCanvas({ type: ActionType.SHOW_TRACKS, showTracks: show });
}}
showTrackHighlights={canvas.showTrackHighlights}
setShowTrackHighlights={(show: boolean) => {
dispatchCanvas({ type: ActionType.SHOW_TRACK_HIGHLIGHTS, showTrackHighlights: show });
}}
setTrackHighlightLength={(length: number) => {
dispatchCanvas({
type: ActionType.MIN_MAX_TIME,
minTime: canvas.curTime - length / 2,
maxTime: canvas.curTime + length / 2,
});
}}
/>
</Box>
<Divider />
<Box flexGrow={0} padding="1em">
<DataControls
dataUrl={dataUrl}
initialDataUrl={initialViewerState.dataUrl}
setDataUrl={setDataUrl}
removeTracksUponNewData={removeTracksUponNewData}
copyShareableUrlToClipboard={copyShareableUrlToClipboard}
trackManager={trackManager}
/>
</Box>
</Box>
</Drawer>
</Drawer>
)}
{/* Box for Scene + playBackControls */}
<Box
sx={{
display: "flex",
flexDirection: "column",
flexGrow: 1,
width: "100%",
height: "100%",
overflow: "hidden",
}}
>
<Scene ref={sceneDivRef} isLoading={isLoadingPoints || numLoadingTracks > 0} />
<Box flexGrow={0} padding="1em">
<TimestampOverlay timestamp={canvas.curTime} />
{/* The canvas (Scene + colormap + timestamp) */}
<Box
ref={sceneDivRef}
sx={{
flexGrow: 1,
width: "100%",
height: "calc(100vh - 100px)", // Ensure canvas does not fill entire screen
overflow: "hidden",
position: "relative", // Add this to make ColorMap and TimestampOverlay relative to the canvas
}}
>
<Scene isLoading={isLoadingPoints || numLoadingTracks > 0} />
{/* <TimestampOverlay timestamp={canvas.curTime} /> */}
<ColorMap />
</Box>

{/* The playback controls */}
<Box
sx={{
flexGrow: 0,
padding: ".5em",
height: detectedDevice.isMobile ? "150px" : "50px", // leaving extra space for mobile
paddingLeft: !detectedDevice.isPhone ? `${drawerWidth}px` : 0, // Ensure playback controls are visible
}}
>
<PlaybackControls
enabled={true}
autoRotate={canvas.controls.autoRotate}
Expand Down
63 changes: 55 additions & 8 deletions src/components/CellControls.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Box, Stack } from "@mui/material";
import { InputSlider, SegmentedControl, SingleButtonDefinition } from "@czi-sds/components";
import { InputSlider, SegmentedControl, SingleButtonDefinition, Button } from "@czi-sds/components";
import { FontS, SmallCapsButton, ControlLabel } from "@/components/Styled";

import { PointSelectionMode } from "@/lib/PointSelector";
Expand All @@ -14,17 +14,35 @@ interface CellControlsProps {
trackManager: TrackManager | null;
pointBrightness: number;
setPointBrightness: (value: number) => void;
selectionMode: PointSelectionMode;
selectionMode: PointSelectionMode | null;
setSelectionMode: (value: PointSelectionMode) => void;
isTablet: boolean;
MobileSelectCells: () => void;
setSelectorScale: (value: number) => void;
selectorScale: number;
}

export default function CellControls(props: CellControlsProps) {
const buttonDefinition: SingleButtonDefinition[] = [
{ icon: "Cube", tooltipText: "Box", value: PointSelectionMode.BOX },
{ icon: "Starburst", tooltipText: "Spherical cursor", value: PointSelectionMode.SPHERICAL_CURSOR },
{ icon: "Globe", tooltipText: "Sphere", value: PointSelectionMode.SPHERE },
{ icon: "Starburst", tooltipText: "Sphere", value: PointSelectionMode.SPHERICAL_CURSOR },
{ icon: "Globe", tooltipText: "Adjustable sphere", value: PointSelectionMode.SPHERE },
];

// Intercept onChange of selection buttons to prevent the first two buttons from being selected on mobile devices
const handleSegmentedControlChange = (_e: React.MouseEvent<HTMLElement>, newValue: PointSelectionMode | null) => {
// If isTablet is true and the selected value corresponds to the first or second button, do nothing
if (
props.isTablet &&
(newValue === PointSelectionMode.BOX || newValue === PointSelectionMode.SPHERICAL_CURSOR)
) {
window.alert("This selection mode is not available on mobile devices.");
console.log("Mobile device detected, preventing selection of box or spherical cursor");
return; // Prevent selection
}
props.setSelectionMode(newValue!); // Otherwise, update the selection mode
};

return (
<Stack spacing="1em">
<Box display="flex" flexDirection="row" alignItems="center" justifyContent="space-between">
Expand All @@ -47,14 +65,43 @@ export default function CellControls(props: CellControlsProps) {
<SegmentedControl
id="selection-mode-control"
buttonDefinition={buttonDefinition}
onChange={(_e, v) => {
props.setSelectionMode(v);
}}
onChange={handleSegmentedControlChange}
value={props.selectionMode}
/>
</Box>
<Box display="flex" justifyContent="center" alignItems="center">
{props.isTablet && props.selectionMode === PointSelectionMode.SPHERE && (
<Button sdsStyle="square" sdsType="primary" onClick={props.MobileSelectCells}>
Select cells
</Button>
)}
</Box>
{(props.selectionMode === PointSelectionMode.SPHERICAL_CURSOR ||
props.selectionMode === PointSelectionMode.SPHERE) &&
props.isTablet && (
<>
<div style={{ display: "flex", justifyContent: "center", alignItems: "center" }}>
<label htmlFor="selector-radius-slider">
<FontS id="input-selector-radius-slider">selector radius:</FontS>
</label>
</div>
<InputSlider
id="selector-radius-slider"
aria-labelledby="input-selector-radius-slider"
min={0.5}
max={5}
step={0.1}
// valueLabelDisplay="on"
valueLabelFormat={(value) => `${value}`}
onChange={(_, value) => {
props.setSelectorScale(value as number);
}}
value={props.selectorScale}
/>
</>
)}
<label htmlFor="points-brightness-slider">
<ControlLabel id="input-slider-points-brightness-slider">Point Brightness</ControlLabel>
<ControlLabel id="input-slider-points-brightness-slider">Cell Brightness</ControlLabel>
</label>
<InputSlider
id="points-brightness-slider"
Expand Down
Loading
Loading