diff --git a/src/renderer/components/+preferences/terminal.tsx b/src/renderer/components/+preferences/terminal.tsx index 7e13737b1e..eafe4e9894 100644 --- a/src/renderer/components/+preferences/terminal.tsx +++ b/src/renderer/components/+preferences/terminal.tsx @@ -8,7 +8,7 @@ import { action } from "mobx"; import { observer } from "mobx-react"; import type { UserStore } from "../../../common/user-store"; import { SubTitle } from "../layout/sub-title"; -import { Input, InputValidators } from "../input"; +import { Input } from "../input"; import { Switch } from "../switch"; import { Select, type SelectOption } from "../select"; import type { ThemeStore } from "../../themes/store"; @@ -104,8 +104,8 @@ const NonInjectedTerminal = observer(( theme="round-black" type="number" min={10} - validators={InputValidators.isNumber} - value={userStore.terminalConfig.fontSize.toString()} + max={50} + defaultValue={userStore.terminalConfig.fontSize.toString()} onChange={(value) => userStore.terminalConfig.fontSize = Number(value)} /> diff --git a/src/renderer/components/input/input.tsx b/src/renderer/components/input/input.tsx index ffb5fab9ca..3056a304f9 100644 --- a/src/renderer/components/input/input.tsx +++ b/src/renderer/components/input/input.tsx @@ -14,7 +14,6 @@ import { Tooltip } from "../tooltip"; import * as Validators from "./input_validators"; import type { InputValidator } from "./input_validators"; import isFunction from "lodash/isFunction"; -import isBoolean from "lodash/isBoolean"; import uniqueId from "lodash/uniqueId"; import { debounce } from "lodash"; @@ -24,7 +23,10 @@ export { InputValidators }; export type { InputValidator }; type InputElement = HTMLInputElement | HTMLTextAreaElement; -type InputElementProps = InputHTMLAttributes & TextareaHTMLAttributes & DOMAttributes; +type InputElementProps = + InputHTMLAttributes + & TextareaHTMLAttributes + & DOMAttributes; export interface IconDataFnArg { isDirty: boolean; @@ -173,22 +175,18 @@ export class Input extends React.Component { error => this.getValidatorError(value, validator) || error, ), ); - } else { - if (!validator.validate(value, this.props)) { - errors.push(this.getValidatorError(value, validator)); - } } - const result = validator.validate(value, this.props); + const isValid = validator.validate(value, this.props); - if (isBoolean(result) && !result) { + if (isValid === false) { errors.push(this.getValidatorError(value, validator)); - } else if (result instanceof Promise) { + } else if (isValid instanceof Promise) { if (!validationId) { this.validationId = validationId = uniqueId("validation_id_"); } asyncValidators.push( - result.then( + isValid.then( () => null, // don't consider any valid result from promise since we interested in errors only error => this.getValidatorError(value, validator) || error, ), @@ -266,9 +264,7 @@ export class Input extends React.Component { setDirtyOnChange = debounce(() => this.setDirty(), 500); - onChange(evt: React.ChangeEvent) { - this.props.onChange?.(evt.currentTarget.value, evt); - this.validate(); + async onChange(evt: React.ChangeEvent) { this.autoFitHeight(); this.setDirtyOnChange(); @@ -277,6 +273,17 @@ export class Input extends React.Component { if (this.isUncontrolled && this.showMaxLenIndicator) { this.forceUpdate(); } + + const newValue = evt.currentTarget.value; + const eventCopy = { ...evt }; + + await this.validate(); // validate first + + // don't propagate changes for invalid values + // possible only with uncontrolled components (defaultValue={} must be used instead value={}) + if (!this.isUncontrolled || (this.isUncontrolled && this.state.valid)) { + this.props.onChange?.(newValue, eventCopy); + } } onKeyDown(evt: React.KeyboardEvent) { @@ -299,7 +306,7 @@ export class Input extends React.Component { this.setDirty(); } - if(this.props.blurOnEnter){ + if (this.props.blurOnEnter) { //pressing enter indicates that the edit is complete, we can unfocus now this.blur(); } @@ -379,7 +386,6 @@ export class Input extends React.Component { multiLine, showValidationLine, validators, theme, maxRows, children, showErrorsAsTooltip, maxLength, rows, disabled, autoSelectOnFocus, iconLeft, iconRight, contentRight, id, dirty: _dirty, // excluded from passing to input-element - defaultValue, trim, blurOnEnter, ...inputProps diff --git a/src/renderer/components/input/input_validators.ts b/src/renderer/components/input/input_validators.ts index 4c0befbd29..a9844bfcd8 100644 --- a/src/renderer/components/input/input_validators.ts +++ b/src/renderer/components/input/input_validators.ts @@ -8,7 +8,8 @@ import type { ReactNode } from "react"; import fse from "fs-extra"; import { TypedRegEx } from "typed-regex"; -export class AsyncInputValidationError extends Error {} +export class AsyncInputValidationError extends Error { +} export type InputValidator = { /** @@ -32,7 +33,7 @@ export type InputValidator = { message?: undefined; debounce: number; } -); + ); export function inputValidator(validator: InputValidator): InputValidator { return validator; @@ -52,7 +53,14 @@ export const isEmail = inputValidator({ export const isNumber = inputValidator({ condition: ({ type }) => type === "number", - message: () => `Invalid number`, + message(value, { min, max }) { + const minMax: string = [ + typeof min === "number" ? `min: ${min}` : undefined, + typeof max === "number" ? `max: ${max}` : undefined, + ].filter(Boolean).join(", "); + + return `Invalid number${minMax ? ` (${minMax})` : ""}`; + }, validate: (value, { min, max }) => { const numVal = +value;