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,