coder/site/src/components/IconField/IconField.tsx

134 lines
4.3 KiB
TypeScript

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 { visuallyHidden } from "@mui/utils";
import { type FC, lazy, Suspense } from "react";
import { DropdownArrow } from "components/DropdownArrow/DropdownArrow";
import { ExternalImage } from "components/ExternalImage/ExternalImage";
import { Loader } from "components/Loader/Loader";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "components/Popover/Popover";
import { Stack } from "components/Stack/Stack";
// See: https://github.com/missive/emoji-mart/issues/51#issuecomment-287353222
const urlFromUnifiedCode = (unified: string) =>
`/emojis/${unified.replace(/-fe0f$/, "")}.png`;
type IconFieldProps = TextFieldProps & {
onPickEmoji: (value: string) => void;
};
const EmojiPicker = lazy(() => import("./EmojiPicker"));
export const IconField: FC<IconFieldProps> = ({
onPickEmoji,
...textFieldProps
}) => {
if (
typeof textFieldProps.value !== "string" &&
typeof textFieldProps.value !== "undefined"
) {
throw new Error(`Invalid icon value "${typeof textFieldProps.value}"`);
}
const theme = useTheme();
const hasIcon = textFieldProps.value && textFieldProps.value !== "";
return (
<Stack spacing={1}>
<TextField
fullWidth
label="Icon"
{...textFieldProps}
InputProps={{
endAdornment: hasIcon ? (
<InputAdornment
position="end"
css={{
width: 24,
height: 24,
display: "flex",
alignItems: "center",
justifyContent: "center",
"& img": {
maxWidth: "100%",
objectFit: "contain",
},
}}
>
<ExternalImage
alt=""
src={textFieldProps.value}
// This prevent browser to display the ugly error icon if the
// image path is wrong or user didn't finish typing the url
onError={(e) => (e.currentTarget.style.display = "none")}
onLoad={(e) => (e.currentTarget.style.display = "inline")}
/>
</InputAdornment>
) : undefined,
}}
/>
<Global
styles={css`
em-emoji-picker {
--rgb-background: ${theme.palette.background.paper};
--rgb-input: ${theme.palette.primary.main};
--rgb-color: ${theme.palette.text.primary};
// Hack to prevent the right side from being cut off
width: 350px;
}
`}
/>
<Popover>
{(popover) => (
<>
<PopoverTrigger>
<Button fullWidth endIcon={<DropdownArrow />}>
Select emoji
</Button>
</PopoverTrigger>
<PopoverContent
id="emoji"
css={{ marginTop: 0, ".MuiPaper-root": { width: "auto" } }}
>
<Suspense fallback={<Loader />}>
<EmojiPicker
onEmojiSelect={(emoji) => {
const value =
emoji.src ?? urlFromUnifiedCode(emoji.unified);
onPickEmoji(value);
popover.setIsOpen(false);
}}
/>
</Suspense>
</PopoverContent>
</>
)}
</Popover>
{/*
- 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" && (
<div css={{ ...visuallyHidden }}>
<Suspense>
<EmojiPicker onEmojiSelect={() => {}} />
</Suspense>
</div>
)}
</Stack>
);
};