Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
2 changes: 2 additions & 0 deletions gui/app/public/i18n/en/translation.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -1336,6 +1336,8 @@ onboarding-user_height-title = What is your height?
onboarding-user_height-description = We need your height to calculate your body proportions and accurately represent your movements. You can either let SlimeVR calculate it, or input your height manually.
onboarding-user_height-need_head_tracker = A headset and controllers with positional tracking are required to perform the calibration.
onboarding-user_height-calculate = Calculate my height automatically
onboarding-user_height-error_bounds = Input too high
onboarding-user_height-error_format = Input wrong format
onboarding-user_height-next_step = Continue and save
onboarding-user_height-prev_step = Back
onboarding-user_height-manual-proportions = Manual Proportions
Expand Down
33 changes: 29 additions & 4 deletions gui/app/src/components/commons/Input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ export const InputInside = forwardRef<
label?: string;
error?: FieldError;
autocomplete?: boolean | string;
className?: string;
errorClassName?: string;
onBlur?: () => void;
} & Partial<React.HTMLProps<HTMLInputElement>>
>(function AppInput(
{
Expand All @@ -37,6 +40,9 @@ export const InputInside = forwardRef<
value,
error,
variant = 'primary',
className,
errorClassName,
onBlur,
},
ref
) {
Expand Down Expand Up @@ -90,16 +96,21 @@ export const InputInside = forwardRef<
<div className="relative w-full">
<input
type={forceText ? 'text' : type}
className={classNames(classes, {
'pr-10 sentry-mask': type === 'password',
})}
className={classNames(
classes,
{
'pr-10 sentry-mask': type === 'password',
},
className
)}
placeholder={placeholder || undefined}
autoComplete={autocomplete ? 'off' : 'on'}
onChange={onChange}
name={name}
value={computedValue} // Do we want that behaviour ?
disabled={disabled}
ref={ref}
onBlur={onBlur}
/>
{type === 'password' && (
<div
Expand All @@ -110,7 +121,12 @@ export const InputInside = forwardRef<
</div>
)}
{error?.message && (
<div className="absolute top-[38px] z-0 pt-1.5 bg-background-70 px-1 w-full rounded-b-md text-status-critical">
<div
className={classNames(
'absolute top-[38px] z-0 pt-1.5 px-1 w-full rounded-b-md bg-dark-background-600 text-status-critical',
errorClassName
)}
>
{error.message}
</div>
)}
Expand All @@ -129,11 +145,17 @@ export const Input = <T extends FieldValues = FieldValues>({
disabled,
variant = 'primary',
rules,
className,
errorClassName,
onBlur,
}: {
rules?: UseControllerProps<T, FieldPath<T>>['rules'];
control: Control<T>;
name: FieldPath<T>;
autocomplete?: boolean | string;
className?: string;
errorClassName?: string;
onBlur?: () => void;
} & Omit<InputProps, 'name'> &
Partial<React.HTMLProps<HTMLInputElement>>) => {
return (
Expand All @@ -157,6 +179,9 @@ export const Input = <T extends FieldValues = FieldValues>({
onChange={onChange}
ref={ref}
name={name}
className={className}
errorClassName={errorClassName}
onBlur={onBlur}
/>
)}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { Input } from '@/components/commons/Input';
import { Typography } from '@/components/commons/Typography';
import { useBreakpoint } from '@/hooks/breakpoint';
import { EYE_HEIGHT_TO_HEIGHT_RATIO } from '@/hooks/height';
import { useLocaleConfig } from '@/i18n/config';
import classNames from 'classnames';
import convert from 'convert';
import { useMemo, useState } from 'react';

import { useMemo, useState, useEffect, useRef } from 'react';
import { useForm } from 'react-hook-form';
import { useLocalization } from '@fluent/react';
function IncrementButton({
value,
unit,
Expand Down Expand Up @@ -121,6 +123,8 @@ export function HeightSelectionInput({
const { isXs } = useBreakpoint('xs');
const [unit, setUnit] = useState<'meter' | 'foot'>('meter');
const { currentLocales } = useLocaleConfig();
const { l10n } = useLocalization();
const isSubmitting = useRef(false);

const formattedHeight = useMemo(() => {
if (!hmdHeight) return '--';
Expand All @@ -140,6 +144,104 @@ export function HeightSelectionInput({
return formatInFoot(displayHeight, currentLocales);
}, [hmdHeight, unit]);

const defaultValues: { height: string } = {
height: formattedHeight,
};

const { reset, control, watch, getValues, setError, clearErrors } = useForm<{
Comment thread
Aed-1 marked this conversation as resolved.
Outdated
height: string;
}>({
defaultValues,
mode: 'onChange',
reValidateMode: 'onChange',
});

const onSubmit = (values: { height: string }) => {
let newFullHeight: number;
clearErrors('height');

// convert formatted height to raw number in meters
if (unit == 'meter') {
newFullHeight = Number(values.height.replace(/[ m]/g, ''));

if (isNaN(newFullHeight)) {
setTimeout(() => {
Comment thread
Aed-1 marked this conversation as resolved.
Outdated
reset({ height: formattedHeight });
setError('height', {
message: l10n.getString('onboarding-user_height-error_format'),
});
}, 0);
} else if (newFullHeight > 2.56) {
setTimeout(() => {
reset({ height: formattedHeight });
setError('height', {
message: l10n.getString('onboarding-user_height-error_bounds'),
});
}, 0);
} else if (newFullHeight != 0) {
isSubmitting.current = true;
setHmdHeight(round4Digit(newFullHeight * EYE_HEIGHT_TO_HEIGHT_RATIO));
}
} else {
const match = values.height.match(
Comment thread
Aed-1 marked this conversation as resolved.
Outdated
/^(\d+)(?:[′'.,\s]+(\d+(?:\.\d+)?)?["”]?)?$/ // regex to convert the formatted text to feet and inches individually in an array
);

if (!values.height) return; // this is to allow for blank inputs, so that the user can type their height from scratch

if (!match) {
setTimeout(() => {
reset({ height: formattedHeight });
setError('height', {
message: l10n.getString('onboarding-user_height-error_format'),
});
}, 0);
return;
}

const feet = Number(match[1]);
const inches = Number(match[2] || 0);
newFullHeight = convert(feet + inches / 12, 'foot').to('meter');

// bounds detection
if ((feet > 8 && inches > 4) || feet > 8) {
Comment thread
Aed-1 marked this conversation as resolved.
Outdated
setTimeout(() => {
reset({ height: formattedHeight });
setError('height', {
message: l10n.getString('onboarding-user_height-error_bounds'),
});
}, 0);
} else if (feet != 0) {
isSubmitting.current = true;
setHmdHeight(round4Digit(newFullHeight * EYE_HEIGHT_TO_HEIGHT_RATIO));
}
}
};

const onSubmitRef = useRef(onSubmit);
onSubmitRef.current = onSubmit;

useEffect(() => {
const subscription = watch((value, { type }) => {
if (type === 'change') {
onSubmitRef.current({ height: value.height ?? '' });
}
});
return () => subscription.unsubscribe();
}, []);

useEffect(() => {
if (isSubmitting.current) {
isSubmitting.current = false;
return;
}
reset({ ...getValues(), height: defaultValues.height });
}, [defaultValues.height]);

useEffect(() => {
setTimeout(() => reset({ height: formattedHeight }), 0);
Comment thread
Aed-1 marked this conversation as resolved.
Outdated
}, [unit]);

const incrementMath = (unit: 'inch' | 'cm' | 'foot', value: number) => {
const incrementInMeters = convert(value, unit).to('meter');
const oldFull = hmdHeight / EYE_HEIGHT_TO_HEIGHT_RATIO;
Expand Down Expand Up @@ -182,6 +284,7 @@ export function HeightSelectionInput({
const newEyeHeight = round4Digit(
snappedHeight * EYE_HEIGHT_TO_HEIGHT_RATIO
);

setHmdHeight(newEyeHeight);
setUnit(newUnit);
};
Expand Down Expand Up @@ -222,11 +325,16 @@ export function HeightSelectionInput({
</>
)}
</div>
<div className="flex w-full xs:w-auto xs:flex-grow bg-background-50 rounded-md px-2 py-2 h-full">
<div className="h-full flex items-center flex-grow justify-center min-w-24">
<Typography variant="main-title">{formattedHeight}</Typography>
</div>
<div className="w-[60px] xs:w-20 h-full gap-2 grid p-1">
<div className="flex w-full xs:w-auto xs:flex-grow bg-background-50 rounded-md px-2 py-2 h-full gap-1 items-center">
<Input
name="height"
control={control}
variant="secondary"
className="text-center !text-3xl !font-bold !w-[210px]"
errorClassName="text-center top-[47px] "
onBlur={() => clearErrors('height')}
/>
<div className="w-[70px] xs:w-20 h-full gap-2 grid p-1">
<UnitSelector
active={unit === 'meter'}
name={isXs ? 'unit-meter' : 'unit-cm'}
Expand Down
Loading