fix(ui): fix inconsistent shift modifier capture

The shift key listener didn't catch pressed when focused in a textarea or input field, causing jank on slider number inputs.

Add keydown and keyup listeners to all such fields, which ensures that the `shift` state is always correct.

Also add the action tracking it to `actionsDenylist` to not clutter up devtools.
This commit is contained in:
psychedelicious 2023-07-08 18:52:37 +10:00
parent 26cea7b13d
commit a0ccb4385f
7 changed files with 167 additions and 9 deletions

View File

@ -9,4 +9,5 @@ export const actionsDenylist = [
'canvas/addPointToCurrentLine', 'canvas/addPointToCurrentLine',
'socket/socketGeneratorProgress', 'socket/socketGeneratorProgress',
'socket/appSocketGeneratorProgress', 'socket/appSocketGeneratorProgress',
'hotkeys/shiftKeyPressed',
]; ];

View File

@ -5,8 +5,10 @@ import {
Input, Input,
InputProps, InputProps,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { useAppDispatch } from 'app/store/storeHooks';
import { stopPastePropagation } from 'common/util/stopPastePropagation'; import { stopPastePropagation } from 'common/util/stopPastePropagation';
import { ChangeEvent, memo } from 'react'; import { shiftKeyPressed } from 'features/ui/store/hotkeysSlice';
import { ChangeEvent, KeyboardEvent, memo, useCallback } from 'react';
interface IAIInputProps extends InputProps { interface IAIInputProps extends InputProps {
label?: string; label?: string;
@ -25,6 +27,25 @@ const IAIInput = (props: IAIInputProps) => {
...rest ...rest
} = props; } = props;
const dispatch = useAppDispatch();
const handleKeyDown = useCallback(
(e: KeyboardEvent<HTMLInputElement>) => {
if (e.shiftKey) {
dispatch(shiftKeyPressed(true));
}
},
[dispatch]
);
const handleKeyUp = useCallback(
(e: KeyboardEvent<HTMLInputElement>) => {
if (!e.shiftKey) {
dispatch(shiftKeyPressed(false));
}
},
[dispatch]
);
return ( return (
<FormControl <FormControl
isInvalid={isInvalid} isInvalid={isInvalid}
@ -32,7 +53,12 @@ const IAIInput = (props: IAIInputProps) => {
{...formControlProps} {...formControlProps}
> >
{label !== '' && <FormLabel>{label}</FormLabel>} {label !== '' && <FormLabel>{label}</FormLabel>}
<Input {...rest} onPaste={stopPastePropagation} /> <Input
{...rest}
onPaste={stopPastePropagation}
onKeyDown={handleKeyDown}
onKeyUp={handleKeyUp}
/>
</FormControl> </FormControl>
); );
}; };

View File

@ -1,7 +1,9 @@
import { Tooltip, useColorMode, useToken } from '@chakra-ui/react'; import { Tooltip, useColorMode, useToken } from '@chakra-ui/react';
import { MultiSelect, MultiSelectProps } from '@mantine/core'; import { MultiSelect, MultiSelectProps } from '@mantine/core';
import { useAppDispatch } from 'app/store/storeHooks';
import { useChakraThemeTokens } from 'common/hooks/useChakraThemeTokens'; import { useChakraThemeTokens } from 'common/hooks/useChakraThemeTokens';
import { RefObject, memo } from 'react'; import { shiftKeyPressed } from 'features/ui/store/hotkeysSlice';
import { KeyboardEvent, RefObject, memo, useCallback } from 'react';
import { mode } from 'theme/util/mode'; import { mode } from 'theme/util/mode';
type IAIMultiSelectProps = MultiSelectProps & { type IAIMultiSelectProps = MultiSelectProps & {
@ -11,6 +13,7 @@ type IAIMultiSelectProps = MultiSelectProps & {
const IAIMantineMultiSelect = (props: IAIMultiSelectProps) => { const IAIMantineMultiSelect = (props: IAIMultiSelectProps) => {
const { searchable = true, tooltip, inputRef, ...rest } = props; const { searchable = true, tooltip, inputRef, ...rest } = props;
const dispatch = useAppDispatch();
const { const {
base50, base50,
base100, base100,
@ -31,10 +34,30 @@ const IAIMantineMultiSelect = (props: IAIMultiSelectProps) => {
const [boxShadow] = useToken('shadows', ['dark-lg']); const [boxShadow] = useToken('shadows', ['dark-lg']);
const { colorMode } = useColorMode(); const { colorMode } = useColorMode();
const handleKeyDown = useCallback(
(e: KeyboardEvent<HTMLInputElement>) => {
if (e.shiftKey) {
dispatch(shiftKeyPressed(true));
}
},
[dispatch]
);
const handleKeyUp = useCallback(
(e: KeyboardEvent<HTMLInputElement>) => {
if (!e.shiftKey) {
dispatch(shiftKeyPressed(false));
}
},
[dispatch]
);
return ( return (
<Tooltip label={tooltip} placement="top" hasArrow isOpen={true}> <Tooltip label={tooltip} placement="top" hasArrow isOpen={true}>
<MultiSelect <MultiSelect
ref={inputRef} ref={inputRef}
onKeyDown={handleKeyDown}
onKeyUp={handleKeyUp}
searchable={searchable} searchable={searchable}
styles={() => ({ styles={() => ({
label: { label: {

View File

@ -1,7 +1,9 @@
import { Tooltip, useColorMode, useToken } from '@chakra-ui/react'; import { Tooltip, useColorMode, useToken } from '@chakra-ui/react';
import { Select, SelectProps } from '@mantine/core'; import { Select, SelectProps } from '@mantine/core';
import { useAppDispatch } from 'app/store/storeHooks';
import { useChakraThemeTokens } from 'common/hooks/useChakraThemeTokens'; import { useChakraThemeTokens } from 'common/hooks/useChakraThemeTokens';
import { memo } from 'react'; import { shiftKeyPressed } from 'features/ui/store/hotkeysSlice';
import { KeyboardEvent, memo, useCallback } from 'react';
import { mode } from 'theme/util/mode'; import { mode } from 'theme/util/mode';
export type IAISelectDataType = { export type IAISelectDataType = {
@ -16,6 +18,7 @@ type IAISelectProps = SelectProps & {
const IAIMantineSelect = (props: IAISelectProps) => { const IAIMantineSelect = (props: IAISelectProps) => {
const { searchable = true, tooltip, ...rest } = props; const { searchable = true, tooltip, ...rest } = props;
const dispatch = useAppDispatch();
const { const {
base50, base50,
base100, base100,
@ -36,11 +39,31 @@ const IAIMantineSelect = (props: IAISelectProps) => {
const { colorMode } = useColorMode(); const { colorMode } = useColorMode();
const handleKeyDown = useCallback(
(e: KeyboardEvent<HTMLInputElement>) => {
if (e.shiftKey) {
dispatch(shiftKeyPressed(true));
}
},
[dispatch]
);
const handleKeyUp = useCallback(
(e: KeyboardEvent<HTMLInputElement>) => {
if (!e.shiftKey) {
dispatch(shiftKeyPressed(false));
}
},
[dispatch]
);
const [boxShadow] = useToken('shadows', ['dark-lg']); const [boxShadow] = useToken('shadows', ['dark-lg']);
return ( return (
<Tooltip label={tooltip} placement="top" hasArrow> <Tooltip label={tooltip} placement="top" hasArrow>
<Select <Select
onKeyDown={handleKeyDown}
onKeyUp={handleKeyUp}
searchable={searchable} searchable={searchable}
styles={() => ({ styles={() => ({
label: { label: {

View File

@ -14,10 +14,19 @@ import {
Tooltip, Tooltip,
TooltipProps, TooltipProps,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { useAppDispatch } from 'app/store/storeHooks';
import { stopPastePropagation } from 'common/util/stopPastePropagation'; import { stopPastePropagation } from 'common/util/stopPastePropagation';
import { shiftKeyPressed } from 'features/ui/store/hotkeysSlice';
import { clamp } from 'lodash-es'; import { clamp } from 'lodash-es';
import { FocusEvent, memo, useEffect, useState } from 'react'; import {
FocusEvent,
KeyboardEvent,
memo,
useCallback,
useEffect,
useState,
} from 'react';
const numberStringRegex = /^-?(0\.)?\.?$/; const numberStringRegex = /^-?(0\.)?\.?$/;
@ -60,6 +69,8 @@ const IAINumberInput = (props: Props) => {
...rest ...rest
} = props; } = props;
const dispatch = useAppDispatch();
/** /**
* Using a controlled input with a value that accepts decimals needs special * Using a controlled input with a value that accepts decimals needs special
* handling. If the user starts to type in "1.5", by the time they press the * handling. If the user starts to type in "1.5", by the time they press the
@ -109,6 +120,24 @@ const IAINumberInput = (props: Props) => {
onChange(clamped); onChange(clamped);
}; };
const handleKeyDown = useCallback(
(e: KeyboardEvent<HTMLInputElement>) => {
if (e.shiftKey) {
dispatch(shiftKeyPressed(true));
}
},
[dispatch]
);
const handleKeyUp = useCallback(
(e: KeyboardEvent<HTMLInputElement>) => {
if (!e.shiftKey) {
dispatch(shiftKeyPressed(false));
}
},
[dispatch]
);
return ( return (
<Tooltip {...tooltipProps}> <Tooltip {...tooltipProps}>
<FormControl <FormControl
@ -128,7 +157,11 @@ const IAINumberInput = (props: Props) => {
{...rest} {...rest}
onPaste={stopPastePropagation} onPaste={stopPastePropagation}
> >
<NumberInputField {...numberInputFieldProps} /> <NumberInputField
{...numberInputFieldProps}
onKeyDown={handleKeyDown}
onKeyUp={handleKeyUp}
/>
{showStepper && ( {showStepper && (
<NumberInputStepper> <NumberInputStepper>
<NumberIncrementStepper {...numberInputStepperProps} /> <NumberIncrementStepper {...numberInputStepperProps} />

View File

@ -26,9 +26,12 @@ import {
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { clamp } from 'lodash-es'; import { clamp } from 'lodash-es';
import { useAppDispatch } from 'app/store/storeHooks';
import { roundDownToMultiple } from 'common/util/roundDownToMultiple'; import { roundDownToMultiple } from 'common/util/roundDownToMultiple';
import { shiftKeyPressed } from 'features/ui/store/hotkeysSlice';
import { import {
FocusEvent, FocusEvent,
KeyboardEvent,
memo, memo,
MouseEvent, MouseEvent,
useCallback, useCallback,
@ -107,7 +110,7 @@ const IAISlider = (props: IAIFullSliderProps) => {
sliderIAIIconButtonProps, sliderIAIIconButtonProps,
...rest ...rest
} = props; } = props;
const dispatch = useAppDispatch();
const { t } = useTranslation(); const { t } = useTranslation();
const [localInputValue, setLocalInputValue] = useState< const [localInputValue, setLocalInputValue] = useState<
@ -167,6 +170,24 @@ const IAISlider = (props: IAIFullSliderProps) => {
} }
}, []); }, []);
const handleKeyDown = useCallback(
(e: KeyboardEvent<HTMLInputElement>) => {
if (e.shiftKey) {
dispatch(shiftKeyPressed(true));
}
},
[dispatch]
);
const handleKeyUp = useCallback(
(e: KeyboardEvent<HTMLInputElement>) => {
if (!e.shiftKey) {
dispatch(shiftKeyPressed(false));
}
},
[dispatch]
);
return ( return (
<FormControl <FormControl
onClick={forceInputBlur} onClick={forceInputBlur}
@ -310,6 +331,8 @@ const IAISlider = (props: IAIFullSliderProps) => {
{...sliderNumberInputProps} {...sliderNumberInputProps}
> >
<NumberInputField <NumberInputField
onKeyDown={handleKeyDown}
onKeyUp={handleKeyUp}
minWidth={inputWidth} minWidth={inputWidth}
{...sliderNumberInputFieldProps} {...sliderNumberInputFieldProps}
/> />

View File

@ -1,9 +1,38 @@
import { Textarea, TextareaProps, forwardRef } from '@chakra-ui/react'; import { Textarea, TextareaProps, forwardRef } from '@chakra-ui/react';
import { useAppDispatch } from 'app/store/storeHooks';
import { stopPastePropagation } from 'common/util/stopPastePropagation'; import { stopPastePropagation } from 'common/util/stopPastePropagation';
import { memo } from 'react'; import { shiftKeyPressed } from 'features/ui/store/hotkeysSlice';
import { KeyboardEvent, memo, useCallback } from 'react';
const IAITextarea = forwardRef((props: TextareaProps, ref) => { const IAITextarea = forwardRef((props: TextareaProps, ref) => {
return <Textarea ref={ref} onPaste={stopPastePropagation} {...props} />; const dispatch = useAppDispatch();
const handleKeyDown = useCallback(
(e: KeyboardEvent<HTMLTextAreaElement>) => {
if (e.shiftKey) {
dispatch(shiftKeyPressed(true));
}
},
[dispatch]
);
const handleKeyUp = useCallback(
(e: KeyboardEvent<HTMLTextAreaElement>) => {
if (!e.shiftKey) {
dispatch(shiftKeyPressed(false));
}
},
[dispatch]
);
return (
<Textarea
ref={ref}
onPaste={stopPastePropagation}
onKeyDown={handleKeyDown}
onKeyUp={handleKeyUp}
{...props}
/>
);
}); });
export default memo(IAITextarea); export default memo(IAITextarea);