Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix RichTextInput toolbar appearance #9018

Merged
merged 10 commits into from
Jun 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions docs/RichTextInput.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,17 @@ title: "The RichTextInput Component"

# `<RichTextInput>`

`<RichTextInput>` is the ideal component to let users edit HTML content. It is powered by [TipTap](https://www.tiptap.dev/).
`<RichTextInput>` lets users edit rich text in a WYSIWYG editor, and store the result as HTML. It is powered by [TipTap](https://www.tiptap.dev/).

<video controls autoplay playsinline muted loop>
<source src="./img/rich-text-input.webm" type="video/webm"/>
<source src="./img/rich-text-input.mp4" type="video/mp4"/>
Your browser does not support the video tag.
</video>


## Usage

**Note**: Due to its size, `<RichTextInput>` is not bundled by default with react-admin. You must install it first, using npm:
Due to its size, `<RichTextInput>` is not bundled by default with react-admin. You must install it first, using npm:

```sh
npm install ra-input-rich-text
Expand Down
Binary file modified docs/img/rich-text-input.mp4
Binary file not shown.
Binary file removed docs/img/rich-text-input.webm
Binary file not shown.
2 changes: 1 addition & 1 deletion examples/simple/src/posts/PostEdit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ const PostEdit = () => {
<TabbedForm.Tab label="post.form.body">
<RichTextInput
source="body"
label=""
label={false}
validate={required()}
fullWidth
/>
Expand Down
42 changes: 38 additions & 4 deletions packages/ra-input-rich-text/src/RichTextInput.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { RichTextInput } from './RichTextInput';
import { RichTextInputToolbar } from './RichTextInputToolbar';
import { useWatch } from 'react-hook-form';

export default { title: 'ra-input-rich-text' };
export default { title: 'ra-input-rich-text/RichTextInput' };

const FormInspector = ({ name = 'body' }) => {
const value = useWatch({ name });
Expand All @@ -32,7 +32,7 @@ export const Basic = (props: Partial<SimpleFormProps>) => (
onSubmit={() => {}}
{...props}
>
<RichTextInput label="Body" source="body" />
<RichTextInput source="body" />
<FormInspector />
</SimpleForm>
</AdminContext>
Expand All @@ -45,7 +45,7 @@ export const Disabled = (props: Partial<SimpleFormProps>) => (
onSubmit={() => {}}
{...props}
>
<RichTextInput label="Body" source="body" disabled />
<RichTextInput source="body" disabled />
<FormInspector />
</SimpleForm>
</AdminContext>
Expand All @@ -68,6 +68,23 @@ export const Small = (props: Partial<SimpleFormProps>) => (
</AdminContext>
);

export const Medium = (props: Partial<SimpleFormProps>) => (
<AdminContext i18nProvider={i18nProvider}>
<SimpleForm
defaultValues={{ body: 'Hello World' }}
onSubmit={() => {}}
{...props}
>
<RichTextInput
toolbar={<RichTextInputToolbar size="medium" />}
label="Body"
source="body"
/>
<FormInspector />
</SimpleForm>
</AdminContext>
);

export const Large = (props: Partial<SimpleFormProps>) => (
<AdminContext i18nProvider={i18nProvider}>
<SimpleForm
Expand All @@ -93,7 +110,7 @@ export const FullWidth = (props: Partial<SimpleFormProps>) => (
{...props}
>
<RichTextInput
toolbar={<RichTextInputToolbar size="large" />}
toolbar={<RichTextInputToolbar />}
label="Body"
source="body"
fullWidth
Expand All @@ -103,6 +120,23 @@ export const FullWidth = (props: Partial<SimpleFormProps>) => (
</AdminContext>
);

export const Sx = (props: Partial<SimpleFormProps>) => (
<AdminContext i18nProvider={i18nProvider}>
<SimpleForm
defaultValues={{ body: 'Hello World' }}
onSubmit={() => {}}
{...props}
>
<RichTextInput
label="Body"
source="body"
sx={{ border: '1px solid red' }}
/>
<FormInspector />
</SimpleForm>
</AdminContext>
);

export const Validation = (props: Partial<SimpleFormProps>) => (
<AdminContext i18nProvider={i18nProvider}>
<SimpleForm onSubmit={() => {}} {...props}>
Expand Down
155 changes: 81 additions & 74 deletions packages/ra-input-rich-text/src/RichTextInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ export const RichTextInput = (props: RichTextInputProps) => {
label,
readOnly = false,
source,
sx,
toolbar,
} = props;

Expand Down Expand Up @@ -168,72 +169,39 @@ export const RichTextInput = (props: RichTextInputProps) => {
}, [editor, field]);

return (
<Labeled
isRequired={isRequired}
label={label}
id={`${id}-label`}
color={fieldState?.invalid ? 'error' : undefined}
source={source}
resource={resource}
fullWidth={fullWidth}
<Root
className={clsx(
'ra-input',
`ra-input-${source}`,
className,
fullWidth ? 'fullWidth' : ''
)}
sx={sx}
>
<RichTextInputContent
className={clsx('ra-input', `ra-input-${source}`, className)}
editor={editor}
error={error}
helperText={helperText}
id={id}
isTouched={isTouched}
isSubmitted={isSubmitted}
invalid={invalid}
toolbar={toolbar || <RichTextInputToolbar />}
/>
</Labeled>
<Labeled
isRequired={isRequired}
label={label}
id={`${id}-label`}
color={fieldState?.invalid ? 'error' : undefined}
source={source}
resource={resource}
fullWidth={fullWidth}
>
<RichTextInputContent
editor={editor}
error={error}
helperText={helperText}
id={id}
isTouched={isTouched}
isSubmitted={isSubmitted}
invalid={invalid}
toolbar={toolbar || <RichTextInputToolbar />}
/>
</Labeled>
</Root>
);
};

/**
* Extracted in a separate component so that we can remove fullWidth from the props injected by Labeled
* and avoid warnings about unknown props on Root.
*/
const RichTextInputContent = ({
className,
editor,
error,
fullWidth,
helperText,
id,
isTouched,
isSubmitted,
invalid,
toolbar,
}: RichTextInputContentProps) => (
<Root className={className}>
<TiptapEditorProvider value={editor}>
{toolbar}
<EditorContent
aria-labelledby={`${id}-label`}
className={classes.editorContent}
editor={editor}
/>
</TiptapEditorProvider>
<FormHelperText
className={
(isTouched || isSubmitted) && invalid
? 'ra-rich-text-input-error'
: ''
}
error={(isTouched || isSubmitted) && invalid}
>
<InputHelperText
touched={isTouched || isSubmitted}
error={error?.message}
helperText={helperText}
/>
</FormHelperText>
</Root>
);

export const DefaultEditorOptions: Partial<EditorOptions> = {
extensions: [
StarterKit,
Expand All @@ -251,6 +219,15 @@ export const DefaultEditorOptions: Partial<EditorOptions> = {
],
};

export type RichTextInputProps = CommonInputProps &
Omit<LabeledProps, 'children'> & {
disabled?: boolean;
readOnly?: boolean;
editorOptions?: Partial<EditorOptions>;
toolbar?: ReactNode;
sx?: typeof Root['defaultProps']['sx'];
};

const PREFIX = 'RaRichTextInput';
const classes = {
editorContent: `${PREFIX}-editorContent`,
Expand All @@ -259,10 +236,9 @@ const Root = styled('div', {
name: PREFIX,
overridesResolver: (props, styles) => styles.root,
})(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
alignItems: 'center',

'&.fullWidth': {
width: '100%',
},
[`& .${classes.editorContent}`]: {
width: '100%',
'& .ProseMirror': {
Expand Down Expand Up @@ -296,19 +272,50 @@ const Root = styled('div', {
},
}));

export type RichTextInputProps = CommonInputProps &
Omit<LabeledProps, 'children'> & {
disabled?: boolean;
readOnly?: boolean;
editorOptions?: Partial<EditorOptions>;
toolbar?: ReactNode;
};
/**
* Extracted in a separate component so that we can remove fullWidth from the props injected by Labeled
* and avoid warnings about unknown props on Root.
*/
const RichTextInputContent = ({
editor,
error,
helperText,
id,
isTouched,
isSubmitted,
invalid,
toolbar,
}: RichTextInputContentProps) => (
<>
<TiptapEditorProvider value={editor}>
{toolbar}
<EditorContent
aria-labelledby={`${id}-label`}
className={classes.editorContent}
editor={editor}
/>
</TiptapEditorProvider>
<FormHelperText
className={
(isTouched || isSubmitted) && invalid
? 'ra-rich-text-input-error'
: ''
}
error={(isTouched || isSubmitted) && invalid}
>
<InputHelperText
touched={isTouched || isSubmitted}
error={error?.message}
helperText={helperText}
/>
</FormHelperText>
</>
);

export type RichTextInputContentProps = {
className?: string;
editor?: Editor;
error?: any;
fullWidth?: boolean;
helperText?: string | ReactElement | false;
id: string;
isTouched: boolean;
Expand Down
12 changes: 12 additions & 0 deletions packages/ra-input-rich-text/src/RichTextInputToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,18 @@ const Root = styled('div')(({ theme }) => ({
'& > *:last-child': {
marginRight: 0,
},
'& button.MuiToggleButton-sizeSmall': {
padding: theme.spacing(0.3),
fontSize: theme.typography.pxToRem(18),
},
'& button.MuiToggleButton-sizeMedium': {
padding: theme.spacing(0.5),
fontSize: theme.typography.pxToRem(24),
},
'& button.MuiToggleButton-sizeLarge': {
padding: theme.spacing(1),
fontSize: theme.typography.pxToRem(24),
},
},
}));

Expand Down
8 changes: 4 additions & 4 deletions packages/ra-input-rich-text/src/buttons/ColorButtons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,15 @@ export const ColorButtons = (props: Omit<ToggleButtonProps, 'value'>) => {
setColorType(colorType);
};

return editor ? (
return (
<Box sx={{ position: 'relative' }}>
<OutsideListener onClick={() => setShowColorChoiceDialog(false)}>
<ToggleButtonGroup>
<ToggleButton
aria-label={colorLabel}
title={colorLabel}
{...props}
disabled={!editor?.isEditable}
disabled={!editor || !editor.isEditable}
value="color"
onClick={() => displayColorChoiceDialog(ColorType.FONT)}
>
Expand All @@ -60,7 +60,7 @@ export const ColorButtons = (props: Omit<ToggleButtonProps, 'value'>) => {
aria-label={highlightLabel}
title={highlightLabel}
{...props}
disabled={!editor?.isEditable}
disabled={!editor || !editor.isEditable}
value="highlight"
onClick={() =>
displayColorChoiceDialog(ColorType.BACKGROUND)
Expand All @@ -78,7 +78,7 @@ export const ColorButtons = (props: Omit<ToggleButtonProps, 'value'>) => {
)}
</OutsideListener>
</Box>
) : null;
);
};

interface ColorChoiceDialogProps {
Expand Down
6 changes: 3 additions & 3 deletions packages/ra-input-rich-text/src/buttons/ImageButtons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,16 @@ export const ImageButtons = (props: Omit<ToggleButtonProps, 'value'>) => {
}
}, [editor, translate]);

return editor ? (
return (
<ToggleButton
aria-label={label}
title={label}
{...props}
disabled={!editor?.isEditable}
disabled={!editor || !editor.isEditable}
value="image"
onClick={addImage}
>
<ImageIcon fontSize="inherit" />
</ToggleButton>
) : null;
);
};
2 changes: 1 addition & 1 deletion packages/ra-input-rich-text/src/buttons/LevelSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export const LevelSelect = (props: LevelSelectProps) => {
setAnchorElement(event.currentTarget);
};

const handleClose = (event: React.MouseEvent<Document, MouseEvent>) => {
const handleClose = (_event: React.MouseEvent<Document, MouseEvent>) => {
setAnchorElement(null);
};

Expand Down