{label}
-
+
diff --git a/app/web/components/Bar/ScoreBar.tsx b/app/web/components/Bar/ScoreBar.tsx
index 355fd695dd..c3c95b1363 100644
--- a/app/web/components/Bar/ScoreBar.tsx
+++ b/app/web/components/Bar/ScoreBar.tsx
@@ -3,7 +3,7 @@ import {
ContainerProps,
LinearProgress,
Typography,
-} from "@material-ui/core";
+} from "@mui/material";
import React from "react";
import makeStyles from "utils/makeStyles";
diff --git a/app/web/components/Button/Button.stories.tsx b/app/web/components/Button/Button.stories.tsx
index 65b25ab470..47db25b812 100644
--- a/app/web/components/Button/Button.stories.tsx
+++ b/app/web/components/Button/Button.stories.tsx
@@ -52,4 +52,4 @@ export const Loading = Template.bind({});
Loading.args = { loading: true };
export const AsyncOnClick = Template.bind({});
-AsyncOnClick.args = { onClick: () => wait(1e3) };
+AsyncOnClick.args = { onClick: async () => (await wait(1e3)) as void };
diff --git a/app/web/components/Button/Button.tsx b/app/web/components/Button/Button.tsx
index 57a28efe3c..6d3036114a 100644
--- a/app/web/components/Button/Button.tsx
+++ b/app/web/components/Button/Button.tsx
@@ -1,7 +1,12 @@
-import { Button as MuiButton, ButtonProps, useTheme } from "@material-ui/core";
+import { Button as MuiButton, ButtonProps, useTheme } from "@mui/material";
import classNames from "classnames";
import Sentry from "platform/sentry";
-import React, { ElementType, ForwardedRef, forwardRef } from "react";
+import React, {
+ ElementType,
+ ForwardedRef,
+ forwardRef,
+ MouseEventHandler,
+} from "react";
import { useIsMounted, useSafeState } from "utils/hooks";
import makeStyles from "utils/makeStyles";
@@ -30,16 +35,20 @@ const useStyles = makeStyles((theme) => ({
},
}));
+type ButtonClasses = {
+ [key: string]: string;
+};
+
//type generics required to allow component prop
//see https://github.com/mui-org/material-ui/issues/15827
-export type AppButtonProps<
- D extends ElementType = "button",
- P = Record
-> = ButtonProps & {
- loading?: boolean;
-};
+export type AppButtonProps =
+ ButtonProps & {
+ loading?: boolean;
+ onClick?: MouseEventHandler; // Dynamic type for different component types
+ classes?: Partial; // Use the flexible ButtonClasses type here
+ };
-function _Button>(
+function _Button(
{
children,
disabled,
@@ -49,17 +58,20 @@ function _Button>(
variant = "contained",
color = "primary",
...otherProps
- }: AppButtonProps,
+ }: AppButtonProps,
ref: ForwardedRef // eslint-disable-line
) {
const isMounted = useIsMounted();
const [waiting, setWaiting] = useSafeState(isMounted, false);
const classes = useStyles();
const theme = useTheme();
- async function asyncOnClick(event: unknown) {
+ async function asyncOnClick(event: React.MouseEvent) {
try {
setWaiting(true);
- await onClick(event);
+
+ if (onClick) {
+ await onClick(event);
+ }
} catch (e) {
Sentry.captureException(e);
} finally {
diff --git a/app/web/components/CircularProgress.tsx b/app/web/components/CircularProgress.tsx
index 6856c3a187..0f359c536f 100644
--- a/app/web/components/CircularProgress.tsx
+++ b/app/web/components/CircularProgress.tsx
@@ -1,7 +1,7 @@
import {
CircularProgress as MuiCircularProgress,
CircularProgressProps,
-} from "@material-ui/core";
+} from "@mui/material";
import classNames from "classnames";
import React, { ForwardedRef } from "react";
import makeStyles from "utils/makeStyles";
diff --git a/app/web/components/Comments/CommentBox.tsx b/app/web/components/Comments/CommentBox.tsx
index 111090c861..7231ba372c 100644
--- a/app/web/components/Comments/CommentBox.tsx
+++ b/app/web/components/Comments/CommentBox.tsx
@@ -1,4 +1,4 @@
-import { Card } from "@material-ui/core";
+import { Card } from "@mui/material";
import Alert from "components/Alert";
import CircularProgress from "components/CircularProgress";
import NewComment from "components/Comments/NewComment";
diff --git a/app/web/components/Comments/NewComment.tsx b/app/web/components/Comments/NewComment.tsx
index 5dd3d1e783..6daca6bdd9 100644
--- a/app/web/components/Comments/NewComment.tsx
+++ b/app/web/components/Comments/NewComment.tsx
@@ -1,4 +1,4 @@
-import { Box, Button, Grid, Link } from "@material-ui/core";
+import { Box, Button, Grid, Link } from "@mui/material";
import Markdown from "components/Markdown";
import TextField from "components/TextField";
import React, { useState } from "react";
diff --git a/app/web/components/ContributorForm/ContributorForm.tsx b/app/web/components/ContributorForm/ContributorForm.tsx
index ad7d743502..cfef9eaaf2 100644
--- a/app/web/components/ContributorForm/ContributorForm.tsx
+++ b/app/web/components/ContributorForm/ContributorForm.tsx
@@ -6,11 +6,11 @@ import {
FormGroup,
FormHelperText,
FormLabel,
- makeStyles,
Radio,
RadioGroup,
Typography,
-} from "@material-ui/core";
+} from "@mui/material";
+import makeStyles from "@mui/styles/makeStyles";
import Alert from "components/Alert";
import Button from "components/Button";
import TextField from "components/TextField";
@@ -185,7 +185,7 @@ export default function ContributorForm({
name="contribute"
defaultValue=""
render={({ onChange, value }) => (
-
+
{CONTRIBUTE_LABEL}
@@ -209,7 +209,7 @@ export default function ContributorForm({
)}
/>
-
+
{CONTRIBUTE_WAYS_LABEL}
diff --git a/app/web/components/ContributorForm/StandaloneContributorForm.tsx b/app/web/components/ContributorForm/StandaloneContributorForm.tsx
index 2a0b0ac97d..85d25ea5d5 100644
--- a/app/web/components/ContributorForm/StandaloneContributorForm.tsx
+++ b/app/web/components/ContributorForm/StandaloneContributorForm.tsx
@@ -1,4 +1,4 @@
-import { Typography } from "@material-ui/core";
+import { Typography } from "@mui/material";
import Alert from "components/Alert";
import Button from "components/Button";
import CircularProgress from "components/CircularProgress";
diff --git a/app/web/components/CookieBanner.tsx b/app/web/components/CookieBanner.tsx
index 265a1155e3..b32c0a472f 100644
--- a/app/web/components/CookieBanner.tsx
+++ b/app/web/components/CookieBanner.tsx
@@ -1,4 +1,4 @@
-import { Typography } from "@material-ui/core";
+import { Typography } from "@mui/material";
import IconButton from "components/IconButton";
import { CloseIcon } from "components/Icons";
import StyledLink from "components/StyledLink";
diff --git a/app/web/components/CustomColorSwitch.tsx b/app/web/components/CustomColorSwitch.tsx
index 083f05b99f..8c6d7b16cc 100644
--- a/app/web/components/CustomColorSwitch.tsx
+++ b/app/web/components/CustomColorSwitch.tsx
@@ -1,52 +1,63 @@
-import { Switch, SwitchProps } from "@material-ui/core";
-import classNames from "classnames";
+import { styled, Switch, SwitchProps } from "@mui/material";
import { useEffect, useState } from "react";
import { theme } from "theme";
-import makeStyles from "utils/makeStyles";
import CircularProgress from "./CircularProgress";
-interface SwitchStyleProps {
+interface CustomSwitchProps extends Omit {
checked: boolean;
- color: string;
- size: SwitchProps["size"];
+ customColor?: string; // renamed to avoid conflict with existing color prop
+ size?: SwitchProps["size"];
+ status?: string;
+ isLoading?: boolean;
}
-const useStyles = makeStyles(({ palette, shadows }) => ({
- circle: {
- display: "flex",
- alignItems: "center",
- justifyContent: "center",
- width: (props: SwitchStyleProps) => (props.size === "medium" ? 20 : 16),
- height: (props: SwitchStyleProps) => (props.size === "medium" ? 20 : 16),
- borderRadius: "50%",
- backgroundColor: palette.grey[600],
- boxShadow: shadows[1],
- },
- active: {
- backgroundColor: palette.grey[600],
- },
- switchBase: {
- color: palette.grey[600],
+const StyledCircle = styled("div", {
+ shouldForwardProp: (prop) => prop !== "customColor" && prop !== "isLoading",
+})(({ theme, size, checked, customColor, isLoading }) => ({
+ display: "flex",
+ alignItems: "center",
+ justifyContent: "center",
+ width: size === "medium" ? 20 : 16,
+ height: size === "medium" ? 20 : 16,
+ borderRadius: "50%",
+ backgroundColor: checked ? customColor : theme.palette.grey[600],
+ boxShadow: theme.shadows[1],
+ ...(isLoading && {
+ backgroundColor: theme.palette.grey[400], // Change color when loading
+ }),
+}));
+
+const dontPassProps = ["customColor", "isLoading", "status"];
+
+const StyledSwitch = styled(Switch, {
+ shouldForwardProp: (prop) => !dontPassProps.includes(prop as string), // Filter out props that shouldn't be forwarded
+})(({ theme, customColor, checked, isLoading, status }) => ({
+ "& .MuiSwitch-switchBase": {
+ color: theme.palette.grey[600],
"& + .MuiSwitch-track": {
- backgroundColor: palette.grey[200],
+ backgroundColor: theme.palette.grey[200],
},
"&.Mui-checked": {
- color: (props: { color: string }) => props.color,
+ color: customColor,
"& + .MuiSwitch-track": {
- backgroundColor: (props: { color: string }) => props.color,
+ backgroundColor: customColor,
},
},
"&.Mui-disabled": {
- color: (props: { checked: boolean; color: string }) =>
- props.checked ? props.color : palette.grey[600],
+ color: checked ? customColor : theme.palette.grey[600],
"& + .MuiSwitch-track": {
- backgroundColor: (props: { checked: boolean; color: string }) =>
- props.checked ? props.color : palette.grey[200],
+ backgroundColor: checked ? customColor : theme.palette.grey[200],
opacity: 0.4,
},
},
},
+ ...(isLoading || status === "loading"
+ ? {
+ opacity: 0.5, // Reduce opacity when loading or status is "loading"
+ pointerEvents: "none", // Disable interaction when loading
+ }
+ : {}),
}));
export default function CustomColorSwitch({
@@ -55,17 +66,8 @@ export default function CustomColorSwitch({
size = "medium",
status,
isLoading = false,
- color = theme.palette.secondary.main,
-}: {
- checked: boolean;
- onClick: SwitchProps["onClick"];
- size?: SwitchProps["size"];
- status?: string;
- isLoading?: boolean;
- color?: string;
-}) {
- const classes = useStyles({ checked, color, size });
-
+ customColor = theme.palette.secondary.main, // renamed to customColor to avoid conflict with MUI Switch color
+}: CustomSwitchProps) {
const [isMounted, setIsMounted] = useState(false);
useEffect(() => {
@@ -73,11 +75,11 @@ export default function CustomColorSwitch({
}, []);
const Icon = () => (
-
{isLoading && (
)}
-
+
);
if (!isMounted) {
@@ -94,16 +96,16 @@ export default function CustomColorSwitch({
}
return (
- }
disabled={isLoading || status === "loading"}
icon={}
onClick={onClick}
size={size}
+ customColor={customColor} // Pass customColor prop to StyledSwitch
+ isLoading={isLoading}
+ status={status}
/>
);
}
diff --git a/app/web/components/Datepicker.test.tsx b/app/web/components/Datepicker.test.tsx
index 03e3e8a6ed..dbd06f0f80 100644
--- a/app/web/components/Datepicker.test.tsx
+++ b/app/web/components/Datepicker.test.tsx
@@ -13,6 +13,7 @@ jest.mock("@mui/x-date-pickers", () => {
return {
...jest.requireActual("@mui/x-date-pickers"),
DatePicker: jest.requireActual("@mui/x-date-pickers").DesktopDatePicker,
+ PickersDay: jest.requireActual("@mui/x-date-pickers").DesktopPickersDay,
};
});
@@ -38,15 +39,23 @@ const Form = ({ setDate }: { setDate: (date: Dayjs) => void }) => {
};
describe("DatePicker", () => {
- beforeEach(() => {
+ beforeAll(() => {
jest.useFakeTimers();
+ });
+
+ beforeEach(() => {
jest.setSystemTime(new Date("2021-03-20"));
+ timezoneMock.register("UTC");
});
- afterEach(() => {
+ afterAll(() => {
jest.useRealTimers();
+ });
+
+ afterEach(() => {
timezoneMock.unregister();
- jest.restoreAllMocks();
+ jest.resetAllMocks();
+ jest.clearAllTimers();
});
it("should submit with proper date for clicking", async () => {
@@ -63,56 +72,168 @@ describe("DatePicker", () => {
});
});
- it.each`
- timezone
- ${"US/Eastern"}
- ${"UTC"}
- ${"Europe/London"}
- ${"Brazil/East"}
- ${"Australia/Adelaide"}
- `("selecting today works with timezone $timezone", async ({ timezone }) => {
- timezoneMock.register(timezone);
+ it("selecting today works with timezone US/Eastern", async () => {
+ timezoneMock.register("US/Eastern");
const mockDate = new Date("2021-03-20 00:00");
//@ts-ignore - ts thinks we mock Date() but actually we want to mock new Date()
- const spy = jest.spyOn(global, "Date").mockImplementation(() => mockDate);
+ jest.spyOn(global, "Date").mockImplementation(() => mockDate);
+
let date: Dayjs | undefined;
render(
);
};
- render();
+ render(, { wrapper });
};
describe("LocationAutocomplete component", () => {
diff --git a/app/web/components/LocationAutocomplete/LocationAutocomplete.tsx b/app/web/components/LocationAutocomplete/LocationAutocomplete.tsx
index 3b26f47acf..f9e2d9b773 100644
--- a/app/web/components/LocationAutocomplete/LocationAutocomplete.tsx
+++ b/app/web/components/LocationAutocomplete/LocationAutocomplete.tsx
@@ -1,4 +1,4 @@
-import { AutocompleteChangeReason } from "@material-ui/lab";
+import { AutocompleteChangeReason } from "@mui/material";
import Autocomplete from "components/Autocomplete";
import IconButton from "components/IconButton";
import { SearchIcon } from "components/Icons";
@@ -25,22 +25,27 @@ interface LocationAutocompleteProps {
disableRegions?: boolean;
}
-export default function LocationAutocomplete({
- className,
- control,
- defaultValue,
- fieldError,
- fullWidth,
- label,
- placeholder,
- id = "location-autocomplete",
- name,
- variant = "standard",
- onChange,
- required,
- showFullDisplayName = false,
- disableRegions = false,
-}: LocationAutocompleteProps) {
+const LocationAutocomplete = React.forwardRef(function LocationAutocomplete(
+ props: LocationAutocompleteProps,
+ ref
+) {
+ const {
+ className,
+ control,
+ defaultValue,
+ fieldError,
+ fullWidth,
+ label,
+ placeholder,
+ id = "location-autocomplete",
+ name,
+ variant = "standard",
+ onChange,
+ required,
+ showFullDisplayName = false,
+ disableRegions = false,
+ } = props;
+
const { t } = useTranslation(GLOBAL);
const controller = useController({
@@ -89,8 +94,8 @@ export default function LocationAutocomplete({
}
if (typeof value === "string") {
- //create-option is when enter is pressed on user-entered string
- if (reason === "create-option") {
+ //createOption is when enter is pressed on user-entered string
+ if (reason === "createOption") {
query(value);
setIsOpen(true);
}
@@ -104,7 +109,7 @@ export default function LocationAutocomplete({
{
if (e.key === "Enter") {
e.preventDefault();
- searchSubmit(controller.field.value, "create-option");
+ searchSubmit(controller.field.value, "createOption");
}
}}
endAdornment={
searchSubmit(controller.field.value, "create-option")}
+ onClick={() => searchSubmit(controller.field.value, "createOption")}
size="small"
>
@@ -148,7 +153,7 @@ export default function LocationAutocomplete({
multiple={false}
/>
);
-}
+});
function geocodeResult2String(option: GeocodeResult | string, full: boolean) {
if (typeof option === "string") {
@@ -159,3 +164,5 @@ function geocodeResult2String(option: GeocodeResult | string, full: boolean) {
}
return option.simplifiedName;
}
+
+export default LocationAutocomplete;
diff --git a/app/web/components/Map.tsx b/app/web/components/Map.tsx
index 1421f08259..50bf41e55b 100644
--- a/app/web/components/Map.tsx
+++ b/app/web/components/Map.tsx
@@ -1,6 +1,6 @@
import "maplibre-gl/dist/maplibre-gl.css";
-import { Typography } from "@material-ui/core";
+import { Typography } from "@mui/material";
import classNames from "classnames";
import { NO_MAP_SUPPORT } from "components/constants";
import {
diff --git a/app/web/components/MapSearch.tsx b/app/web/components/MapSearch.tsx
index d911d47b00..30b83b2fa2 100644
--- a/app/web/components/MapSearch.tsx
+++ b/app/web/components/MapSearch.tsx
@@ -1,5 +1,5 @@
-import { Box, IconButton } from "@material-ui/core";
-import { AutocompleteChangeReason } from "@material-ui/lab/Autocomplete";
+import { Box, IconButton } from "@mui/material";
+import { AutocompleteChangeReason } from "@mui/material/Autocomplete";
import { LngLat } from "maplibre-gl";
import React, { useEffect, useState } from "react";
import { useGeocodeQuery } from "utils/hooks";
@@ -89,8 +89,8 @@ export default function MapSearch({ setError, setResult }: MapSearchProps) {
const searchOption = results?.find((o) => value === o.name);
if (!searchOption) {
- //create-option is when enter is pressed on user-entered string
- if (reason === "create-option") {
+ //createOption is when enter is pressed on user-entered string
+ if (reason === "createOption") {
query(value);
setOpen(true);
}
@@ -109,7 +109,7 @@ export default function MapSearch({ setError, setResult }: MapSearchProps) {
+ ,
+ { wrapper }
);
expect(screen.queryByTestId("bad")).not.toBeInTheDocument();
expect(screen.getByTestId("root")).toContainHTML("