Skip to content

Commit 3f158cb

Browse files
authored
Merge pull request #7585 from marmelab/fix-richtextinput-link-button
Ensure All RichTextInput Buttons Update Correctly
2 parents 453753e + ad68ab7 commit 3f158cb

9 files changed

+220
-66
lines changed

packages/ra-input-rich-text/src/buttons/AlignmentButtons.tsx

+30-9
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as React from 'react';
2-
import { MouseEvent } from 'react';
2+
import { MouseEvent, useEffect, useState } from 'react';
33

44
import { Editor } from '@tiptap/react';
55
import {
@@ -18,25 +18,46 @@ import { useTiptapEditor } from '../useTiptapEditor';
1818
export const AlignmentButtons = (props: ToggleButtonGroupProps) => {
1919
const editor = useTiptapEditor();
2020
const translate = useTranslate();
21+
const [value, setValue] = useState<string>('left');
2122

2223
const leftLabel = translate('ra.tiptap.align_left', { _: 'Align left' });
2324
const rightLabel = translate('ra.tiptap.align_right', { _: 'Align right' });
2425
const centerLabel = translate('ra.tiptap.align_center', { _: 'Center' });
2526
const justifyLabel = translate('ra.tiptap.align_justify', { _: 'Justify' });
2627

28+
useEffect(() => {
29+
const handleUpdate = () => {
30+
setValue(currentValue =>
31+
AlignmentValues.reduce((acc, value) => {
32+
if (editor && editor.isActive({ textAlign: value })) {
33+
return value;
34+
}
35+
return acc;
36+
}, currentValue)
37+
);
38+
};
39+
40+
if (editor) {
41+
editor.on('update', handleUpdate);
42+
editor.on('selectionUpdate', handleUpdate);
43+
}
44+
45+
return () => {
46+
if (editor) {
47+
editor.off('update', handleUpdate);
48+
editor.off('selectionUpdate', handleUpdate);
49+
}
50+
};
51+
}, [editor]);
52+
2753
const handleChange = (
2854
event: MouseEvent<HTMLElement>,
2955
newFormat: string
3056
) => {
31-
AlignmentActions[newFormat](editor);
32-
};
33-
34-
const value = AlignmentValues.reduce((acc, value) => {
35-
if (editor && editor.isActive({ textAlign: value })) {
36-
return value;
57+
if (AlignmentActions[newFormat]) {
58+
AlignmentActions[newFormat](editor);
3759
}
38-
return acc;
39-
}, '');
60+
};
4061

4162
return (
4263
<ToggleButtonGroup

packages/ra-input-rich-text/src/buttons/FormatButtons.tsx

+30-24
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as React from 'react';
2-
import { MouseEvent } from 'react';
2+
import { MouseEvent, useEffect, useState } from 'react';
33

44
import { Editor } from '@tiptap/react';
55
import {
@@ -18,6 +18,7 @@ import { useTiptapEditor } from '../useTiptapEditor';
1818
export const FormatButtons = (props: ToggleButtonGroupProps) => {
1919
const editor = useTiptapEditor();
2020
const translate = useTranslate();
21+
const [values, setValues] = useState<string[]>([]);
2122

2223
const boldLabel = translate('ra.tiptap.bold', {
2324
_: 'Bold',
@@ -39,6 +40,31 @@ export const FormatButtons = (props: ToggleButtonGroupProps) => {
3940
_: 'Code',
4041
});
4142

43+
useEffect(() => {
44+
const handleUpdate = () => {
45+
setValues(() =>
46+
FormatValues.reduce((acc, value) => {
47+
if (editor && editor.isActive(value)) {
48+
acc.push(value);
49+
}
50+
return acc;
51+
}, [])
52+
);
53+
};
54+
55+
if (editor) {
56+
editor.on('update', handleUpdate);
57+
editor.on('selectionUpdate', handleUpdate);
58+
}
59+
60+
return () => {
61+
if (editor) {
62+
editor.off('update', handleUpdate);
63+
editor.off('selectionUpdate', handleUpdate);
64+
}
65+
};
66+
}, [editor]);
67+
4268
const handleChange = (
4369
event: MouseEvent<HTMLElement>,
4470
newFormats: string[]
@@ -59,58 +85,38 @@ export const FormatButtons = (props: ToggleButtonGroupProps) => {
5985
});
6086
};
6187

62-
const value = FormatValues.reduce((acc, value) => {
63-
if (editor && editor.isActive(value)) {
64-
acc.push(value);
65-
}
66-
return acc;
67-
}, []);
68-
6988
return (
7089
<ToggleButtonGroup
7190
{...props}
7291
disabled={!editor?.isEditable}
7392
onChange={handleChange}
74-
value={value}
93+
value={values}
7594
>
76-
<ToggleButton
77-
value="bold"
78-
aria-label={boldLabel}
79-
title={boldLabel}
80-
selected={editor && editor.isActive('bold')}
81-
>
95+
<ToggleButton value="bold" aria-label={boldLabel} title={boldLabel}>
8296
<FormatBold fontSize="inherit" />
8397
</ToggleButton>
8498
<ToggleButton
8599
value="italic"
86100
aria-label={italicLabel}
87101
title={italicLabel}
88-
selected={editor && editor.isActive('italic')}
89102
>
90103
<FormatItalic fontSize="inherit" />
91104
</ToggleButton>
92105
<ToggleButton
93106
value="underline"
94107
aria-label={underlineLabel}
95108
title={underlineLabel}
96-
selected={editor && editor.isActive('underline')}
97109
>
98110
<FormatUnderlined fontSize="inherit" />
99111
</ToggleButton>
100112
<ToggleButton
101113
value="strike"
102114
aria-label={strikeLabel}
103115
title={strikeLabel}
104-
selected={editor && editor.isActive('strike')}
105116
>
106117
<FormatStrikethrough fontSize="inherit" />
107118
</ToggleButton>
108-
<ToggleButton
109-
value="code"
110-
aria-label={codeLabel}
111-
title={codeLabel}
112-
selected={editor && editor.isActive('code')}
113-
>
119+
<ToggleButton value="code" aria-label={codeLabel} title={codeLabel}>
114120
<Code fontSize="inherit" />
115121
</ToggleButton>
116122
</ToggleButtonGroup>

packages/ra-input-rich-text/src/buttons/LevelSelect.tsx

+39-16
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import * as React from 'react';
2-
import { useState } from 'react';
2+
import { useEffect, useState } from 'react';
33
import { List, ListItem, ListItemText, Menu, MenuItem } from '@mui/material';
44
import { styled, alpha } from '@mui/material/styles';
55
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
6-
import { Editor } from '@tiptap/react';
76
import { useTranslate } from 'ra-core';
87
import clsx from 'clsx';
98
import { useTiptapEditor } from '../useTiptapEditor';
@@ -15,6 +14,7 @@ export const LevelSelect = (props: LevelSelectProps) => {
1514
null
1615
);
1716
const { size } = props;
17+
const [selectedOption, setSelectedOption] = useState(options[0]);
1818

1919
const handleMenuItemClick = (
2020
event: React.MouseEvent<HTMLLIElement, MouseEvent>,
@@ -43,8 +43,43 @@ export const LevelSelect = (props: LevelSelectProps) => {
4343
setAnchorElement(null);
4444
};
4545

46-
const selectedOption =
47-
options.find(option => isSelectedOption(editor, option)) || options[0];
46+
useEffect(() => {
47+
const handleUpdate = () => {
48+
setSelectedOption(currentOption =>
49+
options.reduce((acc, option) => {
50+
if (editor) {
51+
if (
52+
option.value === 'paragraph' &&
53+
editor.isActive('paragraph')
54+
) {
55+
return option;
56+
}
57+
58+
if (
59+
editor.isActive('heading', {
60+
level: (option as HeadingLevelOption).level,
61+
})
62+
) {
63+
return option;
64+
}
65+
}
66+
return acc;
67+
}, currentOption)
68+
);
69+
};
70+
71+
if (editor) {
72+
editor.on('update', handleUpdate);
73+
editor.on('selectionUpdate', handleUpdate);
74+
}
75+
76+
return () => {
77+
if (editor) {
78+
editor.off('update', handleUpdate);
79+
editor.off('selectionUpdate', handleUpdate);
80+
}
81+
};
82+
}, [editor]);
4883

4984
return (
5085
<Root>
@@ -160,18 +195,6 @@ const options: Array<LevelOption | HeadingLevelOption> = [
160195
},
161196
];
162197

163-
const isSelectedOption = (editor: Editor, option: LevelOption): boolean => {
164-
if (!editor) {
165-
return false;
166-
}
167-
168-
if (option.value === 'paragraph') {
169-
return editor.isActive('paragraph');
170-
}
171-
172-
return editor.isActive('heading', { level: option.level });
173-
};
174-
175198
const PREFIX = 'RaRichTextInputLevelSelect';
176199
const classes = {
177200
list: `${PREFIX}-list`,

packages/ra-input-rich-text/src/buttons/LinkButtons.tsx

+3-7
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,12 @@ import InsertLink from '@mui/icons-material/InsertLink';
44

55
import { useTranslate } from 'ra-core';
66
import { useTiptapEditor } from '../useTiptapEditor';
7+
import { useEditorSelection } from './useEditorSelection';
78

89
export const LinkButtons = (props: Omit<ToggleButtonProps, 'value'>) => {
910
const editor = useTiptapEditor();
1011
const translate = useTranslate();
11-
const disabled = editor
12-
? editor.state.doc.textBetween(
13-
editor.state.selection.from,
14-
editor.state.selection.to
15-
).length === 0
16-
: false;
12+
const currentTextSelection = useEditorSelection();
1713

1814
const label = translate('ra.tiptap.link', {
1915
_: 'Add a link',
@@ -39,7 +35,7 @@ export const LinkButtons = (props: Omit<ToggleButtonProps, 'value'>) => {
3935
aria-label={label}
4036
title={label}
4137
{...props}
42-
disabled={!editor?.isEditable || disabled}
38+
disabled={!editor?.isEditable || !currentTextSelection}
4339
value="link"
4440
onClick={handleClick}
4541
selected={editor && editor.isActive('link')}

packages/ra-input-rich-text/src/buttons/ListButtons.tsx

+26-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as React from 'react';
2-
import { MouseEvent } from 'react';
2+
import { MouseEvent, useEffect, useState } from 'react';
33

44
import { Editor } from '@tiptap/react';
55
import {
@@ -24,6 +24,8 @@ export const ListButtons = (props: ToggleButtonGroupProps) => {
2424
_: 'Numbered list',
2525
});
2626

27+
const [value, setValue] = useState<string>();
28+
2729
const handleChange = (
2830
event: MouseEvent<HTMLElement>,
2931
newFormat: string
@@ -40,12 +42,30 @@ export const ListButtons = (props: ToggleButtonGroupProps) => {
4042
});
4143
};
4244

43-
const value = ListValues.reduce((acc, value) => {
44-
if (editor && editor.isActive(value)) {
45-
return value;
45+
useEffect(() => {
46+
const handleUpdate = () => {
47+
setValue(() =>
48+
ListValues.reduce((acc, value) => {
49+
if (editor && editor.isActive(value)) {
50+
return value;
51+
}
52+
return acc;
53+
}, undefined)
54+
);
55+
};
56+
57+
if (editor) {
58+
editor.on('update', handleUpdate);
59+
editor.on('selectionUpdate', handleUpdate);
4660
}
47-
return acc;
48-
}, '');
61+
62+
return () => {
63+
if (editor) {
64+
editor.off('update', handleUpdate);
65+
editor.off('selectionUpdate', handleUpdate);
66+
}
67+
};
68+
}, [editor]);
4969

5070
return (
5171
<ToggleButtonGroup

packages/ra-input-rich-text/src/buttons/QuoteButtons.tsx

+21-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as React from 'react';
2+
import { useEffect, useState } from 'react';
23
import { ToggleButton, ToggleButtonProps } from '@mui/material';
34
import FormatQuote from '@mui/icons-material/FormatQuote';
45
import { useTranslate } from 'ra-core';
@@ -7,19 +8,38 @@ import { useTiptapEditor } from '../useTiptapEditor';
78
export const QuoteButtons = (props: Omit<ToggleButtonProps, 'value'>) => {
89
const editor = useTiptapEditor();
910
const translate = useTranslate();
11+
const [isActive, setIsActive] = useState(false);
1012

1113
const label = translate('ra.tiptap.blockquote', {
1214
_: 'Blockquote',
1315
});
1416

17+
useEffect(() => {
18+
const handleUpdate = () => {
19+
setIsActive(editor && editor.isActive('blockquote'));
20+
};
21+
22+
if (editor) {
23+
editor.on('update', handleUpdate);
24+
editor.on('selectionUpdate', handleUpdate);
25+
}
26+
27+
return () => {
28+
if (editor) {
29+
editor.off('update', handleUpdate);
30+
editor.off('selectionUpdate', handleUpdate);
31+
}
32+
};
33+
}, [editor]);
34+
1535
return (
1636
<ToggleButton
1737
aria-label={label}
1838
title={label}
1939
{...props}
2040
disabled={!editor?.isEditable}
2141
onClick={() => editor.chain().focus().toggleBlockquote().run()}
22-
selected={editor && editor.isActive('blockquote')}
42+
selected={isActive}
2343
value="quote"
2444
>
2545
<FormatQuote fontSize="inherit" />

packages/ra-input-rich-text/src/buttons/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ export * from './LinkButtons';
55
export * from './QuoteButtons';
66
export * from './ClearButtons';
77
export * from './LevelSelect';
8+
export * from './useEditorSelection';

0 commit comments

Comments
 (0)