mirror of
https://github.com/bluewave-labs/Checkmate.git
synced 2026-01-24 10:39:40 -06:00
Rewrite Field component as TextInput, extract styles to theme
This commit is contained in:
@@ -7,7 +7,7 @@
|
||||
}
|
||||
|
||||
.field-infrastructure-alert .MuiInputBase-root:has(input) {
|
||||
height: var(--env-var-height-2);
|
||||
/* height: var(--env-var-height-2); */
|
||||
}
|
||||
|
||||
.field h3.MuiTypography-root,
|
||||
@@ -23,7 +23,7 @@
|
||||
padding-right: var(--env-var-spacing-1-minus);
|
||||
}
|
||||
.field .MuiInputBase-root:has(input) {
|
||||
height: var(--env-var-height-2);
|
||||
/* height: var(--env-var-height-2); */
|
||||
}
|
||||
.field .MuiInputBase-root:has(.MuiInputAdornment-root) {
|
||||
padding-right: var(--env-var-spacing-1-minus);
|
||||
|
||||
@@ -46,7 +46,7 @@ const Field = forwardRef(
|
||||
error,
|
||||
disabled,
|
||||
hidden,
|
||||
className
|
||||
className,
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
@@ -186,11 +186,11 @@ const Field = forwardRef(
|
||||
),
|
||||
}}
|
||||
/>
|
||||
{ error && (
|
||||
{error && (
|
||||
<Typography
|
||||
component="span"
|
||||
className="input-error"
|
||||
hidden={className? true: false}
|
||||
hidden={className ? true : false}
|
||||
color={theme.palette.error.main}
|
||||
mt={theme.spacing(2)}
|
||||
sx={{
|
||||
@@ -225,7 +225,7 @@ Field.propTypes = {
|
||||
error: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
hidden: PropTypes.bool,
|
||||
className: PropTypes.string
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
export default Field;
|
||||
|
||||
60
Client/src/Components/Inputs/TextInput/Adornments/index.jsx
Normal file
60
Client/src/Components/Inputs/TextInput/Adornments/index.jsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import { Stack, Typography, InputAdornment, IconButton } from "@mui/material";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useState } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import VisibilityOff from "@mui/icons-material/VisibilityOff";
|
||||
import Visibility from "@mui/icons-material/Visibility";
|
||||
export const HttpAdornment = ({ https }) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
height="100%"
|
||||
sx={{
|
||||
borderRight: `solid 1px ${theme.palette.border.dark}`,
|
||||
backgroundColor: theme.palette.background.accent,
|
||||
pl: theme.spacing(6),
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
component="h5"
|
||||
paddingRight={"var(--env-var-spacing-1-minus)"}
|
||||
color={theme.palette.text.secondary}
|
||||
sx={{ lineHeight: 1, opacity: 0.8 }}
|
||||
>
|
||||
{https ? "https" : "http"}://
|
||||
</Typography>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export const PasswordEndAdornment = ({ fieldType, setFieldType }) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<InputAdornment position="end">
|
||||
<IconButton
|
||||
aria-label="toggle password visibility"
|
||||
onClick={() => setFieldType(fieldType === "password" ? "text" : "password")}
|
||||
sx={{
|
||||
color: theme.palette.border.dark,
|
||||
padding: theme.spacing(1),
|
||||
"&:focus-visible": {
|
||||
outline: `2px solid ${theme.palette.primary.main}`,
|
||||
outlineOffset: `2px`,
|
||||
},
|
||||
"& .MuiTouchRipple-root": {
|
||||
pointerEvents: "none",
|
||||
display: "none",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{fieldType === "password" ? <VisibilityOff /> : <Visibility />}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
);
|
||||
};
|
||||
|
||||
HttpAdornment.propTypes = {
|
||||
https: PropTypes.bool.isRequired,
|
||||
};
|
||||
130
Client/src/Components/Inputs/TextInput/index.jsx
Normal file
130
Client/src/Components/Inputs/TextInput/index.jsx
Normal file
@@ -0,0 +1,130 @@
|
||||
import { Stack, TextField, Typography } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { forwardRef, useState, cloneElement } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const getSx = (theme, type, maxWidth) => {
|
||||
const sx = {
|
||||
maxWidth: maxWidth,
|
||||
};
|
||||
|
||||
if (type === "url") {
|
||||
return {
|
||||
...sx,
|
||||
"& .MuiInputBase-root": { padding: 0 },
|
||||
"& .MuiStack-root": {
|
||||
borderTopLeftRadius: theme.shape.borderRadius,
|
||||
borderBottomLeftRadius: theme.shape.borderRadius,
|
||||
},
|
||||
};
|
||||
}
|
||||
return sx;
|
||||
};
|
||||
|
||||
const Required = () => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Typography
|
||||
component="span"
|
||||
ml={theme.spacing(1)}
|
||||
color={theme.palette.error.main}
|
||||
>
|
||||
*
|
||||
</Typography>
|
||||
);
|
||||
};
|
||||
|
||||
const Optional = ({ optionalLabel }) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Typography
|
||||
component="span"
|
||||
fontSize="inherit"
|
||||
fontWeight={400}
|
||||
ml={theme.spacing(2)}
|
||||
sx={{ opacity: 0.6 }}
|
||||
>
|
||||
{optionalLabel || "(optional)"}
|
||||
</Typography>
|
||||
);
|
||||
};
|
||||
|
||||
Optional.propTypes = {
|
||||
optionalLabel: PropTypes.string,
|
||||
};
|
||||
|
||||
const TextInput = forwardRef(
|
||||
(
|
||||
{
|
||||
type,
|
||||
value,
|
||||
placeholder,
|
||||
isRequired,
|
||||
isOptional,
|
||||
optionalLabel,
|
||||
onChange,
|
||||
error = false,
|
||||
helperText = null,
|
||||
startAdornment = null,
|
||||
endAdornment = null,
|
||||
label = null,
|
||||
maxWidth = "100%",
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const [fieldType, setFieldType] = useState(type);
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Stack>
|
||||
<Typography
|
||||
component="h3"
|
||||
fontSize={"var(--env-var-font-size-medium)"}
|
||||
color={theme.palette.text.secondary}
|
||||
fontWeight={500}
|
||||
>
|
||||
{label}
|
||||
{isRequired && <Required />}
|
||||
{isOptional && <Optional optionalLabel={optionalLabel} />}
|
||||
</Typography>
|
||||
<TextField
|
||||
type={fieldType}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
onChange={onChange}
|
||||
error={error}
|
||||
helperText={helperText}
|
||||
inputRef={ref}
|
||||
sx={getSx(theme, type, maxWidth)}
|
||||
slotProps={{
|
||||
input: {
|
||||
startAdornment: startAdornment,
|
||||
endAdornment: endAdornment
|
||||
? cloneElement(endAdornment, { fieldType, setFieldType })
|
||||
: null,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
TextInput.displayName = "TextInput";
|
||||
|
||||
TextInput.propTypes = {
|
||||
type: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
placeholder: PropTypes.string,
|
||||
isRequired: PropTypes.bool,
|
||||
isOptional: PropTypes.bool,
|
||||
optionalLabel: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
error: PropTypes.bool,
|
||||
helperText: PropTypes.string,
|
||||
startAdornment: PropTypes.node,
|
||||
endAdornment: PropTypes.node,
|
||||
label: PropTypes.string,
|
||||
maxWidth: PropTypes.string,
|
||||
};
|
||||
|
||||
export default TextInput;
|
||||
192
Client/src/Pages/test.jsx
Normal file
192
Client/src/Pages/test.jsx
Normal file
@@ -0,0 +1,192 @@
|
||||
import { Stack, Typography } from "@mui/material";
|
||||
import Field from "../Components/Inputs/Field";
|
||||
import TextInput from "../Components/Inputs/TextInput";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { HttpAdornment } from "../Components/Inputs/TextInput/Adornments";
|
||||
import { PasswordEndAdornment } from "../Components/Inputs/TextInput/Adornments";
|
||||
const Test = () => {
|
||||
const [originalValue, setOriginalValue] = useState("");
|
||||
const [originalError, setOriginalError] = useState("");
|
||||
|
||||
const [newValue, setNewValue] = useState("");
|
||||
const [newError, setNewError] = useState("");
|
||||
|
||||
const [thresholdValue, setThresholdValue] = useState(20);
|
||||
const [thresholdError, setThresholdError] = useState("");
|
||||
|
||||
const [thresholdValue2, setThresholdValue2] = useState(20);
|
||||
const [thresholdError2, setThresholdError2] = useState("");
|
||||
|
||||
const inputRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const checkError = (value) => {
|
||||
if (value !== "clear") {
|
||||
return "This is an error";
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
const checkThresholdError = (value) => {
|
||||
if (value !== 99) {
|
||||
return "This is a threshold error";
|
||||
}
|
||||
return "";
|
||||
};
|
||||
const checkThresholdError2 = (value) => {
|
||||
if (value !== 99) {
|
||||
return "This is a threshold error 2";
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
const handleOriginalValue = (e) => {
|
||||
setOriginalError(checkError(e.target.value));
|
||||
setOriginalValue(e.target.value);
|
||||
};
|
||||
|
||||
const handleNewValue = (e) => {
|
||||
setNewError(checkError(e.target.value));
|
||||
setNewValue(e.target.value);
|
||||
};
|
||||
|
||||
const handleThresholdValue = (e) => {
|
||||
const parsedVal = parseInt(e.target.value);
|
||||
setThresholdError(checkThresholdError(parsedVal));
|
||||
setThresholdValue(parsedVal);
|
||||
};
|
||||
|
||||
const handleThresholdValue2 = (e) => {
|
||||
const parsedVal = parseInt(e.target.value);
|
||||
setThresholdError2(checkThresholdError2(parsedVal));
|
||||
setThresholdValue2(parsedVal);
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack
|
||||
gap={8}
|
||||
direction="column"
|
||||
border="1px dashed blue"
|
||||
padding="1rem"
|
||||
>
|
||||
<Typography>
|
||||
This is a test page for the TextInput component. It is a rationalized Input
|
||||
component.
|
||||
</Typography>
|
||||
<Typography>Type anything for an error.</Typography>
|
||||
<Typography>Typing "clear" will clear the error for text based input</Typography>
|
||||
<Typography>Typing "99" will clear the error for threshold based input</Typography>
|
||||
<Field
|
||||
id="original-field"
|
||||
onChange={handleOriginalValue}
|
||||
type="text"
|
||||
value={originalValue}
|
||||
error={originalError}
|
||||
/>
|
||||
<TextInput
|
||||
value={newValue}
|
||||
onChange={handleNewValue}
|
||||
error={newError !== ""}
|
||||
helperText={newError}
|
||||
/>
|
||||
|
||||
<Field
|
||||
type={"url"}
|
||||
id="monitor-url"
|
||||
label={"URL to monitor"}
|
||||
https={true}
|
||||
placeholder={""}
|
||||
value={originalValue}
|
||||
onChange={handleOriginalValue}
|
||||
error={originalError}
|
||||
/>
|
||||
<TextInput
|
||||
type={"url"}
|
||||
id="monitor-url"
|
||||
label={"URL to monitor"}
|
||||
placeholder={""}
|
||||
value={newValue}
|
||||
startAdornment={<HttpAdornment https={true} />}
|
||||
onChange={handleNewValue}
|
||||
error={newError !== ""}
|
||||
helperText={newError}
|
||||
/>
|
||||
|
||||
<Field
|
||||
type="password"
|
||||
id="login-password-input"
|
||||
label="Password"
|
||||
isRequired={true}
|
||||
placeholder="••••••••••"
|
||||
autoComplete="current-password"
|
||||
value={originalValue}
|
||||
onChange={handleOriginalValue}
|
||||
error={originalError}
|
||||
ref={inputRef}
|
||||
/>
|
||||
<TextInput
|
||||
type="password"
|
||||
id="login-password-input"
|
||||
label="Password"
|
||||
isRequired={true}
|
||||
placeholder="••••••••••"
|
||||
autoComplete="current-password"
|
||||
value={newValue}
|
||||
endAdornment={<PasswordEndAdornment />}
|
||||
onChange={handleNewValue}
|
||||
error={newError !== ""}
|
||||
helperText={newError}
|
||||
ref={inputRef}
|
||||
/>
|
||||
|
||||
<Field
|
||||
id="ttl"
|
||||
label="The days you want to keep monitoring history."
|
||||
isOptional={true}
|
||||
optionalLabel="0 for infinite"
|
||||
value={originalValue}
|
||||
onChange={handleOriginalValue}
|
||||
error={originalError}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
id="ttl"
|
||||
label="The days you want to keep monitoring history."
|
||||
isOptional={true}
|
||||
optionalLabel="0 for infinite"
|
||||
value={newValue}
|
||||
onChange={handleNewValue}
|
||||
error={newError !== ""}
|
||||
helperText={newError}
|
||||
/>
|
||||
|
||||
<Typography>Short field for threshold. Easily show/hide error text</Typography>
|
||||
<TextInput
|
||||
maxWidth="var(--env-var-width-4)"
|
||||
id="threshold"
|
||||
type="number"
|
||||
value={thresholdValue.toString()}
|
||||
onChange={handleThresholdValue}
|
||||
error={thresholdError !== ""}
|
||||
/>
|
||||
<TextInput
|
||||
maxWidth="var(--env-var-width-4)"
|
||||
id="threshold"
|
||||
type="number"
|
||||
value={thresholdValue2.toString()}
|
||||
onChange={handleThresholdValue2}
|
||||
error={thresholdError2 !== ""}
|
||||
/>
|
||||
<Typography sx={{ color: "red" }}>
|
||||
{thresholdError} {thresholdError2}
|
||||
</Typography>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default Test;
|
||||
@@ -209,6 +209,43 @@ const baseTheme = (palette) => ({
|
||||
}),
|
||||
},
|
||||
},
|
||||
MuiTextField: {
|
||||
styleOverrides: {
|
||||
root: ({ theme }) => ({
|
||||
"& fieldset": {
|
||||
borderColor: theme.palette.border.dark,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
},
|
||||
"& .MuiInputBase-input": {
|
||||
height: "var(--env-var-height-2)",
|
||||
fontSize: "var(--env-var-font-size-medium)",
|
||||
fontWeight: 400,
|
||||
color: palette.text.secondary, // or any color from your palette
|
||||
},
|
||||
"& .MuiInputBase-input.MuiOutlinedInput-input": {
|
||||
padding: "0 var(--env-var-spacing-1-minus) !important",
|
||||
},
|
||||
"& .MuiOutlinedInput-root": {
|
||||
borderRadius: 4,
|
||||
},
|
||||
"& .MuiOutlinedInput-notchedOutline": {
|
||||
borderRadius: 4,
|
||||
},
|
||||
"& .MuiFormHelperText-root": {
|
||||
color: palette.error.main,
|
||||
opacity: 0.8,
|
||||
fontSize: "var(--env-var-font-size-medium)",
|
||||
|
||||
marginLeft: 0,
|
||||
},
|
||||
"& .MuiFormHelperText-root.Mui-error": {
|
||||
opacity: 0.8,
|
||||
fontSize: "var(--env-var-font-size-medium)",
|
||||
color: palette.error.main,
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
shape: {
|
||||
borderRadius: 2,
|
||||
|
||||
Reference in New Issue
Block a user