diff --git a/packages/ra-ui-materialui/src/button/BulkDeleteButton.spec.tsx b/packages/ra-ui-materialui/src/button/BulkDeleteButton.spec.tsx
new file mode 100644
index 00000000000..50656f32ec0
--- /dev/null
+++ b/packages/ra-ui-materialui/src/button/BulkDeleteButton.spec.tsx
@@ -0,0 +1,15 @@
+import * as React from 'react';
+import { render, screen } from '@testing-library/react';
+import expect from 'expect';
+
+import { Themed } from './BulkDeleteButton.stories';
+
+describe('', () => {
+ it('should be customized by a theme', async () => {
+ render();
+
+ const button = await screen.findByTestId('themed');
+ expect(button.textContent).toBe('Bulk Delete');
+ expect(button.classList).toContain('custom-class');
+ });
+});
diff --git a/packages/ra-ui-materialui/src/button/BulkDeleteButton.stories.tsx b/packages/ra-ui-materialui/src/button/BulkDeleteButton.stories.tsx
new file mode 100644
index 00000000000..14a963c3df1
--- /dev/null
+++ b/packages/ra-ui-materialui/src/button/BulkDeleteButton.stories.tsx
@@ -0,0 +1,148 @@
+import React from 'react';
+import { ThemeOptions } from '@mui/material';
+import { deepmerge } from '@mui/utils';
+import { Resource } from 'ra-core';
+import polyglotI18nProvider from 'ra-i18n-polyglot';
+import englishMessages from 'ra-language-english';
+import fakeRestDataProvider from 'ra-data-fakerest';
+
+import { AdminContext } from '../AdminContext';
+import { BulkDeleteButton } from './BulkDeleteButton';
+import { defaultLightTheme } from '../theme';
+import { Datagrid, List } from '../list';
+import { NumberField, TextField } from '../field';
+import { AdminUI } from '../AdminUI';
+
+export default { title: 'ra-ui-materialui/button/BulkDeleteButton' };
+
+const i18nProvider = polyglotI18nProvider(
+ () => englishMessages,
+ 'en' // Default locale
+);
+
+const dataProvider = fakeRestDataProvider({
+ books: [
+ {
+ id: 1,
+ title: 'War and Peace',
+ author: 'Leo Tolstoy',
+ reads: 23,
+ },
+ {
+ id: 2,
+ title: 'Pride and Predjudice',
+ author: 'Jane Austen',
+ reads: 854,
+ },
+ {
+ id: 3,
+ title: 'The Picture of Dorian Gray',
+ author: 'Oscar Wilde',
+ reads: 126,
+ },
+ {
+ id: 4,
+ title: 'Le Petit Prince',
+ author: 'Antoine de Saint-Exupéry',
+ reads: 86,
+ },
+ {
+ id: 5,
+ title: "Alice's Adventures in Wonderland",
+ author: 'Lewis Carroll',
+ reads: 125,
+ },
+ {
+ id: 6,
+ title: 'Madame Bovary',
+ author: 'Gustave Flaubert',
+ reads: 452,
+ },
+ {
+ id: 7,
+ title: 'The Lord of the Rings',
+ author: 'J. R. R. Tolkien',
+ reads: 267,
+ },
+ {
+ id: 8,
+ title: "Harry Potter and the Philosopher's Stone",
+ author: 'J. K. Rowling',
+ reads: 1294,
+ },
+ {
+ id: 9,
+ title: 'The Alchemist',
+ author: 'Paulo Coelho',
+ reads: 23,
+ },
+ {
+ id: 10,
+ title: 'A Catcher in the Rye',
+ author: 'J. D. Salinger',
+ reads: 209,
+ },
+ {
+ id: 11,
+ title: 'Ulysses',
+ author: 'James Joyce',
+ reads: 12,
+ },
+ ],
+ authors: [],
+});
+
+const Wrapper = ({ children, ...props }) => {
+ return (
+
+
+ (
+
+
+
+
+
+
+
+
+ )}
+ />
+
+
+ );
+};
+
+export const Basic = () => {
+ return (
+
+
+
+ );
+};
+
+export const Themed = () => {
+ return (
+
+
+
+ );
+};
diff --git a/packages/ra-ui-materialui/src/button/BulkDeleteButton.tsx b/packages/ra-ui-materialui/src/button/BulkDeleteButton.tsx
index 49c437bd840..378ef180d48 100644
--- a/packages/ra-ui-materialui/src/button/BulkDeleteButton.tsx
+++ b/packages/ra-ui-materialui/src/button/BulkDeleteButton.tsx
@@ -1,4 +1,7 @@
import * as React from 'react';
+import { MutationMode, useCanAccess, useResourceContext } from 'ra-core';
+import { useThemeProps } from '@mui/material/styles';
+
import {
BulkDeleteWithConfirmButton,
BulkDeleteWithConfirmButtonProps,
@@ -7,7 +10,6 @@ import {
BulkDeleteWithUndoButton,
BulkDeleteWithUndoButtonProps,
} from './BulkDeleteWithUndoButton';
-import { MutationMode, useCanAccess, useResourceContext } from 'ra-core';
/**
* Deletes the selected rows.
@@ -32,10 +34,12 @@ import { MutationMode, useCanAccess, useResourceContext } from 'ra-core';
*
* );
*/
-export const BulkDeleteButton = ({
- mutationMode = 'undoable',
- ...props
-}: BulkDeleteButtonProps) => {
+export const BulkDeleteButton = (inProps: BulkDeleteButtonProps) => {
+ const { mutationMode = 'undoable', ...props } = useThemeProps({
+ name: PREFIX,
+ props: inProps,
+ });
+
const resource = useResourceContext(props);
if (!resource) {
throw new Error(
@@ -62,3 +66,17 @@ interface Props {
export type BulkDeleteButtonProps = Props &
(BulkDeleteWithUndoButtonProps | BulkDeleteWithConfirmButtonProps);
+
+const PREFIX = 'RaBulkDeleteButton';
+
+declare module '@mui/material/styles' {
+ interface ComponentsPropsList {
+ [PREFIX]: Partial;
+ }
+
+ interface Components {
+ [PREFIX]?: {
+ defaultProps?: ComponentsPropsList[typeof PREFIX];
+ };
+ }
+}
diff --git a/packages/ra-ui-materialui/src/button/BulkExportButton.spec.tsx b/packages/ra-ui-materialui/src/button/BulkExportButton.spec.tsx
index cc9d9e25518..111e0e68967 100644
--- a/packages/ra-ui-materialui/src/button/BulkExportButton.spec.tsx
+++ b/packages/ra-ui-materialui/src/button/BulkExportButton.spec.tsx
@@ -9,16 +9,17 @@ import {
import { createTheme, ThemeProvider } from '@mui/material/styles';
import { BulkExportButton } from './BulkExportButton';
+import { Themed } from './BulkExportButton.stories';
const theme = createTheme();
describe('', () => {
- it('should invoke dataProvider with meta', async () => {
- const exporter = jest.fn().mockName('exporter');
- const dataProvider = testDataProvider({
- getMany: jest.fn().mockResolvedValueOnce({ data: [], total: 0 }),
- });
+ const exporter = jest.fn().mockName('exporter');
+ const dataProvider = testDataProvider({
+ getMany: jest.fn().mockResolvedValueOnce({ data: [], total: 0 }),
+ });
+ it('should invoke dataProvider with meta', async () => {
render(
@@ -46,4 +47,12 @@ describe('', () => {
expect(exporter).toHaveBeenCalled();
});
});
+
+ it('should be customized by a theme', async () => {
+ render();
+
+ const button = await screen.findByTestId('themed');
+ expect(button.textContent).toBe('Bulk Export');
+ expect(button.classList).toContain('custom-class');
+ });
});
diff --git a/packages/ra-ui-materialui/src/button/BulkExportButton.stories.tsx b/packages/ra-ui-materialui/src/button/BulkExportButton.stories.tsx
new file mode 100644
index 00000000000..ca636ed1fd5
--- /dev/null
+++ b/packages/ra-ui-materialui/src/button/BulkExportButton.stories.tsx
@@ -0,0 +1,152 @@
+import React from 'react';
+import { ThemeOptions } from '@mui/material';
+import { deepmerge } from '@mui/utils';
+import { Resource } from 'ra-core';
+import polyglotI18nProvider from 'ra-i18n-polyglot';
+import englishMessages from 'ra-language-english';
+import fakeRestDataProvider from 'ra-data-fakerest';
+
+import { AdminContext } from '../AdminContext';
+import { BulkExportButton } from './BulkExportButton';
+import { defaultLightTheme } from '../theme';
+import { Datagrid, List } from '../list';
+import { NumberField, TextField } from '../field';
+import { AdminUI } from '../AdminUI';
+
+export default { title: 'ra-ui-materialui/button/BulkExportButton' };
+
+const i18nProvider = polyglotI18nProvider(
+ () => englishMessages,
+ 'en' // Default locale
+);
+
+const dataProvider = fakeRestDataProvider({
+ books: [
+ {
+ id: 1,
+ title: 'War and Peace',
+ author: 'Leo Tolstoy',
+ reads: 23,
+ },
+ {
+ id: 2,
+ title: 'Pride and Predjudice',
+ author: 'Jane Austen',
+ reads: 854,
+ },
+ {
+ id: 3,
+ title: 'The Picture of Dorian Gray',
+ author: 'Oscar Wilde',
+ reads: 126,
+ },
+ {
+ id: 4,
+ title: 'Le Petit Prince',
+ author: 'Antoine de Saint-Exupéry',
+ reads: 86,
+ },
+ {
+ id: 5,
+ title: "Alice's Adventures in Wonderland",
+ author: 'Lewis Carroll',
+ reads: 125,
+ },
+ {
+ id: 6,
+ title: 'Madame Bovary',
+ author: 'Gustave Flaubert',
+ reads: 452,
+ },
+ {
+ id: 7,
+ title: 'The Lord of the Rings',
+ author: 'J. R. R. Tolkien',
+ reads: 267,
+ },
+ {
+ id: 8,
+ title: "Harry Potter and the Philosopher's Stone",
+ author: 'J. K. Rowling',
+ reads: 1294,
+ },
+ {
+ id: 9,
+ title: 'The Alchemist',
+ author: 'Paulo Coelho',
+ reads: 23,
+ },
+ {
+ id: 10,
+ title: 'A Catcher in the Rye',
+ author: 'J. D. Salinger',
+ reads: 209,
+ },
+ {
+ id: 11,
+ title: 'Ulysses',
+ author: 'James Joyce',
+ reads: 12,
+ },
+ ],
+ authors: [],
+});
+
+const Wrapper = ({ children, ...props }) => {
+ return (
+
+
+ (
+
+
+
+
+
+
+
+
+ )}
+ />
+
+
+ );
+};
+
+export const Basic = () => {
+ return (
+
+
+
+ );
+};
+
+export const Themed = () => {
+ return (
+
+
+
+ );
+};
diff --git a/packages/ra-ui-materialui/src/button/BulkExportButton.tsx b/packages/ra-ui-materialui/src/button/BulkExportButton.tsx
index b1055bbe497..58a353d324a 100644
--- a/packages/ra-ui-materialui/src/button/BulkExportButton.tsx
+++ b/packages/ra-ui-materialui/src/button/BulkExportButton.tsx
@@ -9,6 +9,11 @@ import {
useListContext,
useResourceContext,
} from 'ra-core';
+import {
+ ComponentsOverrides,
+ styled,
+ useThemeProps,
+} from '@mui/material/styles';
import { Button, ButtonProps } from './Button';
@@ -35,7 +40,12 @@ import { Button, ButtonProps } from './Button';
*
* );
*/
-export const BulkExportButton = (props: BulkExportButtonProps) => {
+export const BulkExportButton = (inProps: BulkExportButtonProps) => {
+ const props = useThemeProps({
+ props: inProps,
+ name: PREFIX,
+ });
+
const {
onClick,
label = 'ra.action.export',
@@ -77,13 +87,13 @@ export const BulkExportButton = (props: BulkExportButtonProps) => {
);
return (
-
+
);
};
@@ -104,3 +114,29 @@ interface Props {
}
export type BulkExportButtonProps = Props & ButtonProps;
+
+const PREFIX = 'RaBulkExportButton';
+
+const StyledButton = styled(Button, {
+ name: PREFIX,
+ overridesResolver: (props, styles) => styles.root,
+})({});
+
+declare module '@mui/material/styles' {
+ interface ComponentNameToClassKey {
+ [PREFIX]: 'root';
+ }
+
+ interface ComponentsPropsList {
+ [PREFIX]: Partial;
+ }
+
+ interface Components {
+ [PREFIX]?: {
+ defaultProps?: ComponentsPropsList[typeof PREFIX];
+ styleOverrides?: ComponentsOverrides<
+ Omit
+ >[typeof PREFIX];
+ };
+ }
+}
diff --git a/packages/ra-ui-materialui/src/button/BulkUpdateButton.spec.tsx b/packages/ra-ui-materialui/src/button/BulkUpdateButton.spec.tsx
index b8aed5e8303..0be39ff8a7f 100644
--- a/packages/ra-ui-materialui/src/button/BulkUpdateButton.spec.tsx
+++ b/packages/ra-ui-materialui/src/button/BulkUpdateButton.spec.tsx
@@ -1,9 +1,22 @@
import * as React from 'react';
import expect from 'expect';
import { render, screen } from '@testing-library/react';
-import { MutationMode } from './BulkUpdateButton.stories';
+import { MutationMode, Themed } from './BulkUpdateButton.stories';
describe('BulkUpdateButton', () => {
+ it('should be customized by a theme', async () => {
+ render();
+
+ const checkbox = await screen.findByRole('checkbox', {
+ name: 'Select all',
+ });
+ checkbox.click();
+
+ const button = screen.queryByTestId('themed-button');
+ expect(button.textContent).toBe('Bulk Update');
+ expect(button.classList).toContain('custom-class');
+ });
+
describe('mutationMode', () => {
it('should ask confirmation before updating in pessimistic mode', async () => {
render();
diff --git a/packages/ra-ui-materialui/src/button/BulkUpdateButton.stories.tsx b/packages/ra-ui-materialui/src/button/BulkUpdateButton.stories.tsx
index b2ea441e5ad..e7fe4ee1e36 100644
--- a/packages/ra-ui-materialui/src/button/BulkUpdateButton.stories.tsx
+++ b/packages/ra-ui-materialui/src/button/BulkUpdateButton.stories.tsx
@@ -9,6 +9,9 @@ import { AdminContext } from '../AdminContext';
import { AdminUI } from '../AdminUI';
import { List, Datagrid } from '../list';
import { TextField, NumberField } from '../field';
+import { deepmerge } from '@mui/utils';
+import { defaultLightTheme } from '../theme';
+import { ThemeOptions } from '@mui/material';
export default { title: 'ra-ui-materialui/button/BulkUpdateButton' };
@@ -89,9 +92,13 @@ const dataProvider = fakeRestDataProvider({
authors: [],
});
-const Wrapper = ({ bulkActionButtons }) => (
+const Wrapper = ({ bulkActionButtons, theme = undefined }) => (
-
+
(
}
/>
);
+
+export const Themed = () => (
+ }
+ theme={deepmerge(defaultLightTheme, {
+ components: {
+ RaBulkUpdateButton: {
+ defaultProps: {
+ label: 'Bulk Update',
+ mutationMode: 'optimistic',
+ className: 'custom-class',
+ 'data-testid': 'themed-button',
+ },
+ },
+ },
+ } as ThemeOptions)}
+ />
+);
diff --git a/packages/ra-ui-materialui/src/button/BulkUpdateButton.tsx b/packages/ra-ui-materialui/src/button/BulkUpdateButton.tsx
index 95ac44fd767..904f7ca98e4 100644
--- a/packages/ra-ui-materialui/src/button/BulkUpdateButton.tsx
+++ b/packages/ra-ui-materialui/src/button/BulkUpdateButton.tsx
@@ -1,4 +1,7 @@
import * as React from 'react';
+import { MutationMode } from 'ra-core';
+import { useThemeProps } from '@mui/material/styles';
+
import {
BulkUpdateWithConfirmButton,
BulkUpdateWithConfirmButtonProps,
@@ -7,7 +10,6 @@ import {
BulkUpdateWithUndoButton,
BulkUpdateWithUndoButtonProps,
} from './BulkUpdateWithUndoButton';
-import { MutationMode } from 'ra-core';
/**
* Updates the selected rows.
@@ -33,7 +35,14 @@ import { MutationMode } from 'ra-core';
* );
*/
export const BulkUpdateButton = (props: BulkUpdateButtonProps) => {
- const { mutationMode = 'undoable', data = defaultData, ...rest } = props;
+ const {
+ mutationMode = 'undoable',
+ data = defaultData,
+ ...rest
+ } = useThemeProps({
+ name: PREFIX,
+ props: props,
+ });
return mutationMode === 'undoable' ? (
@@ -54,3 +63,17 @@ export type BulkUpdateButtonProps = Props &
(BulkUpdateWithUndoButtonProps | BulkUpdateWithConfirmButtonProps);
const defaultData = [];
+
+const PREFIX = 'RaBulkUpdateButton';
+
+declare module '@mui/material/styles' {
+ interface ComponentsPropsList {
+ [PREFIX]: Partial;
+ }
+
+ interface Components {
+ [PREFIX]?: {
+ defaultProps?: ComponentsPropsList[typeof PREFIX];
+ };
+ }
+}
diff --git a/packages/ra-ui-materialui/src/button/CloneButton.spec.tsx b/packages/ra-ui-materialui/src/button/CloneButton.spec.tsx
index 5e87820c22c..239a815ef5f 100644
--- a/packages/ra-ui-materialui/src/button/CloneButton.spec.tsx
+++ b/packages/ra-ui-materialui/src/button/CloneButton.spec.tsx
@@ -4,6 +4,7 @@ import { render, screen } from '@testing-library/react';
import { AdminContext } from '../AdminContext';
import { CloneButton } from './CloneButton';
+import { Basic, Themed } from './CloneButton.stories';
const invalidButtonDomProps = {
record: { id: 123, foo: 'bar' },
@@ -12,14 +13,7 @@ const invalidButtonDomProps = {
describe('', () => {
it('should pass a clone of the record in the location state', () => {
- render(
-
-
-
- );
+ render();
expect(
screen.getByLabelText('ra.action.clone').getAttribute('href')
@@ -42,4 +36,12 @@ describe('', () => {
spy.mockRestore();
});
+
+ it('should be customized by a theme', async () => {
+ render();
+
+ const button = await screen.findByTestId('themed');
+ expect(button.textContent).toBe('Clone');
+ expect(button.classList).toContain('custom-class');
+ });
});
diff --git a/packages/ra-ui-materialui/src/button/CloneButton.stories.tsx b/packages/ra-ui-materialui/src/button/CloneButton.stories.tsx
new file mode 100644
index 00000000000..6450fba0936
--- /dev/null
+++ b/packages/ra-ui-materialui/src/button/CloneButton.stories.tsx
@@ -0,0 +1,41 @@
+import React from 'react';
+import { deepmerge } from '@mui/utils';
+import { ThemeOptions } from '@mui/material';
+
+import { defaultLightTheme } from '../theme';
+import { CloneButton } from './CloneButton';
+import { AdminContext } from '../AdminContext';
+
+export default { title: 'ra-ui-materialui/button/CloneButton' };
+
+const Wrapper = ({ children, ...props }) => {
+ return {children};
+};
+
+export const Basic = () => {
+ return (
+
+
+
+ );
+};
+
+export const Themed = () => {
+ return (
+
+
+
+ );
+};
diff --git a/packages/ra-ui-materialui/src/button/CloneButton.tsx b/packages/ra-ui-materialui/src/button/CloneButton.tsx
index 8ba06de05b1..c8d9bd93b66 100644
--- a/packages/ra-ui-materialui/src/button/CloneButton.tsx
+++ b/packages/ra-ui-materialui/src/button/CloneButton.tsx
@@ -4,10 +4,20 @@ import Queue from '@mui/icons-material/Queue';
import { Link } from 'react-router-dom';
import { stringify } from 'query-string';
import { useResourceContext, useRecordContext, useCreatePath } from 'ra-core';
+import {
+ ComponentsOverrides,
+ styled,
+ useThemeProps,
+} from '@mui/material/styles';
import { Button, ButtonProps } from './Button';
-export const CloneButton = (props: CloneButtonProps) => {
+export const CloneButton = (inProps: CloneButtonProps) => {
+ const props = useThemeProps({
+ props: inProps,
+ name: PREFIX,
+ });
+
const {
label = 'ra.action.clone',
scrollToTop = true,
@@ -19,7 +29,7 @@ export const CloneButton = (props: CloneButtonProps) => {
const createPath = useCreatePath();
const pathname = createPath({ resource, type: 'create' });
return (
-
+
);
};
@@ -63,3 +73,29 @@ interface Props {
export type CloneButtonProps = Props & Omit, 'to'>;
export default memo(CloneButton);
+
+const PREFIX = 'RaCloneButton';
+
+const StyledButton = styled(Button, {
+ name: PREFIX,
+ overridesResolver: (props, styles) => styles.root,
+})({});
+
+declare module '@mui/material/styles' {
+ interface ComponentNameToClassKey {
+ [PREFIX]: 'root';
+ }
+
+ interface ComponentsPropsList {
+ [PREFIX]: Partial;
+ }
+
+ interface Components {
+ [PREFIX]?: {
+ defaultProps?: ComponentsPropsList[typeof PREFIX];
+ styleOverrides?: ComponentsOverrides<
+ Omit
+ >[typeof PREFIX];
+ };
+ }
+}
diff --git a/packages/ra-ui-materialui/src/button/DeleteButton.spec.tsx b/packages/ra-ui-materialui/src/button/DeleteButton.spec.tsx
index cd6b365b871..a7e14481472 100644
--- a/packages/ra-ui-materialui/src/button/DeleteButton.spec.tsx
+++ b/packages/ra-ui-materialui/src/button/DeleteButton.spec.tsx
@@ -5,6 +5,7 @@ import {
NotificationDefault,
NotificationTranslated,
FullApp,
+ Themed,
} from './DeleteButton.stories';
describe('', () => {
@@ -28,6 +29,14 @@ describe('', () => {
expect(screen.queryAllByLabelText('Delete')).toHaveLength(1);
});
});
+
+ it('should be customized by a theme', async () => {
+ render();
+ const button = screen.queryByTestId('themed-button');
+ expect(button.classList).toContain('custom-class');
+ expect(button.textContent).toBe('Delete');
+ });
+
describe('success notification', () => {
it('should use a generic success message by default', async () => {
render();
diff --git a/packages/ra-ui-materialui/src/button/DeleteButton.stories.tsx b/packages/ra-ui-materialui/src/button/DeleteButton.stories.tsx
index 959493baf24..3e5983bc2c1 100644
--- a/packages/ra-ui-materialui/src/button/DeleteButton.stories.tsx
+++ b/packages/ra-ui-materialui/src/button/DeleteButton.stories.tsx
@@ -1,5 +1,5 @@
import * as React from 'react';
-import { colors, createTheme, Alert } from '@mui/material';
+import { colors, createTheme, Alert, ThemeOptions } from '@mui/material';
import polyglotI18nProvider from 'ra-i18n-polyglot';
import englishMessages from 'ra-language-english';
import frenchMessages from 'ra-language-french';
@@ -18,6 +18,8 @@ import { Datagrid } from '../list/datagrid/Datagrid';
import { TextField } from '../field/TextField';
import { AdminUI } from '../AdminUI';
import { Notification } from '../layout';
+import { deepmerge } from '@mui/utils';
+import { defaultLightTheme } from '../theme';
const theme = createTheme({
palette: {
@@ -385,3 +387,22 @@ export const SuccessMessage = () => {
);
};
+
+export const Themed = () => (
+
+
+
+
+
+);
diff --git a/packages/ra-ui-materialui/src/button/DeleteButton.tsx b/packages/ra-ui-materialui/src/button/DeleteButton.tsx
index 9d0aad98ae9..e1d9b688550 100644
--- a/packages/ra-ui-materialui/src/button/DeleteButton.tsx
+++ b/packages/ra-ui-materialui/src/button/DeleteButton.tsx
@@ -1,20 +1,22 @@
import * as React from 'react';
-import { UseMutationOptions } from '@tanstack/react-query';
import {
RaRecord,
- MutationMode,
- DeleteParams,
useRecordContext,
useSaveContext,
SaveContextValue,
- RedirectionSideEffect,
useResourceContext,
useCanAccess,
} from 'ra-core';
+import { useThemeProps } from '@mui/material/styles';
-import { ButtonProps } from './Button';
-import { DeleteWithUndoButton } from './DeleteWithUndoButton';
-import { DeleteWithConfirmButton } from './DeleteWithConfirmButton';
+import {
+ DeleteWithUndoButton,
+ DeleteWithUndoButtonProps,
+} from './DeleteWithUndoButton';
+import {
+ DeleteWithConfirmButton,
+ DeleteWithConfirmButtonProps,
+} from './DeleteWithConfirmButton';
/**
* Button used to delete a single record. Added by default by the of edit and show views.
@@ -28,7 +30,7 @@ import { DeleteWithConfirmButton } from './DeleteWithConfirmButton';
* @prop {string} variant Material UI variant for the button. Defaults to 'contained'.
* @prop {ReactElement} icon Override the icon. Defaults to the Delete icon from Material UI.
*
- * @param {Props} props
+ * @param {Props} inProps
*
* @example Usage in the of an form
*
@@ -51,8 +53,13 @@ import { DeleteWithConfirmButton } from './DeleteWithConfirmButton';
* };
*/
export const DeleteButton = (
- props: DeleteButtonProps
+ inProps: DeleteButtonProps
) => {
+ const props = useThemeProps({
+ name: PREFIX,
+ props: inProps,
+ });
+
const { mutationMode, ...rest } = props;
const record = useRecordContext(props);
const resource = useResourceContext(props);
@@ -89,23 +96,30 @@ export const DeleteButton = (
);
};
-export interface DeleteButtonProps<
+export type DeleteButtonProps<
RecordType extends RaRecord = any,
MutationOptionsError = unknown,
-> extends ButtonProps,
- SaveContextValue {
- confirmTitle?: React.ReactNode;
- confirmContent?: React.ReactNode;
- confirmColor?: 'primary' | 'warning';
- icon?: React.ReactNode;
- mutationMode?: MutationMode;
- mutationOptions?: UseMutationOptions<
- RecordType,
- MutationOptionsError,
- DeleteParams
- >;
- record?: RecordType;
- redirect?: RedirectionSideEffect;
- resource?: string;
- successMessage?: string;
+> = SaveContextValue &
+ (
+ | ({ mutationMode?: 'undoable' } & DeleteWithUndoButtonProps<
+ RecordType,
+ MutationOptionsError
+ >)
+ | ({
+ mutationMode?: 'pessimistic' | 'optimistic';
+ } & DeleteWithConfirmButtonProps)
+ );
+
+const PREFIX = 'RaDeleteButton';
+
+declare module '@mui/material/styles' {
+ interface ComponentsPropsList {
+ [PREFIX]: Partial;
+ }
+
+ interface Components {
+ [PREFIX]?: {
+ defaultProps?: ComponentsPropsList[typeof PREFIX];
+ };
+ }
}
diff --git a/packages/ra-ui-materialui/src/button/ListButton.spec.tsx b/packages/ra-ui-materialui/src/button/ListButton.spec.tsx
index d7bb59e0da1..f40616041d2 100644
--- a/packages/ra-ui-materialui/src/button/ListButton.spec.tsx
+++ b/packages/ra-ui-materialui/src/button/ListButton.spec.tsx
@@ -1,7 +1,7 @@
import * as React from 'react';
import { fireEvent, render, screen } from '@testing-library/react';
import expect from 'expect';
-import { Basic, AccessControl } from './ListButton.stories';
+import { Basic, AccessControl, Themed } from './ListButton.stories';
const invalidButtonDomProps = {
redirect: 'list',
@@ -30,4 +30,11 @@ describe('', () => {
fireEvent.click(screen.getByLabelText('Allow accessing books'));
await screen.findByLabelText('List');
});
+
+ it('should be customized by a theme', async () => {
+ render();
+ const button = screen.queryByTestId('themed-button');
+ expect(button.classList).toContain('custom-class');
+ expect(button.textContent).toBe('List');
+ });
});
diff --git a/packages/ra-ui-materialui/src/button/ListButton.stories.tsx b/packages/ra-ui-materialui/src/button/ListButton.stories.tsx
index ae34365664a..ef9e0631201 100644
--- a/packages/ra-ui-materialui/src/button/ListButton.stories.tsx
+++ b/packages/ra-ui-materialui/src/button/ListButton.stories.tsx
@@ -21,6 +21,9 @@ import { TextInput } from '../input/TextInput';
import { ListButton } from './ListButton';
import { Edit } from '../detail/Edit';
import { TopToolbar } from '../layout';
+import { deepmerge } from '@mui/utils';
+import { defaultLightTheme } from '../theme';
+import { ThemeOptions } from '@mui/material';
export default { title: 'ra-ui-materialui/button/ListButton' };
@@ -234,3 +237,29 @@ const dataProvider = fakeRestDataProvider({
],
authors: [],
});
+
+export const Themed = ({ buttonProps }: { buttonProps?: any }) => (
+
+
+
+
+
+
+
+
+
+);
diff --git a/packages/ra-ui-materialui/src/button/ListButton.tsx b/packages/ra-ui-materialui/src/button/ListButton.tsx
index 0450ceb150e..a24cdc4e311 100644
--- a/packages/ra-ui-materialui/src/button/ListButton.tsx
+++ b/packages/ra-ui-materialui/src/button/ListButton.tsx
@@ -2,6 +2,11 @@ import * as React from 'react';
import ActionList from '@mui/icons-material/List';
import { Link } from 'react-router-dom';
import { useResourceContext, useCreatePath, useCanAccess } from 'ra-core';
+import {
+ ComponentsOverrides,
+ styled,
+ useThemeProps,
+} from '@mui/material/styles';
import { Button, ButtonProps } from './Button';
@@ -31,7 +36,12 @@ import { Button, ButtonProps } from './Button';
*
* );
*/
-export const ListButton = (props: ListButtonProps) => {
+export const ListButton = (inProps: ListButtonProps) => {
+ const props = useThemeProps({
+ props: inProps,
+ name: PREFIX,
+ });
+
const {
icon = defaultIcon,
label = 'ra.action.list',
@@ -56,15 +66,15 @@ export const ListButton = (props: ListButtonProps) => {
}
return (
-
+
);
};
@@ -84,3 +94,29 @@ interface Props {
}
export type ListButtonProps = Props & ButtonProps;
+
+const PREFIX = 'RaListButton';
+
+const StyledButton = styled(Button, {
+ name: PREFIX,
+ overridesResolver: (props, styles) => styles.root,
+})({});
+
+declare module '@mui/material/styles' {
+ interface ComponentNameToClassKey {
+ [PREFIX]: 'root';
+ }
+
+ interface ComponentsPropsList {
+ [PREFIX]: Partial;
+ }
+
+ interface Components {
+ [PREFIX]?: {
+ defaultProps?: ComponentsPropsList[typeof PREFIX];
+ styleOverrides?: ComponentsOverrides<
+ Omit
+ >[typeof PREFIX];
+ };
+ }
+}
diff --git a/packages/ra-ui-materialui/src/button/RefreshButton.tsx b/packages/ra-ui-materialui/src/button/RefreshButton.tsx
index 571957ee86c..5b494b1e492 100644
--- a/packages/ra-ui-materialui/src/button/RefreshButton.tsx
+++ b/packages/ra-ui-materialui/src/button/RefreshButton.tsx
@@ -2,10 +2,20 @@ import * as React from 'react';
import { MouseEvent, useCallback } from 'react';
import NavigationRefresh from '@mui/icons-material/Refresh';
import { useRefresh } from 'ra-core';
+import {
+ ComponentsOverrides,
+ styled,
+ useThemeProps,
+} from '@mui/material/styles';
import { Button, ButtonProps } from './Button';
-export const RefreshButton = (props: RefreshButtonProps) => {
+export const RefreshButton = (inProps: RefreshButtonProps) => {
+ const props = useThemeProps({
+ props: inProps,
+ name: PREFIX,
+ });
+
const {
label = 'ra.action.refresh',
icon = defaultIcon,
@@ -25,9 +35,9 @@ export const RefreshButton = (props: RefreshButtonProps) => {
);
return (
-
+
);
};
@@ -40,3 +50,29 @@ interface Props {
}
export type RefreshButtonProps = Props & ButtonProps;
+
+const PREFIX = 'RaRefreshButton';
+
+const StyledButton = styled(Button, {
+ name: PREFIX,
+ overridesResolver: (props, styles) => styles.root,
+})({});
+
+declare module '@mui/material/styles' {
+ interface ComponentNameToClassKey {
+ [PREFIX]: 'root';
+ }
+
+ interface ComponentsPropsList {
+ [PREFIX]: Partial;
+ }
+
+ interface Components {
+ [PREFIX]?: {
+ defaultProps?: ComponentsPropsList[typeof PREFIX];
+ styleOverrides?: ComponentsOverrides<
+ Omit
+ >[typeof PREFIX];
+ };
+ }
+}
diff --git a/packages/ra-ui-materialui/src/button/ShowButton.spec.tsx b/packages/ra-ui-materialui/src/button/ShowButton.spec.tsx
index 8edb3163ef8..7404ba141ae 100644
--- a/packages/ra-ui-materialui/src/button/ShowButton.spec.tsx
+++ b/packages/ra-ui-materialui/src/button/ShowButton.spec.tsx
@@ -1,7 +1,7 @@
import * as React from 'react';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import expect from 'expect';
-import { Basic, AccessControl } from './ShowButton.stories';
+import { Basic, AccessControl, Themed } from './ShowButton.stories';
const invalidButtonDomProps = {
redirect: 'list',
@@ -43,4 +43,11 @@ describe('', () => {
expect(screen.queryAllByLabelText('Show')).toHaveLength(1);
});
});
+
+ it('should be customized by a theme', async () => {
+ render();
+ const button = screen.queryByTestId('themed-button');
+ expect(button.classList).toContain('custom-class');
+ expect(button.textContent).toBe('Show');
+ });
});
diff --git a/packages/ra-ui-materialui/src/button/ShowButton.stories.tsx b/packages/ra-ui-materialui/src/button/ShowButton.stories.tsx
index c61d1060cf5..6919214e543 100644
--- a/packages/ra-ui-materialui/src/button/ShowButton.stories.tsx
+++ b/packages/ra-ui-materialui/src/button/ShowButton.stories.tsx
@@ -19,6 +19,9 @@ import { TextField } from '../field/TextField';
import ShowButton from './ShowButton';
import { Show } from '../detail/Show';
import { SimpleShowLayout } from '../detail/SimpleShowLayout';
+import { deepmerge } from '@mui/utils';
+import { defaultLightTheme } from '../theme';
+import { ThemeOptions } from '@mui/material';
export default { title: 'ra-ui-materialui/button/ShowButton' };
@@ -248,3 +251,29 @@ const dataProvider = fakeRestDataProvider({
],
authors: [],
});
+
+export const Themed = ({ buttonProps }: { buttonProps?: any }) => (
+
+
+
+
+
+
+
+
+
+);
diff --git a/packages/ra-ui-materialui/src/button/ShowButton.tsx b/packages/ra-ui-materialui/src/button/ShowButton.tsx
index 6c5d596a508..ac42adf8dea 100644
--- a/packages/ra-ui-materialui/src/button/ShowButton.tsx
+++ b/packages/ra-ui-materialui/src/button/ShowButton.tsx
@@ -9,6 +9,11 @@ import {
useCreatePath,
useCanAccess,
} from 'ra-core';
+import {
+ ComponentsOverrides,
+ styled,
+ useThemeProps,
+} from '@mui/material/styles';
import { Button, ButtonProps } from './Button';
@@ -26,8 +31,13 @@ import { Button, ButtonProps } from './Button';
* };
*/
const ShowButton = (
- props: ShowButtonProps
+ inProps: ShowButtonProps
) => {
+ const props = useThemeProps({
+ props: inProps,
+ name: PREFIX,
+ });
+
const {
icon = defaultIcon,
label = 'ra.action.show',
@@ -51,7 +61,7 @@ const ShowButton = (
});
if (!record || !canAccess || isPending) return null;
return (
-
+
);
};
@@ -98,3 +108,29 @@ const PureShowButton = memo(
);
export default PureShowButton;
+
+const PREFIX = 'RaShowButton';
+
+const StyledButton = styled(Button, {
+ name: PREFIX,
+ overridesResolver: (props, styles) => styles.root,
+})({});
+
+declare module '@mui/material/styles' {
+ interface ComponentNameToClassKey {
+ [PREFIX]: 'root';
+ }
+
+ interface ComponentsPropsList {
+ [PREFIX]: Partial;
+ }
+
+ interface Components {
+ [PREFIX]?: {
+ defaultProps?: ComponentsPropsList[typeof PREFIX];
+ styleOverrides?: ComponentsOverrides<
+ Omit
+ >[typeof PREFIX];
+ };
+ }
+}
diff --git a/packages/ra-ui-materialui/src/button/ToggleThemeButton.tsx b/packages/ra-ui-materialui/src/button/ToggleThemeButton.tsx
index f0977468f14..e86d12cb77b 100644
--- a/packages/ra-ui-materialui/src/button/ToggleThemeButton.tsx
+++ b/packages/ra-ui-materialui/src/button/ToggleThemeButton.tsx
@@ -1,5 +1,10 @@
import React from 'react';
import { Tooltip, IconButton, useMediaQuery } from '@mui/material';
+import {
+ ComponentsOverrides,
+ styled,
+ useThemeProps,
+} from '@mui/material/styles';
import Brightness4Icon from '@mui/icons-material/Brightness4';
import Brightness7Icon from '@mui/icons-material/Brightness7';
import { useTranslate } from 'ra-core';
@@ -25,6 +30,11 @@ import { useThemesContext, useTheme } from '../theme';
* );
*/
export const ToggleThemeButton = () => {
+ const props = useThemeProps({
+ props: {},
+ name: PREFIX,
+ });
+
const translate = useTranslate();
const { darkTheme, defaultTheme } = useThemesContext();
const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)', {
@@ -43,13 +53,35 @@ export const ToggleThemeButton = () => {
return (
-
{theme === 'dark' ? : }
-
+
);
};
+
+const PREFIX = 'RaToggleThemeButton';
+
+const StyledIconButton = styled(IconButton, {
+ name: PREFIX,
+ overridesResolver: (props, styles) => styles.root,
+})({});
+
+declare module '@mui/material/styles' {
+ interface ComponentNameToClassKey {
+ [PREFIX]: 'root';
+ }
+
+ interface Components {
+ [PREFIX]?: {
+ styleOverrides?: ComponentsOverrides<
+ Omit
+ >[typeof PREFIX];
+ };
+ }
+}
diff --git a/packages/ra-ui-materialui/src/button/UpdateButton.spec.tsx b/packages/ra-ui-materialui/src/button/UpdateButton.spec.tsx
new file mode 100644
index 00000000000..7f2efec2b13
--- /dev/null
+++ b/packages/ra-ui-materialui/src/button/UpdateButton.spec.tsx
@@ -0,0 +1,13 @@
+import { render, screen } from '@testing-library/react';
+import expect from 'expect';
+import * as React from 'react';
+import { Themed } from './UpdateButton.stories';
+
+describe('UpdateButton', () => {
+ it('should be customized by a theme', async () => {
+ render();
+ expect(screen.queryByTestId('themed-button').classList).toContain(
+ 'custom-class'
+ );
+ });
+});
diff --git a/packages/ra-ui-materialui/src/button/UpdateButton.stories.tsx b/packages/ra-ui-materialui/src/button/UpdateButton.stories.tsx
index c462db62fed..cbb1b30f928 100644
--- a/packages/ra-ui-materialui/src/button/UpdateButton.stories.tsx
+++ b/packages/ra-ui-materialui/src/button/UpdateButton.stories.tsx
@@ -17,6 +17,9 @@ import { NumberField, TextField } from '../field';
import { Show, SimpleShowLayout } from '../detail';
import { TopToolbar } from '../layout';
import { Datagrid, List } from '../list';
+import { deepmerge } from '@mui/utils';
+import { defaultLightTheme } from '../theme';
+import { ThemeOptions } from '@mui/material';
export default { title: 'ra-ui-materialui/button/UpdateButton' };
@@ -309,3 +312,26 @@ export const SideEffects = () => (
);
+
+export const Themed = () => (
+
+
+
+ } />
+
+
+
+);
diff --git a/packages/ra-ui-materialui/src/button/UpdateButton.tsx b/packages/ra-ui-materialui/src/button/UpdateButton.tsx
index 643aa2cb8d7..19b06477aa4 100644
--- a/packages/ra-ui-materialui/src/button/UpdateButton.tsx
+++ b/packages/ra-ui-materialui/src/button/UpdateButton.tsx
@@ -7,6 +7,7 @@ import {
UpdateWithUndoButton,
UpdateWithUndoButtonProps,
} from './UpdateWithUndoButton';
+import { useThemeProps } from '@mui/material/styles';
/**
* Updates the current record.
@@ -30,7 +31,10 @@ import {
* );
*/
export const UpdateButton = (props: UpdateButtonProps) => {
- const { mutationMode = 'undoable', ...rest } = props;
+ const { mutationMode = 'undoable', ...rest } = useThemeProps({
+ name: PREFIX,
+ props: props,
+ });
return mutationMode === 'undoable' ? (
@@ -46,3 +50,17 @@ export type UpdateButtonProps =
| ({
mutationMode?: 'pessimistic' | 'optimistic';
} & UpdateWithConfirmButtonProps);
+
+const PREFIX = 'RaUpdateButton';
+
+declare module '@mui/material/styles' {
+ interface ComponentsPropsList {
+ [PREFIX]: Partial;
+ }
+
+ interface Components {
+ [PREFIX]?: {
+ defaultProps?: ComponentsPropsList[typeof PREFIX];
+ };
+ }
+}
diff --git a/packages/ra-ui-materialui/src/detail/Create.spec.tsx b/packages/ra-ui-materialui/src/detail/Create.spec.tsx
index 49a3a3aded8..abffa876427 100644
--- a/packages/ra-ui-materialui/src/detail/Create.spec.tsx
+++ b/packages/ra-ui-materialui/src/detail/Create.spec.tsx
@@ -11,6 +11,7 @@ import {
TitleElement,
NotificationDefault,
NotificationTranslated,
+ Themed,
} from './Create.stories';
describe('', () => {
@@ -49,6 +50,13 @@ describe('', () => {
expect(screen.queryAllByText('help')).toHaveLength(1);
});
+ it('should be customized by a theme', async () => {
+ render();
+ expect(screen.queryByTestId('themed-view').classList).toContain(
+ 'custom-class'
+ );
+ });
+
describe('title', () => {
it('should display by default the title of the resource', async () => {
render();
diff --git a/packages/ra-ui-materialui/src/detail/Create.stories.tsx b/packages/ra-ui-materialui/src/detail/Create.stories.tsx
index 70e53bdae84..e725efdf7fa 100644
--- a/packages/ra-ui-materialui/src/detail/Create.stories.tsx
+++ b/packages/ra-ui-materialui/src/detail/Create.stories.tsx
@@ -9,13 +9,15 @@ import {
} from 'ra-core';
import polyglotI18nProvider from 'ra-i18n-polyglot';
import englishMessages from 'ra-language-english';
-import { Box, Card, Stack } from '@mui/material';
+import { Box, Card, Stack, ThemeOptions } from '@mui/material';
import { TextInput } from '../input';
import { SimpleForm } from '../form/SimpleForm';
import { ListButton, SaveButton } from '../button';
import TopToolbar from '../layout/TopToolbar';
import { Create } from './Create';
+import { deepmerge } from '@mui/utils';
+import { defaultLightTheme } from '../theme';
export default { title: 'ra-ui-materialui/detail/Create' };
@@ -270,3 +272,29 @@ export const Default = () => (
);
+
+export const Themed = () => (
+
+
+ (
+
+
+
+ )}
+ />
+
+
+);
diff --git a/packages/ra-ui-materialui/src/detail/Create.tsx b/packages/ra-ui-materialui/src/detail/Create.tsx
index 80517d67e11..34468e938f6 100644
--- a/packages/ra-ui-materialui/src/detail/Create.tsx
+++ b/packages/ra-ui-materialui/src/detail/Create.tsx
@@ -7,6 +7,7 @@ import {
RaRecord,
useCheckMinimumRequiredProps,
} from 'ra-core';
+import { useThemeProps } from '@mui/material/styles';
import { CreateView, CreateViewProps } from './CreateView';
import { Loading } from '../layout';
@@ -58,8 +59,13 @@ export const Create = <
RecordType extends Omit = any,
ResultRecordType extends RaRecord = RecordType & { id: Identifier },
>(
- props: CreateProps
+ inProps: CreateProps
): ReactElement => {
+ const props = useThemeProps({
+ props: inProps,
+ name: PREFIX,
+ });
+
useCheckMinimumRequiredProps('Create', ['children'], props);
const {
resource,
@@ -100,3 +106,5 @@ export interface CreateProps<
Omit {}
const defaultLoading = ;
+
+const PREFIX = 'RaCreate'; // Types declared in CreateView.
diff --git a/packages/ra-ui-materialui/src/detail/CreateView.tsx b/packages/ra-ui-materialui/src/detail/CreateView.tsx
index 9a912e6911d..c8adbe49f15 100644
--- a/packages/ra-ui-materialui/src/detail/CreateView.tsx
+++ b/packages/ra-ui-materialui/src/detail/CreateView.tsx
@@ -12,6 +12,7 @@ import { useCreateContext } from 'ra-core';
import clsx from 'clsx';
import { Title } from '../layout';
+import { CreateProps } from './Create';
export const CreateView = (inProps: CreateViewProps) => {
const props = useThemeProps({
@@ -94,7 +95,7 @@ declare module '@mui/material/styles' {
}
interface ComponentsPropsList {
- RaCreate: Partial;
+ RaCreate: Partial;
}
interface Components {
diff --git a/packages/ra-ui-materialui/src/detail/Edit.spec.tsx b/packages/ra-ui-materialui/src/detail/Edit.spec.tsx
index 39369cc9738..58c7fbd5845 100644
--- a/packages/ra-ui-materialui/src/detail/Edit.spec.tsx
+++ b/packages/ra-ui-materialui/src/detail/Edit.spec.tsx
@@ -28,6 +28,7 @@ import {
NotificationDefault,
NotificationTranslated,
EmptyWhileLoading,
+ Themed,
} from './Edit.stories';
describe('', () => {
@@ -140,6 +141,13 @@ describe('', () => {
expect(screen.queryByText('Something went wrong')).toBeNull();
});
+ it('should be customized by a theme', async () => {
+ render();
+ expect(screen.queryByTestId('themed-view').classList).toContain(
+ 'custom-class'
+ );
+ });
+
describe('mutationMode prop', () => {
it('should be undoable by default', async () => {
let post = { id: 1234, title: 'lorem' };
diff --git a/packages/ra-ui-materialui/src/detail/Edit.stories.tsx b/packages/ra-ui-materialui/src/detail/Edit.stories.tsx
index a043c913fef..3ff2c151d0a 100644
--- a/packages/ra-ui-materialui/src/detail/Edit.stories.tsx
+++ b/packages/ra-ui-materialui/src/detail/Edit.stories.tsx
@@ -9,13 +9,15 @@ import {
} from 'ra-core';
import polyglotI18nProvider from 'ra-i18n-polyglot';
import englishMessages from 'ra-language-english';
-import { Box, Card, Stack, Typography } from '@mui/material';
+import { Box, Card, Stack, ThemeOptions, Typography } from '@mui/material';
import { TextInput } from '../input';
import { SimpleForm } from '../form/SimpleForm';
import { ShowButton, SaveButton } from '../button';
import TopToolbar from '../layout/TopToolbar';
import { Edit } from './Edit';
+import { deepmerge } from '@mui/utils';
+import { defaultLightTheme } from '../theme';
export default { title: 'ra-ui-materialui/detail/Edit' };
@@ -362,3 +364,29 @@ const AsideComponentWithRecord = () => {
);
};
+
+export const Themed = () => (
+
+
+ (
+
+
+
+ )}
+ />
+
+
+);
diff --git a/packages/ra-ui-materialui/src/detail/Edit.tsx b/packages/ra-ui-materialui/src/detail/Edit.tsx
index 4f78cc70fb4..be155172461 100644
--- a/packages/ra-ui-materialui/src/detail/Edit.tsx
+++ b/packages/ra-ui-materialui/src/detail/Edit.tsx
@@ -5,6 +5,8 @@ import {
RaRecord,
EditBaseProps,
} from 'ra-core';
+import { useThemeProps } from '@mui/material/styles';
+
import { EditView, EditViewProps } from './EditView';
import { Loading } from '../layout';
@@ -54,8 +56,13 @@ import { Loading } from '../layout';
* export default App;
*/
export const Edit = (
- props: EditProps
+ inProps: EditProps
) => {
+ const props = useThemeProps({
+ props: inProps,
+ name: PREFIX,
+ });
+
useCheckMinimumRequiredProps('Edit', ['children'], props);
const {
resource,
@@ -91,3 +98,5 @@ export interface EditProps
Omit {}
const defaultLoading = ;
+
+const PREFIX = 'RaEdit'; // Types declared in EditView.
diff --git a/packages/ra-ui-materialui/src/detail/EditView.tsx b/packages/ra-ui-materialui/src/detail/EditView.tsx
index c40ef18c952..67d8b7dd314 100644
--- a/packages/ra-ui-materialui/src/detail/EditView.tsx
+++ b/packages/ra-ui-materialui/src/detail/EditView.tsx
@@ -14,6 +14,7 @@ import { useEditContext, useResourceDefinition } from 'ra-core';
import { EditActions } from './EditActions';
import { Title } from '../layout';
+import { EditProps } from './Edit';
const defaultActions = ;
@@ -106,7 +107,7 @@ declare module '@mui/material/styles' {
}
interface ComponentsPropsList {
- RaEdit: Partial;
+ RaEdit: Partial;
}
interface Components {
diff --git a/packages/ra-ui-materialui/src/detail/Show.spec.tsx b/packages/ra-ui-materialui/src/detail/Show.spec.tsx
index 18d6453735b..64c3a0a475f 100644
--- a/packages/ra-ui-materialui/src/detail/Show.spec.tsx
+++ b/packages/ra-ui-materialui/src/detail/Show.spec.tsx
@@ -24,6 +24,7 @@ import {
Title,
TitleFalse,
TitleElement,
+ Themed,
} from './Show.stories';
import { Show } from './Show';
@@ -129,6 +130,13 @@ describe('', () => {
expect(screen.getByTestId('custom-component')).toBeDefined();
});
+ it('should be customized by a theme', async () => {
+ render();
+ expect(screen.queryByTestId('themed-view').classList).toContain(
+ 'custom-class'
+ );
+ });
+
describe('title', () => {
it('should display by default the title of the resource', async () => {
render();
diff --git a/packages/ra-ui-materialui/src/detail/Show.stories.tsx b/packages/ra-ui-materialui/src/detail/Show.stories.tsx
index 4e9ecc5e71a..bf9d9b166ec 100644
--- a/packages/ra-ui-materialui/src/detail/Show.stories.tsx
+++ b/packages/ra-ui-materialui/src/detail/Show.stories.tsx
@@ -1,13 +1,16 @@
import * as React from 'react';
import { Admin } from 'react-admin';
import { Resource, useRecordContext, TestMemoryRouter } from 'ra-core';
-import { Box, Card, Stack } from '@mui/material';
+import { Box, Card, Stack, ThemeOptions } from '@mui/material';
+
import { TextField } from '../field';
import { Labeled } from '../Labeled';
import { SimpleShowLayout } from './SimpleShowLayout';
import { EditButton } from '../button';
import TopToolbar from '../layout/TopToolbar';
import { Show } from './Show';
+import { deepmerge } from '@mui/utils';
+import { defaultLightTheme } from '../theme';
export default { title: 'ra-ui-materialui/detail/Show' };
@@ -244,3 +247,29 @@ export const Default = () => (
);
+
+export const Themed = () => (
+
+
+ (
+
+
+
+ )}
+ />
+
+
+);
diff --git a/packages/ra-ui-materialui/src/detail/Show.tsx b/packages/ra-ui-materialui/src/detail/Show.tsx
index 58df5bb7a05..850bdfe0d2c 100644
--- a/packages/ra-ui-materialui/src/detail/Show.tsx
+++ b/packages/ra-ui-materialui/src/detail/Show.tsx
@@ -1,6 +1,8 @@
import * as React from 'react';
import { ReactElement } from 'react';
import { ShowBase, RaRecord, ShowBaseProps } from 'ra-core';
+import { useThemeProps } from '@mui/material/styles';
+
import { ShowView, ShowViewProps } from './ShowView';
import { Loading } from '../layout';
@@ -43,40 +45,53 @@ import { Loading } from '../layout';
* );
* export default App;
*
- * @param {ShowProps} props
- * @param {ReactElement|false} props.actions An element to display above the page content, or false to disable actions.
- * @param {string} props.className A className to apply to the page content.
- * @param {ElementType} props.component The component to use as root component (div by default).
- * @param {boolean} props.emptyWhileLoading Do not display the page content while loading the initial data.
- * @param {string} props.id The id of the resource to display (grabbed from the route params if not defined).
- * @param {Object} props.queryClient Options to pass to the react-query useQuery hook.
- * @param {string} props.resource The resource to fetch from the data provider (grabbed from the ResourceContext if not defined).
- * @param {Object} props.sx Custom style object.
- * @param {ElementType|string} props.title The title of the page. Defaults to `#{resource} #${id}`.
+ * @param {ShowProps} inProps
+ * @param {ReactElement|false} inProps.actions An element to display above the page content, or false to disable actions.
+ * @param {string} inProps.className A className to apply to the page content.
+ * @param {ElementType} inProps.component The component to use as root component (div by default).
+ * @param {boolean} inProps.emptyWhileLoading Do not display the page content while loading the initial data.
+ * @param {string} inProps.id The id of the resource to display (grabbed from the route params if not defined).
+ * @param {Object} inProps.queryClient Options to pass to the react-query useQuery hook.
+ * @param {string} inProps.resource The resource to fetch from the data provider (grabbed from the ResourceContext if not defined).
+ * @param {Object} inProps.sx Custom style object.
+ * @param {ElementType|string} inProps.title The title of the page. Defaults to `#{resource} #${id}`.
*
* @see ShowView for the actual rendering
*/
-export const Show = ({
- id,
- resource,
- queryOptions,
- disableAuthentication,
- loading = defaultLoading,
- ...rest
-}: ShowProps): ReactElement => (
-
- id={id}
- disableAuthentication={disableAuthentication}
- queryOptions={queryOptions}
- resource={resource}
- loading={loading}
- >
-
-
-);
+export const Show = (
+ inProps: ShowProps
+): ReactElement => {
+ const props = useThemeProps({
+ props: inProps,
+ name: PREFIX,
+ });
+
+ const {
+ id,
+ resource,
+ queryOptions,
+ disableAuthentication,
+ loading = defaultLoading,
+ ...rest
+ } = props;
+
+ return (
+
+ id={id}
+ disableAuthentication={disableAuthentication}
+ queryOptions={queryOptions}
+ resource={resource}
+ loading={loading}
+ >
+
+
+ );
+};
export interface ShowProps
extends ShowBaseProps,
Omit {}
const defaultLoading = ;
+
+const PREFIX = 'RaShow'; // Types declared in ShowView.
diff --git a/packages/ra-ui-materialui/src/detail/ShowView.tsx b/packages/ra-ui-materialui/src/detail/ShowView.tsx
index 5e8d02d35a6..4fb840a8019 100644
--- a/packages/ra-ui-materialui/src/detail/ShowView.tsx
+++ b/packages/ra-ui-materialui/src/detail/ShowView.tsx
@@ -12,6 +12,7 @@ import clsx from 'clsx';
import { useShowContext, useResourceDefinition } from 'ra-core';
import { ShowActions } from './ShowActions';
import { Title } from '../layout';
+import { ShowProps } from './Show';
const defaultActions = ;
@@ -101,7 +102,7 @@ declare module '@mui/material/styles' {
}
interface ComponentsPropsList {
- RaShow: Partial;
+ RaShow: Partial;
}
interface Components {
diff --git a/packages/ra-ui-materialui/src/list/InfiniteList.spec.tsx b/packages/ra-ui-materialui/src/list/InfiniteList.spec.tsx
new file mode 100644
index 00000000000..9fecc9facf5
--- /dev/null
+++ b/packages/ra-ui-materialui/src/list/InfiniteList.spec.tsx
@@ -0,0 +1,14 @@
+import * as React from 'react';
+import expect from 'expect';
+import { render, screen } from '@testing-library/react';
+
+import { Themed } from './List.stories';
+
+describe('', () => {
+ it('should be customized by a theme', async () => {
+ render();
+ expect(screen.queryByTestId('themed-list').classList).toContain(
+ 'custom-class'
+ );
+ });
+});
diff --git a/packages/ra-ui-materialui/src/list/InfiniteList.stories.tsx b/packages/ra-ui-materialui/src/list/InfiniteList.stories.tsx
index a19630c7dad..0be4e22c6a5 100644
--- a/packages/ra-ui-materialui/src/list/InfiniteList.stories.tsx
+++ b/packages/ra-ui-materialui/src/list/InfiniteList.stories.tsx
@@ -8,7 +8,7 @@ import {
useInfinitePaginationContext,
TestMemoryRouter,
} from 'ra-core';
-import { Box, Button, Card, Typography } from '@mui/material';
+import { Box, Button, Card, ThemeOptions, Typography } from '@mui/material';
import { InfiniteList } from './InfiniteList';
import { SimpleList } from './SimpleList';
@@ -24,6 +24,8 @@ import { SearchInput } from '../input';
import { BulkDeleteButton, SelectAllButton, SortButton } from '../button';
import { TopToolbar, Layout } from '../layout';
import { BulkActionsToolbar } from './BulkActionsToolbar';
+import { deepmerge } from '@mui/utils';
+import { defaultLightTheme } from '../theme';
export default {
title: 'ra-ui-materialui/list/InfiniteList',
@@ -89,11 +91,12 @@ const dataProvider = fakeRestProvider(
500
);
-const Admin = ({ children, dataProvider, layout }: any) => (
+const Admin = ({ children, dataProvider, layout, ...props }: any) => (
defaultMessages, 'en')}
+ {...props}
>
{children}
@@ -437,3 +440,31 @@ export const PartialPagination = () => (
/>
);
+
+export const Themed = () => (
+
+ (
+
+
+
+ )}
+ />
+
+);
diff --git a/packages/ra-ui-materialui/src/list/InfiniteList.tsx b/packages/ra-ui-materialui/src/list/InfiniteList.tsx
index 97395664ffc..e27ea5d683b 100644
--- a/packages/ra-ui-materialui/src/list/InfiniteList.tsx
+++ b/packages/ra-ui-materialui/src/list/InfiniteList.tsx
@@ -1,6 +1,7 @@
import * as React from 'react';
import { ReactElement } from 'react';
import { InfiniteListBase, InfiniteListBaseProps, RaRecord } from 'ra-core';
+import { useThemeProps } from '@mui/material/styles';
import { InfinitePagination } from './pagination';
import { ListView, ListViewProps } from './ListView';
@@ -59,39 +60,48 @@ import { Loading } from '../layout';
*
* );
*/
-export const InfiniteList = ({
- debounce,
- disableAuthentication,
- disableSyncWithLocation,
- exporter,
- filter = defaultFilter,
- filterDefaultValues,
- loading = defaultLoading,
- pagination = defaultPagination,
- perPage = 10,
- queryOptions,
- resource,
- sort,
- storeKey,
- ...rest
-}: InfiniteListProps): ReactElement => (
-
- debounce={debounce}
- disableAuthentication={disableAuthentication}
- disableSyncWithLocation={disableSyncWithLocation}
- exporter={exporter}
- filter={filter}
- filterDefaultValues={filterDefaultValues}
- loading={loading}
- perPage={perPage}
- queryOptions={queryOptions}
- resource={resource}
- sort={sort}
- storeKey={storeKey}
- >
- {...rest} pagination={pagination} />
-
-);
+export const InfiniteList = (
+ props: InfiniteListProps
+): ReactElement => {
+ const {
+ debounce,
+ disableAuthentication,
+ disableSyncWithLocation,
+ exporter,
+ filter = defaultFilter,
+ filterDefaultValues,
+ loading = defaultLoading,
+ pagination = defaultPagination,
+ perPage = 10,
+ queryOptions,
+ resource,
+ sort,
+ storeKey,
+ ...rest
+ } = useThemeProps({
+ props: props,
+ name: PREFIX,
+ });
+
+ return (
+
+ debounce={debounce}
+ disableAuthentication={disableAuthentication}
+ disableSyncWithLocation={disableSyncWithLocation}
+ exporter={exporter}
+ filter={filter}
+ filterDefaultValues={filterDefaultValues}
+ loading={loading}
+ perPage={perPage}
+ queryOptions={queryOptions}
+ resource={resource}
+ sort={sort}
+ storeKey={storeKey}
+ >
+ {...rest} pagination={pagination} />
+
+ );
+};
const defaultPagination = ;
const defaultFilter = {};
@@ -100,3 +110,17 @@ const defaultLoading = ;
export interface InfiniteListProps
extends InfiniteListBaseProps,
ListViewProps {}
+
+const PREFIX = 'RaInfiniteList';
+
+declare module '@mui/material/styles' {
+ interface ComponentsPropsList {
+ [PREFIX]: Partial;
+ }
+
+ interface Components {
+ [PREFIX]?: {
+ defaultProps?: ComponentsPropsList[typeof PREFIX];
+ };
+ }
+}
diff --git a/packages/ra-ui-materialui/src/list/List.spec.tsx b/packages/ra-ui-materialui/src/list/List.spec.tsx
index a26abdb22a2..a4da84e28df 100644
--- a/packages/ra-ui-materialui/src/list/List.spec.tsx
+++ b/packages/ra-ui-materialui/src/list/List.spec.tsx
@@ -9,7 +9,7 @@ import {
} from 'ra-core';
import { createTheme, ThemeProvider } from '@mui/material/styles';
-import { defaultTheme } from '../theme/defaultTheme';
+import { defaultTheme } from '../theme';
import { List } from './List';
import { Filter } from './filter';
import { TextInput } from '../input';
@@ -22,6 +22,7 @@ import {
PartialPagination,
Default,
SelectAllLimit,
+ Themed,
} from './List.stories';
const theme = createTheme(defaultTheme);
@@ -112,6 +113,13 @@ describe('
', () => {
expect(screen.queryAllByText('Hello')).toHaveLength(1);
});
+ it('should be customized by a theme', async () => {
+ render();
+ expect(screen.queryByTestId('themed-list').classList).toContain(
+ 'custom-class'
+ );
+ });
+
describe('empty', () => {
it('should render an invite when the list is empty', async () => {
const Dummy = () => {
diff --git a/packages/ra-ui-materialui/src/list/List.stories.tsx b/packages/ra-ui-materialui/src/list/List.stories.tsx
index 31f3bed3cde..4bf5ba41d29 100644
--- a/packages/ra-ui-materialui/src/list/List.stories.tsx
+++ b/packages/ra-ui-materialui/src/list/List.stories.tsx
@@ -8,7 +8,14 @@ import {
DataProvider,
} from 'ra-core';
import fakeRestDataProvider from 'ra-data-fakerest';
-import { Box, Card, Typography, Button, Link as MuiLink } from '@mui/material';
+import {
+ Box,
+ Card,
+ Typography,
+ Button,
+ Link as MuiLink,
+ ThemeOptions,
+} from '@mui/material';
import { List } from './List';
import { SimpleList } from './SimpleList';
@@ -22,6 +29,8 @@ import { BulkDeleteButton, ListButton, SelectAllButton } from '../button';
import { ShowGuesser } from '../detail';
import TopToolbar from '../layout/TopToolbar';
import { BulkActionsToolbar } from './BulkActionsToolbar';
+import { deepmerge } from '@mui/utils';
+import { defaultLightTheme, RaThemeOptions } from '../theme';
export default { title: 'ra-ui-materialui/list/List' };
@@ -801,3 +810,39 @@ export const ResponseMetadata = () => (
);
+
+export const Themed = () => (
+
+
+ (
+
+
+
+ )}
+ />
+
+
+);
diff --git a/packages/ra-ui-materialui/src/list/List.tsx b/packages/ra-ui-materialui/src/list/List.tsx
index e562bf7ad50..503e64272da 100644
--- a/packages/ra-ui-materialui/src/list/List.tsx
+++ b/packages/ra-ui-materialui/src/list/List.tsx
@@ -1,6 +1,7 @@
import * as React from 'react';
import { ReactElement } from 'react';
import { ListBase, ListBaseProps, RaRecord } from 'ra-core';
+import { useThemeProps } from '@mui/material/styles';
import { ListView, ListViewProps } from './ListView';
import { Loading } from '../layout';
@@ -55,38 +56,47 @@ import { Loading } from '../layout';
*
* );
*/
-export const List = ({
- debounce,
- disableAuthentication,
- disableSyncWithLocation,
- exporter,
- filter = defaultFilter,
- filterDefaultValues,
- loading = defaultLoading,
- perPage = 10,
- queryOptions,
- resource,
- sort,
- storeKey,
- ...rest
-}: ListProps): ReactElement => (
-
- debounce={debounce}
- disableAuthentication={disableAuthentication}
- disableSyncWithLocation={disableSyncWithLocation}
- exporter={exporter}
- filter={filter}
- filterDefaultValues={filterDefaultValues}
- loading={loading}
- perPage={perPage}
- queryOptions={queryOptions}
- resource={resource}
- sort={sort}
- storeKey={storeKey}
- >
- {...rest} />
-
-);
+export const List = (
+ props: ListProps
+): ReactElement => {
+ const {
+ debounce,
+ disableAuthentication,
+ disableSyncWithLocation,
+ exporter,
+ filter = defaultFilter,
+ filterDefaultValues,
+ loading = defaultLoading,
+ perPage = 10,
+ queryOptions,
+ resource,
+ sort,
+ storeKey,
+ ...rest
+ } = useThemeProps({
+ props: props,
+ name: PREFIX,
+ });
+
+ return (
+
+ debounce={debounce}
+ disableAuthentication={disableAuthentication}
+ disableSyncWithLocation={disableSyncWithLocation}
+ exporter={exporter}
+ filter={filter}
+ filterDefaultValues={filterDefaultValues}
+ loading={loading}
+ perPage={perPage}
+ queryOptions={queryOptions}
+ resource={resource}
+ sort={sort}
+ storeKey={storeKey}
+ >
+ {...rest} />
+
+ );
+};
export interface ListProps
extends ListBaseProps,
@@ -94,3 +104,5 @@ export interface ListProps
const defaultFilter = {};
const defaultLoading = ;
+
+const PREFIX = 'RaList'; // Types declared in ListView.
diff --git a/packages/ra-ui-materialui/src/list/ListView.tsx b/packages/ra-ui-materialui/src/list/ListView.tsx
index 1e5fc999fef..1838795287d 100644
--- a/packages/ra-ui-materialui/src/list/ListView.tsx
+++ b/packages/ra-ui-materialui/src/list/ListView.tsx
@@ -16,6 +16,7 @@ import { ListToolbar } from './ListToolbar';
import { Pagination as DefaultPagination } from './pagination';
import { ListActions as DefaultActions } from './ListActions';
import { Empty } from './Empty';
+import { ListProps } from './List';
const defaultActions = ;
const defaultPagination = ;
@@ -372,7 +373,7 @@ declare module '@mui/material/styles' {
}
interface ComponentsPropsList {
- RaList: Partial;
+ RaList: Partial;
}
interface Components {
diff --git a/packages/ra-ui-materialui/src/list/SimpleList/SimpleList.spec.tsx b/packages/ra-ui-materialui/src/list/SimpleList/SimpleList.spec.tsx
index 043985cc520..2db79e94d09 100644
--- a/packages/ra-ui-materialui/src/list/SimpleList/SimpleList.spec.tsx
+++ b/packages/ra-ui-materialui/src/list/SimpleList/SimpleList.spec.tsx
@@ -22,6 +22,7 @@ import {
RowClick,
Standalone,
StandaloneEmpty,
+ Themed,
} from './SimpleList.stories';
import { Basic } from '../filter/FilterButton.stories';
@@ -259,6 +260,13 @@ describe('', () => {
await screen.findByText('War and Peace');
});
+ it('should be customized by a theme', async () => {
+ render();
+ expect(screen.queryByTestId('themed-list').classList).toContain(
+ 'custom-class'
+ );
+ });
+
describe('standalone', () => {
it('should work without a ListContext', async () => {
render();
diff --git a/packages/ra-ui-materialui/src/list/SimpleList/SimpleList.stories.tsx b/packages/ra-ui-materialui/src/list/SimpleList/SimpleList.stories.tsx
index 3ce34f44646..7096b5b44d5 100644
--- a/packages/ra-ui-materialui/src/list/SimpleList/SimpleList.stories.tsx
+++ b/packages/ra-ui-materialui/src/list/SimpleList/SimpleList.stories.tsx
@@ -11,7 +11,14 @@ import {
} from 'ra-core';
import defaultMessages from 'ra-language-english';
import polyglotI18nProvider from 'ra-i18n-polyglot';
-import { Alert, Box, FormControlLabel, FormGroup, Switch } from '@mui/material';
+import {
+ Alert,
+ Box,
+ FormControlLabel,
+ FormGroup,
+ Switch,
+ ThemeOptions,
+} from '@mui/material';
import { Location } from 'react-router';
import { AdminUI } from '../../AdminUI';
@@ -21,6 +28,8 @@ import { List, ListProps } from '../List';
import { RowClickFunction } from '../types';
import { SimpleList } from './SimpleList';
import { FunctionLinkType } from './SimpleListItem';
+import { deepmerge } from '@mui/utils';
+import { defaultLightTheme } from '../../theme';
export default { title: 'ra-ui-materialui/list/SimpleList' };
@@ -430,3 +439,39 @@ export const StandaloneEmpty = () => (
);
+
+export const Themed = () => (
+
+
+
+ record.title}
+ secondaryText={record => record.author}
+ tertiaryText={record => record.year}
+ />
+
+
+
+);
diff --git a/packages/ra-ui-materialui/src/list/SimpleList/SimpleList.tsx b/packages/ra-ui-materialui/src/list/SimpleList/SimpleList.tsx
index 187d4829eef..afb4c1d69d8 100644
--- a/packages/ra-ui-materialui/src/list/SimpleList/SimpleList.tsx
+++ b/packages/ra-ui-materialui/src/list/SimpleList/SimpleList.tsx
@@ -7,7 +7,11 @@ import {
ListItemText,
ListProps,
} from '@mui/material';
-import { type ComponentsOverrides, styled } from '@mui/material/styles';
+import {
+ type ComponentsOverrides,
+ styled,
+ useThemeProps,
+} from '@mui/material/styles';
import {
type RaRecord,
RecordContextProvider,
@@ -67,8 +71,13 @@ import {
* );
*/
export const SimpleList = (
- props: SimpleListProps
+ inProps: SimpleListProps
) => {
+ const props = useThemeProps({
+ props: inProps,
+ name: PREFIX,
+ });
+
const {
className,
empty = DefaultEmpty,