feat: select group avatars with the emoji picker (#11395)

This commit is contained in:
Kayla Washburn
2024-01-03 15:47:24 -07:00
committed by GitHub
parent ee2daedae0
commit ffa7722c31
10 changed files with 112 additions and 76 deletions
+5 -3
View File
@@ -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;
}>;
}
export default EmojiPicker;
const EmojiMart: React.FC<EmojiMartProps>;
export default EmojiMart;
}
@@ -1,7 +1,10 @@
import { Component, ReactNode, PropsWithChildren } from "react";
import { Component, type ReactNode } from "react";
import { RuntimeErrorState } from "./RuntimeErrorState";
type ErrorBoundaryProps = PropsWithChildren<unknown>;
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 <RuntimeErrorState error={this.state.error} />;
return (
this.props.fallback ?? <RuntimeErrorState error={this.state.error} />
);
}
return this.props.children;
@@ -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<EmojiPickerProps> = (props) => {
return (
<EmojiMart
theme="dark"
set="twitter"
data={data}
custom={custom}
{...props}
/>
);
};
export default EmojiPicker;
@@ -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<typeof IconField> = {
title: "components/IconField",
+47 -47
View File
@@ -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<IconFieldProps> = ({ onPickEmoji, ...textFieldProps }) => {
export const IconField: FC<IconFieldProps> = ({
onPickEmoji,
...textFieldProps
}) => {
if (
typeof textFieldProps.value !== "string" &&
typeof textFieldProps.value !== "undefined"
@@ -53,9 +40,9 @@ const IconField: FC<IconFieldProps> = ({ onPickEmoji, ...textFieldProps }) => {
return (
<Stack spacing={1}>
<TextField
{...textFieldProps}
fullWidth
label="Icon"
{...textFieldProps}
InputProps={{
endAdornment: hasIcon ? (
<InputAdornment
@@ -86,6 +73,18 @@ const IconField: FC<IconFieldProps> = ({ onPickEmoji, ...textFieldProps }) => {
}}
/>
<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) => (
<>
@@ -98,35 +97,36 @@ const IconField: FC<IconFieldProps> = ({ onPickEmoji, ...textFieldProps }) => {
id="emoji"
css={{ marginTop: 0, ".MuiPaper-root": { width: "auto" } }}
>
<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;
}
`}
/>
<Picker
set="twitter"
theme="dark"
data={data}
custom={custom}
onEmojiSelect={(emoji) => {
const value = emoji.src ?? urlFromUnifiedCode(emoji.unified);
onPickEmoji(value);
popover.setIsOpen(false);
}}
/>
<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>
);
};
export default IconField;
@@ -1,11 +0,0 @@
import { lazy, Suspense, type ComponentProps } from "react";
const IconField = lazy(() => import("./IconField"));
export const LazyIconField = (props: ComponentProps<typeof IconField>) => {
return (
<Suspense fallback={<div role="progressbar" data-testid="loader" />}>
<IconField {...props} />
</Suspense>
);
};
@@ -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<CreateTemplateFormProps> = (props) => {
label="Description"
/>
<LazyIconField
<IconField
{...getFieldHelpers("icon")}
disabled={isSubmitting}
onChange={onChangeTrimmed(form)}
fullWidth
label="Icon"
onPickEmoji={(value) => form.setFieldValue("icon", value)}
/>
</FormFields>
@@ -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<CreateGroupPageViewProps> = ({
fullWidth
label="Display Name"
/>
<TextField
<IconField
{...getFieldHelpers("avatar_url")}
onChange={onChangeTrimmed(form)}
autoComplete="avatar url"
fullWidth
label="Avatar URL"
onPickEmoji={(value) => form.setFieldValue("avatar_url", value)}
/>
</Stack>
<FormFooter onCancel={onCancel} isLoading={isLoading} />
@@ -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<UpdateGroupFormProps> = ({
label="Display Name"
disabled={isEveryoneGroup(group)}
/>
<LazyIconField
<IconField
{...getFieldHelpers("avatar_url")}
onChange={onChangeTrimmed(form)}
fullWidth
@@ -11,7 +11,7 @@ import {
iconValidator,
} from "utils/formUtils";
import * as Yup from "yup";
import { LazyIconField } from "components/IconField/LazyIconField";
import { IconField } from "components/IconField/IconField";
import {
FormFields,
FormSection,
@@ -126,7 +126,7 @@ export const TemplateSettingsForm: FC<TemplateSettingsForm> = ({
rows={2}
/>
<LazyIconField
<IconField
{...getFieldHelpers("icon")}
disabled={isSubmitting}
onChange={onChangeTrimmed(form)}