Skip to content

Commit fea04cb

Browse files
authored
Merge pull request #11122 from marmelab/add-tooltip-forwardref-buttons
Add forwardRef to react-admin buttons to support tooltips
2 parents 8054cc7 + 07befbd commit fea04cb

27 files changed

Lines changed: 966 additions & 798 deletions

packages/ra-ui-materialui/src/button/BulkDeleteButton.tsx

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,10 @@ import {
3434
* </List>
3535
* );
3636
*/
37-
export const BulkDeleteButton = (inProps: BulkDeleteButtonProps) => {
37+
export const BulkDeleteButton = React.forwardRef(function BulkDeleteButton(
38+
inProps: BulkDeleteButtonProps,
39+
ref: React.ForwardedRef<HTMLButtonElement>
40+
) {
3841
const { mutationMode = 'undoable', ...props } = useThemeProps({
3942
name: PREFIX,
4043
props: inProps,
@@ -54,11 +57,15 @@ export const BulkDeleteButton = (inProps: BulkDeleteButtonProps) => {
5457
return null;
5558
}
5659
return mutationMode === 'undoable' ? (
57-
<BulkDeleteWithUndoButton {...props} />
60+
<BulkDeleteWithUndoButton ref={ref} {...props} />
5861
) : (
59-
<BulkDeleteWithConfirmButton mutationMode={mutationMode} {...props} />
62+
<BulkDeleteWithConfirmButton
63+
ref={ref}
64+
mutationMode={mutationMode}
65+
{...props}
66+
/>
6067
);
61-
};
68+
});
6269

6370
interface Props {
6471
mutationMode?: MutationMode;

packages/ra-ui-materialui/src/button/BulkDeleteWithConfirmButton.tsx

Lines changed: 122 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -20,120 +20,136 @@ import { Confirm } from '../layout';
2020
import { Button, type ButtonProps } from './Button';
2121
import { humanize, inflect } from 'inflection';
2222

23-
export const BulkDeleteWithConfirmButton = (
24-
inProps: BulkDeleteWithConfirmButtonProps
25-
) => {
26-
const props = useThemeProps({
27-
props: inProps,
28-
name: PREFIX,
29-
});
30-
const {
31-
confirmTitle = 'ra.message.bulk_delete_title',
32-
confirmContent = 'ra.message.bulk_delete_content',
33-
confirmColor = 'primary',
34-
icon = defaultIcon,
35-
label = 'ra.action.delete',
36-
mutationMode = 'pessimistic',
37-
mutationOptions,
38-
onClick,
39-
...rest
40-
} = props;
41-
const { selectedIds } = useListContext();
42-
const { handleDelete, isPending } = useBulkDeleteController({
43-
mutationMode,
44-
...rest,
45-
mutationOptions: {
46-
...mutationOptions,
47-
onSettled(...args) {
48-
// In pessimistic mode, we wait for the mutation to be completed (either successfully or with an error) before closing
49-
if (mutationMode === 'pessimistic') {
50-
setOpen(false);
51-
}
52-
mutationOptions?.onSettled?.(...args);
23+
export const BulkDeleteWithConfirmButton = React.forwardRef(
24+
function BulkDeleteWithConfirmButton(
25+
inProps: BulkDeleteWithConfirmButtonProps,
26+
ref: React.ForwardedRef<HTMLButtonElement>
27+
) {
28+
const props = useThemeProps({
29+
props: inProps,
30+
name: PREFIX,
31+
});
32+
const {
33+
confirmTitle = 'ra.message.bulk_delete_title',
34+
confirmContent = 'ra.message.bulk_delete_content',
35+
confirmColor = 'primary',
36+
icon = defaultIcon,
37+
label = 'ra.action.delete',
38+
mutationMode = 'pessimistic',
39+
mutationOptions,
40+
onClick,
41+
...rest
42+
} = props;
43+
const { selectedIds } = useListContext();
44+
const { handleDelete, isPending } = useBulkDeleteController({
45+
mutationMode,
46+
...rest,
47+
mutationOptions: {
48+
...mutationOptions,
49+
onSettled(...args) {
50+
// In pessimistic mode, we wait for the mutation to be completed (either successfully or with an error) before closing
51+
if (mutationMode === 'pessimistic') {
52+
setOpen(false);
53+
}
54+
mutationOptions?.onSettled?.(...args);
55+
},
5356
},
54-
},
55-
});
56-
57-
const [isOpen, setOpen] = useState(false);
58-
const resource = useResourceContext(props);
59-
const translate = useTranslate();
60-
61-
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
62-
e.stopPropagation();
63-
setOpen(true);
64-
};
65-
66-
const handleDialogClose = (e: React.MouseEvent) => {
67-
e.stopPropagation();
68-
setOpen(false);
69-
};
70-
71-
const handleConfirm = (e: React.MouseEvent<HTMLButtonElement>) => {
72-
e.stopPropagation();
73-
// We close the dialog immediately here for optimistic/undoable modes instead of in onSuccess/onError
74-
// to avoid reimplementing the default side effects
75-
if (mutationMode !== 'pessimistic') {
57+
});
58+
59+
const [isOpen, setOpen] = useState(false);
60+
const resource = useResourceContext(props);
61+
const translate = useTranslate();
62+
63+
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
64+
e.stopPropagation();
65+
setOpen(true);
66+
};
67+
68+
const handleDialogClose = (e: React.MouseEvent) => {
69+
e.stopPropagation();
7670
setOpen(false);
77-
}
78-
handleDelete();
79-
80-
if (typeof onClick === 'function') {
81-
onClick(e);
82-
}
83-
};
84-
85-
return (
86-
<Fragment>
87-
<StyledButton
88-
onClick={handleClick}
89-
label={label}
90-
color="error"
91-
{...sanitizeRestProps(rest)}
92-
>
93-
{icon}
94-
</StyledButton>
95-
<Confirm
96-
isOpen={isOpen}
97-
loading={isPending}
98-
title={confirmTitle}
99-
content={confirmContent}
100-
confirmColor={confirmColor}
101-
titleTranslateOptions={{
102-
smart_count: selectedIds.length,
103-
name: translate(`resources.${resource}.forcedCaseName`, {
71+
};
72+
73+
const handleConfirm = (e: React.MouseEvent<HTMLButtonElement>) => {
74+
e.stopPropagation();
75+
// We close the dialog immediately here for optimistic/undoable modes instead of in onSuccess/onError
76+
// to avoid reimplementing the default side effects
77+
if (mutationMode !== 'pessimistic') {
78+
setOpen(false);
79+
}
80+
handleDelete();
81+
82+
if (typeof onClick === 'function') {
83+
onClick(e);
84+
}
85+
};
86+
87+
return (
88+
<Fragment>
89+
<StyledButton
90+
ref={ref}
91+
onClick={handleClick}
92+
label={label}
93+
color="error"
94+
{...sanitizeRestProps(rest)}
95+
>
96+
{icon}
97+
</StyledButton>
98+
<Confirm
99+
isOpen={isOpen}
100+
loading={isPending}
101+
title={confirmTitle}
102+
content={confirmContent}
103+
confirmColor={confirmColor}
104+
titleTranslateOptions={{
104105
smart_count: selectedIds.length,
105-
_: humanize(
106-
translate(`resources.${resource}.name`, {
106+
name: translate(
107+
`resources.${resource}.forcedCaseName`,
108+
{
107109
smart_count: selectedIds.length,
108-
_: resource
109-
? inflect(resource, selectedIds.length)
110-
: undefined,
111-
}),
112-
true
110+
_: humanize(
111+
translate(`resources.${resource}.name`, {
112+
smart_count: selectedIds.length,
113+
_: resource
114+
? inflect(
115+
resource,
116+
selectedIds.length
117+
)
118+
: undefined,
119+
}),
120+
true
121+
),
122+
}
113123
),
114-
}),
115-
}}
116-
contentTranslateOptions={{
117-
smart_count: selectedIds.length,
118-
name: translate(`resources.${resource}.forcedCaseName`, {
124+
}}
125+
contentTranslateOptions={{
119126
smart_count: selectedIds.length,
120-
_: humanize(
121-
translate(`resources.${resource}.name`, {
127+
name: translate(
128+
`resources.${resource}.forcedCaseName`,
129+
{
122130
smart_count: selectedIds.length,
123-
_: resource
124-
? inflect(resource, selectedIds.length)
125-
: undefined,
126-
}),
127-
true
131+
_: humanize(
132+
translate(`resources.${resource}.name`, {
133+
smart_count: selectedIds.length,
134+
_: resource
135+
? inflect(
136+
resource,
137+
selectedIds.length
138+
)
139+
: undefined,
140+
}),
141+
true
142+
),
143+
}
128144
),
129-
}),
130-
}}
131-
onConfirm={handleConfirm}
132-
onClose={handleDialogClose}
133-
/>
134-
</Fragment>
135-
);
136-
};
145+
}}
146+
onConfirm={handleConfirm}
147+
onClose={handleDialogClose}
148+
/>
149+
</Fragment>
150+
);
151+
}
152+
);
137153

138154
const sanitizeRestProps = ({
139155
classes,

packages/ra-ui-materialui/src/button/BulkDeleteWithUndoButton.tsx

Lines changed: 36 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -13,40 +13,44 @@ import {
1313

1414
import { Button, type ButtonProps } from './Button';
1515

16-
export const BulkDeleteWithUndoButton = (
17-
inProps: BulkDeleteWithUndoButtonProps
18-
) => {
19-
const props = useThemeProps({
20-
props: inProps,
21-
name: PREFIX,
22-
});
23-
const {
24-
label = 'ra.action.delete',
25-
icon = defaultIcon,
26-
onClick,
27-
...rest
28-
} = props;
29-
const { handleDelete, isPending } = useBulkDeleteController(rest);
16+
export const BulkDeleteWithUndoButton = React.forwardRef(
17+
function BulkDeleteWithUndoButton(
18+
inProps: BulkDeleteWithUndoButtonProps,
19+
ref: React.ForwardedRef<HTMLButtonElement>
20+
) {
21+
const props = useThemeProps({
22+
props: inProps,
23+
name: PREFIX,
24+
});
25+
const {
26+
label = 'ra.action.delete',
27+
icon = defaultIcon,
28+
onClick,
29+
...rest
30+
} = props;
31+
const { handleDelete, isPending } = useBulkDeleteController(rest);
3032

31-
const handleClick = e => {
32-
handleDelete();
33-
if (typeof onClick === 'function') {
34-
onClick(e);
35-
}
36-
};
33+
const handleClick = e => {
34+
handleDelete();
35+
if (typeof onClick === 'function') {
36+
onClick(e);
37+
}
38+
};
3739

38-
return (
39-
<StyledButton
40-
onClick={handleClick}
41-
label={label}
42-
disabled={isPending}
43-
color="error"
44-
{...sanitizeRestProps(rest)}
45-
>
46-
{icon}
47-
</StyledButton>
48-
);
49-
};
40+
return (
41+
<StyledButton
42+
ref={ref}
43+
onClick={handleClick}
44+
label={label}
45+
disabled={isPending}
46+
color="error"
47+
{...sanitizeRestProps(rest)}
48+
>
49+
{icon}
50+
</StyledButton>
51+
);
52+
}
53+
);
5054

5155
const defaultIcon = <ActionDelete />;
5256

packages/ra-ui-materialui/src/button/BulkExportButton.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,10 @@ import { Button, ButtonProps } from './Button';
3737
* </List>
3838
* );
3939
*/
40-
export const BulkExportButton = (inProps: BulkExportButtonProps) => {
40+
export const BulkExportButton = React.forwardRef(function BulkExportButton(
41+
inProps: BulkExportButtonProps,
42+
ref: React.ForwardedRef<HTMLButtonElement>
43+
) {
4144
const props = useThemeProps({
4245
props: inProps,
4346
name: PREFIX,
@@ -70,14 +73,15 @@ export const BulkExportButton = (inProps: BulkExportButtonProps) => {
7073

7174
return (
7275
<StyledButton
76+
ref={ref}
7377
onClick={handleClick}
7478
label={label}
7579
{...sanitizeRestProps(rest)}
7680
>
7781
{icon}
7882
</StyledButton>
7983
);
80-
};
84+
});
8185

8286
const defaultIcon = <DownloadIcon />;
8387

0 commit comments

Comments
 (0)