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 { Criterion } from "src/models/list-filter/criteria/criterion";
|
||||
import { IHierarchicalLabelValue } from "src/models/list-filter/types";
|
||||
import { NumberField } from "src/utils/form";
|
||||
|
||||
interface IHierarchicalLabelValueFilterProps {
|
||||
criterion: Criterion<IHierarchicalLabelValue>;
|
||||
@@ -104,9 +105,8 @@ export const HierarchicalLabelValueFilter: React.FC<
|
||||
|
||||
{criterion.value.depth !== 0 && (
|
||||
<Form.Group>
|
||||
<Form.Control
|
||||
<NumberField
|
||||
className="btn-secondary"
|
||||
type="number"
|
||||
placeholder={intl.formatMessage(messages.studio_depth)}
|
||||
onChange={(e) =>
|
||||
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 { INumberValue } from "../../../models/list-filter/types";
|
||||
import { NumberCriterion } from "../../../models/list-filter/criteria/criterion";
|
||||
import { NumberField } from "src/utils/form";
|
||||
|
||||
interface IDurationFilterProps {
|
||||
criterion: NumberCriterion;
|
||||
@@ -36,9 +37,8 @@ export const NumberFilter: React.FC<IDurationFilterProps> = ({
|
||||
) {
|
||||
equalsControl = (
|
||||
<Form.Group>
|
||||
<Form.Control
|
||||
<NumberField
|
||||
className="btn-secondary"
|
||||
type="number"
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
onChanged(e, "value")
|
||||
}
|
||||
@@ -57,9 +57,8 @@ export const NumberFilter: React.FC<IDurationFilterProps> = ({
|
||||
) {
|
||||
lowerControl = (
|
||||
<Form.Group>
|
||||
<Form.Control
|
||||
<NumberField
|
||||
className="btn-secondary"
|
||||
type="number"
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
onChanged(e, "value")
|
||||
}
|
||||
@@ -78,9 +77,8 @@ export const NumberFilter: React.FC<IDurationFilterProps> = ({
|
||||
) {
|
||||
upperControl = (
|
||||
<Form.Group>
|
||||
<Form.Control
|
||||
<NumberField
|
||||
className="btn-secondary"
|
||||
type="number"
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
onChanged(
|
||||
e,
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useIntl } from "react-intl";
|
||||
import { IPhashDistanceValue } from "../../../models/list-filter/types";
|
||||
import { Criterion } from "../../../models/list-filter/criteria/criterion";
|
||||
import { CriterionModifier } from "src/core/generated-graphql";
|
||||
import { NumberField } from "src/utils/form";
|
||||
|
||||
interface IPhashFilterProps {
|
||||
criterion: Criterion<IPhashDistanceValue>;
|
||||
@@ -49,10 +50,9 @@ export const PhashFilter: React.FC<IPhashFilterProps> = ({
|
||||
{criterion.modifier !== CriterionModifier.IsNull &&
|
||||
criterion.modifier !== CriterionModifier.NotNull && (
|
||||
<Form.Group>
|
||||
<Form.Control
|
||||
<NumberField
|
||||
className="btn-secondary"
|
||||
onChange={distanceChanged}
|
||||
type="number"
|
||||
value={value ? value.distance : ""}
|
||||
placeholder={intl.formatMessage({ id: "distance" })}
|
||||
/>
|
||||
|
||||
@@ -25,6 +25,7 @@ import { keyboardClickHandler } from "src/utils/keyboard";
|
||||
import { useDebounce } from "src/hooks/debounce";
|
||||
import useFocus from "src/utils/focus";
|
||||
import ScreenUtils from "src/utils/screen";
|
||||
import { NumberField } from "src/utils/form";
|
||||
|
||||
interface ISelectedItem {
|
||||
item: ILabeledId;
|
||||
@@ -361,9 +362,8 @@ export const HierarchicalObjectsFilter = <
|
||||
|
||||
{criterion.value.depth !== 0 && (
|
||||
<Form.Group>
|
||||
<Form.Control
|
||||
<NumberField
|
||||
className="btn-secondary"
|
||||
type="number"
|
||||
placeholder={intl.formatMessage(messages.studio_depth)}
|
||||
onChange={(e) =>
|
||||
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 { View } from "./views";
|
||||
import { ClearableInput } from "../Shared/ClearableInput";
|
||||
import { useStopWheelScroll } from "src/utils/form";
|
||||
|
||||
export function useDebouncedSearchInput(
|
||||
filter: ListFilterModel,
|
||||
@@ -126,6 +127,8 @@ export const PageSizeSelector: React.FC<{
|
||||
}
|
||||
}, [customPageSizeShowing, perPageFocus]);
|
||||
|
||||
useStopWheelScroll(perPageInput);
|
||||
|
||||
const pageSizeOptions = useMemo(() => {
|
||||
const ret = PAGE_SIZE_OPTIONS.map((o) => {
|
||||
return {
|
||||
@@ -190,6 +193,7 @@ export const PageSizeSelector: React.FC<{
|
||||
<Popover id="custom_pagesize_popover">
|
||||
<Form inline>
|
||||
<InputGroup>
|
||||
{/* can't use NumberField because of the ref */}
|
||||
<Form.Control
|
||||
type="number"
|
||||
min={1}
|
||||
|
||||
@@ -12,6 +12,7 @@ import { FormattedMessage, FormattedNumber, useIntl } from "react-intl";
|
||||
import useFocus from "src/utils/focus";
|
||||
import { Icon } from "../Shared/Icon";
|
||||
import { faCheck, faChevronDown } from "@fortawesome/free-solid-svg-icons";
|
||||
import { useStopWheelScroll } from "src/utils/form";
|
||||
|
||||
const PageCount: React.FC<{
|
||||
totalPages: number;
|
||||
@@ -32,6 +33,8 @@ const PageCount: React.FC<{
|
||||
}
|
||||
}, [showSelectPage, pageFocus]);
|
||||
|
||||
useStopWheelScroll(pageInput);
|
||||
|
||||
const pageOptions = useMemo(() => {
|
||||
const maxPagesToShow = 10;
|
||||
const min = Math.max(1, currentPage - maxPagesToShow / 2);
|
||||
@@ -98,6 +101,7 @@ const PageCount: React.FC<{
|
||||
<Popover id="select_page_popover">
|
||||
<Form inline>
|
||||
<InputGroup>
|
||||
{/* can't use NumberField because of the ref */}
|
||||
<Form.Control
|
||||
type="number"
|
||||
min={1}
|
||||
|
||||
@@ -4,6 +4,7 @@ import * as GQL from "src/core/generated-graphql";
|
||||
import { Form, Row, Col } from "react-bootstrap";
|
||||
import { Group, GroupSelect } from "src/components/Groups/GroupSelect";
|
||||
import cx from "classnames";
|
||||
import { NumberField } from "src/utils/form";
|
||||
|
||||
export type GroupSceneIndexMap = Map<string, number | undefined>;
|
||||
|
||||
@@ -92,9 +93,8 @@ export const SceneGroupTable: React.FC<IProps> = (props) => {
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={3}>
|
||||
<Form.Control
|
||||
<NumberField
|
||||
className="text-input"
|
||||
type="number"
|
||||
value={m.scene_index ?? ""}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
updateFieldChanged(
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
import { Form } from "react-bootstrap";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { NumberField } from "src/utils/form";
|
||||
|
||||
export type VideoPreviewSettingsInput = Pick<
|
||||
GQL.ConfigGeneralInput,
|
||||
@@ -44,9 +45,8 @@ export const VideoPreviewInput: React.FC<IVideoPreviewInput> = ({
|
||||
id: "dialogs.scene_gen.preview_seg_count_head",
|
||||
})}
|
||||
</h6>
|
||||
<Form.Control
|
||||
<NumberField
|
||||
className="text-input"
|
||||
type="number"
|
||||
value={previewSegments?.toString() ?? 1}
|
||||
min={1}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
@@ -71,9 +71,8 @@ export const VideoPreviewInput: React.FC<IVideoPreviewInput> = ({
|
||||
id: "dialogs.scene_gen.preview_seg_duration_head",
|
||||
})}
|
||||
</h6>
|
||||
<Form.Control
|
||||
<NumberField
|
||||
className="text-input"
|
||||
type="number"
|
||||
value={previewSegmentDuration?.toString() ?? 0}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
set({
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Icon } from "../Shared/Icon";
|
||||
import { StringListInput } from "../Shared/StringListInput";
|
||||
import { PatchComponent } from "src/patch";
|
||||
import { useSettings, useSettingsOptional } from "./context";
|
||||
import { NumberField } from "src/utils/form";
|
||||
|
||||
interface ISetting {
|
||||
id?: string;
|
||||
@@ -484,9 +485,8 @@ export const NumberSetting: React.FC<INumberSetting> = PatchComponent(
|
||||
<ModalSetting<number>
|
||||
{...props}
|
||||
renderField={(value, setValue) => (
|
||||
<Form.Control
|
||||
<NumberField
|
||||
className="text-input"
|
||||
type="number"
|
||||
value={value ?? 0}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setValue(Number.parseInt(e.currentTarget.value || "0", 10))
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Button } from "react-bootstrap";
|
||||
import { Icon } from "../Icon";
|
||||
import { faPencil, faStar } from "@fortawesome/free-solid-svg-icons";
|
||||
import { useFocusOnce } from "src/utils/focus";
|
||||
import { useStopWheelScroll } from "src/utils/form";
|
||||
|
||||
export interface IRatingNumberProps {
|
||||
value: number | null;
|
||||
@@ -26,6 +27,7 @@ export const RatingNumber: React.FC<IRatingNumberProps> = (
|
||||
const showTextField = !props.disabled && (editing || !props.clickToRate);
|
||||
|
||||
const [ratingRef] = useFocusOnce(editing, true);
|
||||
useStopWheelScroll(ratingRef);
|
||||
|
||||
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.
|
||||
// This hook prevents the propagation that causes the window to scroll.
|
||||
export function useStopWheelScroll(ref: React.RefObject<HTMLElement>) {
|
||||
// removed the dependency array because the underlying ref value may change
|
||||
useEffect(() => {
|
||||
const { current } = ref;
|
||||
|
||||
@@ -66,17 +67,18 @@ export function useStopWheelScroll(ref: React.RefObject<HTMLElement>) {
|
||||
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
|
||||
> = (props) => {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
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>>;
|
||||
@@ -134,9 +136,18 @@ export function formikUtils<V extends FormikValues>(
|
||||
isInvalid={!!error}
|
||||
/>
|
||||
);
|
||||
} else if (type === "number") {
|
||||
<NumberField
|
||||
type={type}
|
||||
className="text-input"
|
||||
placeholder={placeholder}
|
||||
{...formikProps}
|
||||
value={value}
|
||||
isInvalid={!!error}
|
||||
/>;
|
||||
} else {
|
||||
control = (
|
||||
<InputField
|
||||
<Form.Control
|
||||
type={type}
|
||||
className="text-input"
|
||||
placeholder={placeholder}
|
||||
|
||||
Reference in New Issue
Block a user