Prevent mouse wheel window scrolling on other number fields (#5283)

This commit is contained in:
WithoutPants
2024-09-22 15:24:54 +10:00
committed by GitHub
parent 4ad0241c53
commit 33050f700e
11 changed files with 42 additions and 24 deletions

View File

@@ -4,6 +4,7 @@ import { defineMessages, MessageDescriptor, useIntl } from "react-intl";
import { FilterSelect, SelectObject } from "src/components/Shared/Select"; import { FilterSelect, SelectObject } from "src/components/Shared/Select";
import { Criterion } from "src/models/list-filter/criteria/criterion"; import { Criterion } from "src/models/list-filter/criteria/criterion";
import { IHierarchicalLabelValue } from "src/models/list-filter/types"; import { IHierarchicalLabelValue } from "src/models/list-filter/types";
import { NumberField } from "src/utils/form";
interface IHierarchicalLabelValueFilterProps { interface IHierarchicalLabelValueFilterProps {
criterion: Criterion<IHierarchicalLabelValue>; criterion: Criterion<IHierarchicalLabelValue>;
@@ -104,9 +105,8 @@ export const HierarchicalLabelValueFilter: React.FC<
{criterion.value.depth !== 0 && ( {criterion.value.depth !== 0 && (
<Form.Group> <Form.Group>
<Form.Control <NumberField
className="btn-secondary" className="btn-secondary"
type="number"
placeholder={intl.formatMessage(messages.studio_depth)} placeholder={intl.formatMessage(messages.studio_depth)}
onChange={(e) => onChange={(e) =>
onDepthChanged(e.target.value ? parseInt(e.target.value, 10) : -1) onDepthChanged(e.target.value ? parseInt(e.target.value, 10) : -1)

View File

@@ -4,6 +4,7 @@ import { useIntl } from "react-intl";
import { CriterionModifier } from "../../../core/generated-graphql"; import { CriterionModifier } from "../../../core/generated-graphql";
import { INumberValue } from "../../../models/list-filter/types"; import { INumberValue } from "../../../models/list-filter/types";
import { NumberCriterion } from "../../../models/list-filter/criteria/criterion"; import { NumberCriterion } from "../../../models/list-filter/criteria/criterion";
import { NumberField } from "src/utils/form";
interface IDurationFilterProps { interface IDurationFilterProps {
criterion: NumberCriterion; criterion: NumberCriterion;
@@ -36,9 +37,8 @@ export const NumberFilter: React.FC<IDurationFilterProps> = ({
) { ) {
equalsControl = ( equalsControl = (
<Form.Group> <Form.Group>
<Form.Control <NumberField
className="btn-secondary" className="btn-secondary"
type="number"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
onChanged(e, "value") onChanged(e, "value")
} }
@@ -57,9 +57,8 @@ export const NumberFilter: React.FC<IDurationFilterProps> = ({
) { ) {
lowerControl = ( lowerControl = (
<Form.Group> <Form.Group>
<Form.Control <NumberField
className="btn-secondary" className="btn-secondary"
type="number"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
onChanged(e, "value") onChanged(e, "value")
} }
@@ -78,9 +77,8 @@ export const NumberFilter: React.FC<IDurationFilterProps> = ({
) { ) {
upperControl = ( upperControl = (
<Form.Group> <Form.Group>
<Form.Control <NumberField
className="btn-secondary" className="btn-secondary"
type="number"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
onChanged( onChanged(
e, e,

View File

@@ -4,6 +4,7 @@ import { useIntl } from "react-intl";
import { IPhashDistanceValue } from "../../../models/list-filter/types"; import { IPhashDistanceValue } from "../../../models/list-filter/types";
import { Criterion } from "../../../models/list-filter/criteria/criterion"; import { Criterion } from "../../../models/list-filter/criteria/criterion";
import { CriterionModifier } from "src/core/generated-graphql"; import { CriterionModifier } from "src/core/generated-graphql";
import { NumberField } from "src/utils/form";
interface IPhashFilterProps { interface IPhashFilterProps {
criterion: Criterion<IPhashDistanceValue>; criterion: Criterion<IPhashDistanceValue>;
@@ -49,10 +50,9 @@ export const PhashFilter: React.FC<IPhashFilterProps> = ({
{criterion.modifier !== CriterionModifier.IsNull && {criterion.modifier !== CriterionModifier.IsNull &&
criterion.modifier !== CriterionModifier.NotNull && ( criterion.modifier !== CriterionModifier.NotNull && (
<Form.Group> <Form.Group>
<Form.Control <NumberField
className="btn-secondary" className="btn-secondary"
onChange={distanceChanged} onChange={distanceChanged}
type="number"
value={value ? value.distance : ""} value={value ? value.distance : ""}
placeholder={intl.formatMessage({ id: "distance" })} placeholder={intl.formatMessage({ id: "distance" })}
/> />

View File

@@ -25,6 +25,7 @@ import { keyboardClickHandler } from "src/utils/keyboard";
import { useDebounce } from "src/hooks/debounce"; import { useDebounce } from "src/hooks/debounce";
import useFocus from "src/utils/focus"; import useFocus from "src/utils/focus";
import ScreenUtils from "src/utils/screen"; import ScreenUtils from "src/utils/screen";
import { NumberField } from "src/utils/form";
interface ISelectedItem { interface ISelectedItem {
item: ILabeledId; item: ILabeledId;
@@ -361,9 +362,8 @@ export const HierarchicalObjectsFilter = <
{criterion.value.depth !== 0 && ( {criterion.value.depth !== 0 && (
<Form.Group> <Form.Group>
<Form.Control <NumberField
className="btn-secondary" className="btn-secondary"
type="number"
placeholder={intl.formatMessage(messages.studio_depth)} placeholder={intl.formatMessage(messages.studio_depth)}
onChange={(e) => onChange={(e) =>
onDepthChanged(e.target.value ? parseInt(e.target.value, 10) : -1) onDepthChanged(e.target.value ? parseInt(e.target.value, 10) : -1)

View File

@@ -35,6 +35,7 @@ import { FilterButton } from "./Filters/FilterButton";
import { useDebounce } from "src/hooks/debounce"; import { useDebounce } from "src/hooks/debounce";
import { View } from "./views"; import { View } from "./views";
import { ClearableInput } from "../Shared/ClearableInput"; import { ClearableInput } from "../Shared/ClearableInput";
import { useStopWheelScroll } from "src/utils/form";
export function useDebouncedSearchInput( export function useDebouncedSearchInput(
filter: ListFilterModel, filter: ListFilterModel,
@@ -126,6 +127,8 @@ export const PageSizeSelector: React.FC<{
} }
}, [customPageSizeShowing, perPageFocus]); }, [customPageSizeShowing, perPageFocus]);
useStopWheelScroll(perPageInput);
const pageSizeOptions = useMemo(() => { const pageSizeOptions = useMemo(() => {
const ret = PAGE_SIZE_OPTIONS.map((o) => { const ret = PAGE_SIZE_OPTIONS.map((o) => {
return { return {
@@ -190,6 +193,7 @@ export const PageSizeSelector: React.FC<{
<Popover id="custom_pagesize_popover"> <Popover id="custom_pagesize_popover">
<Form inline> <Form inline>
<InputGroup> <InputGroup>
{/* can't use NumberField because of the ref */}
<Form.Control <Form.Control
type="number" type="number"
min={1} min={1}

View File

@@ -12,6 +12,7 @@ import { FormattedMessage, FormattedNumber, useIntl } from "react-intl";
import useFocus from "src/utils/focus"; import useFocus from "src/utils/focus";
import { Icon } from "../Shared/Icon"; import { Icon } from "../Shared/Icon";
import { faCheck, faChevronDown } from "@fortawesome/free-solid-svg-icons"; import { faCheck, faChevronDown } from "@fortawesome/free-solid-svg-icons";
import { useStopWheelScroll } from "src/utils/form";
const PageCount: React.FC<{ const PageCount: React.FC<{
totalPages: number; totalPages: number;
@@ -32,6 +33,8 @@ const PageCount: React.FC<{
} }
}, [showSelectPage, pageFocus]); }, [showSelectPage, pageFocus]);
useStopWheelScroll(pageInput);
const pageOptions = useMemo(() => { const pageOptions = useMemo(() => {
const maxPagesToShow = 10; const maxPagesToShow = 10;
const min = Math.max(1, currentPage - maxPagesToShow / 2); const min = Math.max(1, currentPage - maxPagesToShow / 2);
@@ -98,6 +101,7 @@ const PageCount: React.FC<{
<Popover id="select_page_popover"> <Popover id="select_page_popover">
<Form inline> <Form inline>
<InputGroup> <InputGroup>
{/* can't use NumberField because of the ref */}
<Form.Control <Form.Control
type="number" type="number"
min={1} min={1}

View File

@@ -4,6 +4,7 @@ import * as GQL from "src/core/generated-graphql";
import { Form, Row, Col } from "react-bootstrap"; import { Form, Row, Col } from "react-bootstrap";
import { Group, GroupSelect } from "src/components/Groups/GroupSelect"; import { Group, GroupSelect } from "src/components/Groups/GroupSelect";
import cx from "classnames"; import cx from "classnames";
import { NumberField } from "src/utils/form";
export type GroupSceneIndexMap = Map<string, number | undefined>; export type GroupSceneIndexMap = Map<string, number | undefined>;
@@ -92,9 +93,8 @@ export const SceneGroupTable: React.FC<IProps> = (props) => {
/> />
</Col> </Col>
<Col xs={3}> <Col xs={3}>
<Form.Control <NumberField
className="text-input" className="text-input"
type="number"
value={m.scene_index ?? ""} value={m.scene_index ?? ""}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => { onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateFieldChanged( updateFieldChanged(

View File

@@ -2,6 +2,7 @@ import React from "react";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import { Form } from "react-bootstrap"; import { Form } from "react-bootstrap";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { NumberField } from "src/utils/form";
export type VideoPreviewSettingsInput = Pick< export type VideoPreviewSettingsInput = Pick<
GQL.ConfigGeneralInput, GQL.ConfigGeneralInput,
@@ -44,9 +45,8 @@ export const VideoPreviewInput: React.FC<IVideoPreviewInput> = ({
id: "dialogs.scene_gen.preview_seg_count_head", id: "dialogs.scene_gen.preview_seg_count_head",
})} })}
</h6> </h6>
<Form.Control <NumberField
className="text-input" className="text-input"
type="number"
value={previewSegments?.toString() ?? 1} value={previewSegments?.toString() ?? 1}
min={1} min={1}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
@@ -71,9 +71,8 @@ export const VideoPreviewInput: React.FC<IVideoPreviewInput> = ({
id: "dialogs.scene_gen.preview_seg_duration_head", id: "dialogs.scene_gen.preview_seg_duration_head",
})} })}
</h6> </h6>
<Form.Control <NumberField
className="text-input" className="text-input"
type="number"
value={previewSegmentDuration?.toString() ?? 0} value={previewSegmentDuration?.toString() ?? 0}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
set({ set({

View File

@@ -6,6 +6,7 @@ import { Icon } from "../Shared/Icon";
import { StringListInput } from "../Shared/StringListInput"; import { StringListInput } from "../Shared/StringListInput";
import { PatchComponent } from "src/patch"; import { PatchComponent } from "src/patch";
import { useSettings, useSettingsOptional } from "./context"; import { useSettings, useSettingsOptional } from "./context";
import { NumberField } from "src/utils/form";
interface ISetting { interface ISetting {
id?: string; id?: string;
@@ -484,9 +485,8 @@ export const NumberSetting: React.FC<INumberSetting> = PatchComponent(
<ModalSetting<number> <ModalSetting<number>
{...props} {...props}
renderField={(value, setValue) => ( renderField={(value, setValue) => (
<Form.Control <NumberField
className="text-input" className="text-input"
type="number"
value={value ?? 0} value={value ?? 0}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setValue(Number.parseInt(e.currentTarget.value || "0", 10)) setValue(Number.parseInt(e.currentTarget.value || "0", 10))

View File

@@ -3,6 +3,7 @@ import { Button } from "react-bootstrap";
import { Icon } from "../Icon"; import { Icon } from "../Icon";
import { faPencil, faStar } from "@fortawesome/free-solid-svg-icons"; import { faPencil, faStar } from "@fortawesome/free-solid-svg-icons";
import { useFocusOnce } from "src/utils/focus"; import { useFocusOnce } from "src/utils/focus";
import { useStopWheelScroll } from "src/utils/form";
export interface IRatingNumberProps { export interface IRatingNumberProps {
value: number | null; value: number | null;
@@ -26,6 +27,7 @@ export const RatingNumber: React.FC<IRatingNumberProps> = (
const showTextField = !props.disabled && (editing || !props.clickToRate); const showTextField = !props.disabled && (editing || !props.clickToRate);
const [ratingRef] = useFocusOnce(editing, true); const [ratingRef] = useFocusOnce(editing, true);
useStopWheelScroll(ratingRef);
const effectiveValue = editing ? valueStage : props.value; const effectiveValue = editing ? valueStage : props.value;

View File

@@ -48,6 +48,7 @@ export function renderLabel(options: {
// the mouse wheel will change the field value _and_ scroll the window. // the mouse wheel will change the field value _and_ scroll the window.
// This hook prevents the propagation that causes the window to scroll. // This hook prevents the propagation that causes the window to scroll.
export function useStopWheelScroll(ref: React.RefObject<HTMLElement>) { export function useStopWheelScroll(ref: React.RefObject<HTMLElement>) {
// removed the dependency array because the underlying ref value may change
useEffect(() => { useEffect(() => {
const { current } = ref; const { current } = ref;
@@ -66,17 +67,18 @@ export function useStopWheelScroll(ref: React.RefObject<HTMLElement>) {
current.removeEventListener("wheel", stopWheelScroll); current.removeEventListener("wheel", stopWheelScroll);
} }
}; };
}, [ref]); });
} }
const InputField: React.FC< // NumberField is a wrapper around Form.Control that prevents wheel events from scrolling the window.
export const NumberField: React.FC<
InputHTMLAttributes<HTMLInputElement> & FormControlProps InputHTMLAttributes<HTMLInputElement> & FormControlProps
> = (props) => { > = (props) => {
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
useStopWheelScroll(inputRef); useStopWheelScroll(inputRef);
return <Form.Control {...props} ref={inputRef} />; return <Form.Control {...props} type="number" ref={inputRef} />;
}; };
type Formik<V extends FormikValues> = ReturnType<typeof useFormik<V>>; type Formik<V extends FormikValues> = ReturnType<typeof useFormik<V>>;
@@ -134,9 +136,18 @@ export function formikUtils<V extends FormikValues>(
isInvalid={!!error} isInvalid={!!error}
/> />
); );
} else if (type === "number") {
<NumberField
type={type}
className="text-input"
placeholder={placeholder}
{...formikProps}
value={value}
isInvalid={!!error}
/>;
} else { } else {
control = ( control = (
<InputField <Form.Control
type={type} type={type}
className="text-input" className="text-input"
placeholder={placeholder} placeholder={placeholder}