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

Resizeable component #31

Merged
merged 4 commits into from
Jan 29, 2025
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
131 changes: 131 additions & 0 deletions src/components/ResizeableContent/ResizeableContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import {
PropsWithChildren,
TouchEvent as ReactTouchEvent,
useCallback,
useRef,
useState
} from 'react';
import { ActionIcon, Box } from '@mantine/core';

import { DragHandleIcon } from '@/icons/icons';
import { IconSize } from '@/types/enums';
import { useWindowSize } from '@/windowmanagement/Window/hooks';

interface Props extends PropsWithChildren {
minHeight?: number;
defaultHeight: number;
}

export function ResizeableContent({ minHeight = 40, defaultHeight, children }: Props) {
const [height, setHeight] = useState(defaultHeight);
const { pointerEvents: windowPointer } = useWindowSize();

const contentRef = useRef<HTMLDivElement>(null);
const resizerRef = useRef<HTMLDivElement>(null);

const updateCursor = useCallback(() => {
document.body.style.cursor = 'row-resize';
// Disable highlight of the window content when dragging
windowPointer.disable();
}, [windowPointer]);

const resetCursor = useCallback(() => {
document.body.style.removeProperty('cursor');
// Reset the regular behaviour of the window
windowPointer.enable();
}, [windowPointer]);
ylvaselling marked this conversation as resolved.
Show resolved Hide resolved

const setHeightClamped = useCallback(
(newHeight: number) => {
setHeight(Math.max(newHeight, minHeight));
},
[minHeight]
);

const handleMouseDown = useCallback(
(x: number, y: number) => {
const startPos = {
x: x,
y: y
};
const currentFirstHeight = contentRef.current?.clientHeight;

const handleMouseMove = (e: MouseEvent) => {
if (!currentFirstHeight) {
return;
}
const dy = e.clientY - startPos.y;
const newHeight = currentFirstHeight + dy;
setHeightClamped(newHeight);
updateCursor();
};

const handleMouseUp = () => {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
resetCursor();
};

document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
},
[contentRef, setHeightClamped, resetCursor, updateCursor]
);

const handleTouchStart = useCallback(
(e: ReactTouchEvent) => {
const startPos = {
x: e.touches[0].clientX,
y: e.touches[0].clientY
};
const currentFirstHeight = contentRef.current?.clientHeight;

const handleTouchMove = (e: TouchEvent) => {
if (!currentFirstHeight) {
return;
}
// Prevent scrolling when dragging
e.preventDefault();

const [touch] = e.touches;
const dy = touch.clientY - startPos.y;
const newHeight = currentFirstHeight + dy;
setHeightClamped(newHeight);
updateCursor();
};

const handleTouchEnd = () => {
document.removeEventListener('touchmove', handleTouchMove);
document.removeEventListener('touchend', handleTouchEnd);
resetCursor();
};
// To block scrolling we need to have an active listener, not passive
document.addEventListener('touchmove', handleTouchMove, { passive: false });
document.addEventListener('touchend', handleTouchEnd);
},
[contentRef, setHeightClamped, resetCursor, updateCursor]
);

return (
<>
<Box ref={contentRef} style={{ height: height }}>
{children}
</Box>
<Box ref={resizerRef}>
<ActionIcon
w={'100%'}
onMouseDown={(e) => handleMouseDown(e.clientX, e.clientY)}
onTouchStart={handleTouchStart}
style={{
cursor: 'row-resize'
}}
size={'xs'}
radius={0}
variant={'default'}
>
<DragHandleIcon size={IconSize.xs} />
</ActionIcon>
</Box>
</>
);
}
47 changes: 23 additions & 24 deletions src/panels/ExoplanetsPanel/ExoplanetsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Collapsable } from '@/components/Collapsable/Collapsable';
import { FilterList } from '@/components/FilterList/FilterList';
import { wordBeginningSubString } from '@/components/FilterList/util';
import { Property } from '@/components/Property/Property';
import { ResizeableContent } from '@/components/ResizeableContent/ResizeableContent';
import { initializeExoplanets } from '@/redux/exoplanets/exoplanetsSlice';
import { useAppDispatch, useAppSelector } from '@/redux/hooks';
import { Identifier } from '@/types/types';
Expand Down Expand Up @@ -87,9 +88,9 @@ export function ExoplanetsPanel() {
}

return (
<ScrollArea h={'100%'}>
<Container fluid my={'md'}>
<FilterList heightPercent={40} isLoading={allSystemNames.length === 0}>
<Container fluid my={'md'}>
<ResizeableContent defaultHeight={300}>
<FilterList isLoading={allSystemNames.length === 0}>
<FilterList.InputField placeHolderSearchText={'Star name...'} />
<FilterList.Data<string>
data={allSystemNames}
Expand All @@ -104,26 +105,24 @@ export function ExoplanetsPanel() {
matcherFunc={wordBeginningSubString}
/>
</FilterList>

<Divider my={'xs'} />
<Collapsable title={'Settings'}>
<Property uri={HabitableZonePropertyKey} />
<Property uri={UncertaintyDiscPropertyKey} />
<Property uri={Size1AuRingPropertyKey} />
</Collapsable>
<Divider my={'xs'}></Divider>
<Title order={3}>Added Systems</Title>
<ScrollArea my={'md'}>
{addedSystems.length === 0 ? (
<Text>No active systems</Text>
) : (
addedSystems.map(
(hostStar) =>
hostStar && <SceneGraphNodeHeader uri={sgnUri(hostStar.identifier)} />
)
)}
</ScrollArea>
</Container>
</ScrollArea>
</ResizeableContent>
<Collapsable title={'Settings'}>
<Property uri={HabitableZonePropertyKey} />
<Property uri={UncertaintyDiscPropertyKey} />
<Property uri={Size1AuRingPropertyKey} />
</Collapsable>
<Divider my={'xs'}></Divider>
<Title order={3}>Added Systems</Title>
<ScrollArea my={'md'}>
{addedSystems.length === 0 ? (
<Text>No active systems</Text>
) : (
addedSystems.map(
(hostStar) =>
hostStar && <SceneGraphNodeHeader uri={sgnUri(hostStar.identifier)} />
)
)}
</ScrollArea>
</Container>
);
}
44 changes: 22 additions & 22 deletions src/panels/GeoLocationPanel/AnchorPanels/EarthPanel/EarthPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
Button,
Checkbox,
Container,
Divider,
Group,
NumberInput,
Tabs,
Expand All @@ -17,6 +16,7 @@ import {
import { useOpenSpaceApi } from '@/api/hooks';
import { FilterList } from '@/components/FilterList/FilterList';
import { generateMatcherFunctionByKeys } from '@/components/FilterList/util';
import { ResizeableContent } from '@/components/ResizeableContent/ResizeableContent';
import { SettingsPopout } from '@/components/SettingsPopout/SettingsPopout';
import { MinusIcon } from '@/icons/icons';
import { useAppSelector } from '@/redux/hooks';
Expand Down Expand Up @@ -193,25 +193,27 @@ export function EarthPanel({ currentAnchor }: Props) {
</Group>

{places.length > 0 ? (
<FilterList heightPercent={30}>
<FilterList.InputField placeHolderSearchText={'Filter search'} />
<FilterList.Data<Candidate>
data={places}
renderElement={(place) => (
<EarthEntry
key={place.attributes.LongLabel}
place={place}
isCustomAltitude={isCustomAltitude}
customAltitude={customAltitude}
currentAnchor={currentAnchor}
isSceneGraphNodeAdded={isSceneGraphNodeAdded}
addFocusNode={addFocusNode}
removeFocusNode={removeFocusNode}
/>
)}
matcherFunc={generateMatcherFunctionByKeys(['address', 'attributes'])}
/>
</FilterList>
<ResizeableContent defaultHeight={250}>
<FilterList>
<FilterList.InputField placeHolderSearchText={'Filter search'} />
<FilterList.Data<Candidate>
data={places}
renderElement={(place) => (
<EarthEntry
key={place.attributes.LongLabel}
place={place}
isCustomAltitude={isCustomAltitude}
customAltitude={customAltitude}
currentAnchor={currentAnchor}
isSceneGraphNodeAdded={isSceneGraphNodeAdded}
addFocusNode={addFocusNode}
removeFocusNode={removeFocusNode}
/>
)}
matcherFunc={generateMatcherFunctionByKeys(['address', 'attributes'])}
/>
</FilterList>
</ResizeableContent>
) : (
<Text>Nothing found. Try another search!</Text>
)}
Expand All @@ -227,8 +229,6 @@ export function EarthPanel({ currentAnchor }: Props) {
</Tabs.Panel>
</Tabs>

<Divider my={'xs'} />

<Title order={2} my={'md'}>
Added Nodes
</Title>
Expand Down
19 changes: 18 additions & 1 deletion src/windowmanagement/Window/Window.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,28 @@ export function Window({ children }: PropsWithChildren) {
window.location.reload();
}

function disablePointerEvents(): void {
if (!ref.current) {
return;
}
ref.current.style.pointerEvents = 'none';
ref.current.style.userSelect = 'none';
}

function enablePointerEvents(): void {
if (!ref.current) {
return;
}
ref.current.style.removeProperty('user-select');
ref.current.style.removeProperty('pointer-events');
}

return (
<WindowSizeContext.Provider
value={{
width: width,
height: height
height: height,
pointerEvents: { enable: enablePointerEvents, disable: disablePointerEvents }
}}
>
<Box h={'100%'} ref={ref}>
Expand Down
1 change: 1 addition & 0 deletions src/windowmanagement/Window/WindowSizeContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { createContext } from 'react';
export interface ProviderProps {
width: number;
height: number;
pointerEvents: { enable: () => void; disable: () => void };
}

export const WindowSizeContext = createContext<ProviderProps | null>(null);