From 26da227bd699c7b05f28f1f806b7f6daf5a9e244 Mon Sep 17 00:00:00 2001 From: frozenhelium Date: Sat, 1 Aug 2026 03:16:09 +0545 Subject: [PATCH] fix(go-ui): reject non-numeric input in NumberInput - Block letters typed into the number input on Firefox and Safari - Apply the same rule to pasted text - Keep decimal points and minus signs for coordinate fields --- .changeset/quiet-pandas-repeat.md | 5 +++ .../ui/src/components/NumberInput/index.tsx | 37 +++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 .changeset/quiet-pandas-repeat.md diff --git a/.changeset/quiet-pandas-repeat.md b/.changeset/quiet-pandas-repeat.md new file mode 100644 index 000000000..418eedc22 --- /dev/null +++ b/.changeset/quiet-pandas-repeat.md @@ -0,0 +1,5 @@ +--- +"@ifrc-go/ui": minor +--- + +Fix number inputs accepting letters on Firefox and Safari diff --git a/packages/ui/src/components/NumberInput/index.tsx b/packages/ui/src/components/NumberInput/index.tsx index 908026b1e..bc81c59fe 100644 --- a/packages/ui/src/components/NumberInput/index.tsx +++ b/packages/ui/src/components/NumberInput/index.tsx @@ -17,6 +17,20 @@ import { extractInputContainerProps } from '#utils/inputs'; type InheritedProps = Omit & Omit, 'onChange' | 'value' | 'className' | 'elementRef'>; +// NOTE: Firefox and Safari let you type letters into a number input. The browser +// then reports the value as empty, so the stray text never reaches onChange and +// cannot be stripped after the fact. Rejecting the insertion is the only fix. +// Scientific notation is excluded deliberately, no field here needs it. +const NON_NUMERIC_PATTERN = /[^\d.+-]/; + +function isNonNumericText(text: string | undefined | null) { + if (isNotDefined(text)) { + return false; + } + + return NON_NUMERIC_PATTERN.test(text); +} + export interface Props extends InheritedProps { inputElementRef?: React.RefObject; inputClassName?: string; @@ -38,6 +52,8 @@ function NumberInput(props: Props) { value: valueFromProps, required, onChange, + onBeforeInput, + onPaste, withDiffView, value, prevValue, @@ -72,6 +88,25 @@ function NumberInput(props: Props) { } }, [onChange]); + // NOTE: React skips onBeforeInput for ctrl/alt/meta combos, so select all, + // undo and the browser shortcuts are left alone + const handleBeforeInput = useCallback((e: React.InputEvent) => { + onBeforeInput?.(e); + + if (!e.defaultPrevented && isNonNumericText(e.data)) { + e.preventDefault(); + } + }, [onBeforeInput]); + + // NOTE: React does not route paste through onBeforeInput on Firefox + const handlePaste = useCallback((e: React.ClipboardEvent) => { + onPaste?.(e); + + if (!e.defaultPrevented && isNonNumericText(e.clipboardData.getData('text'))) { + e.preventDefault(); + } + }, [onPaste]); + const highlightMode = useMemo( () => getHighlightMode(value, prevValue, withDiffView), [value, prevValue, withDiffView], @@ -92,7 +127,9 @@ function NumberInput(props: Props) { {...rawInputProps} className={inputClassName} disabled={disabled} + onBeforeInput={handleBeforeInput} onChange={handleChange} + onPaste={handlePaste} readOnly={readOnly} type="number" value={tempValue}