Last active
August 17, 2023 21:53
-
-
Save mwood23/6eb49f3542e61da2a1bf98e726e6d72d to your computer and use it in GitHub Desktop.
Better Shopify Polaris Input
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { TextField, TextFieldProps } from "@shopify/polaris"; | |
import React, { | |
ChangeEvent, | |
KeyboardEvent, | |
useCallback, | |
useEffect, | |
useState, | |
} from "react"; | |
type NumberInputCommonProps = Omit< | |
TextFieldProps, | |
"onChange" | "value" | "min" | "max" | "autoComplete" | |
> & { | |
min?: number; | |
max?: number; | |
step?: number; | |
value: number; | |
decimalPlaces?: number; | |
}; | |
type NumberInputReturnZeroWhenEmptyProps = NumberInputCommonProps & { | |
returnZeroWhenEmpty: true; | |
onChange: (value: number) => void; | |
}; | |
type NumberInputReturnNullWhenEmptyProps = NumberInputCommonProps & { | |
returnZeroWhenEmpty: false; | |
onChange: (value: number | null) => void; | |
}; | |
export const NumberInput = ({ | |
min = 0, | |
max = 10, | |
value, | |
step = 1, | |
decimalPlaces = 0, | |
returnZeroWhenEmpty, | |
onChange, | |
...rest | |
}: | |
| NumberInputReturnZeroWhenEmptyProps | |
| NumberInputReturnNullWhenEmptyProps) => { | |
const [internalValue, setInternalValue] = useState(value.toString()); | |
useEffect(() => { | |
setInternalValue( | |
value % 1 === 0 ? value.toString() : value.toFixed(decimalPlaces) | |
); | |
}, [value, decimalPlaces]); | |
const handleIncrementOrDecrement = useCallback( | |
(direction: "up" | "down") => { | |
const increment = direction === "up" ? step : -step; | |
let numberValue = (parseFloat(internalValue) || 0) + increment; | |
if ( | |
(max !== undefined && numberValue > max) || | |
(min !== undefined && numberValue < min) | |
) { | |
return; | |
} | |
setInternalValue( | |
numberValue % 1 === 0 | |
? numberValue.toString() | |
: numberValue.toFixed(decimalPlaces) | |
); | |
onChange(numberValue); | |
}, | |
[setInternalValue, internalValue, onChange, step, decimalPlaces] | |
); | |
const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => { | |
if (event.key.toLowerCase() === "e") { | |
event.preventDefault(); | |
return; | |
} | |
if (event.key === "ArrowUp" || event.key === "ArrowDown") { | |
handleIncrementOrDecrement(event.key === "ArrowUp" ? "up" : "down"); | |
event.preventDefault(); | |
} | |
}; | |
const isValidValue = (valueString, numberValue) => { | |
if ( | |
(max !== undefined && numberValue > max) || | |
(min !== undefined && numberValue < min) | |
) { | |
return false; | |
} | |
// Allow empty string and numeric value | |
return valueString === "" || !isNaN(numberValue); | |
}; | |
const handleInputChange = (event: ChangeEvent<HTMLInputElement>) => { | |
let stringValue = event.target.value; | |
// Remove leading "0" if there's more than one character and it's not followed by a "." | |
if ( | |
stringValue.length > 1 && | |
stringValue.startsWith("0") && | |
!stringValue.startsWith("0.") | |
) { | |
stringValue = stringValue.slice(1); | |
} | |
const parts = stringValue.split("."); | |
if (parts.length === 2 && parts[1].length > decimalPlaces) { | |
stringValue = parts[0] + "." + parts[1].substring(0, decimalPlaces); | |
} | |
const numberValue = stringValue | |
? parseFloat(stringValue) | |
: returnZeroWhenEmpty | |
? 0 | |
: null; | |
if (isValidValue(stringValue, numberValue)) { | |
setInternalValue(stringValue); | |
if (returnZeroWhenEmpty) { | |
onChange(numberValue ?? 0); | |
} else { | |
onChange(numberValue); | |
} | |
} | |
}; | |
useEffect(() => { | |
const container = document.querySelector(".Polaris-TextField__Spinner"); | |
function handleSpinnerClick(event) { | |
// Get the target element that was clicked | |
const target = event.target; | |
// Find the closest parent element with the class "Polaris-TextField__Segment" | |
const segmentElement = target.closest(".Polaris-TextField__Segment"); | |
if (segmentElement) { | |
// Get the parent container | |
const container = document.querySelector(".Polaris-TextField__Spinner"); | |
if (container) { | |
// Get all elements with class "Polaris-TextField__Segment" inside the container | |
const segments = Array.from( | |
container.querySelectorAll(".Polaris-TextField__Segment") | |
); | |
// Find the index of the clicked segment | |
const index = segments.indexOf(segmentElement); | |
handleIncrementOrDecrement(index === 0 ? "up" : "down"); | |
} | |
} | |
} | |
container?.addEventListener("click", handleSpinnerClick); | |
return () => { | |
container?.removeEventListener("click", handleSpinnerClick); | |
}; | |
}, [handleIncrementOrDecrement]); | |
return ( | |
<div onChange={handleInputChange} onKeyDown={handleKeyDown}> | |
<TextField | |
autoComplete="off" | |
value={internalValue} | |
type="number" | |
min={min} | |
max={max} | |
{...rest} | |
/> | |
</div> | |
); | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment