Rewrite Field component as TextInput, extract styles to theme

This commit is contained in:
Alex Holliday
2024-11-21 15:54:02 +08:00
parent 1b51e580e5
commit e7fd4399ba
6 changed files with 425 additions and 6 deletions

View File

@@ -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);

View File

@@ -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;

View 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,
};

View 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
View 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;

View File

@@ -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,