Skip to content

Instantly share code, notes, and snippets.

@mwood23
Last active August 17, 2023 21:53
Show Gist options
  • Save mwood23/6eb49f3542e61da2a1bf98e726e6d72d to your computer and use it in GitHub Desktop.
Save mwood23/6eb49f3542e61da2a1bf98e726e6d72d to your computer and use it in GitHub Desktop.
Better Shopify Polaris Input
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