mirror of
https://github.com/SlimeVR/SlimeVR-Server.git
synced 2026-04-06 02:01:58 +02:00
Make manual proportion list grow if there is space (#784)
Co-authored-by: lucas lelievre <loucass003@gmail.com>
This commit is contained in:
@@ -77,7 +77,7 @@ export function MainLayoutRoute({
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
'flex flex-col rounded-xl w-full overflow-hidden mobile:overflow-y-auto',
|
||||
'flex flex-col rounded-xl w-full overflow-clip mobile:overflow-y-auto',
|
||||
background && 'bg-background-70'
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
useEffect,
|
||||
useRef,
|
||||
UIEvent,
|
||||
MouseEvent,
|
||||
useMemo,
|
||||
} from 'react';
|
||||
import {
|
||||
@@ -17,6 +16,8 @@ import {
|
||||
import { useLocaleConfig } from '../../../../i18n/config';
|
||||
import { Typography } from '../../../commons/Typography';
|
||||
import { ArrowDownIcon, ArrowUpIcon } from '../../../commons/icon/ArrowIcons';
|
||||
import { useBreakpoint } from '../../../../hooks/breakpoint';
|
||||
import { debounce } from '../../../../hooks/timeout';
|
||||
|
||||
function IncrementButton({
|
||||
children,
|
||||
@@ -52,8 +53,14 @@ export function BodyProportions({
|
||||
const { bodyParts, dispatch, state, setRatioMode } = useManualProportions();
|
||||
const { l10n } = useLocalization();
|
||||
const { currentLocales } = useLocaleConfig();
|
||||
const { isTall } = useBreakpoint('tall');
|
||||
|
||||
const srcollerRef = useRef<HTMLDivElement | null>(null);
|
||||
const offsetItems = isTall ? 2 : 1;
|
||||
const itemsToDisplay = offsetItems * 2 + 1;
|
||||
const itemHeight = 80;
|
||||
const scrollHeight = itemHeight * itemsToDisplay;
|
||||
|
||||
const scrollerRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const cmFormat = Intl.NumberFormat(currentLocales, {
|
||||
style: 'unit',
|
||||
@@ -78,41 +85,48 @@ export function BodyProportions({
|
||||
}, [type]);
|
||||
|
||||
useEffect(() => {
|
||||
if (srcollerRef.current && bodyParts.length > 0) {
|
||||
moveToIndex(1);
|
||||
if (scrollerRef.current && bodyParts.length > 0) {
|
||||
selectId(bodyParts[offsetItems].label);
|
||||
}
|
||||
}, [srcollerRef, bodyParts.length]);
|
||||
}, [scrollerRef, bodyParts.length]);
|
||||
|
||||
const handleUIEvent = (e: UIEvent<HTMLDivElement>) => {
|
||||
const target = e.target as HTMLDivElement;
|
||||
|
||||
const itemHeight = target.offsetHeight / 3;
|
||||
|
||||
const itemHeight = target.offsetHeight / itemsToDisplay;
|
||||
const atSnappingPoint = target.scrollTop % itemHeight === 0;
|
||||
const index = Math.round(target.scrollTop / itemHeight);
|
||||
const elem = scrollerRef.current?.childNodes[
|
||||
index + offsetItems
|
||||
] as HTMLDivElement;
|
||||
|
||||
elem.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
|
||||
if (atSnappingPoint) {
|
||||
const index = target.scrollTop / itemHeight;
|
||||
const elem = srcollerRef.current?.childNodes[index + 1] as HTMLDivElement;
|
||||
const elem = scrollerRef.current?.childNodes[
|
||||
index + offsetItems
|
||||
] as HTMLDivElement;
|
||||
const id = elem.getAttribute('itemid');
|
||||
|
||||
if (id) selectNew(id);
|
||||
}
|
||||
};
|
||||
|
||||
const clickPart = (id: string) => (e: MouseEvent<HTMLDivElement>) => {
|
||||
const target = e.target as HTMLDivElement;
|
||||
target.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
const moveToId = (id: string) => {
|
||||
if (!scrollerRef.current) return;
|
||||
const index = bodyParts.findIndex(({ label }) => label === id);
|
||||
scrollerRef.current.scrollTo({
|
||||
top: index * itemHeight,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
};
|
||||
|
||||
const clickPart = (id: string) => () => {
|
||||
moveToId(id);
|
||||
selectNew(id);
|
||||
};
|
||||
|
||||
const moveToIndex = (index: number, smooth = true) => {
|
||||
// We add one because of the offset placeholder
|
||||
const elem = srcollerRef.current?.childNodes[index + 1] as HTMLDivElement;
|
||||
elem?.scrollIntoView({
|
||||
behavior: smooth ? 'smooth' : 'auto',
|
||||
block: 'center',
|
||||
});
|
||||
|
||||
const id = elem.getAttribute('itemid');
|
||||
const selectId = (id: string) => {
|
||||
moveToId(id);
|
||||
if (id) selectNew(id);
|
||||
};
|
||||
|
||||
@@ -170,15 +184,12 @@ export function BodyProportions({
|
||||
}, [state]);
|
||||
|
||||
const move = (action: 'next' | 'prev') => {
|
||||
const elem = srcollerRef.current?.querySelector(
|
||||
const elem = scrollerRef.current?.querySelector(
|
||||
`div[itemid=${state.currentLabel}]`
|
||||
);
|
||||
|
||||
const moveId = (id: string, elem: HTMLDivElement) => {
|
||||
elem?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center',
|
||||
});
|
||||
const moveId = (id: string) => {
|
||||
moveToId(id);
|
||||
selectNew(id);
|
||||
};
|
||||
|
||||
@@ -186,14 +197,14 @@ export function BodyProportions({
|
||||
const prevElem = elem?.previousSibling as HTMLDivElement;
|
||||
const prevId = prevElem.getAttribute('itemid');
|
||||
if (!prevId) return;
|
||||
moveId(prevId, prevElem);
|
||||
moveId(prevId);
|
||||
}
|
||||
|
||||
if (action === 'next') {
|
||||
const nextElem = elem?.nextSibling as HTMLDivElement;
|
||||
const nextId = nextElem.getAttribute('itemid');
|
||||
if (!nextId) return;
|
||||
moveId(nextId, nextElem);
|
||||
moveId(nextId);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -264,7 +275,7 @@ export function BodyProportions({
|
||||
className={classNames(
|
||||
'h-12 w-32 rounded-lg bg-background-60 flex flex-col justify-center',
|
||||
'items-center fill-background-10',
|
||||
srcollerRef?.current?.scrollTop ?? 0 > 0
|
||||
scrollerRef?.current?.scrollTop ?? 0 > 0
|
||||
? 'opacity-100 active:bg-accent-background-30'
|
||||
: 'opacity-50'
|
||||
)}
|
||||
@@ -273,11 +284,17 @@ export function BodyProportions({
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
ref={srcollerRef}
|
||||
onScroll={handleUIEvent}
|
||||
className="h-60 flex-grow flex-col overflow-y-auto snap-y snap-mandatory snap-always no-scrollbar"
|
||||
ref={scrollerRef}
|
||||
onScroll={debounce(handleUIEvent, 150)} // Debounce at 150ms to match the animation speed and prevent snaping between two animations
|
||||
className={classNames(
|
||||
'flex-grow flex-col overflow-y-auto',
|
||||
'no-scrollbar'
|
||||
)}
|
||||
style={{ height: scrollHeight }}
|
||||
>
|
||||
<div className="h-20 snap-start "></div>
|
||||
{Array.from({ length: offsetItems }).map((_, index) => (
|
||||
<div style={{ height: itemHeight }} key={index}></div>
|
||||
))}
|
||||
{bodyParts.map((part) => {
|
||||
const { label, value: originalValue, type, ...props } = part;
|
||||
const value =
|
||||
@@ -292,7 +309,8 @@ export function BodyProportions({
|
||||
key={label}
|
||||
itemID={label}
|
||||
onClick={clickPart(label)}
|
||||
className="snap-start h-20 flex-col flex justify-center"
|
||||
style={{ height: itemHeight }}
|
||||
className="flex-col flex justify-center"
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
@@ -320,7 +338,13 @@ export function BodyProportions({
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className="h-20 snap-start"></div>
|
||||
{Array.from({ length: offsetItems }).map((_, index) => (
|
||||
<div
|
||||
className="h-20"
|
||||
style={{ height: itemHeight }}
|
||||
key={index}
|
||||
></div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex justify-center">
|
||||
<div
|
||||
@@ -328,9 +352,9 @@ export function BodyProportions({
|
||||
className={classNames(
|
||||
'h-12 w-32 rounded-lg bg-background-60 flex flex-col justify-center',
|
||||
'items-center fill-background-10',
|
||||
srcollerRef?.current?.scrollTop !==
|
||||
(srcollerRef?.current?.scrollHeight ?? 0) -
|
||||
(srcollerRef?.current?.offsetHeight ?? 0)
|
||||
scrollerRef?.current?.scrollTop !==
|
||||
(scrollerRef?.current?.scrollHeight ?? 0) -
|
||||
(scrollerRef?.current?.offsetHeight ?? 0)
|
||||
? 'opacity-100 active:bg-accent-background-30'
|
||||
: 'opacity-50'
|
||||
)}
|
||||
|
||||
@@ -22,3 +22,14 @@ export const useDebouncedEffect = (effect: () => void, deps: any[], delay: numbe
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [...(deps || []), delay]);
|
||||
};
|
||||
|
||||
export const debounce = <F extends (...args: any) => any>(func: F, waitFor: number) => {
|
||||
let timeout = 0;
|
||||
|
||||
const debounced = (...args: any) => {
|
||||
window.clearTimeout(timeout);
|
||||
timeout = window.setTimeout(() => func(...args), waitFor);
|
||||
};
|
||||
|
||||
return debounced as (...args: Parameters<F>) => ReturnType<F>;
|
||||
};
|
||||
|
||||
@@ -163,6 +163,7 @@ const config = {
|
||||
'md-max': { raw: 'not (min-width: 1100px)' },
|
||||
lg: '1300px',
|
||||
xl: '1600px',
|
||||
tall: { raw: '(min-height: 800px)'}
|
||||
},
|
||||
extend: {
|
||||
colors: {
|
||||
|
||||
Reference in New Issue
Block a user