Skip to content

Commit 85fbedf

Browse files
committed
Float inputs can now have a fixed number of decimal places.
1 parent cc6faa4 commit 85fbedf

File tree

4 files changed

+499
-14
lines changed

4 files changed

+499
-14
lines changed

react-native/components/createNullableFloatInputComponent/index.tsx

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ import { createInputComponent } from "../createInputComponent";
1818
* @param lessThanOrEqualTo When non-null, entered values must be lesser or
1919
* equal for validation to succeed.
2020
* @param alignment The alignment of the text within the input.
21+
* @param decimalPlaces When null, any number of decimal places may be
22+
* specified. Otherwise, the number of decimal
23+
* places accepted/shown.
2124
* @returns The created component.
2225
*/
2326
export const createNullableFloatInputComponent = (
@@ -28,7 +31,8 @@ export const createNullableFloatInputComponent = (
2831
greaterThanOrEqualTo: null | number,
2932
lessThan: null | number,
3033
lessThanOrEqualTo: null | number,
31-
alignment: `left` | `right`
34+
alignment: `left` | `right`,
35+
decimalPlaces: null | number
3236
): React.FunctionComponent<{
3337
/**
3438
* The value to edit. When undefined, it is treated as an invalid empty
@@ -55,7 +59,12 @@ export const createNullableFloatInputComponent = (
5559
readonly placeholder: string;
5660
}> => {
5761
const NullableFloatInputComponent = createInputComponent<null | number, null>(
58-
(value) => (value === null ? `` : String(value)),
62+
(value) =>
63+
value === null
64+
? ``
65+
: decimalPlaces === null
66+
? String(value)
67+
: value.toFixed(decimalPlaces),
5968
(unparsed) => {
6069
if (unparsed.trim() === ``) {
6170
return null;
@@ -64,6 +73,18 @@ export const createNullableFloatInputComponent = (
6473
unparsed
6574
)
6675
) {
76+
if (decimalPlaces !== null) {
77+
const splitByDecimalPlace = unparsed.split(`.`);
78+
79+
if (
80+
splitByDecimalPlace.length > 1 &&
81+
(splitByDecimalPlace[1] as string).replace(/\D/g, ``).length >
82+
decimalPlaces
83+
) {
84+
return undefined;
85+
}
86+
}
87+
6788
const parsed = Number.parseFloat(unparsed);
6889

6990
if (greaterThan !== null && parsed <= greaterThan) {

react-native/components/createNullableFloatInputComponent/unit.tsx

Lines changed: 229 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,8 @@ test(`renders as expected without bounds`, () => {
8888
null,
8989
null,
9090
null,
91-
`left`
91+
`left`,
92+
null
9293
);
9394

9495
const rendered = unwrapRenderedFunctionComponent(
@@ -300,7 +301,8 @@ test(`renders as expected with an inclusive lower bound`, () => {
300301
-4096.12,
301302
null,
302303
null,
303-
`left`
304+
`left`,
305+
null
304306
);
305307

306308
const rendered = unwrapRenderedFunctionComponent(
@@ -514,7 +516,8 @@ test(`renders as expected with an exclusive lower bound`, () => {
514516
null,
515517
null,
516518
null,
517-
`left`
519+
`left`,
520+
null
518521
);
519522

520523
const rendered = unwrapRenderedFunctionComponent(
@@ -728,7 +731,8 @@ test(`renders as expected with an inclusive upper bound`, () => {
728731
null,
729732
null,
730733
4096.12,
731-
`left`
734+
`left`,
735+
null
732736
);
733737

734738
const rendered = unwrapRenderedFunctionComponent(
@@ -942,7 +946,8 @@ test(`renders as expected with an exclusive upper bound`, () => {
942946
null,
943947
4096.12,
944948
null,
945-
`left`
949+
`left`,
950+
null
946951
);
947952

948953
const rendered = unwrapRenderedFunctionComponent(
@@ -1073,3 +1078,222 @@ test(`renders as expected with an exclusive upper bound`, () => {
10731078

10741079
expect(onChange).not.toHaveBeenCalled();
10751080
});
1081+
1082+
test(`renders as expected without bounds`, () => {
1083+
const controlStyle: ControlStyle = {
1084+
fontFamily: `Example Font Family`,
1085+
fontSize: 37,
1086+
paddingVertical: 12,
1087+
paddingHorizontal: 29,
1088+
blurredValid: {
1089+
textColor: `#FFEE00`,
1090+
placeholderColor: `#E7AA32`,
1091+
backgroundColor: `#32AE12`,
1092+
radius: 5,
1093+
border: {
1094+
width: 4,
1095+
color: `#FF00FF`,
1096+
},
1097+
iconColor: `#43AE21`,
1098+
},
1099+
blurredInvalid: {
1100+
textColor: `#99FE88`,
1101+
placeholderColor: `#CACA3A`,
1102+
backgroundColor: `#259284`,
1103+
radius: 10,
1104+
border: {
1105+
width: 6,
1106+
color: `#9A9A8E`,
1107+
},
1108+
iconColor: `#985E00`,
1109+
},
1110+
focusedValid: {
1111+
textColor: `#55EA13`,
1112+
placeholderColor: `#273346`,
1113+
backgroundColor: `#CABA99`,
1114+
radius: 3,
1115+
border: {
1116+
width: 5,
1117+
color: `#646464`,
1118+
},
1119+
iconColor: `#789521`,
1120+
},
1121+
focusedInvalid: {
1122+
textColor: `#ABAADE`,
1123+
placeholderColor: `#47ADAD`,
1124+
backgroundColor: `#32AA88`,
1125+
radius: 47,
1126+
border: {
1127+
width: 12,
1128+
color: `#98ADAA`,
1129+
},
1130+
iconColor: `#449438`,
1131+
},
1132+
disabledValid: {
1133+
textColor: `#AE2195`,
1134+
placeholderColor: `#FFAAEE`,
1135+
backgroundColor: `#772728`,
1136+
radius: 100,
1137+
border: {
1138+
width: 14,
1139+
color: `#5E5E5E`,
1140+
},
1141+
iconColor: `#ADAADA`,
1142+
},
1143+
disabledInvalid: {
1144+
textColor: `#340297`,
1145+
placeholderColor: `#233832`,
1146+
backgroundColor: `#938837`,
1147+
radius: 2,
1148+
border: {
1149+
width: 19,
1150+
color: `#573829`,
1151+
},
1152+
iconColor: `#709709`,
1153+
},
1154+
};
1155+
const onChange = jest.fn();
1156+
const Component = createNullableFloatInputComponent(
1157+
controlStyle,
1158+
<Text>Example Left Icon</Text>,
1159+
<Text>Example Right Icon</Text>,
1160+
null,
1161+
null,
1162+
null,
1163+
null,
1164+
`left`,
1165+
2
1166+
);
1167+
1168+
const rendered = unwrapRenderedFunctionComponent(
1169+
<Component
1170+
value={124}
1171+
onChange={onChange}
1172+
disabled
1173+
placeholder="Example Placeholder"
1174+
/>
1175+
);
1176+
1177+
expect(rendered.type).toBeAFunctionWithTheStaticProperties({
1178+
inputComponent: {
1179+
stringify: expect.any(Function),
1180+
tryParse: expect.any(Function),
1181+
controlStyle,
1182+
multiLine: false,
1183+
autoComplete: `off`,
1184+
keyboardType: `numeric`,
1185+
autoFocus: false,
1186+
keepFocusOnSubmit: false,
1187+
alignment: `left`,
1188+
},
1189+
});
1190+
1191+
expect(rendered.props).toEqual({
1192+
leftIcon: <Text>Example Left Icon</Text>,
1193+
rightIcon: <Text>Example Right Icon</Text>,
1194+
value: 124,
1195+
onChange,
1196+
disabled: true,
1197+
placeholder: `Example Placeholder`,
1198+
context: null,
1199+
secureTextEntry: false,
1200+
onSubmit: expect.any(Function),
1201+
});
1202+
1203+
expect(rendered.type.inputComponent.stringify(null)).toEqual(``);
1204+
expect(rendered.type.inputComponent.stringify(0.1234)).toEqual(`0.12`);
1205+
expect(rendered.type.inputComponent.stringify(12.34)).toEqual(`12.34`);
1206+
expect(rendered.type.inputComponent.stringify(1234)).toEqual(`1234.00`);
1207+
expect(rendered.type.inputComponent.stringify(-0.1234)).toEqual(`-0.12`);
1208+
expect(rendered.type.inputComponent.stringify(-12.34)).toEqual(`-12.34`);
1209+
expect(rendered.type.inputComponent.stringify(-1234)).toEqual(`-1234.00`);
1210+
1211+
expect(rendered.type.inputComponent.tryParse(``)).toBeNull();
1212+
expect(rendered.type.inputComponent.tryParse(` \n \r \t `)).toBeNull();
1213+
expect(rendered.type.inputComponent.tryParse(`1e1`)).toBeUndefined();
1214+
expect(rendered.type.inputComponent.tryParse(`-1e1`)).toBeUndefined();
1215+
expect(rendered.type.inputComponent.tryParse(`NaN`)).toBeUndefined();
1216+
expect(rendered.type.inputComponent.tryParse(`-NaN`)).toBeUndefined();
1217+
expect(rendered.type.inputComponent.tryParse(`Infinity`)).toBeUndefined();
1218+
expect(rendered.type.inputComponent.tryParse(`-Infinity`)).toBeUndefined();
1219+
expect(rendered.type.inputComponent.tryParse(`.1`)).toEqual(0.1);
1220+
expect(rendered.type.inputComponent.tryParse(`+.1`)).toEqual(0.1);
1221+
expect(rendered.type.inputComponent.tryParse(`-.1`)).toEqual(-0.1);
1222+
expect(rendered.type.inputComponent.tryParse(`.12`)).toEqual(0.12);
1223+
expect(rendered.type.inputComponent.tryParse(`+.12`)).toEqual(0.12);
1224+
expect(rendered.type.inputComponent.tryParse(`-.12`)).toEqual(-0.12);
1225+
expect(rendered.type.inputComponent.tryParse(`.123`)).toBeUndefined();
1226+
expect(rendered.type.inputComponent.tryParse(`+.123`)).toBeUndefined();
1227+
expect(rendered.type.inputComponent.tryParse(`-.123`)).toBeUndefined();
1228+
expect(rendered.type.inputComponent.tryParse(`12.3`)).toEqual(12.3);
1229+
expect(rendered.type.inputComponent.tryParse(`+12.3`)).toEqual(12.3);
1230+
expect(rendered.type.inputComponent.tryParse(`-12.3`)).toEqual(-12.3);
1231+
expect(rendered.type.inputComponent.tryParse(`12.34`)).toEqual(12.34);
1232+
expect(rendered.type.inputComponent.tryParse(`+12.34`)).toEqual(12.34);
1233+
expect(rendered.type.inputComponent.tryParse(`-12.34`)).toEqual(-12.34);
1234+
expect(rendered.type.inputComponent.tryParse(`12.345`)).toBeUndefined();
1235+
expect(rendered.type.inputComponent.tryParse(`+12.345`)).toBeUndefined();
1236+
expect(rendered.type.inputComponent.tryParse(`-12.345`)).toBeUndefined();
1237+
expect(rendered.type.inputComponent.tryParse(`1234`)).toEqual(1234);
1238+
expect(rendered.type.inputComponent.tryParse(`+1234`)).toEqual(1234);
1239+
expect(rendered.type.inputComponent.tryParse(`-1234`)).toEqual(-1234);
1240+
expect(rendered.type.inputComponent.tryParse(`1234.`)).toEqual(1234);
1241+
expect(rendered.type.inputComponent.tryParse(`+1234.`)).toEqual(1234);
1242+
expect(rendered.type.inputComponent.tryParse(`-1234.`)).toEqual(-1234);
1243+
expect(
1244+
rendered.type.inputComponent.tryParse(` \n \r \t .12 \t \n \r `)
1245+
).toEqual(0.12);
1246+
expect(
1247+
rendered.type.inputComponent.tryParse(` \n \r \t +.12 \t \n \r `)
1248+
).toEqual(0.12);
1249+
expect(
1250+
rendered.type.inputComponent.tryParse(` \n \r \t -.12 \t \n \r `)
1251+
).toEqual(-0.12);
1252+
expect(
1253+
rendered.type.inputComponent.tryParse(
1254+
` \n \r \t 12.34 \t \n \r `
1255+
)
1256+
).toEqual(12.34);
1257+
expect(
1258+
rendered.type.inputComponent.tryParse(
1259+
` \n \r \t +12.34 \t \n \r `
1260+
)
1261+
).toEqual(12.34);
1262+
expect(
1263+
rendered.type.inputComponent.tryParse(
1264+
` \n \r \t -12.34 \t \n \r `
1265+
)
1266+
).toEqual(-12.34);
1267+
expect(
1268+
rendered.type.inputComponent.tryParse(` \n \r \t 1234 \t \n \r `)
1269+
).toEqual(1234);
1270+
expect(
1271+
rendered.type.inputComponent.tryParse(
1272+
` \n \r \t +1234 \t \n \r `
1273+
)
1274+
).toEqual(1234);
1275+
expect(
1276+
rendered.type.inputComponent.tryParse(
1277+
` \n \r \t -1234 \t \n \r `
1278+
)
1279+
).toEqual(-1234);
1280+
expect(
1281+
rendered.type.inputComponent.tryParse(
1282+
` \n \r \t 1234. \t \n \r `
1283+
)
1284+
).toEqual(1234);
1285+
expect(
1286+
rendered.type.inputComponent.tryParse(
1287+
` \n \r \t +1234. \t \n \r `
1288+
)
1289+
).toEqual(1234);
1290+
expect(
1291+
rendered.type.inputComponent.tryParse(
1292+
` \n \r \t -1234. \t \n \r `
1293+
)
1294+
).toEqual(-1234);
1295+
1296+
rendered.props.onSubmit();
1297+
1298+
expect(onChange).not.toHaveBeenCalled();
1299+
});

react-native/components/createRequiredFloatInputComponent/index.tsx

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ import { createInputComponent } from "../createInputComponent";
1818
* @param lessThanOrEqualTo When non-null, entered values must be lesser or
1919
* equal for validation to succeed.
2020
* @param alignment The alignment of the text within the input.
21+
* @param decimalPlaces When null, any number of decimal places may be
22+
* specified. Otherwise, the number of decimal
23+
* places accepted/shown.
2124
* @returns The created component.
2225
*/
2326
export const createRequiredFloatInputComponent = (
@@ -28,7 +31,8 @@ export const createRequiredFloatInputComponent = (
2831
greaterThanOrEqualTo: null | number,
2932
lessThan: null | number,
3033
lessThanOrEqualTo: null | number,
31-
alignment: `left` | `right`
34+
alignment: `left` | `right`,
35+
decimalPlaces: null | number
3236
): React.FunctionComponent<{
3337
/**
3438
* The value to edit. When undefined, it is treated as an invalid empty
@@ -55,13 +59,26 @@ export const createRequiredFloatInputComponent = (
5559
readonly placeholder: string;
5660
}> => {
5761
const RequiredFloatInputComponent = createInputComponent<number, null>(
58-
(value) => String(value),
62+
(value) =>
63+
decimalPlaces === null ? String(value) : value.toFixed(decimalPlaces),
5964
(unparsed) => {
6065
if (
6166
/^\s*[+-]?\d+\s*$|^\s*[+-]?\d+\.\d+\s*$|^\s*[+-]?\.\d+\s*$|^\s*[+-]?\d+\.\s*$/.test(
6267
unparsed
6368
)
6469
) {
70+
if (decimalPlaces !== null) {
71+
const splitByDecimalPlace = unparsed.split(`.`);
72+
73+
if (
74+
splitByDecimalPlace.length > 1 &&
75+
(splitByDecimalPlace[1] as string).replace(/\D/g, ``).length >
76+
decimalPlaces
77+
) {
78+
return undefined;
79+
}
80+
}
81+
6582
const parsed = Number.parseFloat(unparsed);
6683

6784
if (greaterThan !== null && parsed <= greaterThan) {

0 commit comments

Comments
 (0)