diff --git a/site/src/@types/emoji-mart.d.ts b/site/src/@types/emoji-mart.d.ts index 18c1d81eabb0e..6d13bf6e2c2b1 100644 --- a/site/src/@types/emoji-mart.d.ts +++ b/site/src/@types/emoji-mart.d.ts @@ -28,7 +28,7 @@ declare module "@emoji-mart/react" { | { unified: undefined; src: string } | { unified: string; src: undefined }; - const EmojiPicker: React.FC<{ + export interface EmojiMartProps { set: "native" | "apple" | "facebook" | "google" | "twitter"; theme: "dark" | "light"; data: unknown; @@ -36,7 +36,9 @@ declare module "@emoji-mart/react" { emojiButtonSize?: number; emojiSize?: number; onEmojiSelect: (emoji: EmojiData) => void; - }>; + } + + const EmojiMart: React.FC; - export default EmojiPicker; + export default EmojiMart; } diff --git a/site/src/components/ErrorBoundary/ErrorBoundary.tsx b/site/src/components/ErrorBoundary/ErrorBoundary.tsx index 9267ae172b9b8..40b3a495c7d9f 100644 --- a/site/src/components/ErrorBoundary/ErrorBoundary.tsx +++ b/site/src/components/ErrorBoundary/ErrorBoundary.tsx @@ -1,7 +1,10 @@ -import { Component, ReactNode, PropsWithChildren } from "react"; +import { Component, type ReactNode } from "react"; import { RuntimeErrorState } from "./RuntimeErrorState"; -type ErrorBoundaryProps = PropsWithChildren; +interface ErrorBoundaryProps { + fallback?: ReactNode; + children: ReactNode; +} interface ErrorBoundaryState { error: Error | null; @@ -9,7 +12,7 @@ interface ErrorBoundaryState { /** * Our app's Error Boundary - * Read more about React Error Boundaries: https://reactjs.org/docs/error-boundaries.html + * Read more about React Error Boundaries: https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary */ export class ErrorBoundary extends Component< ErrorBoundaryProps, @@ -20,13 +23,15 @@ export class ErrorBoundary extends Component< this.state = { error: null }; } - static getDerivedStateFromError(error: Error): { error: Error } { + static getDerivedStateFromError(error: Error): ErrorBoundaryState { return { error }; } render(): ReactNode { if (this.state.error) { - return ; + return ( + this.props.fallback ?? + ); } return this.props.children; diff --git a/site/src/components/IconField/EmojiPicker.tsx b/site/src/components/IconField/EmojiPicker.tsx new file mode 100644 index 0000000000000..3b39f7b3ac519 --- /dev/null +++ b/site/src/components/IconField/EmojiPicker.tsx @@ -0,0 +1,40 @@ +import EmojiMart, { type EmojiMartProps } from "@emoji-mart/react"; +import data from "@emoji-mart/data/sets/14/twitter.json"; +import { type FC } from "react"; +import icons from "theme/icons.json"; + +const custom = [ + { + id: "icons", + name: "Icons", + emojis: icons.map((icon) => { + const id = icon.split(".")[0]; + + return { + id, + name: id, + keywords: id.split("-"), + skins: [{ src: `/icon/${icon}` }], + }; + }), + }, +]; + +type EmojiPickerProps = Omit< + EmojiMartProps, + "custom" | "data" | "set" | "theme" +>; + +const EmojiPicker: FC = (props) => { + return ( + + ); +}; + +export default EmojiPicker; diff --git a/site/src/components/IconField/IconField.stories.tsx b/site/src/components/IconField/IconField.stories.tsx index 35b558d02353c..2d4282a12cbea 100644 --- a/site/src/components/IconField/IconField.stories.tsx +++ b/site/src/components/IconField/IconField.stories.tsx @@ -1,6 +1,6 @@ import { action } from "@storybook/addon-actions"; -import IconField from "./IconField"; import type { Meta, StoryObj } from "@storybook/react"; +import { IconField } from "./IconField"; const meta: Meta = { title: "components/IconField", diff --git a/site/src/components/IconField/IconField.tsx b/site/src/components/IconField/IconField.tsx index ea578d07734b9..aaf9fa096cccb 100644 --- a/site/src/components/IconField/IconField.tsx +++ b/site/src/components/IconField/IconField.tsx @@ -2,12 +2,11 @@ import { css, Global, useTheme } from "@emotion/react"; import Button from "@mui/material/Button"; import InputAdornment from "@mui/material/InputAdornment"; import TextField, { type TextFieldProps } from "@mui/material/TextField"; -import Picker from "@emoji-mart/react"; -import { type FC } from "react"; +import { visuallyHidden } from "@mui/utils"; +import { type FC, lazy, Suspense } from "react"; +import { Loader } from "components/Loader/Loader"; import { DropdownArrow } from "components/DropdownArrow/DropdownArrow"; import { Stack } from "components/Stack/Stack"; -import data from "@emoji-mart/data/sets/14/twitter.json"; -import icons from "theme/icons.json"; import { Popover, PopoverContent, @@ -22,24 +21,12 @@ type IconFieldProps = TextFieldProps & { onPickEmoji: (value: string) => void; }; -const custom = [ - { - id: "icons", - name: "Icons", - emojis: icons.map((icon) => { - const id = icon.split(".")[0]; +const EmojiPicker = lazy(() => import("./EmojiPicker")); - return { - id, - name: id, - keywords: id.split("-"), - skins: [{ src: `/icon/${icon}` }], - }; - }), - }, -]; - -const IconField: FC = ({ onPickEmoji, ...textFieldProps }) => { +export const IconField: FC = ({ + onPickEmoji, + ...textFieldProps +}) => { if ( typeof textFieldProps.value !== "string" && typeof textFieldProps.value !== "undefined" @@ -53,9 +40,9 @@ const IconField: FC = ({ onPickEmoji, ...textFieldProps }) => { return ( = ({ onPickEmoji, ...textFieldProps }) => { }} /> + {(popover) => ( <> @@ -98,35 +97,36 @@ const IconField: FC = ({ onPickEmoji, ...textFieldProps }) => { id="emoji" css={{ marginTop: 0, ".MuiPaper-root": { width: "auto" } }} > - - { - const value = emoji.src ?? urlFromUnifiedCode(emoji.unified); - onPickEmoji(value); - popover.setIsOpen(false); - }} - /> + }> + { + const value = + emoji.src ?? urlFromUnifiedCode(emoji.unified); + onPickEmoji(value); + popover.setIsOpen(false); + }} + /> + )} + + {/* + - This component takes a long time to load (easily several seconds), so we + don't want to wait until the user actually clicks the button to start loading. + Unfortunately, React doesn't provide an API to start warming a lazy component, + so we just have to sneak it into the DOM, which is kind of annoying, but means + that users shouldn't ever spend time waiting for it to load. + - Except we don't do it when running tests, because Jest doesn't define + `IntersectionObserver`, and it would make them slower anyway. */} + {process.env.NODE_ENV !== "test" && ( +
+ + {}} /> + +
+ )}
); }; - -export default IconField; diff --git a/site/src/components/IconField/LazyIconField.tsx b/site/src/components/IconField/LazyIconField.tsx deleted file mode 100644 index be6b444f747be..0000000000000 --- a/site/src/components/IconField/LazyIconField.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { lazy, Suspense, type ComponentProps } from "react"; - -const IconField = lazy(() => import("./IconField")); - -export const LazyIconField = (props: ComponentProps) => { - return ( - }> - - - ); -}; diff --git a/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx b/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx index c87d9cefe3936..6c5bc538310d1 100644 --- a/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx +++ b/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx @@ -27,7 +27,7 @@ import { HelpTooltipText, HelpTooltipTrigger, } from "components/HelpTooltip/HelpTooltip"; -import { LazyIconField } from "components/IconField/LazyIconField"; +import { IconField } from "components/IconField/IconField"; import Link from "@mui/material/Link"; import { HorizontalForm, @@ -345,12 +345,11 @@ export const CreateTemplateForm: FC = (props) => { label="Description" /> - form.setFieldValue("icon", value)} /> diff --git a/site/src/pages/GroupsPage/CreateGroupPageView.tsx b/site/src/pages/GroupsPage/CreateGroupPageView.tsx index 2459e2703547d..798271861a002 100644 --- a/site/src/pages/GroupsPage/CreateGroupPageView.tsx +++ b/site/src/pages/GroupsPage/CreateGroupPageView.tsx @@ -2,6 +2,7 @@ import TextField from "@mui/material/TextField"; import { CreateGroupRequest } from "api/typesGenerated"; import { FormFooter } from "components/FormFooter/FormFooter"; import { FullPageForm } from "components/FullPageForm/FullPageForm"; +import { IconField } from "components/IconField/IconField"; import { Margins } from "components/Margins/Margins"; import { Stack } from "components/Stack/Stack"; import { useFormik } from "formik"; @@ -58,12 +59,12 @@ export const CreateGroupPageView: FC = ({ fullWidth label="Display Name" /> - form.setFieldValue("avatar_url", value)} /> diff --git a/site/src/pages/GroupsPage/SettingsGroupPageView.tsx b/site/src/pages/GroupsPage/SettingsGroupPageView.tsx index b28a5bfba404f..34ddab4841470 100644 --- a/site/src/pages/GroupsPage/SettingsGroupPageView.tsx +++ b/site/src/pages/GroupsPage/SettingsGroupPageView.tsx @@ -3,7 +3,7 @@ import { Group } from "api/typesGenerated"; import { FormFooter } from "components/FormFooter/FormFooter"; import { FullPageForm } from "components/FullPageForm/FullPageForm"; import { Loader } from "components/Loader/Loader"; -import { LazyIconField } from "components/IconField/LazyIconField"; +import { IconField } from "components/IconField/IconField"; import { Margins } from "components/Margins/Margins"; import { useFormik } from "formik"; import { FC } from "react"; @@ -84,7 +84,7 @@ const UpdateGroupForm: FC = ({ label="Display Name" disabled={isEveryoneGroup(group)} /> - = ({ rows={2} /> -