Skip to content

Commit

Permalink
Merge pull request #9018 from marmelab/richtextinput-toolbar-size
Browse files Browse the repository at this point in the history
Fix RichTextInput toolbar appearance
  • Loading branch information
djhi authored Jun 19, 2023
2 parents 175fa19 + 3198d4e commit d832f70
Show file tree
Hide file tree
Showing 10 changed files with 142 additions and 90 deletions.
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

0 comments on commit d832f70

Please sign in to comment.