Skip to content
This repository was archived by the owner on Mar 13, 2025. It is now read-only.

Commit 6104e7d

Browse files
committed
feat(roles): allow manual inputs in integer fields
* Remove +/- buttons in the integer fields of Roles form. * Allow manual input in the integer fields. * Add a logic to make time conversion between human-readable strings (e.g. "2 weeks") and numeric hours (336). * Add proper validations on manual inputs. Addresses: https://github.com/topcoder-platform/taas-app/issues/425, https://github.com/topcoder-platform/taas-app/issues/403#issuecomment-892774361
1 parent 55641f6 commit 6104e7d

File tree

6 files changed

+432
-128
lines changed

6 files changed

+432
-128
lines changed

src/components/IntegerField/index.jsx

+95-32
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
import React from "react";
1+
import React, { useMemo } from "react";
22
import PT from "prop-types";
33
import cn from "classnames";
4+
import IconExclamationMark from "components/Icons/ExclamationMarkCircled";
5+
import Popover from "components/Popover";
46
import styles from "./styles.module.scss";
57

68
/**
@@ -9,58 +11,119 @@ import styles from "./styles.module.scss";
911
* @param {Object} props component properties
1012
* @param {string} [props.className] class name to be added to root element
1113
* @param {boolean} [props.isDisabled] if the field is disabled
14+
* @param {boolean} [props.readOnly] if the field is readOnly
15+
* @param {boolean} [props.displayButtons] whether to display +/- buttons
1216
* @param {string} props.name field's name
1317
* @param {number} props.value field's value
1418
* @param {number} [props.maxValue] maximum allowed value
1519
* @param {number} [props.minValue] minimum allowed value
16-
* @param {(v: number) => void} props.onChange
20+
* @param {(v: number) => void} [props.onChange]
21+
* @param {(v: string) => void} [props.onInputChange]
1722
* @returns {JSX.Element}
1823
*/
1924
const IntegerField = ({
2025
className,
2126
isDisabled = false,
27+
readOnly = true,
28+
displayButtons = true,
2229
name,
30+
onInputChange,
2331
onChange,
2432
value,
2533
maxValue = Infinity,
2634
minValue = -Infinity,
27-
}) => (
28-
<div className={cn(styles.container, className)}>
29-
<input
30-
disabled={isDisabled}
31-
readOnly
32-
className={styles.input}
33-
name={name}
34-
value={value}
35-
/>
36-
<button
37-
className={styles.btnMinus}
38-
onClick={(event) => {
39-
event.stopPropagation();
40-
if (!isDisabled) {
41-
onChange(Math.max(value - 1, minValue));
42-
}
43-
}}
44-
/>
45-
<button
46-
className={styles.btnPlus}
47-
onClick={(event) => {
48-
event.stopPropagation();
49-
if (!isDisabled) {
50-
onChange(Math.min(+value + 1, maxValue));
51-
}
52-
}}
53-
/>
54-
</div>
55-
);
35+
}) => {
36+
const isInvalid = useMemo(
37+
() =>
38+
!!value &&
39+
(isNaN(value) ||
40+
!Number.isInteger(+value) ||
41+
+value > maxValue ||
42+
+value < minValue),
43+
[value, minValue, maxValue]
44+
);
45+
46+
const errorPopupContent = useMemo(() => {
47+
if (value && (isNaN(value) || !Number.isInteger(+value))) {
48+
return <>You must enter a valid integer.</>;
49+
}
50+
if (+value > maxValue) {
51+
return (
52+
<>
53+
You must enter an integer less than or equal to{" "}
54+
<strong>{maxValue}</strong>.
55+
</>
56+
);
57+
}
58+
if (+value < minValue) {
59+
return (
60+
<>
61+
You must enter an integer greater than or equal to{" "}
62+
<strong>{minValue}</strong>.
63+
</>
64+
);
65+
}
66+
}, [value, minValue, maxValue]);
67+
68+
return (
69+
<div className={cn(styles.container, className)}>
70+
{isInvalid && (
71+
<Popover
72+
className={styles.popup}
73+
stopClickPropagation={true}
74+
content={errorPopupContent}
75+
strategy="fixed"
76+
>
77+
<IconExclamationMark className={styles.icon} />
78+
</Popover>
79+
)}
80+
<input
81+
type="number"
82+
onChange={(event) => onInputChange && onInputChange(event.target.value)}
83+
disabled={isDisabled}
84+
readOnly={readOnly}
85+
className={cn(styles.input, {
86+
error: isInvalid,
87+
})}
88+
name={name}
89+
value={value}
90+
/>
91+
{displayButtons && (
92+
<>
93+
<button
94+
className={styles.btnMinus}
95+
onClick={(event) => {
96+
event.stopPropagation();
97+
if (!isDisabled) {
98+
onChange(Math.max(value - 1, minValue));
99+
}
100+
}}
101+
/>
102+
<button
103+
className={styles.btnPlus}
104+
onClick={(event) => {
105+
event.stopPropagation();
106+
if (!isDisabled) {
107+
onChange(Math.min(+value + 1, maxValue));
108+
}
109+
}}
110+
/>
111+
</>
112+
)}
113+
</div>
114+
);
115+
};
56116

57117
IntegerField.propTypes = {
58118
className: PT.string,
59119
isDisabled: PT.bool,
120+
readOnly: PT.bool,
121+
displayButtons: PT.bool,
60122
name: PT.string.isRequired,
61123
maxValue: PT.number,
62124
minValue: PT.number,
63-
onChange: PT.func.isRequired,
125+
onChange: PT.func,
126+
onInputChange: PT.func,
64127
value: PT.number.isRequired,
65128
};
66129

src/components/IntegerField/styles.module.scss

+19
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ input.input {
1919
outline: none !important;
2020
box-shadow: none !important;
2121
text-align: center;
22+
appearance: textfield;
23+
&::-webkit-outer-spin-button, &::-webkit-inner-spin-button {
24+
-webkit-appearance: none;
25+
margin: 0;
26+
}
2227

2328
&:disabled {
2429
border-color: $control-disabled-border-color;
@@ -94,3 +99,17 @@ input.input {
9499
height: 9px;
95100
}
96101
}
102+
103+
.popup {
104+
margin-right: 5px;
105+
max-width: 400px;
106+
max-height: 200px;
107+
line-height: $line-height-px;
108+
white-space: normal;
109+
}
110+
111+
.icon {
112+
padding-top: 1px;
113+
width: 15px;
114+
height: 15px;
115+
}

0 commit comments

Comments
 (0)