mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 12:24:38 +03:00
Prevent mouse wheel window scrolling on other number fields (#5283)
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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" })}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user