diff --git a/.eslintrc b/.eslintrc index 996e0cc6c4f..66ad1b7f93d 100644 --- a/.eslintrc +++ b/.eslintrc @@ -23,9 +23,9 @@ { "paths": [ { - "name": "@material-ui/core", + "name": "@mui/material", "importNames": ["makeStyles", "createMuiTheme"], - "message": "Please import from @material-ui/core/styles instead. See https://material-ui.com/guides/minimizing-bundle-size/#option-2 for more information" + "message": "Please import from @mui/material/styles instead. See https://material-ui.com/guides/minimizing-bundle-size/#option-2 for more information" } ] } diff --git a/__mocks__/@popperjs/core.ts b/__mocks__/@popperjs/core.ts new file mode 100644 index 00000000000..2c9da115781 --- /dev/null +++ b/__mocks__/@popperjs/core.ts @@ -0,0 +1,15 @@ +const mock = () => { + const PopperJS = jest.requireActual('@popperjs/core'); + return { + placements: PopperJS.placements, + destroy: () => {}, + scheduleUpdate: () => {}, + forceUpdate: () => {}, + render: function (this: any) { + return this.$options._renderChildren; + }, + }; +}; + +export default mock; +export { mock as createPopper }; diff --git a/cypress/support/CreatePage.js b/cypress/support/CreatePage.js index fb37454f0e3..7b614586b53 100644 --- a/cypress/support/CreatePage.js +++ b/cypress/support/CreatePage.js @@ -21,7 +21,7 @@ export default url => ({ descInput: '.ql-editor', tab: index => `.form-tab:nth-of-type(${index})`, title: '#react-admin-title', - userMenu: 'button[title="Profile"]', + userMenu: 'button[aria-label="Profile"]', logout: '.logout', }, diff --git a/cypress/support/EditPage.js b/cypress/support/EditPage.js index b9c80aa473e..4f1556cd99a 100644 --- a/cypress/support/EditPage.js +++ b/cypress/support/EditPage.js @@ -11,7 +11,7 @@ export default url => ({ return `.ra-input-${name} label`; } if (type === 'reference-array-input') { - return `.ra-input div[role=combobox]`; + return `.ra-input div[role=combobox] input`; } return `.edit-page [name='${name}']`; }, @@ -22,7 +22,7 @@ export default url => ({ cloneButton: '.button-clone', tab: index => `.form-tab:nth-of-type(${index})`, title: '#react-admin-title', - userMenu: 'button[title="Profile"]', + userMenu: 'button[aria-label="Profile"]', logout: '.logout', }, diff --git a/cypress/support/ListPage.js b/cypress/support/ListPage.js index 70cd3bc19eb..cbc13792cb3 100644 --- a/cypress/support/ListPage.js +++ b/cypress/support/ListPage.js @@ -29,7 +29,7 @@ export default url => ({ selectAll: '.select-all', selectedItem: '.select-item input:checked', selectItem: '.select-item input', - userMenu: 'button[title="Profile"]', + userMenu: 'button[aria-label="Profile"]', title: '#react-admin-title', headroomUnfixed: '.headroom--unfixed', headroomUnpinned: '.headroom--unpinned', diff --git a/cypress/support/ShowPage.js b/cypress/support/ShowPage.js index 7759eaad4e0..22fceb7dc84 100644 --- a/cypress/support/ShowPage.js +++ b/cypress/support/ShowPage.js @@ -6,7 +6,7 @@ export default (url, initialField = 'title') => ({ snackbar: 'div[role="alertdialog"]', tabs: `.show-tab`, tab: index => `.show-tab:nth-of-type(${index})`, - userMenu: 'button[title="Profile"]', + userMenu: 'button[aria-label="Profile"]', logout: '.logout', }, diff --git a/examples/crm/package.json b/examples/crm/package.json index bc0f9dd42b4..0cb0830a624 100644 --- a/examples/crm/package.json +++ b/examples/crm/package.json @@ -1,56 +1,56 @@ { - "name": "react-admin-crm", - "version": "0.1.0", - "private": true, - "dependencies": { - "@material-ui/core": "^4.12.1", - "@material-ui/icons": "^4.11.2", - "@nivo/bar": "^0.67.0", - "@nivo/core": "^0.67.0", - "date-fns": "^2.19.0", - "faker": "~5.4.0", - "lodash": "~4.17.5", - "prop-types": "^15.7.2", - "ra-data-fakerest": "^3.13.4", - "react": "^17.0.0", - "react-admin": "^3.15.0", - "react-beautiful-dnd": "^13.0.0", - "react-dom": "^17.0.0", - "react-scripts": "^4.0.1" - }, - "devDependencies": { - "@testing-library/jest-dom": "^5.11.4", - "@testing-library/react": "^11.1.0", - "@testing-library/user-event": "^12.1.10", - "@types/classnames": "^2.2.9", - "@types/faker": "^5.1.7", - "@types/jest": "^26.0.19", - "@types/lodash": "~4.14.168", - "@types/react": "^17.0.20", - "@types/react-beautiful-dnd": "^13.0.0", - "@types/react-dom": "^17.0.9", - "rewire": "^5.0.0", - "source-map-explorer": "^2.0.0", - "typescript": "^4.4.0", - "web-vitals": "^1.0.1" - }, - "scripts": { - "start": "react-scripts start", - "build": "node ./build.js", - "test": "react-scripts test", - "eject": "react-scripts eject" - }, - "homepage": ".", - "browserslist": { - "production": [ - ">0.2%", - "not dead", - "not op_mini all" - ], - "development": [ - "last 1 chrome version", - "last 1 firefox version", - "last 1 safari version" - ] - } + "name": "react-admin-crm", + "version": "0.1.0", + "private": true, + "dependencies": { + "@mui/material": "^5.0.2", + "@mui/icons-material": "^5.0.1", + "@nivo/bar": "^0.67.0", + "@nivo/core": "^0.67.0", + "date-fns": "^2.19.0", + "faker": "~5.4.0", + "lodash": "~4.17.5", + "prop-types": "^15.7.2", + "ra-data-fakerest": "^3.13.4", + "react": "^17.0.0", + "react-admin": "^3.15.0", + "react-beautiful-dnd": "^13.0.0", + "react-dom": "^17.0.0", + "react-scripts": "^4.0.1" + }, + "devDependencies": { + "@testing-library/jest-dom": "^5.11.4", + "@testing-library/react": "^11.1.0", + "@testing-library/user-event": "^12.1.10", + "@types/classnames": "^2.2.9", + "@types/faker": "^5.1.7", + "@types/jest": "^26.0.19", + "@types/lodash": "~4.14.168", + "@types/react": "^17.0.20", + "@types/react-beautiful-dnd": "^13.0.0", + "@types/react-dom": "^17.0.9", + "rewire": "^5.0.0", + "source-map-explorer": "^2.0.0", + "typescript": "^4.4.0", + "web-vitals": "^1.0.1" + }, + "scripts": { + "start": "react-scripts start", + "build": "node ./build.js", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "homepage": ".", + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } } diff --git a/examples/crm/src/App.tsx b/examples/crm/src/App.tsx index 89a5a1ca989..256ab9d1a82 100644 --- a/examples/crm/src/App.tsx +++ b/examples/crm/src/App.tsx @@ -1,9 +1,6 @@ import * as React from 'react'; import { Admin, Resource, ListGuesser, defaultTheme } from 'react-admin'; -import { - unstable_createMuiStrictModeTheme, - createMuiTheme, -} from '@material-ui/core/styles'; +import { createTheme } from '@mui/material/styles'; import { dataProvider } from './dataProvider'; import { authProvider } from './authProvider'; @@ -14,10 +11,7 @@ import deals from './deals'; import { Dashboard } from './dashboard/Dashboard'; // FIXME MUI bug https://github.com/mui-org/material-ui/issues/13394 -const theme = - process.env.NODE_ENV !== 'production' - ? unstable_createMuiStrictModeTheme(defaultTheme) - : createMuiTheme(defaultTheme); +const theme = createTheme(defaultTheme); const App = () => ( { - const classes = useStyles(); const match = useRouteMatch(['/contacts', '/companies', '/deals']); const currentPath = match?.path ?? '/'; return ( - + ); }; diff --git a/examples/crm/src/Layout.tsx b/examples/crm/src/Layout.tsx index 8cbf953844c..7fc25e5a373 100644 --- a/examples/crm/src/Layout.tsx +++ b/examples/crm/src/Layout.tsx @@ -1,9 +1,8 @@ import React, { Component, ErrorInfo, HtmlHTMLAttributes } from 'react'; import PropTypes from 'prop-types'; import { RouteComponentProps, withRouter } from 'react-router-dom'; -import { createMuiTheme } from '@material-ui/core/styles'; -import { ThemeProvider } from '@material-ui/styles'; -import { CssBaseline, Container } from '@material-ui/core'; +import { createTheme, ThemeProvider } from '@mui/material/styles'; +import { CssBaseline, Container } from '@mui/material'; import { CoreLayoutProps } from 'react-admin'; import { Notification, Error } from 'react-admin'; @@ -42,8 +41,7 @@ class Layout extends Component { const { theme, title, children } = this.props; const { hasError, errorMessage, errorInfo } = this.state; return ( - // @ts-ignore - +
diff --git a/examples/crm/src/companies/CompanyAside.tsx b/examples/crm/src/companies/CompanyAside.tsx index 247e2d7e450..e16b0f4690c 100644 --- a/examples/crm/src/companies/CompanyAside.tsx +++ b/examples/crm/src/companies/CompanyAside.tsx @@ -7,7 +7,7 @@ import { EditButton, ShowButton, } from 'react-admin'; -import { Box, Typography, Divider, Link } from '@material-ui/core'; +import { Box, Typography, Divider, Link } from '@mui/material'; import { Company, Sale } from '../types'; diff --git a/examples/crm/src/companies/CompanyAvatar.tsx b/examples/crm/src/companies/CompanyAvatar.tsx index 44bdac924b1..e9514ea8f55 100644 --- a/examples/crm/src/companies/CompanyAvatar.tsx +++ b/examples/crm/src/companies/CompanyAvatar.tsx @@ -1,24 +1,33 @@ import * as React from 'react'; -import { Avatar } from '@material-ui/core'; -import { makeStyles } from '@material-ui/core/styles'; +import { styled } from '@mui/material/styles'; +import { Avatar } from '@mui/material'; import clsx from 'clsx'; import { Company } from '../types'; -const useStyles = makeStyles({ - avatar: { +const PREFIX = 'CompanyAvatar'; + +const classes = { + avatar: `${PREFIX}-avatar`, + img: `${PREFIX}-img`, + small: `${PREFIX}-small`, + large: `${PREFIX}-large`, +}; + +const StyledAvatar = styled(Avatar)({ + [`& .${classes.avatar}`]: { width: 60, height: 60, backgroundColor: 'aliceblue', }, - img: { + [`& .${classes.img}`]: { objectFit: 'contain', }, - small: { + [`& .${classes.small}`]: { width: 20, height: 20, }, - large: { + [`& .${classes.large}`]: { width: 40, height: 40, }, @@ -31,14 +40,13 @@ export const CompanyAvatar = ({ record?: Company; size?: 'small' | 'large'; }) => { - const classes = useStyles(); if (!record) return null; return ( - ); }; diff --git a/examples/crm/src/companies/CompanyCard.tsx b/examples/crm/src/companies/CompanyCard.tsx index 91b5f8d56c2..95ba6874ee0 100644 --- a/examples/crm/src/companies/CompanyCard.tsx +++ b/examples/crm/src/companies/CompanyCard.tsx @@ -1,9 +1,9 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { useState } from 'react'; -import { Paper, Typography, Link as MuiLink } from '@material-ui/core'; -import { makeStyles } from '@material-ui/core/styles'; -import ContactsIcon from '@material-ui/icons/AccountCircle'; -import DealIcon from '@material-ui/icons/MonetizationOn'; +import { Paper, Typography, Link as MuiLink } from '@mui/material'; +import ContactsIcon from '@mui/icons-material/AccountCircle'; +import DealIcon from '@mui/icons-material/MonetizationOn'; import { linkToRecord, SelectField } from 'react-admin'; import { Link } from 'react-router-dom'; @@ -11,8 +11,19 @@ import { sectors } from './sectors'; import { CompanyAvatar } from './CompanyAvatar'; import { Company } from '../types'; -const useStyles = makeStyles(theme => ({ - paper: { +const PREFIX = 'CompanyCard'; + +const classes = { + paper: `${PREFIX}-paper`, + identity: `${PREFIX}-identity`, + name: `${PREFIX}-name`, + stats: `${PREFIX}-stats`, + singleStat: `${PREFIX}-singleStat`, + statIcon: `${PREFIX}-statIcon`, +}; + +const StyledMuiLink = styled(MuiLink)(({ theme }) => ({ + [`& .${classes.paper}`]: { height: 200, width: 193.5, display: 'flex', @@ -20,34 +31,39 @@ const useStyles = makeStyles(theme => ({ justifyContent: 'space-between', padding: '1em', }, - identity: { + + [`& .${classes.identity}`]: { display: 'flex', flexDirection: 'column', alignItems: 'center', }, - name: { + + [`& .${classes.name}`]: { textAlign: 'center', marginTop: theme.spacing(1), }, - stats: { + + [`& .${classes.stats}`]: { display: 'flex', justifyContent: 'space-around', width: '100%', }, - singleStat: { + + [`& .${classes.singleStat}`]: { display: 'flex', alignItems: 'center', }, - statIcon: { + + [`& .${classes.statIcon}`]: { marginRight: theme.spacing(1), }, })); export const CompanyCard = ({ record }: { record: Company }) => { - const classes = useStyles(); const [elevation, setElevation] = useState(1); return ( - { - + ); }; diff --git a/examples/crm/src/companies/CompanyCreate.tsx b/examples/crm/src/companies/CompanyCreate.tsx index 597f3cb9005..3588a1a4f70 100644 --- a/examples/crm/src/companies/CompanyCreate.tsx +++ b/examples/crm/src/companies/CompanyCreate.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { Create, CreateProps, @@ -8,16 +9,21 @@ import { SelectInput, required, } from 'react-admin'; -import { Box, CardContent, Divider, Avatar } from '@material-ui/core'; -import { makeStyles } from '@material-ui/core/styles'; -import BusinessIcon from '@material-ui/icons/Business'; +import { Box, CardContent, Divider, Avatar } from '@mui/material'; +import BusinessIcon from '@mui/icons-material/Business'; import clsx from 'clsx'; import { sectors } from './sectors'; import { sizes } from './sizes'; -const useStyles = makeStyles({ - inline: { +const PREFIX = 'CompanyCreate'; + +const classes = { + inline: `${PREFIX}-inline`, +}; + +const StyledCreate = styled(Create)({ + [`& .${classes.inline}`]: { display: 'inline-block', marginLeft: '1em', '&.first-child': { @@ -27,9 +33,8 @@ const useStyles = makeStyles({ }); export const CompanyCreate = (props: CreateProps) => { - const classes = useStyles(); return ( - + { /> - + ); }; diff --git a/examples/crm/src/companies/CompanyEdit.tsx b/examples/crm/src/companies/CompanyEdit.tsx index d7448f6b722..75cd1cc950d 100644 --- a/examples/crm/src/companies/CompanyEdit.tsx +++ b/examples/crm/src/companies/CompanyEdit.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { Edit, EditProps, @@ -9,8 +10,7 @@ import { useRecordContext, required, } from 'react-admin'; -import { Box, CardContent, Divider } from '@material-ui/core'; -import { makeStyles } from '@material-ui/core/styles'; +import { Box, CardContent, Divider } from '@mui/material'; import clsx from 'clsx'; import { CompanyAside } from './CompanyAside'; @@ -18,8 +18,14 @@ import { LogoField } from './LogoField'; import { sectors } from './sectors'; import { sizes } from './sizes'; -const useStyles = makeStyles({ - inline: { +const PREFIX = 'CompanyEdit'; + +const classes = { + inline: `${PREFIX}-inline`, +}; + +const StyledEdit = styled(Edit)({ + [`& .${classes.inline}`]: { display: 'inline-block', marginLeft: '1em', '&.first-child': { @@ -29,9 +35,12 @@ const useStyles = makeStyles({ }); export const CompanyEdit = (props: EditProps) => { - const classes = useStyles(); return ( - } actions={false}> + } + actions={false} + > { /> - + ); }; diff --git a/examples/crm/src/companies/CompanyList.tsx b/examples/crm/src/companies/CompanyList.tsx index df46d0d1a55..c7769b65587 100644 --- a/examples/crm/src/companies/CompanyList.tsx +++ b/examples/crm/src/companies/CompanyList.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { List, ListProps, @@ -8,11 +9,22 @@ import { Pagination, useGetIdentity, } from 'react-admin'; -import { makeStyles } from '@material-ui/core/styles'; -import { GridList } from './GridList'; +import { ImageList } from './GridList'; import { CompanyListFilter } from './CompanyListFilter'; +const PREFIX = 'CompanyList'; + +const classes = { + createButton: `${PREFIX}-createButton`, +}; + +const StyledTopToolbar = styled(TopToolbar)(({ theme }) => ({ + [`& .${classes.createButton}`]: { + marginLeft: theme.spacing(2), + }, +})); + export const CompanyList = (props: ListProps) => { const { identity } = useGetIdentity(); return identity ? ( @@ -26,20 +38,14 @@ export const CompanyList = (props: ListProps) => { sort={{ field: 'name', order: 'ASC' }} component="div" > - + ) : null; }; -const useActionStyles = makeStyles(theme => ({ - createButton: { - marginLeft: theme.spacing(2), - }, -})); const CompanyListActions = (props: any) => { - const classes = useActionStyles(); return ( - + { label="New Company" className={classes.createButton} /> - + ); }; diff --git a/examples/crm/src/companies/CompanyListFilter.tsx b/examples/crm/src/companies/CompanyListFilter.tsx index 4bafeb80caa..87971d0afae 100644 --- a/examples/crm/src/companies/CompanyListFilter.tsx +++ b/examples/crm/src/companies/CompanyListFilter.tsx @@ -6,10 +6,10 @@ import { FilterListItem, useGetIdentity, } from 'react-admin'; -import { Box } from '@material-ui/core'; -import BusinessIcon from '@material-ui/icons/Business'; -import LocalShippingIcon from '@material-ui/icons/LocalShipping'; -import SupervisorAccountIcon from '@material-ui/icons/SupervisorAccount'; +import { Box } from '@mui/material'; +import BusinessIcon from '@mui/icons-material/Business'; +import LocalShippingIcon from '@mui/icons-material/LocalShipping'; +import SupervisorAccountIcon from '@mui/icons-material/SupervisorAccount'; import { sizes } from './sizes'; import { sectors } from './sectors'; @@ -17,7 +17,7 @@ import { sectors } from './sectors'; export const CompanyListFilter = () => { const { identity } = useGetIdentity(); return ( - + }> diff --git a/examples/crm/src/companies/CompanyShow.tsx b/examples/crm/src/companies/CompanyShow.tsx index 162b1aabc0d..9f7a2cf9ec5 100644 --- a/examples/crm/src/companies/CompanyShow.tsx +++ b/examples/crm/src/companies/CompanyShow.tsx @@ -24,8 +24,8 @@ import { Tabs, Tab, Divider, -} from '@material-ui/core'; -import PersonAddIcon from '@material-ui/icons/PersonAdd'; +} from '@mui/material'; +import PersonAddIcon from '@mui/icons-material/PersonAdd'; import { Link as RouterLink } from 'react-router-dom'; import { formatDistance } from 'date-fns'; diff --git a/examples/crm/src/companies/GridList.tsx b/examples/crm/src/companies/GridList.tsx index 3322fd891dc..c16c06299f2 100644 --- a/examples/crm/src/companies/GridList.tsx +++ b/examples/crm/src/companies/GridList.tsx @@ -1,44 +1,39 @@ import * as React from 'react'; -import { Box, Paper } from '@material-ui/core'; -import { makeStyles } from '@material-ui/core/styles'; +import { Box, Paper } from '@mui/material'; import { useListContext, Identifier } from 'react-admin'; import { CompanyCard } from './CompanyCard'; import { Company } from '../types'; -const useStyles = makeStyles(theme => ({ - gridList: { - display: 'flex', - flexWrap: 'wrap', - width: 1008, - gap: '10px', - }, - paper: { - height: 200, - width: 194, - display: 'flex', - flexDirection: 'column', - backgroundColor: theme.palette.grey[200], - }, -})); +const PREFIX = 'ImageList'; + +const classes = { + gridList: `${PREFIX}-gridList`, + paper: `${PREFIX}-paper`, +}; const times = (nbChildren: number, fn: (key: number) => any) => Array.from({ length: nbChildren }, (_, key) => fn(key)); -const LoadingGridList = () => { - const classes = useStyles(); - return ( - - {times(15, key => ( - - ))} - - ); -}; +const LoadingGridList = () => ( + + {times(15, key => ( + + ))} + +); const LoadedGridList = () => { const { ids, data } = useListContext(); - const classes = useStyles(); if (!ids || !data) return null; @@ -51,7 +46,7 @@ const LoadedGridList = () => { ); }; -export const GridList = () => { +export const ImageList = () => { const { loaded } = useListContext(); return loaded ? : ; }; diff --git a/examples/crm/src/companies/LogoField.tsx b/examples/crm/src/companies/LogoField.tsx index eb400b3ee78..9ae08452df3 100644 --- a/examples/crm/src/companies/LogoField.tsx +++ b/examples/crm/src/companies/LogoField.tsx @@ -1,8 +1,14 @@ import * as React from 'react'; -import { makeStyles } from '@material-ui/core/styles'; +import { styled } from '@mui/material/styles'; -const useStyles = makeStyles({ - image: { +const PREFIX = 'LogoField'; + +const classes = { + image: `${PREFIX}-image`, +}; + +const StyledImage = styled('img')({ + [`&.${classes.image}`]: { objectFit: 'contain', }, }); @@ -21,10 +27,9 @@ export const LogoField = ({ source?: string; size?: 'small' | 'medium'; }) => { - const classes = useStyles(); if (!record) return null; return ( - {record.name} ({ + [`& .${classes.createButton}`]: { + marginLeft: theme.spacing(2), + }, +})); + const ContactListContent = () => { const { data, ids, loaded, onToggleItem, selectedIds } = useListContext< Contact @@ -106,26 +118,18 @@ const ContactListContent = () => { ); }; -const useActionStyles = makeStyles(theme => ({ - createButton: { - marginLeft: theme.spacing(2), - }, -})); -const ContactListActions = () => { - const classes = useActionStyles(); - return ( - - - - - - ); -}; +const ContactListActions = () => ( + + + + + +); export const ContactList = (props: ListProps) => { const { identity } = useGetIdentity(); diff --git a/examples/crm/src/contacts/ContactListFilter.tsx b/examples/crm/src/contacts/ContactListFilter.tsx index 36033d570e8..b930f40e63e 100644 --- a/examples/crm/src/contacts/ContactListFilter.tsx +++ b/examples/crm/src/contacts/ContactListFilter.tsx @@ -7,11 +7,11 @@ import { useGetIdentity, useGetList, } from 'react-admin'; -import { Box, Chip } from '@material-ui/core'; -import AccessTimeIcon from '@material-ui/icons/AccessTime'; -import TrendingUpIcon from '@material-ui/icons/TrendingUp'; -import LocalOfferIcon from '@material-ui/icons/LocalOffer'; -import SupervisorAccountIcon from '@material-ui/icons/SupervisorAccount'; +import { Box, Chip } from '@mui/material'; +import AccessTimeIcon from '@mui/icons-material/AccessTime'; +import TrendingUpIcon from '@mui/icons-material/TrendingUp'; +import LocalOfferIcon from '@mui/icons-material/LocalOffer'; +import SupervisorAccountIcon from '@mui/icons-material/SupervisorAccount'; import { endOfYesterday, startOfWeek, startOfMonth, subMonths } from 'date-fns'; import { Status } from '../misc/Status'; @@ -24,7 +24,7 @@ export const ContactListFilter = () => { { field: 'name', order: 'ASC' } ); return ( - + }> record ? ( /> ) : null; -const useStyles = makeStyles({ - root: { - display: 'inline-block', - }, -}); - export const TagsList = ({ record }: { record: Contact }) => { - const classes = useStyles(); if (!record) return null; return ( - { size="small" /> - + ); }; diff --git a/examples/crm/src/contacts/TagsListEdit.tsx b/examples/crm/src/contacts/TagsListEdit.tsx index ef8494168c0..06d3c0a547a 100644 --- a/examples/crm/src/contacts/TagsListEdit.tsx +++ b/examples/crm/src/contacts/TagsListEdit.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { useState, FormEvent } from 'react'; import { useGetMany, @@ -18,10 +19,9 @@ import { TextField, MenuItem, Menu, -} from '@material-ui/core'; -import { makeStyles } from '@material-ui/core/styles'; -import ControlPointIcon from '@material-ui/icons/ControlPoint'; -import EditIcon from '@material-ui/icons/Edit'; +} from '@mui/material'; +import ControlPointIcon from '@mui/icons-material/ControlPoint'; +import EditIcon from '@mui/icons-material/Edit'; import { colors } from '../tags/colors'; import { Contact } from '../types'; @@ -212,8 +212,14 @@ export const TagsListEdit = ({ record }: { record: Contact }) => { ); }; -const useStyles = makeStyles({ - root: { +const PREFIX = 'TagsListEdit'; + +const classes = { + root: `${PREFIX}-root`, +}; + +const StyledRoundButton = styled('button')({ + [`&.${classes.root}`]: { width: 30, height: 30, borderRadius: 15, @@ -222,17 +228,14 @@ const useStyles = makeStyles({ }, }); -const RoundButton = ({ color, handleClick, selected }: any) => { - const classes = useStyles(); - return ( - - + ); }; diff --git a/examples/crm/src/deals/ContactList.tsx b/examples/crm/src/deals/ContactList.tsx index 931b3123795..dd97ca9e325 100644 --- a/examples/crm/src/deals/ContactList.tsx +++ b/examples/crm/src/deals/ContactList.tsx @@ -1,17 +1,24 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { useListContext } from 'react-admin'; -import { Link } from '@material-ui/core'; +import { Link } from '@mui/material'; import { Link as RouterLink } from 'react-router-dom'; -import { makeStyles } from '@material-ui/core/styles'; -const useStyles = makeStyles({ - ul: { +const PREFIX = 'ContactList'; + +const classes = { + ul: `${PREFIX}-ul`, + li: `${PREFIX}-li`, +}; + +const Root = styled('ul')({ + [`&.${classes.ul}`]: { listStyle: 'none', padding: 0, margin: 0, display: 'inline-block', }, - li: { + [`& .${classes.li}`]: { display: 'inline', '&:after': { content: '", "', @@ -24,10 +31,10 @@ const useStyles = makeStyles({ export const ContactList = () => { const { ids, data, loaded } = useListContext(); - const classes = useStyles(); + if (!loaded) return
; return ( -
    + {ids.map(id => (
  • {
  • ))} -
+ ); }; diff --git a/examples/crm/src/deals/DealCard.tsx b/examples/crm/src/deals/DealCard.tsx index 3ba5953abf4..70d2c4cd4c2 100644 --- a/examples/crm/src/deals/DealCard.tsx +++ b/examples/crm/src/deals/DealCard.tsx @@ -1,27 +1,36 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { ReferenceField, useRedirect } from 'react-admin'; -import { Card, Typography } from '@material-ui/core'; -import { makeStyles } from '@material-ui/core/styles'; +import { Card, Typography } from '@mui/material'; import { Draggable } from 'react-beautiful-dnd'; import { LogoField } from '../companies/LogoField'; import { Deal } from '../types'; -const useStyles = makeStyles(theme => ({ - root: { +const PREFIX = 'DealCard'; + +const classes = { + root: `${PREFIX}-root`, + cardContent: `${PREFIX}-cardContent`, + cardText: `${PREFIX}-cardText`, +}; + +const Root = styled('div')(({ theme }) => ({ + [`&.${classes.root}`]: { marginBottom: theme.spacing(1), }, - cardContent: { + + [`& .${classes.cardContent}`]: { padding: theme.spacing(1), display: 'flex', }, - cardText: { + + [`& .${classes.cardText}`]: { marginLeft: theme.spacing(1), }, })); export const DealCard = ({ deal, index }: { deal: Deal; index: number }) => { - const classes = useStyles(); const redirect = useRedirect(); if (!deal) return null; @@ -31,7 +40,7 @@ export const DealCard = ({ deal, index }: { deal: Deal; index: number }) => { return ( {(provided, snapshot) => ( -
{
- + )} ); diff --git a/examples/crm/src/deals/DealColumn.tsx b/examples/crm/src/deals/DealColumn.tsx index 28ed38250c2..acc3b06c559 100644 --- a/examples/crm/src/deals/DealColumn.tsx +++ b/examples/crm/src/deals/DealColumn.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; -import { Typography } from '@material-ui/core'; -import { makeStyles } from '@material-ui/core/styles'; +import { styled } from '@mui/material/styles'; +import { Typography } from '@mui/material'; import { Droppable } from 'react-beautiful-dnd'; import { Identifier, RecordMap } from 'react-admin'; @@ -8,8 +8,15 @@ import { DealCard } from './DealCard'; import { stageNames } from './stages'; import { Deal } from '../types'; -const useStyles = makeStyles({ - root: { +const PREFIX = 'DealColumn'; + +const classes = { + root: `${PREFIX}-root`, + droppable: `${PREFIX}-droppable`, +}; + +const Root = styled('div')({ + [`&.${classes.root}`]: { flex: 1, paddingTop: 8, paddingBottom: 16, @@ -23,7 +30,7 @@ const useStyles = makeStyles({ borderTopRightRadius: 5, }, }, - droppable: { + [`& .${classes.droppable}`]: { display: 'flex', flexDirection: 'column', borderRadius: 5, @@ -43,9 +50,8 @@ export const DealColumn = ({ dealIds: Identifier[]; data: RecordMap; }) => { - const classes = useStyles(); return ( -
+ {/* @ts-ignore */} {stageNames[stage]} @@ -67,6 +73,6 @@ export const DealColumn = ({
)} - + ); }; diff --git a/examples/crm/src/deals/DealCreate.tsx b/examples/crm/src/deals/DealCreate.tsx index 468c8f457fe..96741fee92b 100644 --- a/examples/crm/src/deals/DealCreate.tsx +++ b/examples/crm/src/deals/DealCreate.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { Create, SimpleForm, @@ -11,21 +12,25 @@ import { useRedirect, useDataProvider, } from 'react-admin'; -import { Dialog } from '@material-ui/core'; -import { makeStyles } from '@material-ui/core/styles'; +import { Dialog } from '@mui/material'; import { stageChoices } from './stages'; import { typeChoices } from './types'; import { Deal } from '../types'; -const useStyles = makeStyles({ - root: { +const PREFIX = 'DealCreate'; + +const classes = { + root: `${PREFIX}-root`, +}; + +const StyledDialog = styled(Dialog)({ + [`& .${classes.root}`]: { width: 500, }, }); export const DealCreate = ({ open }: { open: boolean }) => { - const classes = useStyles(); const redirect = useRedirect(); const dataProvider = useDataProvider(); @@ -58,7 +63,7 @@ export const DealCreate = ({ open }: { open: boolean }) => { }; return ( - + { - + ); }; diff --git a/examples/crm/src/deals/DealList.tsx b/examples/crm/src/deals/DealList.tsx index e13cf6e4bab..33169c2aa62 100644 --- a/examples/crm/src/deals/DealList.tsx +++ b/examples/crm/src/deals/DealList.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { CreateButton, ExportButton, @@ -11,7 +12,6 @@ import { useGetIdentity, } from 'react-admin'; import { Route } from 'react-router'; -import { makeStyles } from '@material-ui/core/styles'; import { DealListContent } from './DealListContent'; import { DealCreate } from './DealCreate'; @@ -19,6 +19,18 @@ import { DealShow } from './DealShow'; import { OnlyMineInput } from './OnlyMineInput'; import { typeChoices } from './types'; +const PREFIX = 'DealList'; + +const classes = { + createButton: `${PREFIX}-createButton`, +}; + +const StyledTopToolbar = styled(TopToolbar)(({ theme }) => ({ + [`& .${classes.createButton}`]: { + marginLeft: theme.spacing(2), + }, +})); + export const DealList = (props: ListProps) => { const { identity } = useGetIdentity(); return identity ? ( @@ -55,15 +67,9 @@ const dealFilters = [ , ]; -const useActionStyles = makeStyles(theme => ({ - createButton: { - marginLeft: theme.spacing(2), - }, -})); const DealActions = () => { - const classes = useActionStyles(); return ( - + { label="New Deal" className={classes.createButton} /> - + ); }; diff --git a/examples/crm/src/deals/DealListContent.tsx b/examples/crm/src/deals/DealListContent.tsx index 735b1ceaba3..c5103c0d975 100644 --- a/examples/crm/src/deals/DealListContent.tsx +++ b/examples/crm/src/deals/DealListContent.tsx @@ -7,7 +7,7 @@ import { RecordMap, DataProviderContext, } from 'react-admin'; -import { Box } from '@material-ui/core'; +import { Box } from '@mui/material'; import { DragDropContext, OnDragEndResponder } from 'react-beautiful-dnd'; import isEqual from 'lodash/isEqual'; diff --git a/examples/crm/src/deals/DealShow.tsx b/examples/crm/src/deals/DealShow.tsx index 0d24801413d..c68d575186a 100644 --- a/examples/crm/src/deals/DealShow.tsx +++ b/examples/crm/src/deals/DealShow.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { ShowBase, TextField, @@ -9,14 +10,7 @@ import { useRedirect, Identifier, } from 'react-admin'; -import { - Box, - Dialog, - DialogContent, - Typography, - Divider, -} from '@material-ui/core'; -import { makeStyles } from '@material-ui/core/styles'; +import { Box, Dialog, DialogContent, Typography, Divider } from '@mui/material'; import { format } from 'date-fns'; import { CompanyAvatar } from '../companies/CompanyAvatar'; @@ -24,8 +18,14 @@ import { NotesIterator } from '../notes'; import { ContactList } from './ContactList'; import { stageNames } from './stages'; -const useStyles = makeStyles({ - dialog: { +const PREFIX = 'DealShow'; + +const classes = { + dialog: `${PREFIX}-dialog`, +}; + +const Root = styled('div')({ + [`& .${classes.dialog}`]: { position: 'absolute', top: 50, }, @@ -33,7 +33,6 @@ const useStyles = makeStyles({ export const DealShow = ({ open, id }: { open: boolean; id: Identifier }) => { const redirect = useRedirect(); - const classes = useStyles(); const handleClose = () => { redirect('/deals'); @@ -60,7 +59,7 @@ const DealShowContent = () => { const record = useRecordContext(); if (!record) return null; return ( - <> + {
- + ); }; diff --git a/examples/crm/src/deals/OnlyMineInput.tsx b/examples/crm/src/deals/OnlyMineInput.tsx index 3ecdd69d2a5..b7c94176992 100644 --- a/examples/crm/src/deals/OnlyMineInput.tsx +++ b/examples/crm/src/deals/OnlyMineInput.tsx @@ -1,10 +1,16 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { useListFilterContext, useGetIdentity } from 'react-admin'; -import { Switch, FormControlLabel } from '@material-ui/core'; -import { makeStyles } from '@material-ui/core/styles'; +import { Switch, FormControlLabel } from '@mui/material'; -const useStyles = makeStyles(theme => ({ - root: { +const PREFIX = 'OnlyMineInput'; + +const classes = { + root: `${PREFIX}-root`, +}; + +const Root = styled('div')(({ theme }) => ({ + [`&.${classes.root}`]: { marginBottom: theme.spacing(1), marginLeft: theme.spacing(1), }, @@ -17,7 +23,7 @@ export const OnlyMineInput = ({ alwaysOn }: { alwaysOn: boolean }) => { setFilters, } = useListFilterContext(); const { identity } = useGetIdentity(); - const classes = useStyles(); + const handleChange = () => { const newFilterValues = { ...filterValues }; if (typeof filterValues.sales_id !== 'undefined') { @@ -28,7 +34,7 @@ export const OnlyMineInput = ({ alwaysOn }: { alwaysOn: boolean }) => { setFilters(newFilterValues, displayedFilters); }; return ( -
+ { } label="Only companies I manage" /> -
+ ); }; diff --git a/examples/crm/src/misc/Status.tsx b/examples/crm/src/misc/Status.tsx index 2c476929ad0..95e40b5d6c7 100644 --- a/examples/crm/src/misc/Status.tsx +++ b/examples/crm/src/misc/Status.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { Box } from '@material-ui/core'; +import { Box } from '@mui/material'; const getColorFromStatus = (status: string) => status === 'cold' @@ -17,7 +17,7 @@ export const Status = ({ status }: { status: string }) => ( width={10} height={10} display="inline-block" - borderRadius={5} + borderRadius="5px" bgcolor={getColorFromStatus(status)} component="span" /> diff --git a/examples/crm/src/notes/NewNote.tsx b/examples/crm/src/notes/NewNote.tsx index c98f7cd2f4f..7de3e2107fc 100644 --- a/examples/crm/src/notes/NewNote.tsx +++ b/examples/crm/src/notes/NewNote.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { useState, FormEvent } from 'react'; import { useRecordContext, @@ -10,22 +11,31 @@ import { Identifier, useResourceContext, } from 'react-admin'; -import { TextField as TextInput, Button } from '@material-ui/core'; -import { makeStyles } from '@material-ui/core/styles'; +import { TextField as TextInput, Button } from '@mui/material'; import { StatusSelector } from './StatusSelector'; -const useStyles = makeStyles(theme => ({ - root: { +const PREFIX = 'NewNote'; + +const classes = { + root: `${PREFIX}-root`, + toolbar: `${PREFIX}-toolbar`, + small: `${PREFIX}-small`, +}; + +const Root = styled('div')(({ theme }) => ({ + [`&.${classes.root}`]: { marginTop: theme.spacing(4), marginBottom: theme.spacing(1), }, - toolbar: { + + [`& .${classes.toolbar}`]: { display: 'flex', justifyContent: 'space-between', marginTop: theme.spacing(1), }, - small: { + + [`& .${classes.small}`]: { marginRight: '1em', '& .MuiFilledInput-input': { paddingTop: 10, @@ -40,7 +50,6 @@ export const NewNote = ({ showStatus?: boolean; reference: 'contacts' | 'deals'; }) => { - const classes = useStyles(); const record = useRecordContext(); const resource = useResourceContext(); const [text, setText] = useState(''); @@ -85,7 +94,7 @@ export const NewNote = ({ return false; }; return ( -
+
- + ); }; diff --git a/examples/crm/src/notes/Note.tsx b/examples/crm/src/notes/Note.tsx index 821830fabef..f0151137645 100644 --- a/examples/crm/src/notes/Note.tsx +++ b/examples/crm/src/notes/Note.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { useState, FormEvent, ChangeEvent } from 'react'; import { TextField, @@ -16,37 +17,55 @@ import { IconButton, FilledInput, Button, -} from '@material-ui/core'; -import { makeStyles } from '@material-ui/core/styles'; -import EditIcon from '@material-ui/icons/Edit'; -import TrashIcon from '@material-ui/icons/Delete'; +} from '@mui/material'; +import EditIcon from '@mui/icons-material/Edit'; +import TrashIcon from '@mui/icons-material/Delete'; import { Status } from '../misc/Status'; -const useStyles = makeStyles(theme => ({ - root: { +const PREFIX = 'Note'; + +const classes = { + root: `${PREFIX}-root`, + metadata: `${PREFIX}-metadata`, + textarea: `${PREFIX}-textarea`, + buttons: `${PREFIX}-buttons`, + cancel: `${PREFIX}-cancel`, + content: `${PREFIX}-content`, + text: `${PREFIX}-text`, + paragraph: `${PREFIX}-paragraph`, + toolbar: `${PREFIX}-toolbar`, +}; + +const Root = styled('div')(({ theme }) => ({ + [`&.${classes.root}`]: { marginBottom: theme.spacing(2), }, - metadata: { + + [`& .${classes.metadata}`]: { marginBottom: theme.spacing(1), color: theme.palette.text.secondary, }, - textarea: { + + [`& .${classes.textarea}`]: { paddingTop: 16, paddingLeft: 14, paddingRight: 60, paddingBottom: 14, lineHeight: 1.3, }, - buttons: { + + [`& .${classes.buttons}`]: { display: 'flex', justifyContent: 'flex-end', marginTop: theme.spacing(1), }, - cancel: { + + [`& .${classes.cancel}`]: { marginRight: theme.spacing(1), }, - content: { + + [`& .${classes.content}`]: { backgroundColor: '#edf3f0', padding: '0 1em', borderRadius: 10, @@ -54,16 +73,19 @@ const useStyles = makeStyles(theme => ({ alignItems: 'stretch', marginBottom: theme.spacing(1), }, - text: { + + [`& .${classes.text}`]: { flex: 1, }, - paragraph: { + + [`& .${classes.paragraph}`]: { fontFamily: theme.typography.fontFamily, fontSize: theme.typography.body1.fontSize, lineHeight: 1.3, marginBottom: theme.spacing(2.4), }, - toolbar: { + + [`& .${classes.toolbar}`]: { marginLeft: theme.spacing(2), visibility: 'hidden', display: 'flex', @@ -89,7 +111,7 @@ export const Note = ({ const resource = useResourceContext(); const record = useRecordContext(); const notify = useNotify(); - const classes = useStyles(); + const [update, { loading }] = useUpdate(); const [handleDelete] = useDelete(resource, note.id, note, { @@ -131,7 +153,7 @@ export const Note = ({ }; return ( -
setHover(true)} onMouseLeave={() => setHover(false)} @@ -221,6 +243,6 @@ export const Note = ({
)} - + ); }; diff --git a/examples/crm/src/notes/NotesIterator.tsx b/examples/crm/src/notes/NotesIterator.tsx index 5e1fd63aaf4..db34d6a71e1 100644 --- a/examples/crm/src/notes/NotesIterator.tsx +++ b/examples/crm/src/notes/NotesIterator.tsx @@ -1,12 +1,18 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { useListContext } from 'react-admin'; -import { makeStyles } from '@material-ui/core/styles'; import { Note } from './Note'; import { NewNote } from './NewNote'; -const useStyles = makeStyles({ - root: { +const PREFIX = 'NotesIterator'; + +const classes = { + root: `${PREFIX}-root`, +}; + +const Root = styled('div')({ + [`&.${classes.root}`]: { marginTop: '0.5em', }, }); @@ -18,13 +24,12 @@ export const NotesIterator = ({ showStatus?: boolean; reference: 'contacts' | 'deals'; }) => { - const classes = useStyles(); const { data, ids, loaded } = useListContext(); if (!loaded) return null; return ( <> -
+ {ids.map((id, index) => ( ))} -
+ ); }; diff --git a/examples/crm/src/notes/StatusSelector.tsx b/examples/crm/src/notes/StatusSelector.tsx index 43022bef669..62c684f6439 100644 --- a/examples/crm/src/notes/StatusSelector.tsx +++ b/examples/crm/src/notes/StatusSelector.tsx @@ -1,20 +1,25 @@ import * as React from 'react'; -import { TextField, MenuItem } from '@material-ui/core'; -import { makeStyles } from '@material-ui/core/styles'; +import { styled } from '@mui/material/styles'; +import { TextField, MenuItem } from '@mui/material'; import clsx from 'clsx'; import { Status } from '../misc/Status'; -const useStyles = makeStyles({ - root: { +const PREFIX = 'StatusSelector'; + +const classes = { + root: `${PREFIX}-root`, +}; + +const StyledTextField = styled(TextField)({ + [`&.${classes.root}`]: { width: 150, }, }); export const StatusSelector = ({ status, setStatus, className = '' }: any) => { - const classes = useStyles(); return ( - ) => { @@ -38,6 +43,6 @@ export const StatusSelector = ({ status, setStatus, className = '' }: any) => { In Contract - + ); }; diff --git a/examples/demo/package.json b/examples/demo/package.json index f5d6cf19fa7..7ee5e4cc2bd 100644 --- a/examples/demo/package.json +++ b/examples/demo/package.json @@ -3,8 +3,8 @@ "version": "3.0.0", "private": true, "dependencies": { - "@material-ui/core": "^4.12.1", - "@material-ui/icons": "^4.11.2", + "@mui/material": "^5.0.2", + "@mui/icons-material": "^5.0.1", "@types/inflection": "^1.5.28", "@types/recharts": "^1.8.10", "data-generator-retail": "^3.9.0", diff --git a/examples/demo/src/categories/CategoryList.tsx b/examples/demo/src/categories/CategoryList.tsx index d6467dc0231..6fa76398e23 100644 --- a/examples/demo/src/categories/CategoryList.tsx +++ b/examples/demo/src/categories/CategoryList.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { EditButton, List, ListProps, useListContext } from 'react-admin'; import inflection from 'inflection'; import { @@ -8,33 +9,40 @@ import { CardContent, CardActions, Typography, -} from '@material-ui/core'; -import { makeStyles } from '@material-ui/core/styles'; +} from '@mui/material'; import LinkToRelatedProducts from './LinkToRelatedProducts'; import { Category } from '../types'; -const useStyles = makeStyles({ - root: { +const PREFIX = 'CategoryList'; + +const classes = { + root: `${PREFIX}-root`, + media: `${PREFIX}-media`, + title: `${PREFIX}-title`, + actionSpacer: `${PREFIX}-actionSpacer`, +}; + +const StyledGrid = styled(Grid)({ + [`&.${classes.root}`]: { marginTop: '1em', }, - media: { + [`& .${classes.media}`]: { height: 140, }, - title: { + [`& .${classes.title}`]: { paddingBottom: '0.5em', }, - actionSpacer: { + [`& .${classes.actionSpacer}`]: { display: 'flex', justifyContent: 'space-around', }, }); const CategoryGrid = (props: any) => { - const classes = useStyles(props); const { data, ids } = useListContext(); return ids ? ( - + {ids.map(id => ( @@ -63,7 +71,7 @@ const CategoryGrid = (props: any) => { ))} - + ) : null; }; diff --git a/examples/demo/src/categories/LinkToRelatedProducts.tsx b/examples/demo/src/categories/LinkToRelatedProducts.tsx index 7fd4c9ffbf9..361245b0a65 100644 --- a/examples/demo/src/categories/LinkToRelatedProducts.tsx +++ b/examples/demo/src/categories/LinkToRelatedProducts.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; -import Button from '@material-ui/core/Button'; -import { makeStyles } from '@material-ui/core/styles'; +import { styled } from '@mui/material/styles'; +import Button from '@mui/material/Button'; import { Link } from 'react-router-dom'; import { useTranslate, FieldProps } from 'react-admin'; import { stringify } from 'query-string'; @@ -8,9 +8,16 @@ import { stringify } from 'query-string'; import products from '../products'; import { Category } from '../types'; -const useStyles = makeStyles({ - icon: { paddingRight: '0.5em' }, - link: { +const PREFIX = 'LinkToRelatedProducts'; + +const classes = { + icon: `${PREFIX}-icon`, + link: `${PREFIX}-link`, +}; + +const StyledButton = styled(Button)({ + [`& .${classes.icon}`]: { paddingRight: '0.5em' }, + [`&.${classes.link}`]: { display: 'inline-flex', alignItems: 'center', }, @@ -19,11 +26,12 @@ const useStyles = makeStyles({ const LinkToRelatedProducts = (props: FieldProps) => { const { record } = props; const translate = useTranslate(); - const classes = useStyles(); + return record ? ( - + ) : null; }; diff --git a/examples/demo/src/categories/index.ts b/examples/demo/src/categories/index.ts index d2431263b16..040f70d8be4 100644 --- a/examples/demo/src/categories/index.ts +++ b/examples/demo/src/categories/index.ts @@ -1,4 +1,4 @@ -import CategoryIcon from '@material-ui/icons/Bookmark'; +import CategoryIcon from '@mui/icons-material/Bookmark'; import CategoryList from './CategoryList'; import CategoryEdit from './CategoryEdit'; diff --git a/examples/demo/src/configuration/Configuration.tsx b/examples/demo/src/configuration/Configuration.tsx index bc603b49cc5..5b3b9944ea3 100644 --- a/examples/demo/src/configuration/Configuration.tsx +++ b/examples/demo/src/configuration/Configuration.tsx @@ -1,27 +1,34 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { useSelector, useDispatch } from 'react-redux'; -import Card from '@material-ui/core/Card'; -import CardContent from '@material-ui/core/CardContent'; -import Button from '@material-ui/core/Button'; +import Card from '@mui/material/Card'; +import CardContent from '@mui/material/CardContent'; +import Button from '@mui/material/Button'; import { useTranslate, useLocale, useSetLocale, Title } from 'react-admin'; -import { makeStyles } from '@material-ui/core/styles'; import { changeTheme } from './actions'; import { AppState } from '../types'; -const useStyles = makeStyles({ - label: { width: '10em', display: 'inline-block' }, - button: { margin: '1em' }, +const PREFIX = 'Configuration'; + +const classes = { + label: `${PREFIX}-label`, + button: `${PREFIX}-button`, +}; + +const StyledCard = styled(Card)({ + [`& .${classes.label}`]: { width: '10em', display: 'inline-block' }, + [`& .${classes.button}`]: { margin: '1em' }, }); const Configuration = () => { const translate = useTranslate(); const locale = useLocale(); const setLocale = useSetLocale(); - const classes = useStyles(); + const theme = useSelector((state: AppState) => state.theme); const dispatch = useDispatch(); return ( - + <CardContent> <div className={classes.label}> @@ -30,7 +37,7 @@ const Configuration = () => { <Button variant="contained" className={classes.button} - color={theme === 'light' ? 'primary' : 'default'} + color={theme === 'light' ? 'primary' : 'secondary'} onClick={() => dispatch(changeTheme('light'))} > {translate('pos.theme.light')} @@ -38,7 +45,7 @@ const Configuration = () => { <Button variant="contained" className={classes.button} - color={theme === 'dark' ? 'primary' : 'default'} + color={theme === 'dark' ? 'primary' : 'secondary'} onClick={() => dispatch(changeTheme('dark'))} > {translate('pos.theme.dark')} @@ -49,7 +56,7 @@ const Configuration = () => { <Button variant="contained" className={classes.button} - color={locale === 'en' ? 'primary' : 'default'} + color={locale === 'en' ? 'primary' : 'secondary'} onClick={() => setLocale('en')} > en @@ -57,13 +64,13 @@ const Configuration = () => { <Button variant="contained" className={classes.button} - color={locale === 'fr' ? 'primary' : 'default'} + color={locale === 'fr' ? 'primary' : 'secondary'} onClick={() => setLocale('fr')} > fr </Button> </CardContent> - </Card> + </StyledCard> ); }; diff --git a/examples/demo/src/dashboard/CardWithIcon.tsx b/examples/demo/src/dashboard/CardWithIcon.tsx index 62ae91360ea..b0ffd30e479 100644 --- a/examples/demo/src/dashboard/CardWithIcon.tsx +++ b/examples/demo/src/dashboard/CardWithIcon.tsx @@ -1,23 +1,23 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { FC, createElement } from 'react'; -import { Card, Box, Typography, Divider } from '@material-ui/core'; -import { makeStyles } from '@material-ui/core/styles'; +import { Card, Box, Typography, Divider } from '@mui/material'; import { Link } from 'react-router-dom'; import { ReactNode } from 'react'; import cartouche from './cartouche.png'; import cartoucheDark from './cartoucheDark.png'; -interface Props { - icon: FC<any>; - to: string; - title?: string; - subtitle?: string | number; - children?: ReactNode; -} +const PREFIX = 'CardWithIcon'; -const useStyles = makeStyles(theme => ({ - card: { +const classes = { + card: `${PREFIX}-card`, + main: `${PREFIX}-main`, + title: `${PREFIX}-title`, +}; + +const StyledCard = styled(Card)(({ theme }) => ({ + [`&.${classes.card}`]: { minHeight: 52, display: 'flex', flexDirection: 'column', @@ -27,27 +27,38 @@ const useStyles = makeStyles(theme => ({ color: 'inherit', }, }, - main: (props: Props) => ({ + + [`& .${classes.main}`]: (props: Props) => ({ overflow: 'inherit', padding: 16, background: `url(${ - theme.palette.type === 'dark' ? cartoucheDark : cartouche + theme.palette.mode === 'dark' ? cartoucheDark : cartouche }) no-repeat`, display: 'flex', justifyContent: 'space-between', alignItems: 'center', '& .icon': { - color: theme.palette.type === 'dark' ? 'inherit' : '#dc2440', + color: theme.palette.mode === 'dark' ? 'inherit' : '#dc2440', }, }), - title: {}, + + [`& .${classes.title}`]: {}, })); +interface Props { + icon: FC<any>; + to: string; + title?: string; + subtitle?: string | number; + children?: ReactNode; +} + const CardWithIcon = (props: Props) => { const { icon, title, subtitle, to, children } = props; - const classes = useStyles(props); + return ( - <Card className={classes.card}> + // @ts-ignore + <StyledCard className={classes.card}> <Link to={to}> <div className={classes.main}> <Box width="3em" className="icon"> @@ -68,7 +79,7 @@ const CardWithIcon = (props: Props) => { </Link> {children && <Divider />} {children} - </Card> + </StyledCard> ); }; diff --git a/examples/demo/src/dashboard/Dashboard.tsx b/examples/demo/src/dashboard/Dashboard.tsx index f18cca400b7..97166777176 100644 --- a/examples/demo/src/dashboard/Dashboard.tsx +++ b/examples/demo/src/dashboard/Dashboard.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect, useCallback, CSSProperties } from 'react'; import { useVersion, useDataProvider } from 'react-admin'; -import { useMediaQuery, Theme } from '@material-ui/core'; +import { useMediaQuery, Theme } from '@mui/material'; import { subDays } from 'date-fns'; import Welcome from './Welcome'; @@ -50,10 +50,10 @@ const Dashboard = () => { const version = useVersion(); const dataProvider = useDataProvider(); const isXSmall = useMediaQuery((theme: Theme) => - theme.breakpoints.down('xs') + theme.breakpoints.down('sm') ); const isSmall = useMediaQuery((theme: Theme) => - theme.breakpoints.down('md') + theme.breakpoints.down('lg') ); const fetchOrders = useCallback(async () => { diff --git a/examples/demo/src/dashboard/MonthlyRevenue.tsx b/examples/demo/src/dashboard/MonthlyRevenue.tsx index 387eb78fa5c..2300aa46ebe 100644 --- a/examples/demo/src/dashboard/MonthlyRevenue.tsx +++ b/examples/demo/src/dashboard/MonthlyRevenue.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import DollarIcon from '@material-ui/icons/AttachMoney'; +import DollarIcon from '@mui/icons-material/AttachMoney'; import { useTranslate } from 'react-admin'; import CardWithIcon from './CardWithIcon'; diff --git a/examples/demo/src/dashboard/NbNewOrders.tsx b/examples/demo/src/dashboard/NbNewOrders.tsx index e50f5554884..0e04f98affd 100644 --- a/examples/demo/src/dashboard/NbNewOrders.tsx +++ b/examples/demo/src/dashboard/NbNewOrders.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import ShoppingCartIcon from '@material-ui/icons/ShoppingCart'; +import ShoppingCartIcon from '@mui/icons-material/ShoppingCart'; import { useTranslate } from 'react-admin'; import CardWithIcon from './CardWithIcon'; diff --git a/examples/demo/src/dashboard/NewCustomers.tsx b/examples/demo/src/dashboard/NewCustomers.tsx index 0952555b128..37f6ab1344a 100644 --- a/examples/demo/src/dashboard/NewCustomers.tsx +++ b/examples/demo/src/dashboard/NewCustomers.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { Avatar, Box, @@ -7,9 +8,8 @@ import { ListItem, ListItemAvatar, ListItemText, -} from '@material-ui/core'; -import { makeStyles } from '@material-ui/core/styles'; -import CustomerIcon from '@material-ui/icons/PersonAdd'; +} from '@mui/material'; +import CustomerIcon from '@mui/icons-material/PersonAdd'; import { Link } from 'react-router-dom'; import { useTranslate, useQueryWithStore } from 'react-admin'; import { subDays } from 'date-fns'; @@ -17,9 +17,25 @@ import { subDays } from 'date-fns'; import CardWithIcon from './CardWithIcon'; import { Customer } from '../types'; +const PREFIX = 'NewCustomers'; + +const classes = { + link: `${PREFIX}-link`, + linkContent: `${PREFIX}-linkContent`, +}; + +const StyledCardWithIcon = styled(CardWithIcon)(({ theme }) => ({ + [`& .${classes.link}`]: { + borderRadius: 0, + }, + + [`& .${classes.linkContent}`]: { + color: theme.palette.primary.main, + }, +})); + const NewCustomers = () => { const translate = useTranslate(); - const classes = useStyles(); const aMonthAgo = subDays(new Date(), 30); aMonthAgo.setDate(aMonthAgo.getDate() - 30); @@ -45,7 +61,7 @@ const NewCustomers = () => { const nb = visitors ? visitors.reduce((nb: number) => ++nb, 0) : 0; return ( - <CardWithIcon + <StyledCardWithIcon to="/customers" icon={CustomerIcon} title={translate('pos.dashboard.new_customers')} @@ -70,7 +86,7 @@ const NewCustomers = () => { )) : null} </List> - <Box flexGrow="1"> </Box> + <Box flexGrow={1}> </Box> <Button className={classes.link} component={Link} @@ -82,17 +98,8 @@ const NewCustomers = () => { {translate('pos.dashboard.all_customers')} </Box> </Button> - </CardWithIcon> + </StyledCardWithIcon> ); }; -const useStyles = makeStyles(theme => ({ - link: { - borderRadius: 0, - }, - linkContent: { - color: theme.palette.primary.main, - }, -})); - export default NewCustomers; diff --git a/examples/demo/src/dashboard/OrderChart.tsx b/examples/demo/src/dashboard/OrderChart.tsx index 3a58fd3759c..399b3d2314e 100644 --- a/examples/demo/src/dashboard/OrderChart.tsx +++ b/examples/demo/src/dashboard/OrderChart.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { Card, CardHeader, CardContent } from '@material-ui/core'; +import { Card, CardHeader, CardContent } from '@mui/material'; import { ResponsiveContainer, AreaChart, diff --git a/examples/demo/src/dashboard/PendingOrders.tsx b/examples/demo/src/dashboard/PendingOrders.tsx index d3b75793698..f2066378929 100644 --- a/examples/demo/src/dashboard/PendingOrders.tsx +++ b/examples/demo/src/dashboard/PendingOrders.tsx @@ -1,38 +1,46 @@ import * as React from 'react'; -import Card from '@material-ui/core/Card'; -import CardHeader from '@material-ui/core/CardHeader'; -import List from '@material-ui/core/List'; -import ListItem from '@material-ui/core/ListItem'; -import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction'; -import ListItemAvatar from '@material-ui/core/ListItemAvatar'; -import ListItemText from '@material-ui/core/ListItemText'; -import Avatar from '@material-ui/core/Avatar'; -import { makeStyles } from '@material-ui/core/styles'; +import { styled } from '@mui/material/styles'; +import Card from '@mui/material/Card'; +import CardHeader from '@mui/material/CardHeader'; +import List from '@mui/material/List'; +import ListItem from '@mui/material/ListItem'; +import ListItemSecondaryAction from '@mui/material/ListItemSecondaryAction'; +import ListItemAvatar from '@mui/material/ListItemAvatar'; +import ListItemText from '@mui/material/ListItemText'; +import Avatar from '@mui/material/Avatar'; import { Link } from 'react-router-dom'; import { useTranslate } from 'react-admin'; import { Customer, Order } from '../types'; -interface Props { - orders?: Order[]; - customers?: { [key: string]: Customer }; -} +const PREFIX = 'PendingOrders'; -const useStyles = makeStyles(theme => ({ - root: { +const classes = { + root: `${PREFIX}-root`, + cost: `${PREFIX}-cost`, +}; + +const StyledCard = styled(Card)(({ theme }) => ({ + [`&.${classes.root}`]: { flex: 1, }, - cost: { + + [`& .${classes.cost}`]: { marginRight: '1em', color: theme.palette.text.primary, }, })); +interface Props { + orders?: Order[]; + customers?: { [key: string]: Customer }; +} + const PendingOrders = (props: Props) => { const { orders = [], customers = {} } = props; - const classes = useStyles(); + const translate = useTranslate(); return ( - <Card className={classes.root}> + <StyledCard className={classes.root}> <CardHeader title={translate('pos.dashboard.pending_orders')} /> <List dense={true}> {orders.map(record => ( @@ -79,7 +87,7 @@ const PendingOrders = (props: Props) => { </ListItem> ))} </List> - </Card> + </StyledCard> ); }; diff --git a/examples/demo/src/dashboard/PendingReviews.tsx b/examples/demo/src/dashboard/PendingReviews.tsx index 8d5ae2aa0aa..d09a783ac4d 100644 --- a/examples/demo/src/dashboard/PendingReviews.tsx +++ b/examples/demo/src/dashboard/PendingReviews.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { Avatar, Box, @@ -7,9 +8,8 @@ import { ListItem, ListItemAvatar, ListItemText, -} from '@material-ui/core'; -import { makeStyles } from '@material-ui/core/styles'; -import CommentIcon from '@material-ui/icons/Comment'; +} from '@mui/material'; +import CommentIcon from '@mui/icons-material/Comment'; import { Link } from 'react-router-dom'; import { useTranslate } from 'react-admin'; @@ -17,6 +17,37 @@ import CardWithIcon from './CardWithIcon'; import StarRatingField from '../reviews/StarRatingField'; import { Customer, Review } from '../types'; +const PREFIX = 'PendingReviews'; + +const classes = { + avatar: `${PREFIX}-avatar`, + listItemText: `${PREFIX}-listItemText`, + link: `${PREFIX}-link`, + linkContent: `${PREFIX}-linkContent`, +}; + +const StyledCardWithIcon = styled(CardWithIcon)(({ theme }) => ({ + [`& .${classes.avatar}`]: { + background: theme.palette.background.paper, + }, + + [`& .${classes.listItemText}`]: { + overflowY: 'hidden', + height: '4em', + display: '-webkit-box', + WebkitLineClamp: 2, + WebkitBoxOrient: 'vertical', + }, + + [`& .${classes.link}`]: { + borderRadius: 0, + }, + + [`& .${classes.linkContent}`]: { + color: theme.palette.primary.main, + }, +})); + interface Props { reviews?: Review[]; customers?: { [key: string]: Customer }; @@ -24,10 +55,9 @@ interface Props { } const PendingReviews = ({ reviews = [], customers = {}, nb }: Props) => { - const classes = useStyles(); const translate = useTranslate(); return ( - <CardWithIcon + <StyledCardWithIcon to="/reviews" icon={CommentIcon} title={translate('pos.dashboard.pending_reviews')} @@ -64,7 +94,7 @@ const PendingReviews = ({ reviews = [], customers = {}, nb }: Props) => { </ListItem> ))} </List> - <Box flexGrow="1"> </Box> + <Box flexGrow={1}> </Box> <Button className={classes.link} component={Link} @@ -76,27 +106,8 @@ const PendingReviews = ({ reviews = [], customers = {}, nb }: Props) => { {translate('pos.dashboard.all_reviews')} </Box> </Button> - </CardWithIcon> + </StyledCardWithIcon> ); }; -const useStyles = makeStyles(theme => ({ - avatar: { - background: theme.palette.background.paper, - }, - listItemText: { - overflowY: 'hidden', - height: '4em', - display: '-webkit-box', - WebkitLineClamp: 2, - WebkitBoxOrient: 'vertical', - }, - link: { - borderRadius: 0, - }, - linkContent: { - color: theme.palette.primary.main, - }, -})); - export default PendingReviews; diff --git a/examples/demo/src/dashboard/Welcome.tsx b/examples/demo/src/dashboard/Welcome.tsx index 63d16facbbb..4de9a7f985e 100644 --- a/examples/demo/src/dashboard/Welcome.tsx +++ b/examples/demo/src/dashboard/Welcome.tsx @@ -1,16 +1,24 @@ import * as React from 'react'; -import { Box, Card, CardActions, Button, Typography } from '@material-ui/core'; -import HomeIcon from '@material-ui/icons/Home'; -import CodeIcon from '@material-ui/icons/Code'; -import { makeStyles } from '@material-ui/core/styles'; +import { styled } from '@mui/material/styles'; +import { Box, Card, CardActions, Button, Typography } from '@mui/material'; +import HomeIcon from '@mui/icons-material/Home'; +import CodeIcon from '@mui/icons-material/Code'; import { useTranslate } from 'react-admin'; import publishArticleImage from './welcome_illustration.svg'; -const useStyles = makeStyles(theme => ({ - root: { +const PREFIX = 'Welcome'; + +const classes = { + root: `${PREFIX}-root`, + media: `${PREFIX}-media`, + actions: `${PREFIX}-actions`, +}; + +const StyledCard = styled(Card)(({ theme }) => ({ + [`&.${classes.root}`]: { background: - theme.palette.type === 'dark' + theme.palette.mode === 'dark' ? '#535353' : `linear-gradient(to right, #8975fb 0%, #746be7 35%), linear-gradient(to bottom, #8975fb 0%, #6f4ceb 50%), #6f4ceb`, @@ -19,12 +27,14 @@ const useStyles = makeStyles(theme => ({ marginTop: theme.spacing(2), marginBottom: '1em', }, - media: { + + [`& .${classes.media}`]: { background: `url(${publishArticleImage}) top right / cover`, marginLeft: 'auto', }, - actions: { - [theme.breakpoints.down('md')]: { + + [`& .${classes.actions}`]: { + [theme.breakpoints.down('lg')]: { padding: 0, flexWrap: 'wrap', '& a': { @@ -38,9 +48,9 @@ const useStyles = makeStyles(theme => ({ const Welcome = () => { const translate = useTranslate(); - const classes = useStyles(); + return ( - <Card className={classes.root}> + <StyledCard className={classes.root}> <Box display="flex"> <Box flex="1"> <Typography variant="h5" component="h2" gutterBottom> @@ -77,7 +87,7 @@ const Welcome = () => { overflow="hidden" /> </Box> - </Card> + </StyledCard> ); }; diff --git a/examples/demo/src/invoices/InvoiceList.tsx b/examples/demo/src/invoices/InvoiceList.tsx index a12027b552a..848a73f0a0f 100644 --- a/examples/demo/src/invoices/InvoiceList.tsx +++ b/examples/demo/src/invoices/InvoiceList.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { List, ListProps, @@ -9,30 +10,34 @@ import { NumberField, DateInput, } from 'react-admin'; -import { makeStyles } from '@material-ui/core/styles'; import FullNameField from '../visitors/FullNameField'; import AddressField from '../visitors/AddressField'; import InvoiceShow from './InvoiceShow'; -const listFilters = [ - <DateInput source="date_gte" alwaysOn />, - <DateInput source="date_lte" alwaysOn />, -]; +const PREFIX = 'InvoiceList'; + +const classes = { + hiddenOnSmallScreens: `${PREFIX}-hiddenOnSmallScreens`, +}; -const useStyles = makeStyles(theme => ({ - hiddenOnSmallScreens: { +const StyledList = styled(List)(({ theme }) => ({ + [`& .${classes.hiddenOnSmallScreens}`]: { display: 'table-cell', - [theme.breakpoints.down('md')]: { + [theme.breakpoints.down('lg')]: { display: 'none', }, }, })); +const listFilters = [ + <DateInput source="date_gte" alwaysOn />, + <DateInput source="date_lte" alwaysOn />, +]; + const InvoiceList = (props: ListProps) => { - const classes = useStyles(); return ( - <List + <StyledList {...props} filters={listFilters} perPage={25} @@ -62,7 +67,7 @@ const InvoiceList = (props: ListProps) => { <NumberField source="taxes" /> <NumberField source="total" /> </Datagrid> - </List> + </StyledList> ); }; diff --git a/examples/demo/src/invoices/InvoiceShow.tsx b/examples/demo/src/invoices/InvoiceShow.tsx index 5f2149c5c6d..b35fdb980e8 100644 --- a/examples/demo/src/invoices/InvoiceShow.tsx +++ b/examples/demo/src/invoices/InvoiceShow.tsx @@ -1,9 +1,9 @@ import * as React from 'react'; -import Card from '@material-ui/core/Card'; -import CardContent from '@material-ui/core/CardContent'; -import Grid from '@material-ui/core/Grid'; -import Typography from '@material-ui/core/Typography'; -import { makeStyles } from '@material-ui/core/styles'; +import { styled } from '@mui/material/styles'; +import Card from '@mui/material/Card'; +import CardContent from '@mui/material/CardContent'; +import Grid from '@mui/material/Grid'; +import Typography from '@mui/material/Typography'; import { useShowController, ReferenceField, @@ -14,6 +14,20 @@ import { import Basket from '../orders/Basket'; import { Customer, Invoice } from '../types'; +const PREFIX = 'InvoiceShow'; + +const classes = { + root: `${PREFIX}-root`, + spacer: `${PREFIX}-spacer`, + invoices: `${PREFIX}-invoices`, +}; + +const StyledCard = styled(Card)({ + [`&.${classes.root}`]: { width: 600, margin: 'auto' }, + [`& .${classes.spacer}`]: { height: 20 }, + [`& .${classes.invoices}`]: { margin: '10px 0' }, +}); + const CustomerField = ({ record }: FieldProps<Customer>) => record ? ( <Typography> @@ -27,11 +41,10 @@ const CustomerField = ({ record }: FieldProps<Customer>) => const InvoiceShow = (props: any) => { const { record } = useShowController<Invoice>(props); - const classes = useStyles(); if (!record) return null; return ( - <Card className={classes.root}> + <StyledCard className={classes.root}> <CardContent> <Grid container spacing={2}> <Grid item xs={6}> @@ -104,14 +117,8 @@ const InvoiceShow = (props: any) => { </ReferenceField> </div> </CardContent> - </Card> + </StyledCard> ); }; export default InvoiceShow; - -const useStyles = makeStyles({ - root: { width: 600, margin: 'auto' }, - spacer: { height: 20 }, - invoices: { margin: '10px 0' }, -}); diff --git a/examples/demo/src/invoices/index.ts b/examples/demo/src/invoices/index.ts index 19d3deeb647..05456a7b12a 100644 --- a/examples/demo/src/invoices/index.ts +++ b/examples/demo/src/invoices/index.ts @@ -1,4 +1,4 @@ -import InvoiceIcon from '@material-ui/icons/LibraryBooks'; +import InvoiceIcon from '@mui/icons-material/LibraryBooks'; import InvoiceList from './InvoiceList'; diff --git a/examples/demo/src/layout/AppBar.tsx b/examples/demo/src/layout/AppBar.tsx index 496d9900745..8258ec624e8 100644 --- a/examples/demo/src/layout/AppBar.tsx +++ b/examples/demo/src/layout/AppBar.tsx @@ -1,20 +1,27 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { forwardRef } from 'react'; import { AppBar, UserMenu, MenuItemLink, useTranslate } from 'react-admin'; -import Typography from '@material-ui/core/Typography'; -import SettingsIcon from '@material-ui/icons/Settings'; -import { makeStyles } from '@material-ui/core/styles'; +import Typography from '@mui/material/Typography'; +import SettingsIcon from '@mui/icons-material/Settings'; import Logo from './Logo'; -const useStyles = makeStyles({ - title: { +const PREFIX = 'CustomAppBar'; + +const classes = { + title: `${PREFIX}-title`, + spacer: `${PREFIX}-spacer`, +}; + +const StyledAppBar = styled(AppBar)({ + [`& .${classes.title}`]: { flex: 1, textOverflow: 'ellipsis', whiteSpace: 'nowrap', overflow: 'hidden', }, - spacer: { + [`& .${classes.spacer}`]: { flex: 1, }, }); @@ -40,9 +47,8 @@ const CustomUserMenu = (props: any) => ( ); const CustomAppBar = (props: any) => { - const classes = useStyles(); return ( - <AppBar {...props} elevation={1} userMenu={<CustomUserMenu />}> + <StyledAppBar {...props} elevation={1} userMenu={<CustomUserMenu />}> <Typography variant="h6" color="inherit" @@ -51,7 +57,7 @@ const CustomAppBar = (props: any) => { /> <Logo /> <span className={classes.spacer} /> - </AppBar> + </StyledAppBar> ); }; diff --git a/examples/demo/src/layout/Login.tsx b/examples/demo/src/layout/Login.tsx index a769d8f5369..33c134c0cdf 100644 --- a/examples/demo/src/layout/Login.tsx +++ b/examples/demo/src/layout/Login.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { useState } from 'react'; import PropTypes from 'prop-types'; import { Field, withTypes } from 'react-final-form'; @@ -11,16 +12,28 @@ import { CardActions, CircularProgress, TextField, -} from '@material-ui/core'; -import { createMuiTheme, makeStyles } from '@material-ui/core/styles'; -import { ThemeProvider } from '@material-ui/styles'; -import LockIcon from '@material-ui/icons/Lock'; +} from '@mui/material'; +import { createTheme, ThemeProvider } from '@mui/material/styles'; +import LockIcon from '@mui/icons-material/Lock'; import { Notification, useTranslate, useLogin, useNotify } from 'react-admin'; import { lightTheme } from './themes'; -const useStyles = makeStyles(theme => ({ - main: { +const PREFIX = 'LoginWithTheme'; + +const classes = { + main: `${PREFIX}-main`, + card: `${PREFIX}-card`, + avatar: `${PREFIX}-avatar`, + icon: `${PREFIX}-icon`, + hint: `${PREFIX}-hint`, + form: `${PREFIX}-form`, + input: `${PREFIX}-input`, + actions: `${PREFIX}-actions`, +}; + +const StyledForm = styled('form')(({ theme }) => ({ + [`& .${classes.main}`]: { display: 'flex', flexDirection: 'column', minHeight: '100vh', @@ -30,31 +43,38 @@ const useStyles = makeStyles(theme => ({ backgroundRepeat: 'no-repeat', backgroundSize: 'cover', }, - card: { + + [`& .${classes.card}`]: { minWidth: 300, marginTop: '6em', }, - avatar: { + + [`& .${classes.avatar}`]: { margin: '1em', display: 'flex', justifyContent: 'center', }, - icon: { + + [`& .${classes.icon}`]: { backgroundColor: theme.palette.secondary.main, }, - hint: { + + [`& .${classes.hint}`]: { marginTop: '1em', display: 'flex', justifyContent: 'center', color: theme.palette.grey[500], }, - form: { + + [`& .${classes.form}`]: { padding: '0 1em 1em 1em', }, - input: { + + [`& .${classes.input}`]: { marginTop: '1em', }, - actions: { + + [`& .${classes.actions}`]: { padding: '0 1em 1em 1em', }, })); @@ -83,7 +103,7 @@ const { Form } = withTypes<FormValues>(); const Login = () => { const [loading, setLoading] = useState(false); const translate = useTranslate(); - const classes = useStyles(); + const notify = useNotify(); const login = useLogin(); const location = useLocation<{ nextPathname: string } | null>(); @@ -129,7 +149,7 @@ const Login = () => { onSubmit={handleSubmit} validate={validate} render={({ handleSubmit }) => ( - <form onSubmit={handleSubmit} noValidate> + <StyledForm onSubmit={handleSubmit} noValidate> <div className={classes.main}> <Card className={classes.card}> <div className={classes.avatar}> @@ -182,7 +202,7 @@ const Login = () => { </Card> <Notification /> </div> - </form> + </StyledForm> )} /> ); @@ -194,10 +214,10 @@ Login.propTypes = { }; // We need to put the ThemeProvider decoration in another component -// Because otherwise the useStyles() hook used in Login won't get + // the right theme const LoginWithTheme = (props: any) => ( - <ThemeProvider theme={createMuiTheme(lightTheme)}> + <ThemeProvider theme={createTheme(lightTheme)}> <Login {...props} /> </ThemeProvider> ); diff --git a/examples/demo/src/layout/Logo.tsx b/examples/demo/src/layout/Logo.tsx index e2d7e7a47c7..5ef06756e43 100644 --- a/examples/demo/src/layout/Logo.tsx +++ b/examples/demo/src/layout/Logo.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { SVGProps } from 'react'; -import { useTheme } from '@material-ui/core/styles'; +import { useTheme } from '@mui/material/styles'; const Logo = (props: SVGProps<SVGSVGElement>) => { const theme = useTheme(); diff --git a/examples/demo/src/layout/Menu.tsx b/examples/demo/src/layout/Menu.tsx index 8edf0e0039c..b631abf0eac 100644 --- a/examples/demo/src/layout/Menu.tsx +++ b/examples/demo/src/layout/Menu.tsx @@ -1,8 +1,8 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { useState } from 'react'; import { useSelector } from 'react-redux'; -import LabelIcon from '@material-ui/icons/Label'; -import { makeStyles } from '@material-ui/core/styles'; +import LabelIcon from '@mui/icons-material/Label'; import classnames from 'classnames'; import { useTranslate, @@ -21,6 +21,33 @@ import reviews from '../reviews'; import SubMenu from './SubMenu'; import { AppState } from '../types'; +const PREFIX = 'Menu'; + +const classes = { + root: `${PREFIX}-root`, + open: `${PREFIX}-open`, + closed: `${PREFIX}-closed`, +}; + +const Root = styled('div')(({ theme }) => ({ + [`&.${classes.root}`]: { + marginTop: theme.spacing(1), + marginBottom: theme.spacing(1), + transition: theme.transitions.create('width', { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.leavingScreen, + }), + }, + + [`&.${classes.open}`]: { + width: 200, + }, + + [`&.${classes.closed}`]: { + width: 55, + }, +})); + type MenuName = 'menuCatalog' | 'menuSales' | 'menuCustomers'; const Menu = ({ dense = false }: MenuProps) => { @@ -32,14 +59,13 @@ const Menu = ({ dense = false }: MenuProps) => { const translate = useTranslate(); const open = useSelector((state: ReduxState) => state.admin.ui.sidebarOpen); useSelector((state: AppState) => state.theme); // force rerender on theme change - const classes = useStyles(); const handleToggle = (menu: MenuName) => { setState(state => ({ ...state, [menu]: !state[menu] })); }; return ( - <div + <Root className={classnames(classes.root, { [classes.open]: open, [classes.closed]: !open, @@ -148,25 +174,8 @@ const Menu = ({ dense = false }: MenuProps) => { leftIcon={<reviews.icon />} dense={dense} /> - </div> + </Root> ); }; -const useStyles = makeStyles(theme => ({ - root: { - marginTop: theme.spacing(1), - marginBottom: theme.spacing(1), - transition: theme.transitions.create('width', { - easing: theme.transitions.easing.sharp, - duration: theme.transitions.duration.leavingScreen, - }), - }, - open: { - width: 200, - }, - closed: { - width: 55, - }, -})); - export default Menu; diff --git a/examples/demo/src/layout/SubMenu.tsx b/examples/demo/src/layout/SubMenu.tsx index 9d7f695c6f2..5fc285bb63a 100644 --- a/examples/demo/src/layout/SubMenu.tsx +++ b/examples/demo/src/layout/SubMenu.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; -import { Fragment, ReactElement, ReactNode } from 'react'; +import { styled } from '@mui/material/styles'; +import { ReactElement, ReactNode } from 'react'; import { useSelector } from 'react-redux'; import { List, @@ -8,20 +9,30 @@ import { Typography, Collapse, Tooltip, -} from '@material-ui/core'; -import { makeStyles } from '@material-ui/core/styles'; -import ExpandMore from '@material-ui/icons/ExpandMore'; +} from '@mui/material'; +import ExpandMore from '@mui/icons-material/ExpandMore'; import { useTranslate, ReduxState } from 'react-admin'; -const useStyles = makeStyles(theme => ({ - icon: { minWidth: theme.spacing(5) }, - sidebarIsOpen: { +const PREFIX = 'SubMenu'; + +const classes = { + icon: `${PREFIX}-icon`, + sidebarIsOpen: `${PREFIX}-sidebarIsOpen`, + sidebarIsClosed: `${PREFIX}-sidebarIsClosed`, +}; + +// TODO jss-to-styled codemod: The Fragment root was replaced by div. Change the tag if needed. +const Root = styled('div')(({ theme }) => ({ + [`& .${classes.icon}`]: { minWidth: theme.spacing(5) }, + + [`& .${classes.sidebarIsOpen}`]: { '& a': { transition: 'padding-left 195ms cubic-bezier(0.4, 0, 0.6, 1) 0ms', paddingLeft: theme.spacing(4), }, }, - sidebarIsClosed: { + + [`& .${classes.sidebarIsClosed}`]: { '& a': { transition: 'padding-left 195ms cubic-bezier(0.4, 0, 0.6, 1) 0ms', paddingLeft: theme.spacing(2), @@ -41,13 +52,13 @@ interface Props { const SubMenu = (props: Props) => { const { handleToggle, isOpen, name, icon, children, dense } = props; const translate = useTranslate(); - const classes = useStyles(); + const sidebarIsOpen = useSelector<ReduxState, boolean>( state => state.admin.ui.sidebarOpen ); const header = ( - <MenuItem dense={dense} button onClick={handleToggle}> + <MenuItem dense={dense} onClick={handleToggle}> <ListItemIcon className={classes.icon}> {isOpen ? <ExpandMore /> : icon} </ListItemIcon> @@ -58,7 +69,7 @@ const SubMenu = (props: Props) => { ); return ( - <Fragment> + <Root> {sidebarIsOpen || isOpen ? ( header ) : ( @@ -80,7 +91,7 @@ const SubMenu = (props: Props) => { {children} </List> </Collapse> - </Fragment> + </Root> ); }; diff --git a/examples/demo/src/orders/Basket.tsx b/examples/demo/src/orders/Basket.tsx index 370a686026b..96bc7fde288 100644 --- a/examples/demo/src/orders/Basket.tsx +++ b/examples/demo/src/orders/Basket.tsx @@ -1,23 +1,29 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { Table, TableBody, TableCell, TableHead, TableRow, -} from '@material-ui/core'; -import { makeStyles } from '@material-ui/core/styles'; +} from '@mui/material'; import { Link, FieldProps, useTranslate, useQueryWithStore } from 'react-admin'; import { AppState, Order, Product } from '../types'; -const useStyles = makeStyles({ - rightAlignedCell: { textAlign: 'right' }, +const PREFIX = 'Basket'; + +const classes = { + rightAlignedCell: `${PREFIX}-rightAlignedCell`, +}; + +const StyledTable = styled(Table)({ + [`& .${classes.rightAlignedCell}`]: { textAlign: 'right' }, }); const Basket = (props: FieldProps<Order>) => { const { record } = props; - const classes = useStyles(); + const translate = useTranslate(); const { loaded, data: products } = useQueryWithStore<AppState>( @@ -52,7 +58,7 @@ const Basket = (props: FieldProps<Order>) => { if (!loaded || !record) return null; return ( - <Table> + <StyledTable> <TableHead> <TableRow> <TableCell> @@ -107,7 +113,7 @@ const Basket = (props: FieldProps<Order>) => { ) )} </TableBody> - </Table> + </StyledTable> ); }; diff --git a/examples/demo/src/orders/MobileGrid.tsx b/examples/demo/src/orders/MobileGrid.tsx index e3e98a979b6..debd4429920 100644 --- a/examples/demo/src/orders/MobileGrid.tsx +++ b/examples/demo/src/orders/MobileGrid.tsx @@ -1,7 +1,7 @@ // in src/comments.js import * as React from 'react'; -import { Card, CardHeader, CardContent } from '@material-ui/core'; -import { makeStyles } from '@material-ui/core/styles'; +import { styled } from '@mui/material/styles'; +import { Card, CardHeader, CardContent } from '@mui/material'; import { DateField, EditButton, @@ -16,21 +16,33 @@ import { import CustomerReferenceField from '../visitors/CustomerReferenceField'; -const useListStyles = makeStyles(theme => ({ - card: { +const PREFIX = 'MobileGrid'; + +const classes = { + card: `${PREFIX}-card`, + cardTitleContent: `${PREFIX}-cardTitleContent`, + cardContent: `${PREFIX}-cardContent`, + cardContentRow: `${PREFIX}-cardContentRow`, +}; + +const Root = styled('div')(({ theme }) => ({ + [`& .${classes.card}`]: { height: '100%', display: 'flex', flexDirection: 'column', margin: '0.5rem 0', }, - cardTitleContent: { + + [`& .${classes.cardTitleContent}`]: { display: 'flex', flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', }, - cardContent: theme.typography.body1, - cardContentRow: { + + [`& .${classes.cardContent}`]: theme.typography.body1, + + [`& .${classes.cardContentRow}`]: { display: 'flex', flexDirection: 'row', alignItems: 'center', @@ -47,14 +59,13 @@ interface MobileGridProps { const MobileGrid = (props: MobileGridProps) => { const { ids, data, basePath } = props; const translate = useTranslate(); - const classes = useListStyles(); if (!ids || !data || !basePath) { return null; } return ( - <div style={{ margin: '1em' }}> + <Root style={{ margin: '1em' }}> {ids.map(id => ( <Card key={id} className={classes.card}> <CardHeader @@ -116,7 +127,7 @@ const MobileGrid = (props: MobileGridProps) => { </CardContent> </Card> ))} - </div> + </Root> ); }; diff --git a/examples/demo/src/orders/OrderEdit.tsx b/examples/demo/src/orders/OrderEdit.tsx index baa6019a3b3..9bf42d2d864 100644 --- a/examples/demo/src/orders/OrderEdit.tsx +++ b/examples/demo/src/orders/OrderEdit.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { BooleanInput, DateField, @@ -13,20 +14,22 @@ import { useTranslate, } from 'react-admin'; import { Link as RouterLink } from 'react-router-dom'; -import { - Card, - CardContent, - Box, - Grid, - Typography, - Link, -} from '@material-ui/core'; -import { makeStyles } from '@material-ui/core/styles'; +import { Card, CardContent, Box, Grid, Typography, Link } from '@mui/material'; import { Order, Customer } from '../types'; import Basket from './Basket'; import Totals from './Totals'; +const PREFIX = 'OrderEdit'; + +const classes = { + root: `${PREFIX}-root`, +}; + +const StyledEdit = styled(Edit)({ + [`& .${classes.root}`]: { alignItems: 'flex-start' }, +}); + interface OrderTitleProps { record?: Order; } @@ -75,10 +78,6 @@ const CustomerAddress = ({ record }: { record?: Customer }) => ( </Box> ); -const useEditStyles = makeStyles({ - root: { alignItems: 'flex-start' }, -}); - const Spacer = () => <Box m={1}> </Box>; const OrderForm = (props: any) => { @@ -227,18 +226,15 @@ const OrderForm = (props: any) => { /> ); }; -const OrderEdit = (props: EditProps) => { - const classes = useEditStyles(); - return ( - <Edit - title={<OrderTitle />} - classes={classes} - {...props} - component="div" - > - <OrderForm /> - </Edit> - ); -}; +const OrderEdit = (props: EditProps) => ( + <StyledEdit + title={<OrderTitle />} + classes={classes} + {...props} + component="div" + > + <OrderForm /> + </StyledEdit> +); export default OrderEdit; diff --git a/examples/demo/src/orders/OrderList.tsx b/examples/demo/src/orders/OrderList.tsx index 279534dc20d..e49faf49cd5 100644 --- a/examples/demo/src/orders/OrderList.tsx +++ b/examples/demo/src/orders/OrderList.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { Fragment, useCallback, useEffect, useState } from 'react'; import { AutocompleteInput, @@ -21,9 +22,7 @@ import { useGetList, useListContext, } from 'react-admin'; -import { useMediaQuery, Divider, Tabs, Tab, Theme } from '@material-ui/core'; - -import { makeStyles } from '@material-ui/core/styles'; +import { useMediaQuery, Divider, Tabs, Tab, Theme } from '@mui/material'; import NbItemsField from './NbItemsField'; import CustomerReferenceField from '../visitors/CustomerReferenceField'; @@ -31,6 +30,16 @@ import AddressField from '../visitors/AddressField'; import MobileGrid from './MobileGrid'; import { Customer } from '../types'; +const PREFIX = 'OrderList'; + +const classes = { + total: `${PREFIX}-total`, +}; + +const StyledDatagrid = styled(Datagrid)({ + [`& .${classes.total}`]: { fontWeight: 'bold' }, +}); + const orderFilters = [ <SearchInput source="q" alwaysOn />, <ReferenceInput source="customer_id" reference="customers"> @@ -48,10 +57,6 @@ const orderFilters = [ <NullableBooleanInput source="returned" />, ]; -const useDatagridStyles = makeStyles({ - total: { fontWeight: 'bold' }, -}); - const tabs = [ { id: 'ordered', name: 'ordered' }, { id: 'delivered', name: 'delivered' }, @@ -90,9 +95,8 @@ const useGetTotals = (filterValues: any) => { const TabbedDatagrid = (props: TabbedDatagridProps) => { const listContext = useListContext(); const { ids, filterValues, setFilters, displayedFilters } = listContext; - const classes = useDatagridStyles(); const isXSmall = useMediaQuery<Theme>(theme => - theme.breakpoints.down('xs') + theme.breakpoints.down('sm') ); const [ordered, setOrdered] = useState<Identifier[]>([] as Identifier[]); const [delivered, setDelivered] = useState<Identifier[]>( @@ -171,7 +175,11 @@ const TabbedDatagrid = (props: TabbedDatagridProps) => { <ListContextProvider value={{ ...listContext, ids: ordered }} > - <Datagrid {...props} optimized rowClick="edit"> + <StyledDatagrid + {...props} + optimized + rowClick="edit" + > <DateField source="date" showTime /> <TextField source="reference" /> <CustomerReferenceField /> @@ -192,14 +200,14 @@ const TabbedDatagrid = (props: TabbedDatagridProps) => { }} className={classes.total} /> - </Datagrid> + </StyledDatagrid> </ListContextProvider> )} {filterValues.status === 'delivered' && ( <ListContextProvider value={{ ...listContext, ids: delivered }} > - <Datagrid {...props} rowClick="edit"> + <StyledDatagrid {...props} rowClick="edit"> <DateField source="date" showTime /> <TextField source="reference" /> <CustomerReferenceField /> @@ -221,14 +229,14 @@ const TabbedDatagrid = (props: TabbedDatagridProps) => { className={classes.total} /> <BooleanField source="returned" /> - </Datagrid> + </StyledDatagrid> </ListContextProvider> )} {filterValues.status === 'cancelled' && ( <ListContextProvider value={{ ...listContext, ids: cancelled }} > - <Datagrid {...props} rowClick="edit"> + <StyledDatagrid {...props} rowClick="edit"> <DateField source="date" showTime /> <TextField source="reference" /> <CustomerReferenceField /> @@ -250,7 +258,7 @@ const TabbedDatagrid = (props: TabbedDatagridProps) => { className={classes.total} /> <BooleanField source="returned" /> - </Datagrid> + </StyledDatagrid> </ListContextProvider> )} </div> diff --git a/examples/demo/src/orders/Totals.tsx b/examples/demo/src/orders/Totals.tsx index 378c5943137..705792b8b70 100644 --- a/examples/demo/src/orders/Totals.tsx +++ b/examples/demo/src/orders/Totals.tsx @@ -1,24 +1,32 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import classnames from 'classnames'; -import { Table, TableBody, TableCell, TableRow } from '@material-ui/core'; -import { makeStyles } from '@material-ui/core/styles'; +import { Table, TableBody, TableCell, TableRow } from '@mui/material'; import { FieldProps, useTranslate } from 'react-admin'; import { Order } from '../types'; -const useStyles = makeStyles({ - container: { minWidth: '35em' }, - rightAlignedCell: { textAlign: 'right' }, - boldCell: { fontWeight: 'bold' }, +const PREFIX = 'Totals'; + +const classes = { + container: `${PREFIX}-container`, + rightAlignedCell: `${PREFIX}-rightAlignedCell`, + boldCell: `${PREFIX}-boldCell`, +}; + +const StyledTable = styled(Table)({ + [`&.${classes.container}`]: { minWidth: '35em' }, + [`& .${classes.rightAlignedCell}`]: { textAlign: 'right' }, + [`& .${classes.boldCell}`]: { fontWeight: 'bold' }, }); const Totals = (props: FieldProps<Order>) => { const { record } = props; - const classes = useStyles(); + const translate = useTranslate(); return ( - <Table className={classes.container}> + <StyledTable className={classes.container}> <TableBody> <TableRow> <TableCell> @@ -74,7 +82,7 @@ const Totals = (props: FieldProps<Order>) => { </TableCell> </TableRow> </TableBody> - </Table> + </StyledTable> ); }; diff --git a/examples/demo/src/orders/index.ts b/examples/demo/src/orders/index.ts index 3f171bee1b1..0a69454368d 100644 --- a/examples/demo/src/orders/index.ts +++ b/examples/demo/src/orders/index.ts @@ -1,4 +1,4 @@ -import OrderIcon from '@material-ui/icons/AttachMoney'; +import OrderIcon from '@mui/icons-material/AttachMoney'; import OrderList from './OrderList'; import OrderEdit from './OrderEdit'; diff --git a/examples/demo/src/products/Aside.tsx b/examples/demo/src/products/Aside.tsx index ce0502cdf54..572ca4eeef9 100644 --- a/examples/demo/src/products/Aside.tsx +++ b/examples/demo/src/products/Aside.tsx @@ -1,10 +1,10 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import inflection from 'inflection'; -import { Card, CardContent } from '@material-ui/core'; -import { makeStyles } from '@material-ui/core/styles'; -import LocalOfferIcon from '@material-ui/icons/LocalOfferOutlined'; -import BarChartIcon from '@material-ui/icons/BarChart'; -import AttachMoneyIcon from '@material-ui/icons/AttachMoney'; +import { Card, CardContent } from '@mui/material'; +import LocalOfferIcon from '@mui/icons-material/LocalOfferOutlined'; +import BarChartIcon from '@mui/icons-material/BarChart'; +import AttachMoneyIcon from '@mui/icons-material/AttachMoney'; import { FilterList, FilterListItem, @@ -14,14 +14,20 @@ import { import { Category } from '../types'; -const useStyles = makeStyles(theme => ({ - root: { +const PREFIX = 'Aside'; + +const classes = { + root: `${PREFIX}-root`, +}; + +const StyledCard = styled(Card)(({ theme }) => ({ + [`&.${classes.root}`]: { [theme.breakpoints.up('sm')]: { width: '15em', marginRight: '1em', overflow: 'initial', }, - [theme.breakpoints.down('sm')]: { + [theme.breakpoints.down('md')]: { display: 'none', }, }, @@ -34,9 +40,9 @@ const Aside = () => { { field: 'name', order: 'ASC' }, {} ); - const classes = useStyles(); + return ( - <Card className={classes.root}> + <StyledCard className={classes.root}> <CardContent> <FilterLiveSearch /> @@ -131,7 +137,7 @@ const Aside = () => { ))} </FilterList> </CardContent> - </Card> + </StyledCard> ); }; diff --git a/examples/demo/src/products/GridList.tsx b/examples/demo/src/products/GridList.tsx index 9f210b039f3..5f242afb66b 100644 --- a/examples/demo/src/products/GridList.tsx +++ b/examples/demo/src/products/GridList.tsx @@ -1,9 +1,8 @@ import * as React from 'react'; -import MuiGridList from '@material-ui/core/GridList'; -import GridListTile from '@material-ui/core/GridListTile'; -import GridListTileBar from '@material-ui/core/GridListTileBar'; -import { makeStyles } from '@material-ui/core/styles'; -import withWidth, { WithWidth } from '@material-ui/core/withWidth'; +import { styled, useTheme, useMediaQuery } from '@mui/material'; +import MuiGridList from '@mui/material/ImageList'; +import ImageListItem from '@mui/material/ImageListItem'; +import ImageListItemBar from '@mui/material/ImageListItemBar'; import { linkToRecord, NumberField, @@ -12,81 +11,100 @@ import { Identifier, } from 'react-admin'; import { Link } from 'react-router-dom'; -import { Breakpoint } from '@material-ui/core/styles/createBreakpoints'; -const useStyles = makeStyles(theme => ({ - gridList: { +const PREFIX = 'GridList'; + +const classes = { + gridList: `${PREFIX}-gridList`, + tileBar: `${PREFIX}-tileBar`, + placeholder: `${PREFIX}-placeholder`, + price: `${PREFIX}-price`, + link: `${PREFIX}-link`, +}; + +const StyledGridList = styled(MuiGridList)(({ theme }) => ({ + [`& .${classes.gridList}`]: { margin: 0, }, - tileBar: { + + [`& .${classes.tileBar}`]: { background: 'linear-gradient(to top, rgba(0,0,0,0.8) 0%,rgba(0,0,0,0.4) 70%,rgba(0,0,0,0) 100%)', }, - placeholder: { + + [`& .${classes.placeholder}`]: { backgroundColor: theme.palette.grey[300], height: '100%', }, - price: { + + [`& .${classes.price}`]: { display: 'inline', fontSize: '1em', }, - link: { + + [`& .${classes.link}`]: { color: '#fff', }, })); -const getColsForWidth = (width: Breakpoint) => { - if (width === 'xs') return 2; - if (width === 'sm') return 3; - if (width === 'md') return 3; - if (width === 'lg') return 5; +const useColsForWidth = () => { + const theme = useTheme(); + const xs = useMediaQuery(theme.breakpoints.up('xs')); + const sm = useMediaQuery(theme.breakpoints.up('sm')); + const md = useMediaQuery(theme.breakpoints.up('md')); + const lg = useMediaQuery(theme.breakpoints.up('lg')); + + if (xs) return 2; + if (sm) return 3; + if (md) return 3; + if (lg) return 5; + return 6; }; const times = (nbChildren: number, fn: (key: number) => any) => Array.from({ length: nbChildren }, (_, key) => fn(key)); -const LoadingGridList = (props: GridProps & { nbItems?: number }) => { - const { width, nbItems = 20 } = props; - const classes = useStyles(); +const LoadingGridList = (props: DatagridProps & { nbItems?: number }) => { + const { nbItems = 20 } = props; + const cols = useColsForWidth(); return ( - <MuiGridList - cellHeight={180} - cols={getColsForWidth(width)} + <StyledGridList + rowHeight={180} + cols={cols} className={classes.gridList} > {' '} {times(nbItems, key => ( - <GridListTile key={key}> + <ImageListItem key={key}> <div className={classes.placeholder} /> - </GridListTile> + </ImageListItem> ))} - </MuiGridList> + </StyledGridList> ); }; -const LoadedGridList = (props: GridProps) => { - const { width } = props; +const LoadedGridList = (props: DatagridProps) => { const { ids, data, basePath } = useListContext(); - const classes = useStyles(); + const cols = useColsForWidth(); if (!ids || !data) return null; return ( - <MuiGridList - cellHeight={180} - cols={getColsForWidth(width)} + <StyledGridList + rowHeight={180} + cols={cols} className={classes.gridList} > {ids.map((id: Identifier) => ( - <GridListTile + <ImageListItem // @ts-ignore component={Link} key={id} to={linkToRecord(basePath, data[id].id)} > <img src={data[id].thumbnail} alt="" /> - <GridListTileBar + <ImageListItemBar className={classes.tileBar} title={data[id].reference} subtitle={ @@ -105,22 +123,15 @@ const LoadedGridList = (props: GridProps) => { </span> } /> - </GridListTile> + </ImageListItem> ))} - </MuiGridList> + </StyledGridList> ); }; -interface GridProps extends Omit<DatagridProps, 'width'>, WithWidth {} - -const GridList = (props: WithWidth) => { - const { width } = props; +const ImageList = () => { const { loaded } = useListContext(); - return loaded ? ( - <LoadedGridList width={width} /> - ) : ( - <LoadingGridList width={width} /> - ); + return loaded ? <LoadedGridList /> : <LoadingGridList />; }; -export default withWidth()(GridList); +export default ImageList; diff --git a/examples/demo/src/products/Poster.tsx b/examples/demo/src/products/Poster.tsx index bf6587789eb..15e5fa3f1b6 100644 --- a/examples/demo/src/products/Poster.tsx +++ b/examples/demo/src/products/Poster.tsx @@ -1,14 +1,26 @@ import * as React from 'react'; -import Card from '@material-ui/core/Card'; -import CardContent from '@material-ui/core/CardContent'; -import { makeStyles } from '@material-ui/core/styles'; +import { styled } from '@mui/material/styles'; +import Card from '@mui/material/Card'; +import CardContent from '@mui/material/CardContent'; import { FieldProps } from 'react-admin'; import { Product } from '../types'; -const useStyles = makeStyles({ - root: { display: 'inline-block', marginTop: '1em', zIndex: 2 }, - content: { padding: 0, '&:last-child': { padding: 0 } }, - img: { +const PREFIX = 'Poster'; + +const classes = { + root: `${PREFIX}-root`, + content: `${PREFIX}-content`, + img: `${PREFIX}-img`, +}; + +const StyledCard = styled(Card)({ + [`&.${classes.root}`]: { + display: 'inline-block', + marginTop: '1em', + zIndex: 2, + }, + [`& .${classes.content}`]: { padding: 0, '&:last-child': { padding: 0 } }, + [`& .${classes.img}`]: { width: 'initial', minWidth: 'initial', maxWidth: '42em', @@ -18,16 +30,15 @@ const useStyles = makeStyles({ const Poster = (props: FieldProps<Product>) => { const { record } = props; - const classes = useStyles(); if (!record) return null; return ( - <Card className={classes.root}> + <StyledCard className={classes.root}> <CardContent className={classes.content}> <img src={record.image} alt="" className={classes.img} /> </CardContent> - </Card> + </StyledCard> ); }; diff --git a/examples/demo/src/products/ProductCreate.tsx b/examples/demo/src/products/ProductCreate.tsx index 9563f1ebb28..129db2cad59 100644 --- a/examples/demo/src/products/ProductCreate.tsx +++ b/examples/demo/src/products/ProductCreate.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { Create, FormTab, @@ -10,25 +11,35 @@ import { required, CreateProps, } from 'react-admin'; -import { InputAdornment } from '@material-ui/core'; -import { makeStyles } from '@material-ui/core/styles'; +import { InputAdornment } from '@mui/material'; import RichTextInput from 'ra-input-rich-text'; -export const styles = { - price: { width: '7em' }, - width: { width: '7em' }, - height: { width: '7em' }, - stock: { width: '7em' }, - widthFormGroup: { display: 'inline-block' }, - heightFormGroup: { display: 'inline-block', marginLeft: 32 }, +const PREFIX = 'ProductCreate'; + +const classes = { + price: `${PREFIX}-price`, + width: `${PREFIX}-width`, + height: `${PREFIX}-height`, + stock: `${PREFIX}-stock`, + widthFormGroup: `${PREFIX}-widthFormGroup`, + heightFormGroup: `${PREFIX}-heightFormGroup`, }; -const useStyles = makeStyles(styles); +const StyledCreate = styled(Create)({ + [`& .${classes.price}`]: { width: '7em' }, + [`& .${classes.width}`]: { width: '7em' }, + [`& .${classes.height}`]: { width: '7em' }, + [`& .${classes.stock}`]: { width: '7em' }, + [`& .${classes.widthFormGroup}`]: { display: 'inline-block' }, + [`& .${classes.heightFormGroup}`]: { + display: 'inline-block', + marginLeft: 32, + }, +}); const ProductCreate = (props: CreateProps) => { - const classes = useStyles(); return ( - <Create {...props}> + <StyledCreate {...props}> <TabbedForm> <FormTab label="resources.products.tabs.image"> <TextInput @@ -103,7 +114,7 @@ const ProductCreate = (props: CreateProps) => { <RichTextInput source="description" label="" /> </FormTab> </TabbedForm> - </Create> + </StyledCreate> ); }; diff --git a/examples/demo/src/products/ProductEdit.tsx b/examples/demo/src/products/ProductEdit.tsx index e2e1c636151..b65105321ed 100644 --- a/examples/demo/src/products/ProductEdit.tsx +++ b/examples/demo/src/products/ProductEdit.tsx @@ -16,14 +16,12 @@ import { TextField, TextInput, } from 'react-admin'; -import { InputAdornment } from '@material-ui/core'; -import { makeStyles } from '@material-ui/core/styles'; +import { InputAdornment, styled } from '@mui/material'; import RichTextInput from 'ra-input-rich-text'; import CustomerReferenceField from '../visitors/CustomerReferenceField'; import StarRatingField from '../reviews/StarRatingField'; import Poster from './Poster'; -import { styles as createStyles } from './ProductCreate'; import { Product } from '../types'; interface ProductTitleProps { @@ -33,139 +31,151 @@ interface ProductTitleProps { const ProductTitle = ({ record }: ProductTitleProps) => record ? <span>Poster #{record.reference}</span> : null; -const useStyles = makeStyles({ - ...createStyles, - comment: { +const PREFIX = 'ProductEdit'; + +const classes = { + price: `${PREFIX}-price`, + width: `${PREFIX}-width`, + height: `${PREFIX}-height`, + stock: `${PREFIX}-stock`, + widthFormGroup: `${PREFIX}-widthFormGroup`, + heightFormGroup: `${PREFIX}-heightFormGroup`, + comment: `${PREFIX}-comment`, + tab: `${PREFIX}-tab`, +}; + +const StyledEdit = styled(Edit)({ + [`& .${classes.price}`]: { width: '7em' }, + [`& .${classes.width}`]: { width: '7em' }, + [`& .${classes.height}`]: { width: '7em' }, + [`& .${classes.stock}`]: { width: '7em' }, + [`& .${classes.widthFormGroup}`]: { display: 'inline-block' }, + [`& .${classes.heightFormGroup}`]: { + display: 'inline-block', + marginLeft: 32, + }, + [`& .${classes.comment}`]: { maxWidth: '20em', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', }, - tab: { + [`& .${classes.tab}`]: { maxWidth: '40em', display: 'block', }, }); -const ProductEdit = (props: EditProps) => { - const classes = useStyles(); - return ( - <Edit {...props} title={<ProductTitle />}> - <TabbedForm> - <FormTab - label="resources.products.tabs.image" - contentClassName={classes.tab} - > - <Poster /> - <TextInput - source="image" - fullWidth - validate={requiredValidate} - /> - <TextInput - source="thumbnail" - fullWidth - validate={requiredValidate} - /> - </FormTab> - <FormTab - label="resources.products.tabs.details" - path="details" - contentClassName={classes.tab} +const ProductEdit = (props: EditProps) => ( + <StyledEdit {...props} title={<ProductTitle />}> + <TabbedForm> + <FormTab + label="resources.products.tabs.image" + contentClassName={classes.tab} + > + <Poster /> + <TextInput + source="image" + fullWidth + validate={requiredValidate} + /> + <TextInput + source="thumbnail" + fullWidth + validate={requiredValidate} + /> + </FormTab> + <FormTab + label="resources.products.tabs.details" + path="details" + contentClassName={classes.tab} + > + <TextInput source="reference" validate={requiredValidate} /> + <NumberInput + source="price" + className={classes.price} + InputProps={{ + startAdornment: ( + <InputAdornment position="start">€</InputAdornment> + ), + }} + validate={requiredValidate} + /> + <NumberInput + source="width" + className={classes.width} + formClassName={classes.widthFormGroup} + InputProps={{ + endAdornment: ( + <InputAdornment position="start">cm</InputAdornment> + ), + }} + validate={requiredValidate} + /> + <NumberInput + source="height" + className={classes.height} + formClassName={classes.heightFormGroup} + InputProps={{ + endAdornment: ( + <InputAdornment position="start">cm</InputAdornment> + ), + }} + validate={requiredValidate} + /> + <ReferenceInput + source="category_id" + reference="categories" + validate={requiredValidate} > - <TextInput source="reference" validate={requiredValidate} /> - <NumberInput - source="price" - className={classes.price} - InputProps={{ - startAdornment: ( - <InputAdornment position="start"> - € - </InputAdornment> - ), - }} - validate={requiredValidate} - /> - <NumberInput - source="width" - className={classes.width} - formClassName={classes.widthFormGroup} - InputProps={{ - endAdornment: ( - <InputAdornment position="start"> - cm - </InputAdornment> - ), - }} - validate={requiredValidate} - /> - <NumberInput - source="height" - className={classes.height} - formClassName={classes.heightFormGroup} - InputProps={{ - endAdornment: ( - <InputAdornment position="start"> - cm - </InputAdornment> - ), - }} - validate={requiredValidate} - /> - <ReferenceInput - source="category_id" - reference="categories" - validate={requiredValidate} - > - <SelectInput source="name" /> - </ReferenceInput> - <NumberInput - source="stock" - className={classes.stock} - validate={requiredValidate} - /> - <NumberInput - source="sales" - className={classes.stock} - validate={requiredValidate} - /> - </FormTab> - <FormTab - label="resources.products.tabs.description" - path="description" - contentClassName={classes.tab} + <SelectInput source="name" /> + </ReferenceInput> + <NumberInput + source="stock" + className={classes.stock} + validate={requiredValidate} + /> + <NumberInput + source="sales" + className={classes.stock} + validate={requiredValidate} + /> + </FormTab> + <FormTab + label="resources.products.tabs.description" + path="description" + contentClassName={classes.tab} + > + <RichTextInput + source="description" + label="" + validate={requiredValidate} + /> + </FormTab> + <FormTab label="resources.products.tabs.reviews" path="reviews"> + <ReferenceManyField + reference="reviews" + target="product_id" + addLabel={false} + pagination={<Pagination />} + fullWidth > - <RichTextInput - source="description" - label="" - validate={requiredValidate} - /> - </FormTab> - <FormTab label="resources.products.tabs.reviews" path="reviews"> - <ReferenceManyField - reference="reviews" - target="product_id" - addLabel={false} - pagination={<Pagination />} - fullWidth - > - <Datagrid> - <DateField source="date" /> - <CustomerReferenceField /> - <StarRatingField /> - <TextField - source="comment" - cellClassName={classes.comment} - /> - <TextField source="status" /> - <EditButton /> - </Datagrid> - </ReferenceManyField> - </FormTab> - </TabbedForm> - </Edit> - ); -}; + <Datagrid> + <DateField source="date" /> + <CustomerReferenceField /> + <StarRatingField /> + <TextField + source="comment" + cellClassName={classes.comment} + /> + <TextField source="status" /> + <EditButton /> + </Datagrid> + </ReferenceManyField> + </FormTab> + </TabbedForm> + </StyledEdit> +); const requiredValidate = [required()]; diff --git a/examples/demo/src/products/ProductList.tsx b/examples/demo/src/products/ProductList.tsx index 43161f4f490..02e2a962b5b 100644 --- a/examples/demo/src/products/ProductList.tsx +++ b/examples/demo/src/products/ProductList.tsx @@ -1,6 +1,5 @@ import * as React from 'react'; -import { Box, Chip, useMediaQuery, Theme } from '@material-ui/core'; -import { makeStyles } from '@material-ui/core/styles'; +import { Box, Chip, useMediaQuery, Theme } from '@mui/material'; import { CreateButton, ExportButton, @@ -22,19 +21,24 @@ import { useTranslate, } from 'react-admin'; -import GridList from './GridList'; +import ImageList from './GridList'; import Aside from './Aside'; -const useQuickFilterStyles = makeStyles(theme => ({ - root: { - marginBottom: theme.spacing(1), - }, -})); +const PREFIX = 'ProductList'; + +const classes = { + root: `${PREFIX}-root`, +}; const QuickFilter = ({ label }: InputProps) => { const translate = useTranslate(); - const classes = useQuickFilterStyles(); - return <Chip className={classes.root} label={translate(label)} />; + return ( + <Chip + sx={{ marginBottom: 1 }} + className={classes.root} + label={translate(label)} + /> + ); }; export const productFilters = [ @@ -67,7 +71,7 @@ const ListActions = ({ isSmall }: any) => ( ); const ProductList = (props: ListProps) => { - const isSmall = useMediaQuery<Theme>(theme => theme.breakpoints.down('sm')); + const isSmall = useMediaQuery<Theme>(theme => theme.breakpoints.down('md')); return ( <ListBase perPage={20} @@ -95,7 +99,7 @@ const ProductListView = ({ isSmall }: { isSmall: boolean }) => { <Box display="flex"> <Aside /> <Box width={isSmall ? 'auto' : 'calc(100% - 16em)'}> - <GridList /> + <ImageList /> <Pagination rowsPerPageOptions={[10, 20, 40]} /> </Box> </Box> diff --git a/examples/demo/src/products/ThumbnailField.tsx b/examples/demo/src/products/ThumbnailField.tsx index 8af77d83231..6c626f43458 100644 --- a/examples/demo/src/products/ThumbnailField.tsx +++ b/examples/demo/src/products/ThumbnailField.tsx @@ -1,17 +1,23 @@ import * as React from 'react'; -import { makeStyles } from '@material-ui/core/styles'; +import { styled } from '@mui/material/styles'; import { FieldProps } from 'react-admin'; import { Product } from '../types'; -const useStyles = makeStyles({ - root: { width: 25, maxWidth: 25, maxHeight: 25 }, +const PREFIX = 'ThumbnailField'; + +const classes = { + root: `${PREFIX}-root`, +}; + +const Root = styled('img')({ + [`&.${classes.root}`]: { width: 25, maxWidth: 25, maxHeight: 25 }, }); const ThumbnailField = (props: FieldProps<Product>) => { const { record } = props; - const classes = useStyles(); + return record ? ( - <img src={record.thumbnail} className={classes.root} alt="" /> + <Root src={record.thumbnail} className={classes.root} alt="" /> ) : null; }; diff --git a/examples/demo/src/products/index.tsx b/examples/demo/src/products/index.tsx index e0ea1d4b0ec..aea0e734e9a 100644 --- a/examples/demo/src/products/index.tsx +++ b/examples/demo/src/products/index.tsx @@ -1,4 +1,4 @@ -import ProductIcon from '@material-ui/icons/Collections'; +import ProductIcon from '@mui/icons-material/Collections'; import ProductList from './ProductList'; import ProductEdit from './ProductEdit'; import ProductCreate from './ProductCreate'; diff --git a/examples/demo/src/reviews/AcceptButton.tsx b/examples/demo/src/reviews/AcceptButton.tsx index b50828f288d..f83dd88213e 100644 --- a/examples/demo/src/reviews/AcceptButton.tsx +++ b/examples/demo/src/reviews/AcceptButton.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import PropTypes from 'prop-types'; -import Button from '@material-ui/core/Button'; -import ThumbUp from '@material-ui/icons/ThumbUp'; +import Button from '@mui/material/Button'; +import ThumbUp from '@mui/icons-material/ThumbUp'; import { useTranslate, useUpdate, useNotify, useRedirect } from 'react-admin'; import { Review } from './../types'; diff --git a/examples/demo/src/reviews/BulkAcceptButton.tsx b/examples/demo/src/reviews/BulkAcceptButton.tsx index 952e7bcbe80..61302f8f6ed 100644 --- a/examples/demo/src/reviews/BulkAcceptButton.tsx +++ b/examples/demo/src/reviews/BulkAcceptButton.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import PropTypes from 'prop-types'; -import ThumbUp from '@material-ui/icons/ThumbUp'; +import ThumbUp from '@mui/icons-material/ThumbUp'; import { Button, diff --git a/examples/demo/src/reviews/BulkRejectButton.tsx b/examples/demo/src/reviews/BulkRejectButton.tsx index 9e87c5e30c0..1d319ca2982 100644 --- a/examples/demo/src/reviews/BulkRejectButton.tsx +++ b/examples/demo/src/reviews/BulkRejectButton.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import PropTypes from 'prop-types'; -import ThumbDown from '@material-ui/icons/ThumbDown'; +import ThumbDown from '@mui/icons-material/ThumbDown'; import { Button, diff --git a/examples/demo/src/reviews/RejectButton.tsx b/examples/demo/src/reviews/RejectButton.tsx index 029499632e6..833533ea012 100644 --- a/examples/demo/src/reviews/RejectButton.tsx +++ b/examples/demo/src/reviews/RejectButton.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import PropTypes from 'prop-types'; -import Button from '@material-ui/core/Button'; -import ThumbDown from '@material-ui/icons/ThumbDown'; +import Button from '@mui/material/Button'; +import ThumbDown from '@mui/icons-material/ThumbDown'; import { useTranslate, useUpdate, useNotify, useRedirect } from 'react-admin'; import { Review } from '../types'; diff --git a/examples/demo/src/reviews/ReviewEdit.tsx b/examples/demo/src/reviews/ReviewEdit.tsx index 99c7180d5ba..5c88d80b31c 100644 --- a/examples/demo/src/reviews/ReviewEdit.tsx +++ b/examples/demo/src/reviews/ReviewEdit.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { useEditController, EditContextProvider, @@ -8,9 +9,8 @@ import { DateField, EditProps, } from 'react-admin'; -import { IconButton, Typography } from '@material-ui/core'; -import { makeStyles } from '@material-ui/core/styles'; -import CloseIcon from '@material-ui/icons/Close'; +import { IconButton, Typography } from '@mui/material'; +import CloseIcon from '@mui/icons-material/Close'; import ProductReferenceField from '../products/ProductReferenceField'; import CustomerReferenceField from '../visitors/CustomerReferenceField'; @@ -18,26 +18,38 @@ import StarRatingField from './StarRatingField'; import ReviewEditToolbar from './ReviewEditToolbar'; import { Review } from '../types'; -const useStyles = makeStyles(theme => ({ - root: { +const PREFIX = 'ReviewEdit'; + +const classes = { + root: `${PREFIX}-root`, + title: `${PREFIX}-title`, + form: `${PREFIX}-form`, + inlineField: `${PREFIX}-inlineField`, +}; + +const Root = styled('div')(({ theme }) => ({ + [`&.${classes.root}`]: { paddingTop: 40, }, - title: { + + [`& .${classes.title}`]: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', margin: '1em', }, - form: { + + [`& .${classes.form}`]: { [theme.breakpoints.up('xs')]: { width: 400, }, - [theme.breakpoints.down('xs')]: { + [theme.breakpoints.down('sm')]: { width: '100vw', marginTop: -30, }, }, - inlineField: { + + [`& .${classes.inlineField}`]: { display: 'inline-block', width: '50%', }, @@ -48,19 +60,18 @@ interface Props extends EditProps { } const ReviewEdit = ({ onCancel, ...props }: Props) => { - const classes = useStyles(); const controllerProps = useEditController<Review>(props); const translate = useTranslate(); if (!controllerProps.record) { return null; } return ( - <div className={classes.root}> + <Root className={classes.root}> <div className={classes.title}> <Typography variant="h6"> {translate('resources.reviews.detail')} </Typography> - <IconButton onClick={onCancel}> + <IconButton onClick={onCancel} size="large"> <CloseIcon /> </IconButton> </div> @@ -94,7 +105,7 @@ const ReviewEdit = ({ onCancel, ...props }: Props) => { /> </SimpleForm> </EditContextProvider> - </div> + </Root> ); }; diff --git a/examples/demo/src/reviews/ReviewEditToolbar.tsx b/examples/demo/src/reviews/ReviewEditToolbar.tsx index 8adf4a5cf0a..aab2db58cc1 100644 --- a/examples/demo/src/reviews/ReviewEditToolbar.tsx +++ b/examples/demo/src/reviews/ReviewEditToolbar.tsx @@ -1,15 +1,21 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { Fragment } from 'react'; -import MuiToolbar from '@material-ui/core/Toolbar'; -import { makeStyles } from '@material-ui/core/styles'; +import MuiToolbar from '@mui/material/Toolbar'; import { SaveButton, DeleteButton, ToolbarProps } from 'react-admin'; import AcceptButton from './AcceptButton'; import RejectButton from './RejectButton'; import { Review } from '../types'; -const useStyles = makeStyles(theme => ({ - root: { +const PREFIX = 'ReviewEditToolbar'; + +const classes = { + root: `${PREFIX}-root`, +}; + +const StyledMuiToolbar = styled(MuiToolbar)(({ theme }) => ({ + [`&.${classes.root}`]: { backgroundColor: theme.palette.background.paper, display: 'flex', justifyContent: 'space-between', @@ -25,11 +31,10 @@ const ReviewEditToolbar = (props: ToolbarProps<Review>) => { resource, saving, } = props; - const classes = useStyles(); if (!record) return null; return ( - <MuiToolbar className={classes.root}> + <StyledMuiToolbar className={classes.root}> {record.status === 'pending' ? ( <Fragment> <AcceptButton record={record} /> @@ -51,7 +56,7 @@ const ReviewEditToolbar = (props: ToolbarProps<Review>) => { /> </Fragment> )} - </MuiToolbar> + </StyledMuiToolbar> ); }; diff --git a/examples/demo/src/reviews/ReviewList.tsx b/examples/demo/src/reviews/ReviewList.tsx index a7db917aae5..b2522e85605 100644 --- a/examples/demo/src/reviews/ReviewList.tsx +++ b/examples/demo/src/reviews/ReviewList.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { Fragment, useCallback } from 'react'; import classnames from 'classnames'; import { @@ -8,8 +9,7 @@ import { BulkActionProps, } from 'react-admin'; import { Route, RouteChildrenProps, useHistory } from 'react-router-dom'; -import { Drawer, useMediaQuery, Theme } from '@material-ui/core'; -import { makeStyles } from '@material-ui/core/styles'; +import { Drawer, useMediaQuery, Theme } from '@mui/material'; import BulkAcceptButton from './BulkAcceptButton'; import BulkRejectButton from './BulkRejectButton'; @@ -18,37 +18,48 @@ import ReviewListDesktop from './ReviewListDesktop'; import reviewFilters from './reviewFilters'; import ReviewEdit from './ReviewEdit'; -const ReviewsBulkActionButtons = (props: BulkActionProps) => ( - <Fragment> - <BulkAcceptButton {...props} /> - <BulkRejectButton {...props} /> - <BulkDeleteButton {...props} /> - </Fragment> -); +const PREFIX = 'ReviewList'; -const useStyles = makeStyles(theme => ({ - root: { +const classes = { + root: `${PREFIX}-root`, + list: `${PREFIX}-list`, + listWithDrawer: `${PREFIX}-listWithDrawer`, + drawerPaper: `${PREFIX}-drawerPaper`, +}; + +const Root = styled('div')(({ theme }) => ({ + [`&.${classes.root}`]: { display: 'flex', }, - list: { + + [`& .${classes.list}`]: { flexGrow: 1, transition: theme.transitions.create(['all'], { duration: theme.transitions.duration.enteringScreen, }), marginRight: 0, }, - listWithDrawer: { + + [`& .${classes.listWithDrawer}`]: { marginRight: 400, }, - drawerPaper: { + + [`& .${classes.drawerPaper}`]: { zIndex: 100, }, })); +const ReviewsBulkActionButtons = (props: BulkActionProps) => ( + <Fragment> + <BulkAcceptButton {...props} /> + <BulkRejectButton {...props} /> + <BulkDeleteButton {...props} /> + </Fragment> +); + const ReviewList = (props: ListProps) => { - const classes = useStyles(); const isXSmall = useMediaQuery<Theme>(theme => - theme.breakpoints.down('xs') + theme.breakpoints.down('sm') ); const history = useHistory(); @@ -57,7 +68,7 @@ const ReviewList = (props: ListProps) => { }, [history]); return ( - <div className={classes.root}> + <Root className={classes.root}> <Route path="/reviews/:id"> {({ match }: RouteChildrenProps<{ id: string }>) => { const isMatch = !!( @@ -116,7 +127,7 @@ const ReviewList = (props: ListProps) => { ); }} </Route> - </div> + </Root> ); }; diff --git a/examples/demo/src/reviews/ReviewListDesktop.tsx b/examples/demo/src/reviews/ReviewListDesktop.tsx index 51194f318dc..c63415579a7 100644 --- a/examples/demo/src/reviews/ReviewListDesktop.tsx +++ b/examples/demo/src/reviews/ReviewListDesktop.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { Identifier, Datagrid, @@ -6,26 +7,34 @@ import { TextField, DatagridProps, } from 'react-admin'; -import { makeStyles } from '@material-ui/core/styles'; import ProductReferenceField from '../products/ProductReferenceField'; import CustomerReferenceField from '../visitors/CustomerReferenceField'; import StarRatingField from './StarRatingField'; import rowStyle from './rowStyle'; -const useListStyles = makeStyles({ - headerRow: { +const PREFIX = 'ReviewListDesktop'; + +const classes = { + headerRow: `${PREFIX}-headerRow`, + headerCell: `${PREFIX}-headerCell`, + rowCell: `${PREFIX}-rowCell`, + comment: `${PREFIX}-comment`, +}; + +const StyledDatagrid = styled(Datagrid)({ + [`& .${classes.headerRow}`]: { borderLeftColor: 'transparent', borderLeftWidth: 5, borderLeftStyle: 'solid', }, - headerCell: { + [`& .${classes.headerCell}`]: { padding: '6px 8px 6px 8px', }, - rowCell: { + [`& .${classes.rowCell}`]: { padding: '6px 8px 6px 8px', }, - comment: { + [`& .${classes.comment}`]: { maxWidth: '18em', overflow: 'hidden', textOverflow: 'ellipsis', @@ -41,9 +50,8 @@ const ReviewListDesktop = ({ selectedRow, ...props }: ReviewListDesktopProps) => { - const classes = useListStyles(); return ( - <Datagrid + <StyledDatagrid rowClick="edit" // @ts-ignore rowStyle={rowStyle(selectedRow)} @@ -61,7 +69,7 @@ const ReviewListDesktop = ({ <StarRatingField size="small" /> <TextField source="comment" cellClassName={classes.comment} /> <TextField source="status" /> - </Datagrid> + </StyledDatagrid> ); }; diff --git a/examples/demo/src/reviews/ReviewListMobile.tsx b/examples/demo/src/reviews/ReviewListMobile.tsx index d04379676e4..220c1b2892a 100644 --- a/examples/demo/src/reviews/ReviewListMobile.tsx +++ b/examples/demo/src/reviews/ReviewListMobile.tsx @@ -1,11 +1,11 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { Fragment } from 'react'; import PropTypes from 'prop-types'; -import List from '@material-ui/core/List'; -import ListItem from '@material-ui/core/ListItem'; -import ListItemAvatar from '@material-ui/core/ListItemAvatar'; -import ListItemText from '@material-ui/core/ListItemText'; -import { makeStyles } from '@material-ui/core/styles'; +import List from '@mui/material/List'; +import ListItem from '@mui/material/ListItem'; +import ListItemAvatar from '@mui/material/ListItemAvatar'; +import ListItemText from '@mui/material/ListItemText'; import { Link } from 'react-router-dom'; import { linkToRecord, @@ -19,25 +19,32 @@ import { import AvatarField from '../visitors/AvatarField'; import { Review, Customer } from './../types'; -const useStyles = makeStyles({ - root: { +const PREFIX = 'ReviewListMobile'; + +const classes = { + root: `${PREFIX}-root`, + link: `${PREFIX}-link`, + inline: `${PREFIX}-inline`, +}; + +const StyledList = styled(List)({ + [`&.${classes.root}`]: { width: '100vw', }, - link: { + [`& .${classes.link}`]: { textDecoration: 'none', color: 'inherit', }, - inline: { + [`& .${classes.inline}`]: { display: 'inline', }, }); const ReviewListMobile = () => { - const classes = useStyles(); const { basePath, data, ids, loaded, total } = useListContext<Review>(); return loaded || Number(total) > 0 ? ( - <List className={classes.root}> + <StyledList className={classes.root}> {(ids as Exclude<typeof ids, undefined>).map(id => { const item = (data as Exclude<typeof data, undefined>)[id]; if (!item) return null; @@ -109,7 +116,7 @@ const ReviewListMobile = () => { </Link> ); })} - </List> + </StyledList> ) : null; }; diff --git a/examples/demo/src/reviews/StarRatingField.tsx b/examples/demo/src/reviews/StarRatingField.tsx index ef493d0f2c1..a43d231b3f3 100644 --- a/examples/demo/src/reviews/StarRatingField.tsx +++ b/examples/demo/src/reviews/StarRatingField.tsx @@ -1,20 +1,28 @@ import * as React from 'react'; -import Icon from '@material-ui/icons/Stars'; -import { makeStyles } from '@material-ui/core/styles'; +import { styled } from '@mui/material/styles'; +import Icon from '@mui/icons-material/Stars'; import { FieldProps } from 'react-admin'; -const useStyles = makeStyles({ - root: { +const PREFIX = 'StarRatingField'; + +const classes = { + root: `${PREFIX}-root`, + large: `${PREFIX}-large`, + small: `${PREFIX}-small`, +}; + +const Root = styled('span')({ + [`&.${classes.root}`]: { opacity: 0.87, whiteSpace: 'nowrap', display: 'flex', }, - large: { + [`& .${classes.large}`]: { width: 20, height: 20, }, - small: { + [`& .${classes.small}`]: { width: 15, height: 15, }, @@ -25,9 +33,8 @@ interface OwnProps { } const StarRatingField = ({ record, size = 'large' }: FieldProps & OwnProps) => { - const classes = useStyles(); return record ? ( - <span className={classes.root}> + <Root className={classes.root}> {Array(record.rating) .fill(true) .map((_, i) => ( @@ -38,7 +45,7 @@ const StarRatingField = ({ record, size = 'large' }: FieldProps & OwnProps) => { } /> ))} - </span> + </Root> ) : null; }; diff --git a/examples/demo/src/reviews/index.ts b/examples/demo/src/reviews/index.ts index 7ba84437c51..6f6abed029c 100644 --- a/examples/demo/src/reviews/index.ts +++ b/examples/demo/src/reviews/index.ts @@ -1,4 +1,4 @@ -import ReviewIcon from '@material-ui/icons/Comment'; +import ReviewIcon from '@mui/icons-material/Comment'; import ReviewList from './ReviewList'; export default { diff --git a/examples/demo/src/reviews/rowStyle.tsx b/examples/demo/src/reviews/rowStyle.tsx index 86cc2ee7a78..7fa1ebab12f 100644 --- a/examples/demo/src/reviews/rowStyle.tsx +++ b/examples/demo/src/reviews/rowStyle.tsx @@ -1,7 +1,7 @@ -import green from '@material-ui/core/colors/green'; -import orange from '@material-ui/core/colors/orange'; -import red from '@material-ui/core/colors/red'; -import { useTheme } from '@material-ui/core/styles'; +import green from '@mui/material/colors/green'; +import orange from '@mui/material/colors/orange'; +import red from '@mui/material/colors/red'; +import { useTheme } from '@mui/material/styles'; import { Identifier } from 'react-admin'; import { Review } from './../types'; diff --git a/examples/demo/src/segments/LinkToRelatedCustomers.tsx b/examples/demo/src/segments/LinkToRelatedCustomers.tsx index 75724655776..04818d1541b 100644 --- a/examples/demo/src/segments/LinkToRelatedCustomers.tsx +++ b/examples/demo/src/segments/LinkToRelatedCustomers.tsx @@ -1,15 +1,22 @@ import * as React from 'react'; -import Button from '@material-ui/core/Button'; -import { makeStyles } from '@material-ui/core/styles'; +import { styled } from '@mui/material/styles'; +import Button from '@mui/material/Button'; import { Link } from 'react-router-dom'; import { useTranslate } from 'react-admin'; import { stringify } from 'query-string'; import visitors from '../visitors'; -const useStyles = makeStyles({ - icon: { paddingRight: '0.5em' }, - link: { +const PREFIX = 'LinkToRelatedCustomers'; + +const classes = { + icon: `${PREFIX}-icon`, + link: `${PREFIX}-link`, +}; + +const StyledButton = styled(Button)({ + [`& .${classes.icon}`]: { paddingRight: '0.5em' }, + [`&.${classes.link}`]: { display: 'inline-flex', alignItems: 'center', }, @@ -17,11 +24,12 @@ const useStyles = makeStyles({ const LinkToRelatedCustomers = ({ segment }: { segment: string }) => { const translate = useTranslate(); - const classes = useStyles(); + return ( - <Button + <StyledButton size="small" color="primary" + // @ts-ignore component={Link} to={{ pathname: '/customers', @@ -33,7 +41,7 @@ const LinkToRelatedCustomers = ({ segment }: { segment: string }) => { > <visitors.icon className={classes.icon} /> {translate('resources.segments.fields.customers')} - </Button> + </StyledButton> ); }; diff --git a/examples/demo/src/segments/Segments.tsx b/examples/demo/src/segments/Segments.tsx index 29d692107ab..6f476793bde 100644 --- a/examples/demo/src/segments/Segments.tsx +++ b/examples/demo/src/segments/Segments.tsx @@ -1,27 +1,33 @@ import * as React from 'react'; -import Card from '@material-ui/core/Card'; -import Table from '@material-ui/core/Table'; -import TableBody from '@material-ui/core/TableBody'; -import TableCell from '@material-ui/core/TableCell'; -import TableHead from '@material-ui/core/TableHead'; -import TableRow from '@material-ui/core/TableRow'; -import { makeStyles } from '@material-ui/core/styles'; +import { styled } from '@mui/material/styles'; +import Card from '@mui/material/Card'; +import Table from '@mui/material/Table'; +import TableBody from '@mui/material/TableBody'; +import TableCell from '@mui/material/TableCell'; +import TableHead from '@mui/material/TableHead'; +import TableRow from '@mui/material/TableRow'; import { useTranslate, Title } from 'react-admin'; import LinkToRelatedCustomers from './LinkToRelatedCustomers'; import segments from './data'; -const useStyles = makeStyles({ - root: { +const PREFIX = 'Segments'; + +const classes = { + root: `${PREFIX}-root`, +}; + +const StyledCard = styled(Card)({ + [`&.${classes.root}`]: { marginTop: 16, }, }); const Segments = () => { const translate = useTranslate(); - const classes = useStyles(); + return ( - <Card className={classes.root}> + <StyledCard className={classes.root}> <Title title={translate('resources.segments.name', { smart_count: 2 })} /> @@ -45,7 +51,7 @@ const Segments = () => { ))} </TableBody> </Table> - </Card> + </StyledCard> ); }; diff --git a/examples/demo/src/visitors/Aside.tsx b/examples/demo/src/visitors/Aside.tsx index 97760304865..d2114e1df30 100644 --- a/examples/demo/src/visitors/Aside.tsx +++ b/examples/demo/src/visitors/Aside.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import PropTypes from 'prop-types'; import { NumberField, @@ -22,20 +23,25 @@ import { Step, StepLabel, StepContent, -} from '@material-ui/core'; +} from '@mui/material'; import { Link as RouterLink } from 'react-router-dom'; -import AccessTimeIcon from '@material-ui/icons/AccessTime'; -import { makeStyles } from '@material-ui/core/styles'; +import AccessTimeIcon from '@mui/icons-material/AccessTime'; import order from '../orders'; import review from '../reviews'; import StarRatingField from '../reviews/StarRatingField'; import { Order as OrderRecord, Review as ReviewRecord } from '../types'; -const useAsideStyles = makeStyles(theme => ({ - root: { +const PREFIX = 'Aside'; + +const classes = { + root: `${PREFIX}-root`, +}; + +const AsideRoot = styled('div')(({ theme }) => ({ + [`& .${classes.root}`]: { width: 400, - [theme.breakpoints.down('md')]: { + [theme.breakpoints.down('lg')]: { display: 'none', }, }, @@ -46,14 +52,11 @@ interface AsideProps { basePath?: string; } -const Aside = ({ record, basePath }: AsideProps) => { - const classes = useAsideStyles(); - return ( - <div className={classes.root}> - {record && <EventList record={record} basePath={basePath} />} - </div> - ); -}; +const Aside = ({ record, basePath }: AsideProps) => ( + <AsideRoot className={classes.root}> + {record && <EventList record={record} basePath={basePath} />} + </AsideRoot> +); Aside.propTypes = { record: PropTypes.any, @@ -65,8 +68,8 @@ interface EventListProps { basePath?: string; } -const useEventStyles = makeStyles({ - stepper: { +const StyledStepper = styled(Stepper)({ + '&': { background: 'none', border: 'none', marginLeft: '0.3em', @@ -75,7 +78,6 @@ const useEventStyles = makeStyles({ const EventList = ({ record, basePath }: EventListProps) => { const translate = useTranslate(); - const classes = useEventStyles(); const locale = useLocale(); const { data: orders, ids: orderIds } = useGetList<OrderRecord>( 'commands', @@ -190,7 +192,7 @@ const EventList = ({ record, basePath }: EventListProps) => { </CardContent> </Card> </Box> - <Stepper orientation="vertical" classes={{ root: classes.stepper }}> + <StyledStepper orientation="vertical"> {events.map(event => ( <Step key={`${event.type}-${event.data.id}`} @@ -239,7 +241,7 @@ const EventList = ({ record, basePath }: EventListProps) => { </StepContent> </Step> ))} - </Stepper> + </StyledStepper> </> ); }; @@ -327,8 +329,8 @@ interface ReviewProps { basePath?: string; } -const useReviewStyles = makeStyles({ - clamp: { +const ReviewRoot = styled('div')({ + '& .clamp': { display: '-webkit-box', '-webkit-line-clamp': 3, '-webkit-box-orient': 'vertical', @@ -337,10 +339,9 @@ const useReviewStyles = makeStyles({ }); const Review = ({ record, basePath }: ReviewProps) => { - const classes = useReviewStyles(); const translate = useTranslate(); return record ? ( - <> + <ReviewRoot> <Typography variant="body2" gutterBottom> <Link to={`/reviews/${record.id}`} component={RouterLink}> {translate('resources.reviews.relative_to_poster')} " @@ -360,14 +361,10 @@ const Review = ({ record, basePath }: ReviewProps) => { <Typography variant="body2" color="textSecondary" gutterBottom> <StarRatingField record={record} /> </Typography> - <Typography - variant="body2" - color="textSecondary" - className={classes.clamp} - > + <Typography variant="body2" color="textSecondary" className="clamp"> {record.comment} </Typography> - </> + </ReviewRoot> ) : null; }; diff --git a/examples/demo/src/visitors/AvatarField.tsx b/examples/demo/src/visitors/AvatarField.tsx index df6414daf86..f6372d82633 100644 --- a/examples/demo/src/visitors/AvatarField.tsx +++ b/examples/demo/src/visitors/AvatarField.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import Avatar from '@material-ui/core/Avatar'; +import Avatar from '@mui/material/Avatar'; import { FieldProps } from 'react-admin'; import { Customer } from '../types'; diff --git a/examples/demo/src/visitors/FullNameField.tsx b/examples/demo/src/visitors/FullNameField.tsx index 14ff8e93241..2854ccff6bb 100644 --- a/examples/demo/src/visitors/FullNameField.tsx +++ b/examples/demo/src/visitors/FullNameField.tsx @@ -1,18 +1,26 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { memo } from 'react'; -import { makeStyles } from '@material-ui/core/styles'; import { FieldProps } from 'react-admin'; import AvatarField from './AvatarField'; import { Customer } from '../types'; -const useStyles = makeStyles(theme => ({ - root: { +const PREFIX = 'FullNameField'; + +const classes = { + root: `${PREFIX}-root`, + avatar: `${PREFIX}-avatar`, +}; + +const Root = styled('div')(({ theme }) => ({ + [`& .${classes.root}`]: { display: 'flex', flexWrap: 'nowrap', alignItems: 'center', }, - avatar: { + + [`& .${classes.avatar}`]: { marginRight: theme.spacing(1), marginTop: -theme.spacing(0.5), marginBottom: -theme.spacing(0.5), @@ -25,16 +33,16 @@ interface Props extends FieldProps<Customer> { const FullNameField = (props: Props) => { const { record, size } = props; - const classes = useStyles(); + return record ? ( - <div className={classes.root}> + <Root className={classes.root}> <AvatarField className={classes.avatar} record={record} size={size} /> {record.first_name} {record.last_name} - </div> + </Root> ) : null; }; diff --git a/examples/demo/src/visitors/MobileGrid.tsx b/examples/demo/src/visitors/MobileGrid.tsx index e3159be0d9a..31f29eb7908 100644 --- a/examples/demo/src/visitors/MobileGrid.tsx +++ b/examples/demo/src/visitors/MobileGrid.tsx @@ -1,9 +1,9 @@ // in src/comments.js import * as React from 'react'; -import Card from '@material-ui/core/Card'; -import CardContent from '@material-ui/core/CardContent'; -import CardHeader from '@material-ui/core/CardHeader'; -import { makeStyles } from '@material-ui/core/styles'; +import { styled } from '@mui/material/styles'; +import Card from '@mui/material/Card'; +import CardContent from '@mui/material/CardContent'; +import CardHeader from '@mui/material/CardHeader'; import { DateField, EditButton, @@ -17,21 +17,33 @@ import ColoredNumberField from './ColoredNumberField'; import SegmentsField from './SegmentsField'; import { Customer } from '../types'; -const useStyles = makeStyles(theme => ({ - root: { margin: '1em' }, - card: { +const PREFIX = 'MobileGrid'; + +const classes = { + root: `${PREFIX}-root`, + card: `${PREFIX}-card`, + cardTitleContent: `${PREFIX}-cardTitleContent`, + cardContent: `${PREFIX}-cardContent`, +}; + +const Root = styled('div')(({ theme }) => ({ + [`&.${classes.root}`]: { margin: '1em' }, + + [`& .${classes.card}`]: { height: '100%', display: 'flex', flexDirection: 'column', margin: '0.5rem 0', }, - cardTitleContent: { + + [`& .${classes.cardTitleContent}`]: { display: 'flex', flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', }, - cardContent: { + + [`& .${classes.cardContent}`]: { ...theme.typography.body1, display: 'flex', flexDirection: 'column', @@ -46,14 +58,13 @@ interface Props { const MobileGrid = ({ ids, data, basePath }: Props) => { const translate = useTranslate(); - const classes = useStyles(); if (!ids || !data) { return null; } return ( - <div className={classes.root}> + <Root className={classes.root}> {ids.map(id => ( <Card key={id} className={classes.card}> <CardHeader @@ -108,7 +119,7 @@ const MobileGrid = ({ ids, data, basePath }: Props) => { )} </Card> ))} - </div> + </Root> ); }; diff --git a/examples/demo/src/visitors/SegmentInput.tsx b/examples/demo/src/visitors/SegmentInput.tsx index 967b21748e1..3ddc469741f 100644 --- a/examples/demo/src/visitors/SegmentInput.tsx +++ b/examples/demo/src/visitors/SegmentInput.tsx @@ -1,11 +1,11 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { useTranslate, SelectInput, InputProps } from 'react-admin'; -import { makeStyles } from '@material-ui/core/styles'; import segments from '../segments/data'; -const useStyles = makeStyles({ - input: { width: 150 }, +const StyledSelectInput = styled(SelectInput)({ + '&': { width: 150 }, }); interface Props extends Omit<InputProps, 'source'> { @@ -14,15 +14,14 @@ interface Props extends Omit<InputProps, 'source'> { const SegmentInput = (props: Props) => { const translate = useTranslate(); - const classes = useStyles(); + return ( - <SelectInput + <StyledSelectInput {...props} choices={segments.map(segment => ({ id: segment.id, name: translate(segment.name), }))} - className={classes.input} /> ); }; diff --git a/examples/demo/src/visitors/SegmentsField.tsx b/examples/demo/src/visitors/SegmentsField.tsx index fcf7602f140..4802580dcc2 100644 --- a/examples/demo/src/visitors/SegmentsField.tsx +++ b/examples/demo/src/visitors/SegmentsField.tsx @@ -1,26 +1,32 @@ import * as React from 'react'; -import Chip from '@material-ui/core/Chip'; +import { styled } from '@mui/material/styles'; +import Chip from '@mui/material/Chip'; import { useTranslate, FieldProps } from 'react-admin'; import segments from '../segments/data'; import { Customer } from '../types'; -import { makeStyles } from '@material-ui/core/styles'; -const useStyles = makeStyles({ - main: { +const PREFIX = 'SegmentsField'; + +const classes = { + main: `${PREFIX}-main`, + chip: `${PREFIX}-chip`, +}; + +const Root = styled('span')({ + [`&.${classes.main}`]: { display: 'flex', flexWrap: 'wrap', marginTop: -8, marginBottom: -8, }, - chip: { margin: 4 }, + [`& .${classes.chip}`]: { margin: 4 }, }); const SegmentsField = ({ record }: FieldProps<Customer>) => { const translate = useTranslate(); - const classes = useStyles(); return record ? ( - <span className={classes.main}> + <Root className={classes.main}> {record.groups && record.groups.map(segmentId => { const segment = segments.find(s => s.id === segmentId); @@ -34,7 +40,7 @@ const SegmentsField = ({ record }: FieldProps<Customer>) => { /> ) : null; })} - </span> + </Root> ) : null; }; diff --git a/examples/demo/src/visitors/VisitorCreate.tsx b/examples/demo/src/visitors/VisitorCreate.tsx index b6fea086e95..1538a60771f 100644 --- a/examples/demo/src/visitors/VisitorCreate.tsx +++ b/examples/demo/src/visitors/VisitorCreate.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { Create, CreateProps, @@ -11,28 +12,43 @@ import { email, } from 'react-admin'; import { AnyObject } from 'react-final-form'; -import { Typography, Box } from '@material-ui/core'; -import { makeStyles, Theme } from '@material-ui/core/styles'; -import { Styles } from '@material-ui/styles/withStyles'; +import { Typography, Box } from '@mui/material'; -export const styles: Styles<Theme, any> = { - first_name: { display: 'inline-block' }, - last_name: { display: 'inline-block', marginLeft: 32 }, - email: { width: 544 }, - address: { maxWidth: 544 }, - zipcode: { display: 'inline-block' }, - city: { display: 'inline-block', marginLeft: 32 }, - comment: { +const PREFIX = 'VisitorCreate'; + +const classes = { + first_name: `${PREFIX}-first_name`, + last_name: `${PREFIX}-last_name`, + email: `${PREFIX}-email`, + address: `${PREFIX}-address`, + zipcode: `${PREFIX}-zipcode`, + city: `${PREFIX}-city`, + comment: `${PREFIX}-comment`, + password: `${PREFIX}-password`, + confirm_password: `${PREFIX}-confirm_password`, +}; + +const StyledSimpleForm = styled(SimpleForm)({ + [`& .${classes.first_name}`]: { display: 'inline-block' }, + [`& .${classes.last_name}`]: { display: 'inline-block', marginLeft: 32 }, + [`& .${classes.email}`]: { width: 544 }, + [`& .${classes.address}`]: { maxWidth: 544 }, + [`& .${classes.zipcode}`]: { display: 'inline-block' }, + [`& .${classes.city}`]: { display: 'inline-block', marginLeft: 32 }, + [`& .${classes.comment}`]: { maxWidth: '20em', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', }, - password: { display: 'inline-block' }, - confirm_password: { display: 'inline-block', marginLeft: 32 }, -}; + [`& .${classes.password}`]: { display: 'inline-block' }, + [`& .${classes.confirm_password}`]: { + display: 'inline-block', + marginLeft: 32, + }, +}); -const useStyles = makeStyles(styles); +export {}; export const validatePasswords = ({ password, @@ -49,66 +65,59 @@ export const validatePasswords = ({ return errors; }; -const VisitorCreate = (props: CreateProps) => { - const classes = useStyles(props); - - return ( - <Create {...props}> - <SimpleForm validate={validatePasswords}> - <SectionTitle label="resources.customers.fieldGroups.identity" /> - <TextInput - autoFocus - source="first_name" - formClassName={classes.first_name} - validate={requiredValidate} - /> - <TextInput - source="last_name" - formClassName={classes.last_name} - validate={requiredValidate} - /> - <TextInput - type="email" - source="email" - validation={{ email: true }} - fullWidth - formClassName={classes.email} - validate={[required(), email()]} - /> - <DateInput source="birthday" /> - <Separator /> - <SectionTitle label="resources.customers.fieldGroups.address" /> - <TextInput - source="address" - formClassName={classes.address} - multiline - fullWidth - helperText={false} - /> - <TextInput - source="zipcode" - formClassName={classes.zipcode} - helperText={false} - /> - <TextInput - source="city" - formClassName={classes.city} - helperText={false} - /> - <Separator /> - <SectionTitle label="resources.customers.fieldGroups.password" /> - <PasswordInput - source="password" - formClassName={classes.password} - /> - <PasswordInput - source="confirm_password" - formClassName={classes.confirm_password} - /> - </SimpleForm> - </Create> - ); -}; +const VisitorCreate = (props: CreateProps) => ( + <Create {...props}> + <StyledSimpleForm validate={validatePasswords}> + <SectionTitle label="resources.customers.fieldGroups.identity" /> + <TextInput + autoFocus + source="first_name" + formClassName={classes.first_name} + validate={requiredValidate} + /> + <TextInput + source="last_name" + formClassName={classes.last_name} + validate={requiredValidate} + /> + <TextInput + type="email" + source="email" + validation={{ email: true }} + fullWidth + formClassName={classes.email} + validate={[required(), email()]} + /> + <DateInput source="birthday" /> + <Separator /> + <SectionTitle label="resources.customers.fieldGroups.address" /> + <TextInput + source="address" + formClassName={classes.address} + multiline + fullWidth + helperText={false} + /> + <TextInput + source="zipcode" + formClassName={classes.zipcode} + helperText={false} + /> + <TextInput + source="city" + formClassName={classes.city} + helperText={false} + /> + <Separator /> + <SectionTitle label="resources.customers.fieldGroups.password" /> + <PasswordInput source="password" formClassName={classes.password} /> + <PasswordInput + source="confirm_password" + formClassName={classes.confirm_password} + /> + </StyledSimpleForm> + </Create> +); const requiredValidate = [required()]; diff --git a/examples/demo/src/visitors/VisitorEdit.tsx b/examples/demo/src/visitors/VisitorEdit.tsx index 1bdec3ddc03..83bd9e0c690 100644 --- a/examples/demo/src/visitors/VisitorEdit.tsx +++ b/examples/demo/src/visitors/VisitorEdit.tsx @@ -13,7 +13,7 @@ import { email, FieldProps, } from 'react-admin'; -import { Box, Card, CardContent, Typography } from '@material-ui/core'; +import { Box, Card, CardContent, Typography } from '@mui/material'; import Aside from './Aside'; import FullNameField from './FullNameField'; diff --git a/examples/demo/src/visitors/VisitorList.tsx b/examples/demo/src/visitors/VisitorList.tsx index c3ff4969832..d7593eb7d44 100644 --- a/examples/demo/src/visitors/VisitorList.tsx +++ b/examples/demo/src/visitors/VisitorList.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { BooleanField, Datagrid, @@ -10,8 +11,7 @@ import { NumberField, SearchInput, } from 'react-admin'; -import { useMediaQuery, Theme } from '@material-ui/core'; -import { makeStyles } from '@material-ui/core/styles'; +import { useMediaQuery, Theme } from '@mui/material'; import SegmentsField from './SegmentsField'; import SegmentInput from './SegmentInput'; @@ -21,6 +21,24 @@ import MobileGrid from './MobileGrid'; import VisitorListAside from './VisitorListAside'; import { ReactElement } from 'react'; +const PREFIX = 'VisitorList'; + +const classes = { + nb_commands: `${PREFIX}-nb_commands`, + hiddenOnSmallScreens: `${PREFIX}-hiddenOnSmallScreens`, +}; + +const StyledList = styled(List)(({ theme }) => ({ + [`& .${classes.nb_commands}`]: { color: 'purple' }, + + [`& .${classes.hiddenOnSmallScreens}`]: { + display: 'table-cell', + [theme.breakpoints.down('lg')]: { + display: 'none', + }, + }, +})); + const visitorFilters = [ <SearchInput source="q" alwaysOn />, <DateInput source="last_seen_gte" />, @@ -29,24 +47,13 @@ const visitorFilters = [ <SegmentInput />, ]; -const useStyles = makeStyles(theme => ({ - nb_commands: { color: 'purple' }, - hiddenOnSmallScreens: { - display: 'table-cell', - [theme.breakpoints.down('md')]: { - display: 'none', - }, - }, -})); - const VisitorList = (props: ListProps): ReactElement => { - const classes = useStyles(); const isXsmall = useMediaQuery<Theme>(theme => - theme.breakpoints.down('xs') + theme.breakpoints.down('sm') ); - const isSmall = useMediaQuery<Theme>(theme => theme.breakpoints.down('sm')); + const isSmall = useMediaQuery<Theme>(theme => theme.breakpoints.down('md')); return ( - <List + <StyledList {...props} filters={isSmall ? visitorFilters : undefined} sort={{ field: 'last_seen', order: 'DESC' }} @@ -76,7 +83,7 @@ const VisitorList = (props: ListProps): ReactElement => { /> </Datagrid> )} - </List> + </StyledList> ); }; diff --git a/examples/demo/src/visitors/VisitorListAside.tsx b/examples/demo/src/visitors/VisitorListAside.tsx index 11132ee8b28..72ffc718805 100644 --- a/examples/demo/src/visitors/VisitorListAside.tsx +++ b/examples/demo/src/visitors/VisitorListAside.tsx @@ -1,10 +1,10 @@ import * as React from 'react'; -import { Card as MuiCard, CardContent } from '@material-ui/core'; -import { withStyles } from '@material-ui/core/styles'; -import AccessTimeIcon from '@material-ui/icons/AccessTime'; -import MonetizationOnIcon from '@material-ui/icons/MonetizationOnOutlined'; -import MailIcon from '@material-ui/icons/MailOutline'; -import LocalOfferIcon from '@material-ui/icons/LocalOfferOutlined'; +import { styled } from '@mui/material/styles'; +import { Card, CardContent } from '@mui/material'; +import AccessTimeIcon from '@mui/icons-material/AccessTime'; +import MonetizationOnIcon from '@mui/icons-material/MonetizationOnOutlined'; +import MailIcon from '@mui/icons-material/MailOutline'; +import LocalOfferIcon from '@mui/icons-material/LocalOfferOutlined'; import { FilterList, FilterListItem, FilterLiveSearch } from 'react-admin'; import { endOfYesterday, @@ -16,21 +16,27 @@ import { import segments from '../segments/data'; -const Card = withStyles(theme => ({ - root: { +const PREFIX = 'Aside'; + +const classes = { + root: `${PREFIX}-root`, +}; + +const StyledCard = styled(Card)(({ theme }) => ({ + [`& .${classes.root}`]: { [theme.breakpoints.up('sm')]: { order: -1, width: '15em', marginRight: '1em', }, - [theme.breakpoints.down('sm')]: { + [theme.breakpoints.down('md')]: { display: 'none', }, }, -}))(MuiCard); +})); const Aside = () => ( - <Card> + <StyledCard> <CardContent> <FilterLiveSearch /> @@ -138,7 +144,7 @@ const Aside = () => ( ))} </FilterList> </CardContent> - </Card> + </StyledCard> ); export default Aside; diff --git a/examples/demo/src/visitors/index.ts b/examples/demo/src/visitors/index.ts index 8e186e82aa1..510d038769a 100644 --- a/examples/demo/src/visitors/index.ts +++ b/examples/demo/src/visitors/index.ts @@ -1,4 +1,4 @@ -import VisitorIcon from '@material-ui/icons/People'; +import VisitorIcon from '@mui/icons-material/People'; import VisitorList from './VisitorList'; import VisitorCreate from './VisitorCreate'; diff --git a/examples/no-code/src/main.tsx b/examples/no-code/src/main.tsx index 77c0ef213ef..c0f35ab4942 100644 --- a/examples/no-code/src/main.tsx +++ b/examples/no-code/src/main.tsx @@ -2,16 +2,10 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Root } from 'ra-no-code'; import { defaultTheme } from 'react-admin'; -import { - unstable_createMuiStrictModeTheme, - createMuiTheme, -} from '@material-ui/core/styles'; +import { createTheme } from '@mui/material/styles'; // FIXME MUI bug https://github.com/mui-org/material-ui/issues/13394 -const theme = - process.env.NODE_ENV !== 'production' - ? unstable_createMuiStrictModeTheme(defaultTheme) - : createMuiTheme(defaultTheme); +const theme = createTheme(defaultTheme); ReactDOM.render( <React.StrictMode> diff --git a/examples/no-code/vite.config.ts b/examples/no-code/vite.config.ts index a5c06b7a4b9..d0406d9b75e 100644 --- a/examples/no-code/vite.config.ts +++ b/examples/no-code/vite.config.ts @@ -1,54 +1,43 @@ -import { defineConfig } from 'vite'; import reactRefresh from '@vitejs/plugin-react-refresh'; import path from 'path'; -// https://vitejs.dev/config/ -export default defineConfig({ +/** + * https://vitejs.dev/config/ + * @type { import('vite').UserConfig } + */ +export default { plugins: [reactRefresh()], - alias: [ - { - find: /^react-admin$/, - replacement: path.resolve( - __dirname, - '../../packages/react-admin/src' - ), - }, - { - find: /^ra-core$/, - replacement: path.resolve(__dirname, '../../packages/ra-core/src'), - }, - { - find: /^ra-ui-materialui$/, - replacement: path.resolve( - __dirname, - '../../packages/ra-ui-materialui/src' - ), - }, - { - find: /^ra-data-local-storage$/, - replacement: path.resolve( - __dirname, - '../../packages/ra-data-localstorage/src' - ), - }, - { - find: /^ra-no-code$/, - replacement: path.resolve( - __dirname, - '../../packages/ra-no-code/src' - ), - }, - { - find: /^@material-ui\/icons\/(.*)/, - replacement: '@material-ui/icons/esm/$1', - }, - { - find: /^@material-ui\/core\/(.+)/, - replacement: '@material-ui/core/es/$1', - }, - { - find: /^@material-ui\/core$/, - replacement: '@material-ui/core/es', - }, - ], -}); + resolve: { + alias: [ + { + find: /^react-admin$/, + replacement: path.resolve( + __dirname, + '../../packages/react-admin/src' + ), + }, + { + find: /^ra-data-local-storage$/, + replacement: path.resolve( + __dirname, + '../../packages/ra-data-localstorage/src' + ), + }, + { + find: /^ra-(.*)$/, + replacement: path.resolve( + __dirname, + '../../packages/ra-$1/src' + ), + }, + { + find: /^@mui\/icons-material\/(.*)/, + replacement: '@mui/icons-material/esm/$1', + }, + ], + }, + server: { + port: 8080, + }, + define: { 'process.env': {} }, +}; diff --git a/examples/simple/package.json b/examples/simple/package.json index 6f7fdc1647a..ea3870db8c8 100644 --- a/examples/simple/package.json +++ b/examples/simple/package.json @@ -8,8 +8,8 @@ "serve": "vite preview" }, "dependencies": { - "@material-ui/core": "^4.12.1", - "@material-ui/icons": "^4.11.2", + "@mui/material": "^5.0.2", + "@mui/icons-material": "^5.0.1", "ra-data-fakerest": "^3.9.3", "ra-i18n-polyglot": "^3.12.4", "ra-input-rich-text": "^3.12.0", diff --git a/examples/simple/src/Layout.tsx b/examples/simple/src/Layout.tsx index e9f6fe04037..c6f92857814 100644 --- a/examples/simple/src/Layout.tsx +++ b/examples/simple/src/Layout.tsx @@ -1,37 +1,47 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { forwardRef, memo } from 'react'; import { Layout, AppBar, UserMenu, useLocale, useSetLocale } from 'react-admin'; -import { MenuItem, ListItemIcon } from '@material-ui/core'; -import { makeStyles } from '@material-ui/core/styles'; -import Language from '@material-ui/icons/Language'; +import { MenuItem, MenuItemProps, ListItemIcon } from '@mui/material'; +import Language from '@mui/icons-material/Language'; -const useStyles = makeStyles(theme => ({ - menuItem: { +const PREFIX = 'Layout'; + +const classes = { + menuItem: `${PREFIX}-menuItem`, + icon: `${PREFIX}-icon`, +}; + +const StyledMenuItem = styled(MenuItem)(({ theme }) => ({ + [`&.${classes.menuItem}`]: { color: theme.palette.text.secondary, }, - icon: { minWidth: theme.spacing(5) }, + + [`& .${classes.icon}`]: { minWidth: theme.spacing(5) }, })); -const SwitchLanguage = forwardRef((props, ref) => { - const locale = useLocale(); - const setLocale = useSetLocale(); - const classes = useStyles(); - return ( - <MenuItem - ref={ref} - className={classes.menuItem} - onClick={() => { - setLocale(locale === 'en' ? 'fr' : 'en'); - props.onClick(); - }} - > - <ListItemIcon className={classes.icon}> - <Language /> - </ListItemIcon> - Switch Language - </MenuItem> - ); -}); +const SwitchLanguage = forwardRef<HTMLLIElement, MenuItemProps>( + (props, ref) => { + const locale = useLocale(); + const setLocale = useSetLocale(); + + return ( + <StyledMenuItem + ref={ref} + className={classes.menuItem} + onClick={event => { + setLocale(locale === 'en' ? 'fr' : 'en'); + props.onClick(event); + }} + > + <ListItemIcon className={classes.icon}> + <Language /> + </ListItemIcon> + Switch Language + </StyledMenuItem> + ); + } +); const MyUserMenu = props => ( <UserMenu {...props}> diff --git a/examples/simple/src/comments/CommentEdit.tsx b/examples/simple/src/comments/CommentEdit.tsx index 079c0a36477..9c072d61edf 100644 --- a/examples/simple/src/comments/CommentEdit.tsx +++ b/examples/simple/src/comments/CommentEdit.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { Card, Typography, @@ -7,8 +8,7 @@ import { TextField as MuiTextField, DialogActions, Button, -} from '@material-ui/core'; -import { makeStyles } from '@material-ui/core/styles'; +} from '@mui/material'; import { AutocompleteInput, DateInput, @@ -26,24 +26,31 @@ import { useCreate, } from 'react-admin'; // eslint-disable-line import/no-unresolved -const LinkToRelatedPost = ({ record }: { record?: Record }) => ( - <Link to={`/posts/${record?.post_id}`}> - <Typography variant="caption" color="inherit" align="right"> - See related post - </Typography> - </Link> -); +const PREFIX = 'CommentEdit'; + +const classes = { + actions: `${PREFIX}-actions`, + card: `${PREFIX}-card`, +}; -const useEditStyles = makeStyles({ - actions: { +const Root = styled('div')({ + [`& .${classes.actions}`]: { float: 'right', }, - card: { + [`& .${classes.card}`]: { marginTop: '1em', maxWidth: '30em', }, }); +const LinkToRelatedPost = ({ record }: { record?: Record }) => ( + <Link to={`/posts/${record?.post_id}`}> + <Typography variant="caption" color="inherit" align="right"> + See related post + </Typography> + </Link> +); + const OptionRenderer = ({ record }: { record?: Record }) => { return record.id === '@@ra-create' ? ( <span>{record.name}</span> @@ -104,7 +111,6 @@ const CreatePost = () => { }; const CommentEdit = props => { - const classes = useEditStyles(); const controllerProps = useEditController(props); const { resource, @@ -117,7 +123,7 @@ const CommentEdit = props => { return ( <EditContextProvider value={controllerProps}> - <div className="edit-page"> + <Root className="edit-page"> <Title defaultTitle={`Comment #${record ? record.id : ''}`} /> <div className={classes.actions}> <EditActions @@ -177,7 +183,7 @@ const CommentEdit = props => { </SimpleForm> )} </Card> - </div> + </Root> </EditContextProvider> ); }; diff --git a/examples/simple/src/comments/CommentList.tsx b/examples/simple/src/comments/CommentList.tsx index 41eafc43f3d..3b7fd314664 100644 --- a/examples/simple/src/comments/CommentList.tsx +++ b/examples/simple/src/comments/CommentList.tsx @@ -1,7 +1,8 @@ import * as React from 'react'; -import ChevronLeft from '@material-ui/icons/ChevronLeft'; -import ChevronRight from '@material-ui/icons/ChevronRight'; -import PersonIcon from '@material-ui/icons/Person'; +import { styled } from '@mui/material/styles'; +import ChevronLeft from '@mui/icons-material/ChevronLeft'; +import ChevronRight from '@mui/icons-material/ChevronRight'; +import PersonIcon from '@mui/icons-material/Person'; import { Avatar, Button, @@ -13,8 +14,8 @@ import { Toolbar, Typography, useMediaQuery, -} from '@material-ui/core'; -import { makeStyles, Theme } from '@material-ui/core/styles'; + Theme, +} from '@mui/material'; import jsonExport from 'jsonexport/dist'; import { ListBase, @@ -36,6 +37,40 @@ import { useTranslate, } from 'react-admin'; // eslint-disable-line import/no-unresolved +const PREFIX = 'CommentList'; + +const classes = { + card: `${PREFIX}-card`, + cardContent: `${PREFIX}-cardContent`, + cardLink: `${PREFIX}-cardLink`, + cardLinkLink: `${PREFIX}-cardLinkLink`, + cardActions: `${PREFIX}-cardActions`, +}; + +// TODO jss-to-styled codemod: The Fragment root was replaced by div. Change the tag if needed. +const Root = styled(Grid)(({ theme }) => ({ + [`& .${classes.card}`]: { + height: '100%', + display: 'flex', + flexDirection: 'column', + }, + + [`& .${classes.cardContent}`]: theme.typography.body1, + + [`& .${classes.cardLink}`]: { + ...theme.typography.body1, + flexGrow: 1, + }, + + [`& .${classes.cardLinkLink}`]: { + display: 'inline', + }, + + [`& .${classes.cardActions}`]: { + justifyContent: 'flex-end', + }, +})); + const commentFilters = [ <SearchInput source="q" alwaysOn />, <ReferenceInput source="post_id" reference="posts"> @@ -105,32 +140,12 @@ const CommentPagination = () => { ); }; -const useListStyles = makeStyles(theme => ({ - card: { - height: '100%', - display: 'flex', - flexDirection: 'column', - }, - cardContent: theme.typography.body1, - cardLink: { - ...theme.typography.body1, - flexGrow: 1, - }, - cardLinkLink: { - display: 'inline', - }, - cardActions: { - justifyContent: 'flex-end', - }, -})); - const CommentGrid = () => { const { ids, data } = useListContext(); const translate = useTranslate(); - const classes = useListStyles(); return ( - <Grid spacing={2} container> + <Root spacing={2} container> {ids.map(id => ( <Grid item key={id} sm={12} md={6} lg={4}> <Card className={classes.card}> @@ -179,7 +194,7 @@ const CommentGrid = () => { </Card> </Grid> ))} - </Grid> + </Root> ); }; @@ -206,15 +221,15 @@ const CommentList = props => ( ); const ListView = () => { - const isSmall = useMediaQuery<Theme>(theme => theme.breakpoints.down('sm')); + const isSmall = useMediaQuery<Theme>(theme => theme.breakpoints.down('md')); const { defaultTitle } = useListContext(); return ( - <> + <Root> <Title defaultTitle={defaultTitle} /> <ListToolbar filters={commentFilters} actions={<ListActions />} /> {isSmall ? <CommentMobileList /> : <CommentGrid />} <CommentPagination /> - </> + </Root> ); }; diff --git a/examples/simple/src/comments/PostQuickCreate.tsx b/examples/simple/src/comments/PostQuickCreate.tsx index df4b1ca1914..8b521feddf8 100644 --- a/examples/simple/src/comments/PostQuickCreate.tsx +++ b/examples/simple/src/comments/PostQuickCreate.tsx @@ -1,8 +1,8 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { useCallback } from 'react'; import PropTypes from 'prop-types'; import { useSelector, useDispatch } from 'react-redux'; -import { makeStyles } from '@material-ui/core/styles'; import { CREATE, SaveButton, @@ -16,6 +16,16 @@ import { import CancelButton from './PostQuickCreateCancelButton'; +const PREFIX = 'PostQuickCreate'; + +const classes = { + form: `${PREFIX}-form`, +}; + +const StyledSimpleForm = styled(SimpleForm)({ + [`& .${classes.form}`]: { padding: 0 }, +}); + // We need a custom toolbar to add our custom buttons // The CancelButton allows to close the modal without submitting anything const PostQuickCreateToolbar = ({ submitting, onCancel, ...props }) => ( @@ -30,12 +40,7 @@ PostQuickCreateToolbar.propTypes = { onCancel: PropTypes.func.isRequired, }; -const useStyles = makeStyles({ - form: { padding: 0 }, -}); - const PostQuickCreate = ({ onCancel, onSave, ...props }) => { - const classes = useStyles(); const dispatch = useDispatch(); const submitting = useSelector<ReduxState, boolean>( state => state.admin.loading > 0 @@ -64,7 +69,7 @@ const PostQuickCreate = ({ onCancel, onSave, ...props }) => { ); return ( - <SimpleForm + <StyledSimpleForm save={handleSave} saving={submitting} redirect={false} @@ -84,7 +89,7 @@ const PostQuickCreate = ({ onCancel, onSave, ...props }) => { fullWidth={true} multiline={true} /> - </SimpleForm> + </StyledSimpleForm> ); }; diff --git a/examples/simple/src/comments/PostQuickCreateCancelButton.tsx b/examples/simple/src/comments/PostQuickCreateCancelButton.tsx index 07319471862..26c15076cc3 100644 --- a/examples/simple/src/comments/PostQuickCreateCancelButton.tsx +++ b/examples/simple/src/comments/PostQuickCreateCancelButton.tsx @@ -1,18 +1,25 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import PropTypes from 'prop-types'; -import { Button } from '@material-ui/core'; -import IconCancel from '@material-ui/icons/Cancel'; -import { makeStyles } from '@material-ui/core/styles'; +import { Button } from '@mui/material'; +import IconCancel from '@mui/icons-material/Cancel'; import { useTranslate } from 'react-admin'; -const useStyles = makeStyles({ - button: { +const PREFIX = 'PostQuickCreateCancelButton'; + +const classes = { + button: `${PREFIX}-button`, + iconPaddingStyle: `${PREFIX}-iconPaddingStyle`, +}; + +const StyledButton = styled(Button)({ + [`&.${classes.button}`]: { margin: '10px 24px', position: 'relative', }, - iconPaddingStyle: { + [`& .${classes.iconPaddingStyle}`]: { paddingRight: '0.5em', }, }); @@ -22,12 +29,12 @@ const PostQuickCreateCancelButton = ({ label = 'ra.action.cancel', }) => { const translate = useTranslate(); - const classes = useStyles(); + return ( - <Button className={classes.button} onClick={onClick}> + <StyledButton className={classes.button} onClick={onClick}> <IconCancel className={classes.iconPaddingStyle} /> {label && translate(label, { _: label })} - </Button> + </StyledButton> ); }; diff --git a/examples/simple/src/comments/PostReferenceInput.tsx b/examples/simple/src/comments/PostReferenceInput.tsx index 1bc95fd2298..2fc22e575c4 100644 --- a/examples/simple/src/comments/PostReferenceInput.tsx +++ b/examples/simple/src/comments/PostReferenceInput.tsx @@ -1,23 +1,31 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { Fragment, useState, useCallback } from 'react'; import { FormSpy, useForm } from 'react-final-form'; -import { makeStyles } from '@material-ui/core/styles'; +import { makeStyles } from '@mui/styles'; import { Button, Dialog, DialogTitle, DialogContent, DialogActions, -} from '@material-ui/core'; +} from '@mui/material'; import { ReferenceInput, SelectInput, useTranslate } from 'react-admin'; // eslint-disable-line import/no-unresolved import PostQuickCreate from './PostQuickCreate'; import PostPreview from './PostPreview'; -const useStyles = makeStyles({ - button: { +const PREFIX = 'PostReferenceInput'; + +const classes = { + button: `${PREFIX}-button`, +}; + +// TODO jss-to-styled codemod: The Fragment root was replaced by div. Change the tag if needed. +const Root = styled('div')({ + [`& .${classes.button}`]: { margin: '10px 24px', position: 'relative', }, @@ -25,7 +33,7 @@ const useStyles = makeStyles({ const PostReferenceInput = props => { const translate = useTranslate(); - const classes = useStyles(); + const { change } = useForm(); const [showCreateDialog, setShowCreateDialog] = useState(false); @@ -68,7 +76,7 @@ const PostReferenceInput = props => { ); return ( - <Fragment> + <Root> <ReferenceInput key={version} {...props} defaultValue={newPostId}> <SelectInput optionText="title" /> </ReferenceInput> @@ -138,7 +146,7 @@ const PostReferenceInput = props => { /> </DialogContent> </Dialog> - </Fragment> + </Root> ); }; diff --git a/examples/simple/src/comments/index.tsx b/examples/simple/src/comments/index.tsx index f959929032e..8309a02b8a2 100644 --- a/examples/simple/src/comments/index.tsx +++ b/examples/simple/src/comments/index.tsx @@ -1,4 +1,4 @@ -import ChatBubbleIcon from '@material-ui/icons/ChatBubble'; +import ChatBubbleIcon from '@mui/icons-material/ChatBubble'; import CommentCreate from './CommentCreate'; import CommentEdit from './CommentEdit'; import CommentList from './CommentList'; diff --git a/examples/simple/src/posts/PostEdit.tsx b/examples/simple/src/posts/PostEdit.tsx index a7ea7c9284c..5799694921f 100644 --- a/examples/simple/src/posts/PostEdit.tsx +++ b/examples/simple/src/posts/PostEdit.tsx @@ -39,7 +39,7 @@ import { DialogActions, DialogContent, TextField as MuiTextField, -} from '@material-ui/core'; +} from '@mui/material'; import PostTitle from './PostTitle'; import TagReferenceInput from './TagReferenceInput'; diff --git a/examples/simple/src/posts/PostList.tsx b/examples/simple/src/posts/PostList.tsx index 3d677466242..789b4a603d7 100644 --- a/examples/simple/src/posts/PostList.tsx +++ b/examples/simple/src/posts/PostList.tsx @@ -1,8 +1,8 @@ import * as React from 'react'; import { Children, Fragment, cloneElement, memo } from 'react'; -import BookIcon from '@material-ui/icons/Book'; -import { Chip, useMediaQuery } from '@material-ui/core'; -import { makeStyles, Theme } from '@material-ui/core/styles'; +import BookIcon from '@mui/icons-material/Book'; +import { Box, Chip, useMediaQuery } from '@mui/material'; +import { Theme, styled } from '@mui/material/styles'; import lodashGet from 'lodash/get'; import jsonExport from 'jsonexport/dist'; import { @@ -29,15 +29,9 @@ import { import ResetViewsButton from './ResetViewsButton'; export const PostIcon = BookIcon; -const useQuickFilterStyles = makeStyles(theme => ({ - chip: { - marginBottom: theme.spacing(1), - }, -})); const QuickFilter = ({ label, source, defaultValue }) => { const translate = useTranslate(); - const classes = useQuickFilterStyles(); - return <Chip className={classes.chip} label={translate(label)} />; + return <Chip sx={{ marginBottom: 1 }} label={translate(label)} />; }; const postFilter = [ @@ -60,19 +54,19 @@ const exporter = posts => { jsonExport(data, (err, csv) => downloadCSV(csv, 'posts')); }; -const useStyles = makeStyles(theme => ({ - title: { +const useStyles = styled(Datagrid)(({ theme }) => ({ + '& .title': { maxWidth: '20em', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', }, - hiddenOnSmallScreens: { - [theme.breakpoints.down('md')]: { + '& .hiddenOnSmallScreens': { + [theme.breakpoints.down('lg')]: { display: 'none', }, }, - publishedAt: { fontStyle: 'italic' }, + '& .publishedAt': { fontStyle: 'italic' }, })); const PostListBulkActions = memo(({ children, ...props }) => ( @@ -83,21 +77,19 @@ const PostListBulkActions = memo(({ children, ...props }) => ( </Fragment> )); -const usePostListActionToolbarStyles = makeStyles({ - toolbar: { - alignItems: 'center', - display: 'flex', - marginTop: -1, - marginBottom: -1, - }, -}); - const PostListActionToolbar = ({ children, ...props }) => { - const classes = usePostListActionToolbarStyles(); return ( - <div className={classes.toolbar}> + // @ts-ignore + <Box + sx={{ + alignItems: 'center', + display: 'flex', + marginTop: -1, + marginBotton: -1, + }} + > {Children.map(children, button => cloneElement(button, props))} - </div> + </Box> ); }; @@ -114,8 +106,7 @@ const PostPanel = ({ id, record, resource }) => ( ); const PostList = props => { - const classes = useStyles(); - const isSmall = useMediaQuery<Theme>(theme => theme.breakpoints.down('sm')); + const isSmall = useMediaQuery<Theme>(theme => theme.breakpoints.down('md')); return ( <List {...props} @@ -135,11 +126,11 @@ const PostList = props => { ) : ( <Datagrid rowClick={rowClick} expand={PostPanel} optimized> <TextField source="id" /> - <TextField source="title" cellClassName={classes.title} /> + <TextField source="title" cellClassName="title" /> <DateField source="published_at" sortByOrder="DESC" - cellClassName={classes.publishedAt} + cellClassName="publishedAt" /> <BooleanField @@ -154,8 +145,8 @@ const PostList = props => { source="tags" sortBy="tags.name" sort={tagSort} - cellClassName={classes.hiddenOnSmallScreens} - headerClassName={classes.hiddenOnSmallScreens} + cellClassName="hiddenOnSmallScreens" + headerClassName="hiddenOnSmallScreens" > <SingleFieldList> <ChipField source="name.en" size="small" /> diff --git a/examples/simple/src/posts/PostShow.tsx b/examples/simple/src/posts/PostShow.tsx index 75688fc5520..d038477931b 100644 --- a/examples/simple/src/posts/PostShow.tsx +++ b/examples/simple/src/posts/PostShow.tsx @@ -24,7 +24,7 @@ import { Record, } from 'react-admin'; import { Link } from 'react-router-dom'; -import { Button } from '@material-ui/core'; +import { Button } from '@mui/material'; import PostTitle from './PostTitle'; const CreateRelatedComment = ({ record }: { record?: Record }) => ( diff --git a/examples/simple/src/posts/ResetViewsButton.tsx b/examples/simple/src/posts/ResetViewsButton.tsx index ede7b7afa39..2d9df7d007e 100644 --- a/examples/simple/src/posts/ResetViewsButton.tsx +++ b/examples/simple/src/posts/ResetViewsButton.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import PropTypes from 'prop-types'; -import VisibilityOff from '@material-ui/icons/VisibilityOff'; +import VisibilityOff from '@mui/icons-material/VisibilityOff'; import { useUpdateMany, useNotify, diff --git a/examples/simple/src/posts/TagReferenceInput.tsx b/examples/simple/src/posts/TagReferenceInput.tsx index 413e6fa5b4b..b15190254ce 100644 --- a/examples/simple/src/posts/TagReferenceInput.tsx +++ b/examples/simple/src/posts/TagReferenceInput.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { useState } from 'react'; import { useForm } from 'react-final-form'; import { @@ -13,15 +14,21 @@ import { DialogContent, DialogActions, TextField as MuiTextField, -} from '@material-ui/core'; -import { makeStyles } from '@material-ui/core/styles'; +} from '@mui/material'; -const useStyles = makeStyles({ - button: { +const PREFIX = 'TagReferenceInput'; + +const classes = { + button: `${PREFIX}-button`, + input: `${PREFIX}-input`, +}; + +const StyledDialog = styled(Dialog)({ + [`& .${classes.button}`]: { margin: '0 24px', position: 'relative', }, - input: { + [`& .${classes.input}`]: { display: 'flex', flexDirection: 'row', justifyContent: 'flex-start', @@ -36,7 +43,6 @@ const TagReferenceInput = ({ source: string; label?: string; }) => { - const classes = useStyles(); const { change } = useForm(); const [filter, setFilter] = useState(true); @@ -91,7 +97,7 @@ const CreateTag = () => { return false; }; return ( - <Dialog open onClose={onCancel}> + <StyledDialog open onClose={onCancel}> <form onSubmit={handleSubmit}> <DialogContent> <MuiTextField @@ -106,7 +112,7 @@ const CreateTag = () => { <Button onClick={onCancel}>Cancel</Button> </DialogActions> </form> - </Dialog> + </StyledDialog> ); }; diff --git a/examples/simple/src/posts/index.tsx b/examples/simple/src/posts/index.tsx index c9ac54f331e..b909fc9b996 100644 --- a/examples/simple/src/posts/index.tsx +++ b/examples/simple/src/posts/index.tsx @@ -1,4 +1,4 @@ -import BookIcon from '@material-ui/icons/Book'; +import BookIcon from '@mui/icons-material/Book'; import PostCreate from './PostCreate'; import PostEdit from './PostEdit'; import PostList from './PostList'; diff --git a/examples/simple/src/tags/TagList.tsx b/examples/simple/src/tags/TagList.tsx index c10d9ad0879..85aced5db33 100644 --- a/examples/simple/src/tags/TagList.tsx +++ b/examples/simple/src/tags/TagList.tsx @@ -15,9 +15,9 @@ import { ListItemSecondaryAction, Collapse, Card, -} from '@material-ui/core'; -import ExpandLess from '@material-ui/icons/ExpandLess'; -import ExpandMore from '@material-ui/icons/ExpandMore'; +} from '@mui/material'; +import ExpandLess from '@mui/icons-material/ExpandLess'; +import ExpandMore from '@mui/icons-material/ExpandMore'; const TagList = props => ( <ListBase perPage={1000} {...props}> diff --git a/examples/simple/src/users/Aside.tsx b/examples/simple/src/users/Aside.tsx index 9aeb0aa9035..23f2ea29c5c 100644 --- a/examples/simple/src/users/Aside.tsx +++ b/examples/simple/src/users/Aside.tsx @@ -1,14 +1,20 @@ import * as React from 'react'; -import { Typography } from '@material-ui/core'; -import { makeStyles } from '@material-ui/core/styles'; +import { styled } from '@mui/material/styles'; +import { Typography } from '@mui/material'; -const useStyles = makeStyles(theme => ({ - root: { +const PREFIX = 'Aside'; + +const classes = { + root: `${PREFIX}-root`, +}; + +const Root = styled('div')(({ theme }) => ({ + [`&.${classes.root}`]: { [theme.breakpoints.up('sm')]: { width: 200, margin: '1em', }, - [theme.breakpoints.down('sm')]: { + [theme.breakpoints.down('md')]: { width: 0, overflowX: 'hidden', margin: 0, @@ -17,9 +23,8 @@ const useStyles = makeStyles(theme => ({ })); const Aside = () => { - const classes = useStyles(); return ( - <div className={classes.root}> + <Root className={classes.root}> <Typography variant="h6">App Users</Typography> <Typography variant="body2"> Eiusmod adipisicing tempor duis qui. Ullamco aliqua tempor @@ -29,7 +34,7 @@ const Aside = () => { dolore elit sit occaecat ad amet tempor esse occaecat enim. Laborum aliqua excepteur qui ipsum in dolor et cillum est. </Typography> - </div> + </Root> ); }; diff --git a/examples/simple/src/users/UserEdit.tsx b/examples/simple/src/users/UserEdit.tsx index b62afc9c66b..bd7f6084521 100644 --- a/examples/simple/src/users/UserEdit.tsx +++ b/examples/simple/src/users/UserEdit.tsx @@ -1,5 +1,6 @@ /* eslint react/jsx-key: off */ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import PropTypes from 'prop-types'; import { CloneButton, @@ -15,13 +16,25 @@ import { Toolbar, TopToolbar, } from 'react-admin'; -import { makeStyles } from '@material-ui/core/styles'; import UserTitle from './UserTitle'; import Aside from './Aside'; -const useToolbarStyles = makeStyles({ - toolbar: { +const PREFIX = 'UserEdit'; + +const classes = { + toolbar: `${PREFIX}-toolbar`, +}; + +const StyledEdit = styled(Edit)({ + [`& .${classes.toolbar}`]: { + display: 'flex', + justifyContent: 'space-between', + }, +}); + +const StyledToolbar = styled(Toolbar)({ + [`& .RaToolbar-toolbar`]: { display: 'flex', justifyContent: 'space-between', }, @@ -33,16 +46,23 @@ const useToolbarStyles = makeStyles({ * Save with undo, but delete with confirm */ const UserEditToolbar = props => { - const classes = useToolbarStyles(); return ( - <Toolbar {...props} classes={classes}> + <StyledToolbar {...props}> <SaveButton /> <DeleteWithConfirmButton /> - </Toolbar> + </StyledToolbar> ); }; -const EditActions = ({ basePath, data, hasShow }) => ( +const EditActions = ({ + basePath, + data, + hasShow, +}: { + basePath?: string; + data?: any; + hasShow?: boolean; +}) => ( <TopToolbar> <CloneButton className="button-clone" @@ -53,7 +73,14 @@ const EditActions = ({ basePath, data, hasShow }) => ( </TopToolbar> ); -const UserEditForm = ({ permissions, save, ...props }) => { +const UserEditForm = ({ + permissions, + save, + ...props +}: { + permissions?: any; + save?: any; +}) => { const newSave = values => new Promise((resolve, reject) => { if (values.name === 'test') { @@ -101,14 +128,14 @@ const UserEditForm = ({ permissions, save, ...props }) => { }; const UserEdit = ({ permissions, ...props }) => { return ( - <Edit + <StyledEdit title={<UserTitle />} aside={<Aside />} actions={<EditActions />} {...props} > <UserEditForm permissions={permissions} /> - </Edit> + </StyledEdit> ); }; diff --git a/examples/simple/src/users/UserList.tsx b/examples/simple/src/users/UserList.tsx index e38d79b52a6..881ea713359 100644 --- a/examples/simple/src/users/UserList.tsx +++ b/examples/simple/src/users/UserList.tsx @@ -1,7 +1,7 @@ /* eslint react/jsx-key: off */ -import PeopleIcon from '@material-ui/icons/People'; +import PeopleIcon from '@mui/icons-material/People'; import memoize from 'lodash/memoize'; -import { useMediaQuery, Theme } from '@material-ui/core'; +import { useMediaQuery, Theme } from '@mui/material'; import * as React from 'react'; import { BulkDeleteWithConfirmButton, @@ -43,7 +43,7 @@ const UserList = ({ permissions, ...props }) => ( aside={<Aside />} bulkActionButtons={<UserBulkActionButtons />} > - {useMediaQuery((theme: Theme) => theme.breakpoints.down('sm')) ? ( + {useMediaQuery((theme: Theme) => theme.breakpoints.down('md')) ? ( <SimpleList primaryText={record => record.name} secondaryText={record => diff --git a/examples/simple/src/users/UserTitle.tsx b/examples/simple/src/users/UserTitle.tsx index 574dbbd72c3..9459a1878e8 100644 --- a/examples/simple/src/users/UserTitle.tsx +++ b/examples/simple/src/users/UserTitle.tsx @@ -1,8 +1,8 @@ /* eslint react/jsx-key: off */ import * as React from 'react'; -import { useTranslate } from 'react-admin'; +import { useTranslate, Record } from 'react-admin'; -const UserTitle = ({ record }) => { +const UserTitle = ({ record }: { record?: Record }) => { const translate = useTranslate(); return ( <span> diff --git a/examples/simple/src/users/index.tsx b/examples/simple/src/users/index.tsx index eb9905565ae..411828e14de 100644 --- a/examples/simple/src/users/index.tsx +++ b/examples/simple/src/users/index.tsx @@ -1,4 +1,4 @@ -import PeopleIcon from '@material-ui/icons/People'; +import PeopleIcon from '@mui/icons-material/People'; import UserCreate from './UserCreate'; import UserEdit from './UserEdit'; import UserList from './UserList'; diff --git a/examples/simple/vite.config.js b/examples/simple/vite.config.js index 9cc1cb9cd31..51947a779f7 100644 --- a/examples/simple/vite.config.js +++ b/examples/simple/vite.config.js @@ -7,31 +7,28 @@ import path from 'path'; */ export default { plugins: [reactRefresh()], - alias: [ - { - find: /^react-admin$/, - replacement: path.resolve( - __dirname, - '../../packages/react-admin/src' - ), - }, - { - find: /^ra-(.*)$/, - replacement: path.resolve(__dirname, '../../packages/ra-$1/src'), - }, - { - find: /^@material-ui\/icons\/(.*)/, - replacement: '@material-ui/icons/esm/$1', - }, - { - find: /^@material-ui\/core\/(.+)/, - replacement: '@material-ui/core/es/$1', - }, - { - find: /^@material-ui\/core$/, - replacement: '@material-ui/core/es', - }, - ], + resolve: { + alias: [ + { + find: /^react-admin$/, + replacement: path.resolve( + __dirname, + '../../packages/react-admin/src' + ), + }, + { + find: /^ra-(.*)$/, + replacement: path.resolve( + __dirname, + '../../packages/ra-$1/src' + ), + }, + { + find: /^@mui\/icons-material\/(.*)/, + replacement: '@mui/icons-material/esm/$1', + }, + ], + }, server: { port: 8080, }, diff --git a/examples/tutorial/package.json b/examples/tutorial/package.json index aa06255afa7..71e596faafb 100644 --- a/examples/tutorial/package.json +++ b/examples/tutorial/package.json @@ -3,7 +3,7 @@ "version": "3.0.0", "private": true, "dependencies": { - "@material-ui/core": "^4.12.1", + "@mui/material": "^5.0.2", "ra-data-json-server": "^3.9.0", "react": "^17.0.0", "react-admin": "^3.9.0", diff --git a/examples/tutorial/src/App.js b/examples/tutorial/src/App.js index bca2147d085..e029454e3c7 100644 --- a/examples/tutorial/src/App.js +++ b/examples/tutorial/src/App.js @@ -1,6 +1,6 @@ import * as React from 'react'; -import PostIcon from '@material-ui/icons/Book'; -import UserIcon from '@material-ui/icons/Group'; +import PostIcon from '@mui/icons-material/Book'; +import UserIcon from '@mui/icons-material/Group'; import { Admin, Resource, ListGuesser } from 'react-admin'; import jsonServerProvider from 'ra-data-json-server'; diff --git a/examples/tutorial/src/Dashboard.js b/examples/tutorial/src/Dashboard.js index 33c02138349..11df2736ddb 100644 --- a/examples/tutorial/src/Dashboard.js +++ b/examples/tutorial/src/Dashboard.js @@ -1,7 +1,7 @@ import * as React from 'react'; -import Card from '@material-ui/core/Card'; -import CardContent from '@material-ui/core/CardContent'; -import CardHeader from '@material-ui/core/CardHeader'; +import Card from '@mui/material/Card'; +import CardContent from '@mui/material/CardContent'; +import CardHeader from '@mui/material/CardHeader'; export default () => ( <Card> diff --git a/examples/tutorial/src/users.js b/examples/tutorial/src/users.js index 9701268e002..17784698efe 100644 --- a/examples/tutorial/src/users.js +++ b/examples/tutorial/src/users.js @@ -1,9 +1,9 @@ import * as React from 'react'; -import { useMediaQuery } from '@material-ui/core'; +import { useMediaQuery } from '@mui/material'; import { SimpleList, List, Datagrid, EmailField, TextField } from 'react-admin'; export const UserList = props => { - const isSmall = useMediaQuery(theme => theme.breakpoints.down('sm')); + const isSmall = useMediaQuery(theme => theme.breakpoints.down('md')); return ( <List title="All users" {...props}> diff --git a/packages/ra-core/src/controller/button/useDeleteWithUndoController.tsx b/packages/ra-core/src/controller/button/useDeleteWithUndoController.tsx index b684a13a6e5..401f0d25f4e 100644 --- a/packages/ra-core/src/controller/button/useDeleteWithUndoController.tsx +++ b/packages/ra-core/src/controller/button/useDeleteWithUndoController.tsx @@ -16,7 +16,7 @@ import { useResourceContext } from '../../core'; * @example * * import React from 'react'; - * import ActionDelete from '@material-ui/icons/Delete'; + * import ActionDelete from '@mui/icons-material/Delete'; * import { Button, useDeleteWithUndoController } from 'react-admin'; * * const DeleteButton = ({ diff --git a/packages/ra-core/src/controller/useListContext.ts b/packages/ra-core/src/controller/useListContext.ts index 704848cf04c..dbeabd3122f 100644 --- a/packages/ra-core/src/controller/useListContext.ts +++ b/packages/ra-core/src/controller/useListContext.ts @@ -67,9 +67,9 @@ import { Record } from '../types'; * @example // custom pagination * * import { useListContext } from 'react-admin'; - * import { Button, Toolbar } from '@material-ui/core'; - * import ChevronLeft from '@material-ui/icons/ChevronLeft'; - * import ChevronRight from '@material-ui/icons/ChevronRight'; + * import { Button, Toolbar } from '@mui/material'; + * import ChevronLeft from '@mui/icons-material/ChevronLeft'; + * import ChevronRight from '@mui/icons-material/ChevronRight'; * * const PrevNextPagination = () => { * const { page, perPage, total, setPage } = useListContext(); diff --git a/packages/ra-core/src/controller/useListController.spec.tsx b/packages/ra-core/src/controller/useListController.spec.tsx index 2a462f066b3..1731fe81c4b 100644 --- a/packages/ra-core/src/controller/useListController.spec.tsx +++ b/packages/ra-core/src/controller/useListController.spec.tsx @@ -2,7 +2,8 @@ import * as React from 'react'; import expect from 'expect'; import { fireEvent, waitFor, act } from '@testing-library/react'; import lolex from 'lolex'; -import TextField from '@material-ui/core/TextField/TextField'; +// TODO: we shouldn't import mui components in ra-core +import { TextField } from '@mui/material'; import { DataProviderContext } from '../dataProvider'; import ListController from './ListController'; @@ -112,10 +113,12 @@ describe('useListController', () => { describe('setFilters', () => { let clock; let fakeComponent = ({ setFilters, filterValues }) => ( + // TODO: we shouldn't import mui components in ra-core <TextField inputProps={{ 'aria-label': 'search', }} + type="text" value={filterValues.q || ''} onChange={event => { setFilters({ q: event.target.value }); diff --git a/packages/ra-core/src/core/useScrollToTop.tsx b/packages/ra-core/src/core/useScrollToTop.tsx index 8ab67fbca84..28e589072b7 100644 --- a/packages/ra-core/src/core/useScrollToTop.tsx +++ b/packages/ra-core/src/core/useScrollToTop.tsx @@ -8,7 +8,7 @@ import { useHistory } from 'react-router-dom'; * * @example // usage in buttons * import { Link } from 'react-router-dom'; - * import { Button } from '@material-ui/core'; + * import { Button } from '@mui/material'; * * const FooButton = () => ( * <Button diff --git a/packages/ra-core/src/form/FormGroupContextProvider.tsx b/packages/ra-core/src/form/FormGroupContextProvider.tsx index e58d83667ba..04a07e1740c 100644 --- a/packages/ra-core/src/form/FormGroupContextProvider.tsx +++ b/packages/ra-core/src/form/FormGroupContextProvider.tsx @@ -10,7 +10,7 @@ import { useFormContext } from './useFormContext'; * * @example * import { Edit, SimpleForm, TextInput, FormGroupContextProvider, useFormGroup } from 'react-admin'; - * import { Accordion, AccordionDetails, AccordionSummary, Typography } from '@material-ui/core'; + * import { Accordion, AccordionDetails, AccordionSummary, Typography } from '@mui/material'; * * const PostEdit = (props) => ( * <Edit {...props}> diff --git a/packages/ra-core/src/form/useFormGroup.ts b/packages/ra-core/src/form/useFormGroup.ts index cb88fee2c99..dd852c3ba38 100644 --- a/packages/ra-core/src/form/useFormGroup.ts +++ b/packages/ra-core/src/form/useFormGroup.ts @@ -19,7 +19,7 @@ type FormGroupState = { * * @example * import { Edit, SimpleForm, TextInput, FormGroupContextProvider, useFormGroup } from 'react-admin'; - * import { Accordion, AccordionDetails, AccordionSummary, Typography } from '@material-ui/core'; + * import { Accordion, AccordionDetails, AccordionSummary, Typography } from '@mui/material'; * * const PostEdit = (props) => ( * <Edit {...props}> diff --git a/packages/ra-core/src/types.ts b/packages/ra-core/src/types.ts index 81d94935528..452782a87a7 100644 --- a/packages/ra-core/src/types.ts +++ b/packages/ra-core/src/types.ts @@ -4,7 +4,7 @@ import { RouteComponentProps, match as Match, } from 'react-router-dom'; -import { ThemeOptions } from '@material-ui/core'; +import { DeprecatedThemeOptions } from '@mui/material'; import { StaticContext } from 'react-router'; import { Location, History, LocationState } from 'history'; @@ -416,13 +416,13 @@ export interface CoreLayoutProps { logout?: ReactNode; hasDashboard?: boolean; }>; - theme?: ThemeOptions; + theme?: DeprecatedThemeOptions; title?: TitleComponent; } export type LayoutComponent = ComponentType<CoreLayoutProps>; export type LoadingComponent = ComponentType<{ - theme?: ThemeOptions; + theme?: DeprecatedThemeOptions; loadingPrimary?: string; loadingSecondary?: string; }>; @@ -497,7 +497,7 @@ export interface AdminProps { logoutButton?: ComponentType; menu?: ComponentType; ready?: ComponentType; - theme?: ThemeOptions; + theme?: DeprecatedThemeOptions; title?: TitleComponent; } diff --git a/packages/ra-input-rich-text/src/QuillBubbleStylesheet.ts b/packages/ra-input-rich-text/src/QuillBubbleStylesheet.ts index 3e3eb2d197c..d006a1df43b 100644 --- a/packages/ra-input-rich-text/src/QuillBubbleStylesheet.ts +++ b/packages/ra-input-rich-text/src/QuillBubbleStylesheet.ts @@ -1,813 +1,808 @@ -import { StyleRules } from '@material-ui/core/styles'; - // converted from vendor (node_modules/quill/dist/quill.bubble.css) using the jss cli export default { - '@global': { - '.ql-container': { - boxSizing: 'border-box', - fontFamily: 'Helvetica, Arial, sans-serif', - fontSize: 13, - height: '100%', - margin: 0, - position: 'relative', - }, - '.ql-container.ql-disabled .ql-tooltip': { - visibility: 'hidden', - }, - '.ql-container.ql-disabled .ql-editor ul[data-checked] > li::before': { - pointerEvents: 'none', - }, - '.ql-clipboard': { - left: -100000, - height: 1, - overflowY: 'hidden', - position: 'absolute', - top: '50%', - }, - '.ql-clipboard p': { - margin: '0', - padding: '0', - }, - '.ql-editor': { - boxSizing: 'border-box', - lineHeight: '1.42', - height: '100%', - outline: 'none', - overflowY: 'auto', - padding: '12px 15px', - tabSize: '4', - M: '4', - textAlign: 'left', - whiteSpace: 'pre-wrap', - wordWrap: 'break-word', - }, - '.ql-editor > *': { - cursor: 'text', - }, - '.ql-editor p, .ql-editor ol, .ql-editor ul, .ql-editor pre, .ql-editor blockquote, .ql-editor h1, .ql-editor h2, .ql-editor h3, .ql-editor h4, .ql-editor h5, .ql-editor h6': { - margin: '0', - padding: '0', - counterReset: - 'list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9', - }, - '.ql-editor ol, .ql-editor ul': { - paddingLeft: '1.5em', - }, - '.ql-editor ol > li, .ql-editor ul > li': { - listStyleType: 'none', - }, - '.ql-editor ul > li::before': { - content: "'\\2022'", - }, - '.ql-editor ul[data-checked=true], .ql-editor ul[data-checked=false]': { - pointerEvents: 'none', - }, - '.ql-editor ul[data-checked=true] > li *, .ql-editor ul[data-checked=false] > li *': { - pointerEvents: 'all', - }, - '.ql-editor ul[data-checked=true] > li::before, .ql-editor ul[data-checked=false] > li::before': { - color: '#777', - cursor: 'pointer', - pointerEvents: 'all', - }, - '.ql-editor ul[data-checked=true] > li::before': { - content: "'\\2611'", - }, - '.ql-editor ul[data-checked=false] > li::before': { - content: "'\\2610'", - }, - '.ql-editor li::before': { - display: 'inline-block', - whiteSpace: 'nowrap', - width: '1.2em', - }, - '.ql-editor li:not(.ql-direction-rtl)::before': { - marginLeft: '-1.5em', - marginRight: '0.3em', - textAlign: 'right', - }, - '.ql-editor li.ql-direction-rtl::before': { - marginLeft: '0.3em', - marginRight: '-1.5em', - }, - '.ql-editor ol li:not(.ql-direction-rtl), .ql-editor ul li:not(.ql-direction-rtl)': { - paddingLeft: '1.5em', - }, - '.ql-editor ol li.ql-direction-rtl, .ql-editor ul li.ql-direction-rtl': { - paddingRight: '1.5em', - }, - '.ql-editor ol li': { - counterReset: - 'list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9', - counterIncrement: 'list-0', - }, - '.ql-editor ol li:before': { - content: "counter(list-0, decimal) '. '", - }, - '.ql-editor ol li.ql-indent-1': { - counterIncrement: 'list-1', - counterReset: - 'list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9', - }, - '.ql-editor ol li.ql-indent-1:before': { - content: "counter(list-1, lower-alpha) '. '", - }, - '.ql-editor ol li.ql-indent-2': { - counterIncrement: 'list-2', - counterReset: 'list-3 list-4 list-5 list-6 list-7 list-8 list-9', - }, - '.ql-editor ol li.ql-indent-2:before': { - content: "counter(list-2, lower-roman) '. '", - }, - '.ql-editor ol li.ql-indent-3': { - counterIncrement: 'list-3', - counterReset: 'list-4 list-5 list-6 list-7 list-8 list-9', - }, - '.ql-editor ol li.ql-indent-3:before': { - content: "counter(list-3, decimal) '. '", - }, - '.ql-editor ol li.ql-indent-4': { - counterIncrement: 'list-4', - counterReset: 'list-5 list-6 list-7 list-8 list-9', - }, - '.ql-editor ol li.ql-indent-4:before': { - content: "counter(list-4, lower-alpha) '. '", - }, - '.ql-editor ol li.ql-indent-5': { - counterIncrement: 'list-5', - counterReset: 'list-6 list-7 list-8 list-9', - }, - '.ql-editor ol li.ql-indent-5:before': { - content: "counter(list-5, lower-roman) '. '", - }, - '.ql-editor ol li.ql-indent-6': { - counterIncrement: 'list-6', - counterReset: 'list-7 list-8 list-9', - }, - '.ql-editor ol li.ql-indent-6:before': { - content: "counter(list-6, decimal) '. '", - }, - '.ql-editor ol li.ql-indent-7': { - counterIncrement: 'list-7', - counterReset: 'list-8 list-9', - }, - '.ql-editor ol li.ql-indent-7:before': { - content: "counter(list-7, lower-alpha) '. '", - }, - '.ql-editor ol li.ql-indent-8': { - counterIncrement: 'list-8', - counterReset: 'list-9', - }, - '.ql-editor ol li.ql-indent-8:before': { - content: "counter(list-8, lower-roman) '. '", - }, - '.ql-editor ol li.ql-indent-9': { - counterIncrement: 'list-9', - }, - '.ql-editor ol li.ql-indent-9:before': { - content: "counter(list-9, decimal) '. '", - }, - '.ql-editor .ql-indent-1:not(.ql-direction-rtl)': { - paddingLeft: '3em', - }, - '.ql-editor li.ql-indent-1:not(.ql-direction-rtl)': { - paddingLeft: '4.5em', - }, - '.ql-editor .ql-indent-1.ql-direction-rtl.ql-align-right': { - paddingRight: '3em', - }, - '.ql-editor li.ql-indent-1.ql-direction-rtl.ql-align-right': { - paddingRight: '4.5em', - }, - '.ql-editor .ql-indent-2:not(.ql-direction-rtl)': { - paddingLeft: '6em', - }, - '.ql-editor li.ql-indent-2:not(.ql-direction-rtl)': { - paddingLeft: '7.5em', - }, - '.ql-editor .ql-indent-2.ql-direction-rtl.ql-align-right': { - paddingRight: '6em', - }, - '.ql-editor li.ql-indent-2.ql-direction-rtl.ql-align-right': { - paddingRight: '7.5em', - }, - '.ql-editor .ql-indent-3:not(.ql-direction-rtl)': { - paddingLeft: '9em', - }, - '.ql-editor li.ql-indent-3:not(.ql-direction-rtl)': { - paddingLeft: '10.5em', - }, - '.ql-editor .ql-indent-3.ql-direction-rtl.ql-align-right': { - paddingRight: '9em', - }, - '.ql-editor li.ql-indent-3.ql-direction-rtl.ql-align-right': { - paddingRight: '10.5em', - }, - '.ql-editor .ql-indent-4:not(.ql-direction-rtl)': { - paddingLeft: '12em', - }, - '.ql-editor li.ql-indent-4:not(.ql-direction-rtl)': { - paddingLeft: '13.5em', - }, - '.ql-editor .ql-indent-4.ql-direction-rtl.ql-align-right': { - paddingRight: '12em', - }, - '.ql-editor li.ql-indent-4.ql-direction-rtl.ql-align-right': { - paddingRight: '13.5em', - }, - '.ql-editor .ql-indent-5:not(.ql-direction-rtl)': { - paddingLeft: '15em', - }, - '.ql-editor li.ql-indent-5:not(.ql-direction-rtl)': { - paddingLeft: '16.5em', - }, - '.ql-editor .ql-indent-5.ql-direction-rtl.ql-align-right': { - paddingRight: '15em', - }, - '.ql-editor li.ql-indent-5.ql-direction-rtl.ql-align-right': { - paddingRight: '16.5em', - }, - '.ql-editor .ql-indent-6:not(.ql-direction-rtl)': { - paddingLeft: '18em', - }, - '.ql-editor li.ql-indent-6:not(.ql-direction-rtl)': { - paddingLeft: '19.5em', - }, - '.ql-editor .ql-indent-6.ql-direction-rtl.ql-align-right': { - paddingRight: '18em', - }, - '.ql-editor li.ql-indent-6.ql-direction-rtl.ql-align-right': { - paddingRight: '19.5em', - }, - '.ql-editor .ql-indent-7:not(.ql-direction-rtl)': { - paddingLeft: '21em', - }, - '.ql-editor li.ql-indent-7:not(.ql-direction-rtl)': { - paddingLeft: '22.5em', - }, - '.ql-editor .ql-indent-7.ql-direction-rtl.ql-align-right': { - paddingRight: '21em', - }, - '.ql-editor li.ql-indent-7.ql-direction-rtl.ql-align-right': { - paddingRight: '22.5em', - }, - '.ql-editor .ql-indent-8:not(.ql-direction-rtl)': { - paddingLeft: '24em', - }, - '.ql-editor li.ql-indent-8:not(.ql-direction-rtl)': { - paddingLeft: '25.5em', - }, - '.ql-editor .ql-indent-8.ql-direction-rtl.ql-align-right': { - paddingRight: '24em', - }, - '.ql-editor li.ql-indent-8.ql-direction-rtl.ql-align-right': { - paddingRight: '25.5em', - }, - '.ql-editor .ql-indent-9:not(.ql-direction-rtl)': { - paddingLeft: '27em', - }, - '.ql-editor li.ql-indent-9:not(.ql-direction-rtl)': { - paddingLeft: '28.5em', - }, - '.ql-editor .ql-indent-9.ql-direction-rtl.ql-align-right': { - paddingRight: '27em', - }, - '.ql-editor li.ql-indent-9.ql-direction-rtl.ql-align-right': { - paddingRight: '28.5em', - }, - '.ql-editor .ql-video': { - display: 'block', - maxWidth: '100%', - }, - '.ql-editor .ql-video.ql-align-center': { - margin: '0 auto', - }, - '.ql-editor .ql-video.ql-align-right': { - margin: '0 0 0 auto', - }, - '.ql-editor .ql-bg-black': { - backgroundColor: '#000', - }, - '.ql-editor .ql-bg-red': { - backgroundColor: '#e60000', - }, - '.ql-editor .ql-bg-orange': { - backgroundColor: '#f90', - }, - '.ql-editor .ql-bg-yellow': { - backgroundColor: '#ff0', - }, - '.ql-editor .ql-bg-green': { - backgroundColor: '#008a00', - }, - '.ql-editor .ql-bg-blue': { - backgroundColor: '#06c', - }, - '.ql-editor .ql-bg-purple': { - backgroundColor: '#93f', - }, - '.ql-editor .ql-color-white': { - color: '#fff', - }, - '.ql-editor .ql-color-red': { - color: '#e60000', - }, - '.ql-editor .ql-color-orange': { - color: '#f90', - }, - '.ql-editor .ql-color-yellow': { - color: '#ff0', - }, - '.ql-editor .ql-color-green': { - color: '#008a00', - }, - '.ql-editor .ql-color-blue': { - color: '#06c', - }, - '.ql-editor .ql-color-purple': { - color: '#93f', - }, - '.ql-editor .ql-font-serif': { - fontFamily: 'Georgia, Times New Roman, serif', - }, - '.ql-editor .ql-font-monospace': { - fontFamily: 'Monaco, Courier New, monospace', - }, - '.ql-editor .ql-size-small': { - fontSize: '0.75em', - }, - '.ql-editor .ql-size-large': { - fontSize: '1.5em', - }, - '.ql-editor .ql-size-huge': { - fontSize: '2.5em', - }, - '.ql-editor .ql-direction-rtl': { - direction: 'rtl', - textAlign: 'inherit', - }, - '.ql-editor .ql-align-center': { - textAlign: 'center', - }, - '.ql-editor .ql-align-justify': { - textAlign: 'justify', - }, - '.ql-editor .ql-align-right': { - textAlign: 'right', - }, - '.ql-editor.ql-blank::before': { - color: 'rgba(0,0,0,0.6)', - content: 'attr(data-placeholder)', - fontStyle: 'italic', - left: 15, - pointerEvents: 'none', - position: 'absolute', - right: 15, - }, - '.ql-bubble.ql-toolbar:after, .ql-bubble .ql-toolbar:after': { - clear: 'both', - content: "''", - display: 'table', - }, - '.ql-bubble.ql-toolbar button, .ql-bubble .ql-toolbar button': { - background: 'none', - border: 'none', - cursor: 'pointer', - display: 'inline-block', - float: 'left', - height: 24, - padding: '3px 5px', - width: 28, - }, - '.ql-bubble.ql-toolbar button svg, .ql-bubble .ql-toolbar button svg': { - float: 'left', - height: '100%', - }, - '.ql-bubble.ql-toolbar button:active:hover, .ql-bubble .ql-toolbar button:active:hover': { - outline: 'none', - }, - '.ql-bubble.ql-toolbar input.ql-image[type=file], .ql-bubble .ql-toolbar input.ql-image[type=file]': { - display: 'none', - }, - '.ql-bubble.ql-toolbar button:hover, .ql-bubble .ql-toolbar button:hover, .ql-bubble.ql-toolbar button:focus, .ql-bubble .ql-toolbar button:focus, .ql-bubble.ql-toolbar button.ql-active, .ql-bubble .ql-toolbar button.ql-active, .ql-bubble.ql-toolbar .ql-picker-label:hover, .ql-bubble .ql-toolbar .ql-picker-label:hover, .ql-bubble.ql-toolbar .ql-picker-label.ql-active, .ql-bubble .ql-toolbar .ql-picker-label.ql-active, .ql-bubble.ql-toolbar .ql-picker-item:hover, .ql-bubble .ql-toolbar .ql-picker-item:hover, .ql-bubble.ql-toolbar .ql-picker-item.ql-selected, .ql-bubble .ql-toolbar .ql-picker-item.ql-selected': { - color: '#fff', - }, - '.ql-bubble.ql-toolbar button:hover .ql-fill, .ql-bubble .ql-toolbar button:hover .ql-fill, .ql-bubble.ql-toolbar button:focus .ql-fill, .ql-bubble .ql-toolbar button:focus .ql-fill, .ql-bubble.ql-toolbar button.ql-active .ql-fill, .ql-bubble .ql-toolbar button.ql-active .ql-fill, .ql-bubble.ql-toolbar .ql-picker-label:hover .ql-fill, .ql-bubble .ql-toolbar .ql-picker-label:hover .ql-fill, .ql-bubble.ql-toolbar .ql-picker-label.ql-active .ql-fill, .ql-bubble .ql-toolbar .ql-picker-label.ql-active .ql-fill, .ql-bubble.ql-toolbar .ql-picker-item:hover .ql-fill, .ql-bubble .ql-toolbar .ql-picker-item:hover .ql-fill, .ql-bubble.ql-toolbar .ql-picker-item.ql-selected .ql-fill, .ql-bubble .ql-toolbar .ql-picker-item.ql-selected .ql-fill, .ql-bubble.ql-toolbar button:hover .ql-stroke.ql-fill, .ql-bubble .ql-toolbar button:hover .ql-stroke.ql-fill, .ql-bubble.ql-toolbar button:focus .ql-stroke.ql-fill, .ql-bubble .ql-toolbar button:focus .ql-stroke.ql-fill, .ql-bubble.ql-toolbar button.ql-active .ql-stroke.ql-fill, .ql-bubble .ql-toolbar button.ql-active .ql-stroke.ql-fill, .ql-bubble.ql-toolbar .ql-picker-label:hover .ql-stroke.ql-fill, .ql-bubble .ql-toolbar .ql-picker-label:hover .ql-stroke.ql-fill, .ql-bubble.ql-toolbar .ql-picker-label.ql-active .ql-stroke.ql-fill, .ql-bubble .ql-toolbar .ql-picker-label.ql-active .ql-stroke.ql-fill, .ql-bubble.ql-toolbar .ql-picker-item:hover .ql-stroke.ql-fill, .ql-bubble .ql-toolbar .ql-picker-item:hover .ql-stroke.ql-fill, .ql-bubble.ql-toolbar .ql-picker-item.ql-selected .ql-stroke.ql-fill, .ql-bubble .ql-toolbar .ql-picker-item.ql-selected .ql-stroke.ql-fill': { - fill: '#fff', - }, - '.ql-bubble.ql-toolbar button:hover .ql-stroke, .ql-bubble .ql-toolbar button:hover .ql-stroke, .ql-bubble.ql-toolbar button:focus .ql-stroke, .ql-bubble .ql-toolbar button:focus .ql-stroke, .ql-bubble.ql-toolbar button.ql-active .ql-stroke, .ql-bubble .ql-toolbar button.ql-active .ql-stroke, .ql-bubble.ql-toolbar .ql-picker-label:hover .ql-stroke, .ql-bubble .ql-toolbar .ql-picker-label:hover .ql-stroke, .ql-bubble.ql-toolbar .ql-picker-label.ql-active .ql-stroke, .ql-bubble .ql-toolbar .ql-picker-label.ql-active .ql-stroke, .ql-bubble.ql-toolbar .ql-picker-item:hover .ql-stroke, .ql-bubble .ql-toolbar .ql-picker-item:hover .ql-stroke, .ql-bubble.ql-toolbar .ql-picker-item.ql-selected .ql-stroke, .ql-bubble .ql-toolbar .ql-picker-item.ql-selected .ql-stroke, .ql-bubble.ql-toolbar button:hover .ql-stroke-miter, .ql-bubble .ql-toolbar button:hover .ql-stroke-miter, .ql-bubble.ql-toolbar button:focus .ql-stroke-miter, .ql-bubble .ql-toolbar button:focus .ql-stroke-miter, .ql-bubble.ql-toolbar button.ql-active .ql-stroke-miter, .ql-bubble .ql-toolbar button.ql-active .ql-stroke-miter, .ql-bubble.ql-toolbar .ql-picker-label:hover .ql-stroke-miter, .ql-bubble .ql-toolbar .ql-picker-label:hover .ql-stroke-miter, .ql-bubble.ql-toolbar .ql-picker-label.ql-active .ql-stroke-miter, .ql-bubble .ql-toolbar .ql-picker-label.ql-active .ql-stroke-miter, .ql-bubble.ql-toolbar .ql-picker-item:hover .ql-stroke-miter, .ql-bubble .ql-toolbar .ql-picker-item:hover .ql-stroke-miter, .ql-bubble.ql-toolbar .ql-picker-item.ql-selected .ql-stroke-miter, .ql-bubble .ql-toolbar .ql-picker-item.ql-selected .ql-stroke-miter': { - stroke: '#fff', - }, - '@media (pointer: coarse)': { - '.ql-bubble.ql-toolbar button:hover:not(.ql-active), .ql-bubble .ql-toolbar button:hover:not(.ql-active)': { - color: '#ccc', - }, - '.ql-bubble.ql-toolbar button:hover:not(.ql-active) .ql-fill, .ql-bubble .ql-toolbar button:hover:not(.ql-active) .ql-fill, .ql-bubble.ql-toolbar button:hover:not(.ql-active) .ql-stroke.ql-fill, .ql-bubble .ql-toolbar button:hover:not(.ql-active) .ql-stroke.ql-fill': { - fill: '#ccc', - }, - '.ql-bubble.ql-toolbar button:hover:not(.ql-active) .ql-stroke, .ql-bubble .ql-toolbar button:hover:not(.ql-active) .ql-stroke, .ql-bubble.ql-toolbar button:hover:not(.ql-active) .ql-stroke-miter, .ql-bubble .ql-toolbar button:hover:not(.ql-active) .ql-stroke-miter': { - stroke: '#ccc', - }, - }, - '.ql-bubble': { - boxSizing: 'border-box', - }, - '.ql-bubble *': { - boxSizing: 'border-box', - }, - '.ql-bubble .ql-hidden': { - display: 'none', - }, - '.ql-bubble .ql-out-bottom, .ql-bubble .ql-out-top': { - visibility: 'hidden', - }, - '.ql-bubble .ql-tooltip': { - position: 'absolute', - transform: 'translateY(10px)', - backgroundColor: '#444', - borderRadius: 25, - color: '#fff', - }, - '.ql-bubble .ql-tooltip a': { - cursor: 'pointer', - textDecoration: 'none', - }, - '.ql-bubble .ql-tooltip.ql-flip': { - transform: 'translateY(-10px)', - }, - '.ql-bubble .ql-formats': { - display: 'inline-block', - verticalAlign: 'middle', - }, - '.ql-bubble .ql-formats:after': { - clear: 'both', - content: "''", - display: 'table', - }, - '.ql-bubble .ql-stroke': { - fill: 'none', - stroke: '#ccc', - strokeLinecap: 'round', - strokeLinejoin: 'round', - strokeWidth: '2', - }, - '.ql-bubble .ql-stroke-miter': { - fill: 'none', - stroke: '#ccc', - strokeMiterlimit: 10, - strokeWidth: '2', - }, - '.ql-bubble .ql-fill, .ql-bubble .ql-stroke.ql-fill': { - fill: '#ccc', - }, - '.ql-bubble .ql-empty': { - fill: 'none', - }, - '.ql-bubble .ql-even': { - fillRule: 'evenodd', - }, - '.ql-bubble .ql-thin, .ql-bubble .ql-stroke.ql-thin': { - strokeWidth: '1', - }, - '.ql-bubble .ql-transparent': { - opacity: 0.4, - }, - '.ql-bubble .ql-direction svg:last-child': { - display: 'none', - }, - '.ql-bubble .ql-direction.ql-active svg:last-child': { - display: 'inline', - }, - '.ql-bubble .ql-direction.ql-active svg:first-child': { - display: 'none', - }, - '.ql-bubble .ql-editor h1': { - fontSize: '2em', - }, - '.ql-bubble .ql-editor h2': { - fontSize: '1.5em', - }, - '.ql-bubble .ql-editor h3': { - fontSize: '1.17em', - }, - '.ql-bubble .ql-editor h4': { - fontSize: '1em', - }, - '.ql-bubble .ql-editor h5': { - fontSize: '0.83em', - }, - '.ql-bubble .ql-editor h6': { - fontSize: '0.67em', - }, - '.ql-bubble .ql-editor a': { - textDecoration: 'underline', - }, - '.ql-bubble .ql-editor blockquote': { - borderLeft: '4px solid #ccc', - marginBottom: 5, - marginTop: 5, - paddingLeft: 16, - }, - '.ql-bubble .ql-editor code, .ql-bubble .ql-editor pre': { - backgroundColor: '#f0f0f0', - borderRadius: 3, - }, - '.ql-bubble .ql-editor pre': { - whiteSpace: 'pre-wrap', - marginBottom: 5, - marginTop: 5, - padding: '5px 10px', - }, - '.ql-bubble .ql-editor code': { - fontSize: '85%', - padding: '2px 4px', - }, - '.ql-bubble .ql-editor pre.ql-syntax': { - backgroundColor: '#23241f', - color: '#f8f8f2', - overflow: 'visible', - }, - '.ql-bubble .ql-editor img': { - maxWidth: '100%', - }, - '.ql-bubble .ql-picker': { - color: '#ccc', - display: 'inline-block', - float: 'left', - fontSize: 14, - fontWeight: 500, - height: 24, - position: 'relative', - verticalAlign: 'middle', - }, - '.ql-bubble .ql-picker-label': { - cursor: 'pointer', - display: 'inline-block', - height: '100%', - paddingLeft: 8, - paddingRight: 2, - position: 'relative', - width: '100%', - }, - '.ql-bubble .ql-picker-label::before': { - display: 'inline-block', - lineHeight: 22, - }, - '.ql-bubble .ql-picker-options': { - backgroundColor: '#444', - display: 'none', - minWidth: '100%', - padding: '4px 8px', - position: 'absolute', - whiteSpace: 'nowrap', - }, - '.ql-bubble .ql-picker-options .ql-picker-item': { - cursor: 'pointer', - display: 'block', - paddingBottom: 5, - paddingTop: 5, - }, - '.ql-bubble .ql-picker.ql-expanded .ql-picker-label': { - color: '#777', - zIndex: 2, - }, - '.ql-bubble .ql-picker.ql-expanded .ql-picker-label .ql-fill': { - fill: '#777', - }, - '.ql-bubble .ql-picker.ql-expanded .ql-picker-label .ql-stroke': { - stroke: '#777', - }, - '.ql-bubble .ql-picker.ql-expanded .ql-picker-options': { - display: 'block', - marginTop: -1, - top: '100%', - zIndex: 1, - }, - '.ql-bubble .ql-color-picker, .ql-bubble .ql-icon-picker': { - width: 28, - }, - '.ql-bubble .ql-color-picker .ql-picker-label, .ql-bubble .ql-icon-picker .ql-picker-label': { - padding: '2px 4px', - }, - '.ql-bubble .ql-color-picker .ql-picker-label svg, .ql-bubble .ql-icon-picker .ql-picker-label svg': { - right: 4, - }, - '.ql-bubble .ql-icon-picker .ql-picker-options': { - padding: '4px 0px', - }, - '.ql-bubble .ql-icon-picker .ql-picker-item': { - height: 24, - width: 24, - padding: '2px 4px', - }, - '.ql-bubble .ql-color-picker .ql-picker-options': { - padding: '3px 5px', - width: 152, - }, - '.ql-bubble .ql-color-picker .ql-picker-item': { - border: '1px solid transparent', - float: 'left', - height: 16, - margin: 2, - padding: 0, - width: 16, - }, - '.ql-bubble .ql-picker:not(.ql-color-picker):not(.ql-icon-picker) svg': { - position: 'absolute', - marginTop: -9, - right: '0', - top: '50%', - width: 18, - }, - ".ql-bubble .ql-picker.ql-header .ql-picker-label[data-label]:not([data-label=''])::before, .ql-bubble .ql-picker.ql-font .ql-picker-label[data-label]:not([data-label=''])::before, .ql-bubble .ql-picker.ql-size .ql-picker-label[data-label]:not([data-label=''])::before, .ql-bubble .ql-picker.ql-header .ql-picker-item[data-label]:not([data-label=''])::before, .ql-bubble .ql-picker.ql-font .ql-picker-item[data-label]:not([data-label=''])::before, .ql-bubble .ql-picker.ql-size .ql-picker-item[data-label]:not([data-label=''])::before": { - content: 'attr(data-label)', - }, - '.ql-bubble .ql-picker.ql-header': { - width: 98, - }, - '.ql-bubble .ql-picker.ql-header .ql-picker-label::before, .ql-bubble .ql-picker.ql-header .ql-picker-item::before': { - content: "'Normal'", - }, - '.ql-bubble .ql-picker.ql-header .ql-picker-label[data-value="1"]::before, .ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="1"]::before': { - content: "'Heading 1'", - }, - '.ql-bubble .ql-picker.ql-header .ql-picker-label[data-value="2"]::before, .ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="2"]::before': { - content: "'Heading 2'", - }, - '.ql-bubble .ql-picker.ql-header .ql-picker-label[data-value="3"]::before, .ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="3"]::before': { - content: "'Heading 3'", - }, - '.ql-bubble .ql-picker.ql-header .ql-picker-label[data-value="4"]::before, .ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="4"]::before': { - content: "'Heading 4'", - }, - '.ql-bubble .ql-picker.ql-header .ql-picker-label[data-value="5"]::before, .ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="5"]::before': { - content: "'Heading 5'", - }, - '.ql-bubble .ql-picker.ql-header .ql-picker-label[data-value="6"]::before, .ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="6"]::before': { - content: "'Heading 6'", - }, - '.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="1"]::before': { - fontSize: '2em', - }, - '.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="2"]::before': { - fontSize: '1.5em', - }, - '.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="3"]::before': { - fontSize: '1.17em', - }, - '.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="4"]::before': { - fontSize: '1em', - }, - '.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="5"]::before': { - fontSize: '0.83em', - }, - '.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="6"]::before': { - fontSize: '0.67em', - }, - '.ql-bubble .ql-picker.ql-font': { - width: 108, - }, - '.ql-bubble .ql-picker.ql-font .ql-picker-label::before, .ql-bubble .ql-picker.ql-font .ql-picker-item::before': { - content: "'Sans Serif'", - }, - '.ql-bubble .ql-picker.ql-font .ql-picker-label[data-value=serif]::before, .ql-bubble .ql-picker.ql-font .ql-picker-item[data-value=serif]::before': { - content: "'Serif'", - }, - '.ql-bubble .ql-picker.ql-font .ql-picker-label[data-value=monospace]::before, .ql-bubble .ql-picker.ql-font .ql-picker-item[data-value=monospace]::before': { - content: "'Monospace'", - }, - '.ql-bubble .ql-picker.ql-font .ql-picker-item[data-value=serif]::before': { - fontFamily: 'Georgia, Times New Roman, serif', - }, - '.ql-bubble .ql-picker.ql-font .ql-picker-item[data-value=monospace]::before': { - fontFamily: 'Monaco, Courier New, monospace', - }, - '.ql-bubble .ql-picker.ql-size': { - width: 98, - }, - '.ql-bubble .ql-picker.ql-size .ql-picker-label::before, .ql-bubble .ql-picker.ql-size .ql-picker-item::before': { - content: "'Normal'", - }, - '.ql-bubble .ql-picker.ql-size .ql-picker-label[data-value=small]::before, .ql-bubble .ql-picker.ql-size .ql-picker-item[data-value=small]::before': { - content: "'Small'", - }, - '.ql-bubble .ql-picker.ql-size .ql-picker-label[data-value=large]::before, .ql-bubble .ql-picker.ql-size .ql-picker-item[data-value=large]::before': { - content: "'Large'", - }, - '.ql-bubble .ql-picker.ql-size .ql-picker-label[data-value=huge]::before, .ql-bubble .ql-picker.ql-size .ql-picker-item[data-value=huge]::before': { - content: "'Huge'", - }, - '.ql-bubble .ql-picker.ql-size .ql-picker-item[data-value=small]::before': { - fontSize: 10, - }, - '.ql-bubble .ql-picker.ql-size .ql-picker-item[data-value=large]::before': { - fontSize: 18, - }, - '.ql-bubble .ql-picker.ql-size .ql-picker-item[data-value=huge]::before': { - fontSize: 32, - }, - '.ql-bubble .ql-color-picker.ql-background .ql-picker-item': { - backgroundColor: '#fff', - }, - '.ql-bubble .ql-color-picker.ql-color .ql-picker-item': { - backgroundColor: '#000', - }, - '.ql-bubble .ql-toolbar .ql-formats': { - margin: '8px 12px 8px 0px', - }, - '.ql-bubble .ql-toolbar .ql-formats:first-child': { - marginLeft: 12, - }, - '.ql-bubble .ql-color-picker svg': { - margin: 1, - }, - '.ql-bubble .ql-color-picker .ql-picker-item.ql-selected, .ql-bubble .ql-color-picker .ql-picker-item:hover': { - borderColor: '#fff', - }, - '.ql-bubble .ql-tooltip-arrow': { - borderLeft: '6px solid transparent', - borderRight: '6px solid transparent', - content: '" "', - display: 'block', - left: '50%', - marginLeft: -6, - position: 'absolute', - }, - '.ql-bubble .ql-tooltip:not(.ql-flip) .ql-tooltip-arrow': { - borderBottom: '6px solid #444', - top: -6, - }, - '.ql-bubble .ql-tooltip.ql-flip .ql-tooltip-arrow': { - borderTop: '6px solid #444', - bottom: -6, - }, - '.ql-bubble .ql-tooltip.ql-editing .ql-tooltip-editor': { - display: 'block', - }, - '.ql-bubble .ql-tooltip.ql-editing .ql-formats': { - visibility: 'hidden', - }, - '.ql-bubble .ql-tooltip-editor': { - display: 'none', - }, - '.ql-bubble .ql-tooltip-editor input[type=text]': { - background: 'transparent', - border: 'none', - color: '#fff', - fontSize: 13, - height: '100%', - outline: 'none', - padding: '10px 20px', - position: 'absolute', - width: '100%', - }, - '.ql-bubble .ql-tooltip-editor a': { - top: 10, - position: 'absolute', - right: 20, - }, - '.ql-bubble .ql-tooltip-editor a:before': { + '.ql-container': { + boxSizing: 'border-box', + fontFamily: 'Helvetica, Arial, sans-serif', + fontSize: 13, + height: '100%', + margin: 0, + position: 'relative', + }, + '.ql-container.ql-disabled .ql-tooltip': { + visibility: 'hidden', + }, + '.ql-container.ql-disabled .ql-editor ul[data-checked] > li::before': { + pointerEvents: 'none', + }, + '.ql-clipboard': { + left: -100000, + height: 1, + overflowY: 'hidden', + position: 'absolute', + top: '50%', + }, + '.ql-clipboard p': { + margin: '0', + padding: '0', + }, + '.ql-editor': { + boxSizing: 'border-box', + lineHeight: '1.42', + height: '100%', + outline: 'none', + overflowY: 'auto', + padding: '12px 15px', + tabSize: '4', + M: '4', + textAlign: 'left', + whiteSpace: 'pre-wrap', + wordWrap: 'break-word', + }, + '.ql-editor > *': { + cursor: 'text', + }, + '.ql-editor p, .ql-editor ol, .ql-editor ul, .ql-editor pre, .ql-editor blockquote, .ql-editor h1, .ql-editor h2, .ql-editor h3, .ql-editor h4, .ql-editor h5, .ql-editor h6': { + margin: '0', + padding: '0', + counterReset: + 'list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9', + }, + '.ql-editor ol, .ql-editor ul': { + paddingLeft: '1.5em', + }, + '.ql-editor ol > li, .ql-editor ul > li': { + listStyleType: 'none', + }, + '.ql-editor ul > li::before': { + content: "'\\2022'", + }, + '.ql-editor ul[data-checked=true], .ql-editor ul[data-checked=false]': { + pointerEvents: 'none', + }, + '.ql-editor ul[data-checked=true] > li *, .ql-editor ul[data-checked=false] > li *': { + pointerEvents: 'all', + }, + '.ql-editor ul[data-checked=true] > li::before, .ql-editor ul[data-checked=false] > li::before': { + color: '#777', + cursor: 'pointer', + pointerEvents: 'all', + }, + '.ql-editor ul[data-checked=true] > li::before': { + content: "'\\2611'", + }, + '.ql-editor ul[data-checked=false] > li::before': { + content: "'\\2610'", + }, + '.ql-editor li::before': { + display: 'inline-block', + whiteSpace: 'nowrap', + width: '1.2em', + }, + '.ql-editor li:not(.ql-direction-rtl)::before': { + marginLeft: '-1.5em', + marginRight: '0.3em', + textAlign: 'right', + }, + '.ql-editor li.ql-direction-rtl::before': { + marginLeft: '0.3em', + marginRight: '-1.5em', + }, + '.ql-editor ol li:not(.ql-direction-rtl), .ql-editor ul li:not(.ql-direction-rtl)': { + paddingLeft: '1.5em', + }, + '.ql-editor ol li.ql-direction-rtl, .ql-editor ul li.ql-direction-rtl': { + paddingRight: '1.5em', + }, + '.ql-editor ol li': { + counterReset: + 'list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9', + counterIncrement: 'list-0', + }, + '.ql-editor ol li:before': { + content: "counter(list-0, decimal) '. '", + }, + '.ql-editor ol li.ql-indent-1': { + counterIncrement: 'list-1', + counterReset: 'list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9', + }, + '.ql-editor ol li.ql-indent-1:before': { + content: "counter(list-1, lower-alpha) '. '", + }, + '.ql-editor ol li.ql-indent-2': { + counterIncrement: 'list-2', + counterReset: 'list-3 list-4 list-5 list-6 list-7 list-8 list-9', + }, + '.ql-editor ol li.ql-indent-2:before': { + content: "counter(list-2, lower-roman) '. '", + }, + '.ql-editor ol li.ql-indent-3': { + counterIncrement: 'list-3', + counterReset: 'list-4 list-5 list-6 list-7 list-8 list-9', + }, + '.ql-editor ol li.ql-indent-3:before': { + content: "counter(list-3, decimal) '. '", + }, + '.ql-editor ol li.ql-indent-4': { + counterIncrement: 'list-4', + counterReset: 'list-5 list-6 list-7 list-8 list-9', + }, + '.ql-editor ol li.ql-indent-4:before': { + content: "counter(list-4, lower-alpha) '. '", + }, + '.ql-editor ol li.ql-indent-5': { + counterIncrement: 'list-5', + counterReset: 'list-6 list-7 list-8 list-9', + }, + '.ql-editor ol li.ql-indent-5:before': { + content: "counter(list-5, lower-roman) '. '", + }, + '.ql-editor ol li.ql-indent-6': { + counterIncrement: 'list-6', + counterReset: 'list-7 list-8 list-9', + }, + '.ql-editor ol li.ql-indent-6:before': { + content: "counter(list-6, decimal) '. '", + }, + '.ql-editor ol li.ql-indent-7': { + counterIncrement: 'list-7', + counterReset: 'list-8 list-9', + }, + '.ql-editor ol li.ql-indent-7:before': { + content: "counter(list-7, lower-alpha) '. '", + }, + '.ql-editor ol li.ql-indent-8': { + counterIncrement: 'list-8', + counterReset: 'list-9', + }, + '.ql-editor ol li.ql-indent-8:before': { + content: "counter(list-8, lower-roman) '. '", + }, + '.ql-editor ol li.ql-indent-9': { + counterIncrement: 'list-9', + }, + '.ql-editor ol li.ql-indent-9:before': { + content: "counter(list-9, decimal) '. '", + }, + '.ql-editor .ql-indent-1:not(.ql-direction-rtl)': { + paddingLeft: '3em', + }, + '.ql-editor li.ql-indent-1:not(.ql-direction-rtl)': { + paddingLeft: '4.5em', + }, + '.ql-editor .ql-indent-1.ql-direction-rtl.ql-align-right': { + paddingRight: '3em', + }, + '.ql-editor li.ql-indent-1.ql-direction-rtl.ql-align-right': { + paddingRight: '4.5em', + }, + '.ql-editor .ql-indent-2:not(.ql-direction-rtl)': { + paddingLeft: '6em', + }, + '.ql-editor li.ql-indent-2:not(.ql-direction-rtl)': { + paddingLeft: '7.5em', + }, + '.ql-editor .ql-indent-2.ql-direction-rtl.ql-align-right': { + paddingRight: '6em', + }, + '.ql-editor li.ql-indent-2.ql-direction-rtl.ql-align-right': { + paddingRight: '7.5em', + }, + '.ql-editor .ql-indent-3:not(.ql-direction-rtl)': { + paddingLeft: '9em', + }, + '.ql-editor li.ql-indent-3:not(.ql-direction-rtl)': { + paddingLeft: '10.5em', + }, + '.ql-editor .ql-indent-3.ql-direction-rtl.ql-align-right': { + paddingRight: '9em', + }, + '.ql-editor li.ql-indent-3.ql-direction-rtl.ql-align-right': { + paddingRight: '10.5em', + }, + '.ql-editor .ql-indent-4:not(.ql-direction-rtl)': { + paddingLeft: '12em', + }, + '.ql-editor li.ql-indent-4:not(.ql-direction-rtl)': { + paddingLeft: '13.5em', + }, + '.ql-editor .ql-indent-4.ql-direction-rtl.ql-align-right': { + paddingRight: '12em', + }, + '.ql-editor li.ql-indent-4.ql-direction-rtl.ql-align-right': { + paddingRight: '13.5em', + }, + '.ql-editor .ql-indent-5:not(.ql-direction-rtl)': { + paddingLeft: '15em', + }, + '.ql-editor li.ql-indent-5:not(.ql-direction-rtl)': { + paddingLeft: '16.5em', + }, + '.ql-editor .ql-indent-5.ql-direction-rtl.ql-align-right': { + paddingRight: '15em', + }, + '.ql-editor li.ql-indent-5.ql-direction-rtl.ql-align-right': { + paddingRight: '16.5em', + }, + '.ql-editor .ql-indent-6:not(.ql-direction-rtl)': { + paddingLeft: '18em', + }, + '.ql-editor li.ql-indent-6:not(.ql-direction-rtl)': { + paddingLeft: '19.5em', + }, + '.ql-editor .ql-indent-6.ql-direction-rtl.ql-align-right': { + paddingRight: '18em', + }, + '.ql-editor li.ql-indent-6.ql-direction-rtl.ql-align-right': { + paddingRight: '19.5em', + }, + '.ql-editor .ql-indent-7:not(.ql-direction-rtl)': { + paddingLeft: '21em', + }, + '.ql-editor li.ql-indent-7:not(.ql-direction-rtl)': { + paddingLeft: '22.5em', + }, + '.ql-editor .ql-indent-7.ql-direction-rtl.ql-align-right': { + paddingRight: '21em', + }, + '.ql-editor li.ql-indent-7.ql-direction-rtl.ql-align-right': { + paddingRight: '22.5em', + }, + '.ql-editor .ql-indent-8:not(.ql-direction-rtl)': { + paddingLeft: '24em', + }, + '.ql-editor li.ql-indent-8:not(.ql-direction-rtl)': { + paddingLeft: '25.5em', + }, + '.ql-editor .ql-indent-8.ql-direction-rtl.ql-align-right': { + paddingRight: '24em', + }, + '.ql-editor li.ql-indent-8.ql-direction-rtl.ql-align-right': { + paddingRight: '25.5em', + }, + '.ql-editor .ql-indent-9:not(.ql-direction-rtl)': { + paddingLeft: '27em', + }, + '.ql-editor li.ql-indent-9:not(.ql-direction-rtl)': { + paddingLeft: '28.5em', + }, + '.ql-editor .ql-indent-9.ql-direction-rtl.ql-align-right': { + paddingRight: '27em', + }, + '.ql-editor li.ql-indent-9.ql-direction-rtl.ql-align-right': { + paddingRight: '28.5em', + }, + '.ql-editor .ql-video': { + display: 'block', + maxWidth: '100%', + }, + '.ql-editor .ql-video.ql-align-center': { + margin: '0 auto', + }, + '.ql-editor .ql-video.ql-align-right': { + margin: '0 0 0 auto', + }, + '.ql-editor .ql-bg-black': { + backgroundColor: '#000', + }, + '.ql-editor .ql-bg-red': { + backgroundColor: '#e60000', + }, + '.ql-editor .ql-bg-orange': { + backgroundColor: '#f90', + }, + '.ql-editor .ql-bg-yellow': { + backgroundColor: '#ff0', + }, + '.ql-editor .ql-bg-green': { + backgroundColor: '#008a00', + }, + '.ql-editor .ql-bg-blue': { + backgroundColor: '#06c', + }, + '.ql-editor .ql-bg-purple': { + backgroundColor: '#93f', + }, + '.ql-editor .ql-color-white': { + color: '#fff', + }, + '.ql-editor .ql-color-red': { + color: '#e60000', + }, + '.ql-editor .ql-color-orange': { + color: '#f90', + }, + '.ql-editor .ql-color-yellow': { + color: '#ff0', + }, + '.ql-editor .ql-color-green': { + color: '#008a00', + }, + '.ql-editor .ql-color-blue': { + color: '#06c', + }, + '.ql-editor .ql-color-purple': { + color: '#93f', + }, + '.ql-editor .ql-font-serif': { + fontFamily: 'Georgia, Times New Roman, serif', + }, + '.ql-editor .ql-font-monospace': { + fontFamily: 'Monaco, Courier New, monospace', + }, + '.ql-editor .ql-size-small': { + fontSize: '0.75em', + }, + '.ql-editor .ql-size-large': { + fontSize: '1.5em', + }, + '.ql-editor .ql-size-huge': { + fontSize: '2.5em', + }, + '.ql-editor .ql-direction-rtl': { + direction: 'rtl', + textAlign: 'inherit', + }, + '.ql-editor .ql-align-center': { + textAlign: 'center', + }, + '.ql-editor .ql-align-justify': { + textAlign: 'justify', + }, + '.ql-editor .ql-align-right': { + textAlign: 'right', + }, + '.ql-editor.ql-blank::before': { + color: 'rgba(0,0,0,0.6)', + content: 'attr(data-placeholder)', + fontStyle: 'italic', + left: 15, + pointerEvents: 'none', + position: 'absolute', + right: 15, + }, + '.ql-bubble.ql-toolbar:after, .ql-bubble .ql-toolbar:after': { + clear: 'both', + content: "''", + display: 'table', + }, + '.ql-bubble.ql-toolbar button, .ql-bubble .ql-toolbar button': { + background: 'none', + border: 'none', + cursor: 'pointer', + display: 'inline-block', + float: 'left', + height: 24, + padding: '3px 5px', + width: 28, + }, + '.ql-bubble.ql-toolbar button svg, .ql-bubble .ql-toolbar button svg': { + float: 'left', + height: '100%', + }, + '.ql-bubble.ql-toolbar button:active:hover, .ql-bubble .ql-toolbar button:active:hover': { + outline: 'none', + }, + '.ql-bubble.ql-toolbar input.ql-image[type=file], .ql-bubble .ql-toolbar input.ql-image[type=file]': { + display: 'none', + }, + '.ql-bubble.ql-toolbar button:hover, .ql-bubble .ql-toolbar button:hover, .ql-bubble.ql-toolbar button:focus, .ql-bubble .ql-toolbar button:focus, .ql-bubble.ql-toolbar button.ql-active, .ql-bubble .ql-toolbar button.ql-active, .ql-bubble.ql-toolbar .ql-picker-label:hover, .ql-bubble .ql-toolbar .ql-picker-label:hover, .ql-bubble.ql-toolbar .ql-picker-label.ql-active, .ql-bubble .ql-toolbar .ql-picker-label.ql-active, .ql-bubble.ql-toolbar .ql-picker-item:hover, .ql-bubble .ql-toolbar .ql-picker-item:hover, .ql-bubble.ql-toolbar .ql-picker-item.ql-selected, .ql-bubble .ql-toolbar .ql-picker-item.ql-selected': { + color: '#fff', + }, + '.ql-bubble.ql-toolbar button:hover .ql-fill, .ql-bubble .ql-toolbar button:hover .ql-fill, .ql-bubble.ql-toolbar button:focus .ql-fill, .ql-bubble .ql-toolbar button:focus .ql-fill, .ql-bubble.ql-toolbar button.ql-active .ql-fill, .ql-bubble .ql-toolbar button.ql-active .ql-fill, .ql-bubble.ql-toolbar .ql-picker-label:hover .ql-fill, .ql-bubble .ql-toolbar .ql-picker-label:hover .ql-fill, .ql-bubble.ql-toolbar .ql-picker-label.ql-active .ql-fill, .ql-bubble .ql-toolbar .ql-picker-label.ql-active .ql-fill, .ql-bubble.ql-toolbar .ql-picker-item:hover .ql-fill, .ql-bubble .ql-toolbar .ql-picker-item:hover .ql-fill, .ql-bubble.ql-toolbar .ql-picker-item.ql-selected .ql-fill, .ql-bubble .ql-toolbar .ql-picker-item.ql-selected .ql-fill, .ql-bubble.ql-toolbar button:hover .ql-stroke.ql-fill, .ql-bubble .ql-toolbar button:hover .ql-stroke.ql-fill, .ql-bubble.ql-toolbar button:focus .ql-stroke.ql-fill, .ql-bubble .ql-toolbar button:focus .ql-stroke.ql-fill, .ql-bubble.ql-toolbar button.ql-active .ql-stroke.ql-fill, .ql-bubble .ql-toolbar button.ql-active .ql-stroke.ql-fill, .ql-bubble.ql-toolbar .ql-picker-label:hover .ql-stroke.ql-fill, .ql-bubble .ql-toolbar .ql-picker-label:hover .ql-stroke.ql-fill, .ql-bubble.ql-toolbar .ql-picker-label.ql-active .ql-stroke.ql-fill, .ql-bubble .ql-toolbar .ql-picker-label.ql-active .ql-stroke.ql-fill, .ql-bubble.ql-toolbar .ql-picker-item:hover .ql-stroke.ql-fill, .ql-bubble .ql-toolbar .ql-picker-item:hover .ql-stroke.ql-fill, .ql-bubble.ql-toolbar .ql-picker-item.ql-selected .ql-stroke.ql-fill, .ql-bubble .ql-toolbar .ql-picker-item.ql-selected .ql-stroke.ql-fill': { + fill: '#fff', + }, + '.ql-bubble.ql-toolbar button:hover .ql-stroke, .ql-bubble .ql-toolbar button:hover .ql-stroke, .ql-bubble.ql-toolbar button:focus .ql-stroke, .ql-bubble .ql-toolbar button:focus .ql-stroke, .ql-bubble.ql-toolbar button.ql-active .ql-stroke, .ql-bubble .ql-toolbar button.ql-active .ql-stroke, .ql-bubble.ql-toolbar .ql-picker-label:hover .ql-stroke, .ql-bubble .ql-toolbar .ql-picker-label:hover .ql-stroke, .ql-bubble.ql-toolbar .ql-picker-label.ql-active .ql-stroke, .ql-bubble .ql-toolbar .ql-picker-label.ql-active .ql-stroke, .ql-bubble.ql-toolbar .ql-picker-item:hover .ql-stroke, .ql-bubble .ql-toolbar .ql-picker-item:hover .ql-stroke, .ql-bubble.ql-toolbar .ql-picker-item.ql-selected .ql-stroke, .ql-bubble .ql-toolbar .ql-picker-item.ql-selected .ql-stroke, .ql-bubble.ql-toolbar button:hover .ql-stroke-miter, .ql-bubble .ql-toolbar button:hover .ql-stroke-miter, .ql-bubble.ql-toolbar button:focus .ql-stroke-miter, .ql-bubble .ql-toolbar button:focus .ql-stroke-miter, .ql-bubble.ql-toolbar button.ql-active .ql-stroke-miter, .ql-bubble .ql-toolbar button.ql-active .ql-stroke-miter, .ql-bubble.ql-toolbar .ql-picker-label:hover .ql-stroke-miter, .ql-bubble .ql-toolbar .ql-picker-label:hover .ql-stroke-miter, .ql-bubble.ql-toolbar .ql-picker-label.ql-active .ql-stroke-miter, .ql-bubble .ql-toolbar .ql-picker-label.ql-active .ql-stroke-miter, .ql-bubble.ql-toolbar .ql-picker-item:hover .ql-stroke-miter, .ql-bubble .ql-toolbar .ql-picker-item:hover .ql-stroke-miter, .ql-bubble.ql-toolbar .ql-picker-item.ql-selected .ql-stroke-miter, .ql-bubble .ql-toolbar .ql-picker-item.ql-selected .ql-stroke-miter': { + stroke: '#fff', + }, + '@media (pointer: coarse)': { + '.ql-bubble.ql-toolbar button:hover:not(.ql-active), .ql-bubble .ql-toolbar button:hover:not(.ql-active)': { color: '#ccc', - content: '"D7"', - fontSize: 16, - fontWeight: 'bold', - }, - '.ql-container.ql-bubble:not(.ql-disabled) a': { - position: 'relative', - whiteSpace: 'nowrap', }, - '.ql-container.ql-bubble:not(.ql-disabled) a::before': { - backgroundColor: '#444', - borderRadius: 15, - top: -5, - fontSize: 12, - color: '#fff', - content: 'attr(href)', - fontWeight: 'normal', - overflow: 'hidden', - padding: '5px 15px', - textDecoration: 'none', - zIndex: 1, - }, - '.ql-container.ql-bubble:not(.ql-disabled) a::after': { - borderTop: '6px solid #444', - borderLeft: '6px solid transparent', - borderRight: '6px solid transparent', - top: '0', - content: '" "', - height: '0', - width: '0', - }, - '.ql-container.ql-bubble:not(.ql-disabled) a::before, .ql-container.ql-bubble:not(.ql-disabled) a::after': { - left: '0', - marginLeft: '50%', - position: 'absolute', - transform: 'translate(-50%, -100%)', - transition: 'visibility 0s ease 200ms', - visibility: 'hidden', + '.ql-bubble.ql-toolbar button:hover:not(.ql-active) .ql-fill, .ql-bubble .ql-toolbar button:hover:not(.ql-active) .ql-fill, .ql-bubble.ql-toolbar button:hover:not(.ql-active) .ql-stroke.ql-fill, .ql-bubble .ql-toolbar button:hover:not(.ql-active) .ql-stroke.ql-fill': { + fill: '#ccc', }, - '.ql-container.ql-bubble:not(.ql-disabled) a:hover::before, .ql-container.ql-bubble:not(.ql-disabled) a:hover::after': { - visibility: 'visible', + '.ql-bubble.ql-toolbar button:hover:not(.ql-active) .ql-stroke, .ql-bubble .ql-toolbar button:hover:not(.ql-active) .ql-stroke, .ql-bubble.ql-toolbar button:hover:not(.ql-active) .ql-stroke-miter, .ql-bubble .ql-toolbar button:hover:not(.ql-active) .ql-stroke-miter': { + stroke: '#ccc', }, }, -} as StyleRules; + '.ql-bubble': { + boxSizing: 'border-box', + }, + '.ql-bubble *': { + boxSizing: 'border-box', + }, + '.ql-bubble .ql-hidden': { + display: 'none', + }, + '.ql-bubble .ql-out-bottom, .ql-bubble .ql-out-top': { + visibility: 'hidden', + }, + '.ql-bubble .ql-tooltip': { + position: 'absolute', + transform: 'translateY(10px)', + backgroundColor: '#444', + borderRadius: 25, + color: '#fff', + }, + '.ql-bubble .ql-tooltip a': { + cursor: 'pointer', + textDecoration: 'none', + }, + '.ql-bubble .ql-tooltip.ql-flip': { + transform: 'translateY(-10px)', + }, + '.ql-bubble .ql-formats': { + display: 'inline-block', + verticalAlign: 'middle', + }, + '.ql-bubble .ql-formats:after': { + clear: 'both', + content: "''", + display: 'table', + }, + '.ql-bubble .ql-stroke': { + fill: 'none', + stroke: '#ccc', + strokeLinecap: 'round', + strokeLinejoin: 'round', + strokeWidth: '2', + }, + '.ql-bubble .ql-stroke-miter': { + fill: 'none', + stroke: '#ccc', + strokeMiterlimit: 10, + strokeWidth: '2', + }, + '.ql-bubble .ql-fill, .ql-bubble .ql-stroke.ql-fill': { + fill: '#ccc', + }, + '.ql-bubble .ql-empty': { + fill: 'none', + }, + '.ql-bubble .ql-even': { + fillRule: 'evenodd', + }, + '.ql-bubble .ql-thin, .ql-bubble .ql-stroke.ql-thin': { + strokeWidth: '1', + }, + '.ql-bubble .ql-transparent': { + opacity: 0.4, + }, + '.ql-bubble .ql-direction svg:last-child': { + display: 'none', + }, + '.ql-bubble .ql-direction.ql-active svg:last-child': { + display: 'inline', + }, + '.ql-bubble .ql-direction.ql-active svg:first-child': { + display: 'none', + }, + '.ql-bubble .ql-editor h1': { + fontSize: '2em', + }, + '.ql-bubble .ql-editor h2': { + fontSize: '1.5em', + }, + '.ql-bubble .ql-editor h3': { + fontSize: '1.17em', + }, + '.ql-bubble .ql-editor h4': { + fontSize: '1em', + }, + '.ql-bubble .ql-editor h5': { + fontSize: '0.83em', + }, + '.ql-bubble .ql-editor h6': { + fontSize: '0.67em', + }, + '.ql-bubble .ql-editor a': { + textDecoration: 'underline', + }, + '.ql-bubble .ql-editor blockquote': { + borderLeft: '4px solid #ccc', + marginBottom: 5, + marginTop: 5, + paddingLeft: 16, + }, + '.ql-bubble .ql-editor code, .ql-bubble .ql-editor pre': { + backgroundColor: '#f0f0f0', + borderRadius: 3, + }, + '.ql-bubble .ql-editor pre': { + whiteSpace: 'pre-wrap', + marginBottom: 5, + marginTop: 5, + padding: '5px 10px', + }, + '.ql-bubble .ql-editor code': { + fontSize: '85%', + padding: '2px 4px', + }, + '.ql-bubble .ql-editor pre.ql-syntax': { + backgroundColor: '#23241f', + color: '#f8f8f2', + overflow: 'visible', + }, + '.ql-bubble .ql-editor img': { + maxWidth: '100%', + }, + '.ql-bubble .ql-picker': { + color: '#ccc', + display: 'inline-block', + float: 'left', + fontSize: 14, + fontWeight: 500, + height: 24, + position: 'relative', + verticalAlign: 'middle', + }, + '.ql-bubble .ql-picker-label': { + cursor: 'pointer', + display: 'inline-block', + height: '100%', + paddingLeft: 8, + paddingRight: 2, + position: 'relative', + width: '100%', + }, + '.ql-bubble .ql-picker-label::before': { + display: 'inline-block', + lineHeight: 22, + }, + '.ql-bubble .ql-picker-options': { + backgroundColor: '#444', + display: 'none', + minWidth: '100%', + padding: '4px 8px', + position: 'absolute', + whiteSpace: 'nowrap', + }, + '.ql-bubble .ql-picker-options .ql-picker-item': { + cursor: 'pointer', + display: 'block', + paddingBottom: 5, + paddingTop: 5, + }, + '.ql-bubble .ql-picker.ql-expanded .ql-picker-label': { + color: '#777', + zIndex: 2, + }, + '.ql-bubble .ql-picker.ql-expanded .ql-picker-label .ql-fill': { + fill: '#777', + }, + '.ql-bubble .ql-picker.ql-expanded .ql-picker-label .ql-stroke': { + stroke: '#777', + }, + '.ql-bubble .ql-picker.ql-expanded .ql-picker-options': { + display: 'block', + marginTop: -1, + top: '100%', + zIndex: 1, + }, + '.ql-bubble .ql-color-picker, .ql-bubble .ql-icon-picker': { + width: 28, + }, + '.ql-bubble .ql-color-picker .ql-picker-label, .ql-bubble .ql-icon-picker .ql-picker-label': { + padding: '2px 4px', + }, + '.ql-bubble .ql-color-picker .ql-picker-label svg, .ql-bubble .ql-icon-picker .ql-picker-label svg': { + right: 4, + }, + '.ql-bubble .ql-icon-picker .ql-picker-options': { + padding: '4px 0px', + }, + '.ql-bubble .ql-icon-picker .ql-picker-item': { + height: 24, + width: 24, + padding: '2px 4px', + }, + '.ql-bubble .ql-color-picker .ql-picker-options': { + padding: '3px 5px', + width: 152, + }, + '.ql-bubble .ql-color-picker .ql-picker-item': { + border: '1px solid transparent', + float: 'left', + height: 16, + margin: 2, + padding: 0, + width: 16, + }, + '.ql-bubble .ql-picker:not(.ql-color-picker):not(.ql-icon-picker) svg': { + position: 'absolute', + marginTop: -9, + right: '0', + top: '50%', + width: 18, + }, + ".ql-bubble .ql-picker.ql-header .ql-picker-label[data-label]:not([data-label=''])::before, .ql-bubble .ql-picker.ql-font .ql-picker-label[data-label]:not([data-label=''])::before, .ql-bubble .ql-picker.ql-size .ql-picker-label[data-label]:not([data-label=''])::before, .ql-bubble .ql-picker.ql-header .ql-picker-item[data-label]:not([data-label=''])::before, .ql-bubble .ql-picker.ql-font .ql-picker-item[data-label]:not([data-label=''])::before, .ql-bubble .ql-picker.ql-size .ql-picker-item[data-label]:not([data-label=''])::before": { + content: 'attr(data-label)', + }, + '.ql-bubble .ql-picker.ql-header': { + width: 98, + }, + '.ql-bubble .ql-picker.ql-header .ql-picker-label::before, .ql-bubble .ql-picker.ql-header .ql-picker-item::before': { + content: "'Normal'", + }, + '.ql-bubble .ql-picker.ql-header .ql-picker-label[data-value="1"]::before, .ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="1"]::before': { + content: "'Heading 1'", + }, + '.ql-bubble .ql-picker.ql-header .ql-picker-label[data-value="2"]::before, .ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="2"]::before': { + content: "'Heading 2'", + }, + '.ql-bubble .ql-picker.ql-header .ql-picker-label[data-value="3"]::before, .ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="3"]::before': { + content: "'Heading 3'", + }, + '.ql-bubble .ql-picker.ql-header .ql-picker-label[data-value="4"]::before, .ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="4"]::before': { + content: "'Heading 4'", + }, + '.ql-bubble .ql-picker.ql-header .ql-picker-label[data-value="5"]::before, .ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="5"]::before': { + content: "'Heading 5'", + }, + '.ql-bubble .ql-picker.ql-header .ql-picker-label[data-value="6"]::before, .ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="6"]::before': { + content: "'Heading 6'", + }, + '.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="1"]::before': { + fontSize: '2em', + }, + '.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="2"]::before': { + fontSize: '1.5em', + }, + '.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="3"]::before': { + fontSize: '1.17em', + }, + '.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="4"]::before': { + fontSize: '1em', + }, + '.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="5"]::before': { + fontSize: '0.83em', + }, + '.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="6"]::before': { + fontSize: '0.67em', + }, + '.ql-bubble .ql-picker.ql-font': { + width: 108, + }, + '.ql-bubble .ql-picker.ql-font .ql-picker-label::before, .ql-bubble .ql-picker.ql-font .ql-picker-item::before': { + content: "'Sans Serif'", + }, + '.ql-bubble .ql-picker.ql-font .ql-picker-label[data-value=serif]::before, .ql-bubble .ql-picker.ql-font .ql-picker-item[data-value=serif]::before': { + content: "'Serif'", + }, + '.ql-bubble .ql-picker.ql-font .ql-picker-label[data-value=monospace]::before, .ql-bubble .ql-picker.ql-font .ql-picker-item[data-value=monospace]::before': { + content: "'Monospace'", + }, + '.ql-bubble .ql-picker.ql-font .ql-picker-item[data-value=serif]::before': { + fontFamily: 'Georgia, Times New Roman, serif', + }, + '.ql-bubble .ql-picker.ql-font .ql-picker-item[data-value=monospace]::before': { + fontFamily: 'Monaco, Courier New, monospace', + }, + '.ql-bubble .ql-picker.ql-size': { + width: 98, + }, + '.ql-bubble .ql-picker.ql-size .ql-picker-label::before, .ql-bubble .ql-picker.ql-size .ql-picker-item::before': { + content: "'Normal'", + }, + '.ql-bubble .ql-picker.ql-size .ql-picker-label[data-value=small]::before, .ql-bubble .ql-picker.ql-size .ql-picker-item[data-value=small]::before': { + content: "'Small'", + }, + '.ql-bubble .ql-picker.ql-size .ql-picker-label[data-value=large]::before, .ql-bubble .ql-picker.ql-size .ql-picker-item[data-value=large]::before': { + content: "'Large'", + }, + '.ql-bubble .ql-picker.ql-size .ql-picker-label[data-value=huge]::before, .ql-bubble .ql-picker.ql-size .ql-picker-item[data-value=huge]::before': { + content: "'Huge'", + }, + '.ql-bubble .ql-picker.ql-size .ql-picker-item[data-value=small]::before': { + fontSize: 10, + }, + '.ql-bubble .ql-picker.ql-size .ql-picker-item[data-value=large]::before': { + fontSize: 18, + }, + '.ql-bubble .ql-picker.ql-size .ql-picker-item[data-value=huge]::before': { + fontSize: 32, + }, + '.ql-bubble .ql-color-picker.ql-background .ql-picker-item': { + backgroundColor: '#fff', + }, + '.ql-bubble .ql-color-picker.ql-color .ql-picker-item': { + backgroundColor: '#000', + }, + '.ql-bubble .ql-toolbar .ql-formats': { + margin: '8px 12px 8px 0px', + }, + '.ql-bubble .ql-toolbar .ql-formats:first-child': { + marginLeft: 12, + }, + '.ql-bubble .ql-color-picker svg': { + margin: 1, + }, + '.ql-bubble .ql-color-picker .ql-picker-item.ql-selected, .ql-bubble .ql-color-picker .ql-picker-item:hover': { + borderColor: '#fff', + }, + '.ql-bubble .ql-tooltip-arrow': { + borderLeft: '6px solid transparent', + borderRight: '6px solid transparent', + content: '" "', + display: 'block', + left: '50%', + marginLeft: -6, + position: 'absolute', + }, + '.ql-bubble .ql-tooltip:not(.ql-flip) .ql-tooltip-arrow': { + borderBottom: '6px solid #444', + top: -6, + }, + '.ql-bubble .ql-tooltip.ql-flip .ql-tooltip-arrow': { + borderTop: '6px solid #444', + bottom: -6, + }, + '.ql-bubble .ql-tooltip.ql-editing .ql-tooltip-editor': { + display: 'block', + }, + '.ql-bubble .ql-tooltip.ql-editing .ql-formats': { + visibility: 'hidden', + }, + '.ql-bubble .ql-tooltip-editor': { + display: 'none', + }, + '.ql-bubble .ql-tooltip-editor input[type=text]': { + background: 'transparent', + border: 'none', + color: '#fff', + fontSize: 13, + height: '100%', + outline: 'none', + padding: '10px 20px', + position: 'absolute', + width: '100%', + }, + '.ql-bubble .ql-tooltip-editor a': { + top: 10, + position: 'absolute', + right: 20, + }, + '.ql-bubble .ql-tooltip-editor a:before': { + color: '#ccc', + content: '"D7"', + fontSize: 16, + fontWeight: 'bold', + }, + '.ql-container.ql-bubble:not(.ql-disabled) a': { + position: 'relative', + whiteSpace: 'nowrap', + }, + '.ql-container.ql-bubble:not(.ql-disabled) a::before': { + backgroundColor: '#444', + borderRadius: 15, + top: -5, + fontSize: 12, + color: '#fff', + content: 'attr(href)', + fontWeight: 'normal', + overflow: 'hidden', + padding: '5px 15px', + textDecoration: 'none', + zIndex: 1, + }, + '.ql-container.ql-bubble:not(.ql-disabled) a::after': { + borderTop: '6px solid #444', + borderLeft: '6px solid transparent', + borderRight: '6px solid transparent', + top: '0', + content: '" "', + height: '0', + width: '0', + }, + '.ql-container.ql-bubble:not(.ql-disabled) a::before, .ql-container.ql-bubble:not(.ql-disabled) a::after': { + left: '0', + marginLeft: '50%', + position: 'absolute', + transform: 'translate(-50%, -100%)', + transition: 'visibility 0s ease 200ms', + visibility: 'hidden', + }, + '.ql-container.ql-bubble:not(.ql-disabled) a:hover::before, .ql-container.ql-bubble:not(.ql-disabled) a:hover::after': { + visibility: 'visible', + }, +}; diff --git a/packages/ra-input-rich-text/src/QuillSnowStylesheet.ts b/packages/ra-input-rich-text/src/QuillSnowStylesheet.ts index e31ed6184d3..db398791254 100644 --- a/packages/ra-input-rich-text/src/QuillSnowStylesheet.ts +++ b/packages/ra-input-rich-text/src/QuillSnowStylesheet.ts @@ -1,5 +1,3 @@ -import { StyleRules } from '@material-ui/core/styles'; - // converted from vendor (node_modules/quill/dist/quill.snow.css) using the jss cli export default { '.ql-container': { @@ -799,4 +797,4 @@ export default { '.ql-container.ql-snow': { border: '1px solid #ccc', }, -} as StyleRules; +}; diff --git a/packages/ra-input-rich-text/src/index.tsx b/packages/ra-input-rich-text/src/index.tsx index 43526c8683b..30a36e99810 100644 --- a/packages/ra-input-rich-text/src/index.tsx +++ b/packages/ra-input-rich-text/src/index.tsx @@ -7,44 +7,19 @@ import { FormHelperText, FormControl, InputLabel, - PropTypes as MuiPropTypes, -} from '@material-ui/core'; -import { makeStyles } from '@material-ui/core/styles'; + styled, + GlobalStyles, +} from '@mui/material'; import PropTypes from 'prop-types'; -import styles from './styles'; - -const useStyles = makeStyles(styles, { name: 'RaRichTextInput' }); - -export interface RichTextInputProps { - label?: string | false; - options?: QuillOptionsStatic; - source: string; - toolbar?: - | boolean - | string[] - | Array<any>[] - | string - | { - container: string | string[] | Array<any>[]; - handlers?: Record<string, Function>; - }; - fullWidth?: boolean; - configureQuill?: (instance: Quill) => void; - helperText?: ComponentProps<typeof InputHelperText>['helperText']; - record?: Record<any, any>; - resource?: string; - variant?: string; - margin?: MuiPropTypes.Margin; - [key: string]: any; -} +import { RaRichTextClasses, RaRichTextStyles } from './styles'; +import QuillSnowStylesheet from './QuillSnowStylesheet'; const RichTextInput = (props: RichTextInputProps) => { const { options = {}, // Quill editor options toolbar = true, fullWidth = true, - classes: classesOverride, configureQuill, helperText, label, @@ -54,7 +29,6 @@ const RichTextInput = (props: RichTextInputProps) => { margin = 'dense', ...rest } = props; - const classes = useStyles(props); const quillInstance = useRef<Quill>(); const divRef = useRef<HTMLDivElement>(); const editor = useRef<HTMLElement>(); @@ -125,13 +99,15 @@ const RichTextInput = (props: RichTextInputProps) => { }, [value]); return ( - <FormControl + <StyledFormControl error={!!(touched && error)} fullWidth={fullWidth} - className="ra-rich-text-input" + className={`ra-rich-text-input ${RaRichTextClasses.root}`} margin={margin} > - <InputLabel shrink htmlFor={id} className={classes.label}> + {/* @ts-ignore */} + <GlobalStyles styles={QuillSnowStylesheet} /> + <InputLabel shrink htmlFor={id} className={RaRichTextClasses.label}> <FieldTitle label={label} source={source} @@ -150,10 +126,33 @@ const RichTextInput = (props: RichTextInputProps) => { touched={touched} /> </FormHelperText> - </FormControl> + </StyledFormControl> ); }; +export interface RichTextInputProps { + label?: string | false; + options?: QuillOptionsStatic; + source: string; + toolbar?: + | boolean + | string[] + | Array<any>[] + | string + | { + container: string | string[] | Array<any>[]; + handlers?: Record<string, Function>; + }; + fullWidth?: boolean; + configureQuill?: (instance: Quill) => void; + helperText?: ComponentProps<typeof InputHelperText>['helperText']; + record?: Record<any, any>; + resource?: string; + variant?: string; + margin?: 'normal' | 'none' | 'dense'; + [key: string]: any; +} + RichTextInput.propTypes = { // @ts-ignore label: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), @@ -163,4 +162,6 @@ RichTextInput.propTypes = { configureQuill: PropTypes.func, }; +const StyledFormControl = styled(FormControl)(RaRichTextStyles); + export default RichTextInput; diff --git a/packages/ra-input-rich-text/src/styles.ts b/packages/ra-input-rich-text/src/styles.ts index d649c9c0a8a..db8a73d33dd 100644 --- a/packages/ra-input-rich-text/src/styles.ts +++ b/packages/ra-input-rich-text/src/styles.ts @@ -1,169 +1,170 @@ -import { Theme, StyleRules } from '@material-ui/core/styles'; +import { Theme } from '@mui/material/styles'; -import QuillSnowStylesheet from './QuillSnowStylesheet'; +const PREFIX = 'RaRichTextInput'; -export default (theme: Theme): StyleRules<string, any> => ({ +export const RaRichTextClasses = { + root: `${PREFIX}-root`, + label: `${PREFIX}-label`, +}; + +export const RaRichTextStyles = ({ theme }: { theme?: Theme }) => ({ label: { position: 'relative', }, - '@global': { - ...QuillSnowStylesheet, - '.ra-rich-text-input': { - '& .ql-editor': { - fontSize: '1rem', - fontFamily: 'Roboto, sans-serif', - padding: '6px 12px', + root: { + '& .ql-editor': { + fontSize: '1rem', + fontFamily: 'Roboto, sans-serif', + padding: '6px 12px', + backgroundColor: + theme.palette.mode === 'dark' + ? 'rgba(255, 255, 255, 0.04)' + : 'rgba(0, 0, 0, 0.04)', + '&:hover::before': { backgroundColor: - theme.palette.type === 'dark' - ? 'rgba(255, 255, 255, 0.04)' - : 'rgba(0, 0, 0, 0.04)', - '&:hover::before': { - backgroundColor: - theme.palette.type === 'dark' - ? 'rgba(255, 255, 255, 1)' - : 'rgba(0, 0, 0, 1)', - height: 2, - }, - - '&::before': { - left: 0, - right: 0, - bottom: 0, - height: 1, - content: '""', - position: 'absolute', - transition: - 'background-color 200ms cubic-bezier(0.4, 0, 0.2, 1) 0ms', - backgroundColor: - theme.palette.type === 'dark' - ? 'rgba(255, 255, 255, 0.7)' - : 'rgba(0, 0, 0, 0.5)', - }, - - '&::after': { - left: 0, - right: 0, - bottom: 0, - height: 2, - content: '""', - position: 'absolute', - transform: 'scaleX(0)', - transition: - 'transform 200ms cubic-bezier(0, 0, 0.2, 1) 0ms', - backgroundColor: theme.palette.primary.main, - }, - - '& p:not(:last-child)': { - marginBottom: '1rem', - }, - - '& strong': { - fontWeight: 700, - }, - '& h1': { - margin: '1rem 0 0.5rem 0', - fontWeight: 500, - }, - '& h2': { - margin: '1rem 0 0.5rem 0', - fontWeight: 500, - }, - '& h3': { - margin: '1rem 0 0.5rem 0', - fontWeight: 500, - }, - '& a': { - color: theme.palette.primary.main, - }, - '& ul': { - marginBottom: '1rem', - }, - - '& li:not(.ql-direction-rtl)::before': { - fontSize: '0.5rem', - position: 'relative', - top: '-0.2rem', - marginRight: '0.5rem', - }, - - '&:focus::after': { - transform: 'scaleX(1)', - }, - }, - '& .standard .ql-editor': { - backgroundColor: theme.palette.background.paper, + theme.palette.mode === 'dark' + ? 'rgba(255, 255, 255, 1)' + : 'rgba(0, 0, 0, 1)', + height: 2, + }, + + '&::before': { + left: 0, + right: 0, + bottom: 0, + height: 1, + content: '""', + position: 'absolute', + transition: + 'background-color 200ms cubic-bezier(0.4, 0, 0.2, 1) 0ms', + backgroundColor: + theme.palette.mode === 'dark' + ? 'rgba(255, 255, 255, 0.7)' + : 'rgba(0, 0, 0, 0.5)', + }, + + '&::after': { + left: 0, + right: 0, + bottom: 0, + height: 2, + content: '""', + position: 'absolute', + transform: 'scaleX(0)', + transition: 'transform 200ms cubic-bezier(0, 0, 0.2, 1) 0ms', + backgroundColor: theme.palette.primary.main, + }, + + '& p:not(:last-child)': { + marginBottom: '1rem', + }, + + '& strong': { + fontWeight: 700, + }, + '& h1': { + margin: '1rem 0 0.5rem 0', + fontWeight: 500, + }, + '& h2': { + margin: '1rem 0 0.5rem 0', + fontWeight: 500, + }, + '& h3': { + margin: '1rem 0 0.5rem 0', + fontWeight: 500, + }, + '& a': { + color: theme.palette.primary.main, + }, + '& ul': { + marginBottom: '1rem', + }, + + '& li:not(.ql-direction-rtl)::before': { + fontSize: '0.5rem', + position: 'relative', + top: '-0.2rem', + marginRight: '0.5rem', + }, + + '&:focus::after': { + transform: 'scaleX(1)', + }, + }, + '& .standard .ql-editor': { + backgroundColor: theme.palette.background.paper, + }, + '& .outlined .ql-editor': { + backgroundColor: theme.palette.background.paper, + }, + '& .ql-toolbar.ql-snow': { + margin: '0.5rem 0', + border: 0, + padding: 0, + + '& .ql-picker-item': { + color: theme.palette.text.primary, + }, + '& .ql-stroke': { + stroke: theme.palette.text.primary, + }, + '& .ql-fill': { + fill: theme.palette.text.primary, + }, + '& .ql-picker-item.ql-active': { + color: theme.palette.primary.main, + }, + '& .ql-picker-item:hover': { + color: theme.palette.primary.main, + }, + '& .ql-picker-item.ql-selected': { + color: theme.palette.primary.main, + }, + '& .ql-picker-label.ql-active': { + color: theme.palette.primary.main, + }, + '& .ql-picker-label.ql-selected': { + color: theme.palette.primary.main, + }, + '& .ql-picker-label:hover': { + color: theme.palette.primary.main, + }, + + '& button:hover .ql-fill': { + fill: theme.palette.primary.main, }, - '& .outlined .ql-editor': { + '& button.ql-active .ql-fill': { + fill: theme.palette.primary.main, + }, + + '& button:hover .ql-stroke': { + stroke: theme.palette.primary.main, + }, + '& button.ql-active .ql-stroke': { + stroke: theme.palette.primary.main, + }, + '& .ql-picker-label:hover .ql-stroke': { + stroke: theme.palette.primary.main, + }, + + '& .ql-picker.ql-expanded .ql-picker-options': { backgroundColor: theme.palette.background.paper, + borderColor: theme.palette.background.paper, }, - '& .ql-toolbar.ql-snow': { - margin: '0.5rem 0', + + '& .ql-snow .ql-picker.ql-expanded .ql-picker-options': { + background: '#fff', + zIndex: 10, + }, + + '& .ql-picker-label': { + paddingLeft: 0, + color: theme.palette.text.primary, + }, + + '& + .ql-container.ql-snow': { border: 0, - padding: 0, - - '& .ql-picker-item': { - color: theme.palette.text.primary, - }, - '& .ql-stroke': { - stroke: theme.palette.text.primary, - }, - '& .ql-fill': { - fill: theme.palette.text.primary, - }, - '& .ql-picker-item.ql-active': { - color: theme.palette.primary.main, - }, - '& .ql-picker-item:hover': { - color: theme.palette.primary.main, - }, - '& .ql-picker-item.ql-selected': { - color: theme.palette.primary.main, - }, - '& .ql-picker-label.ql-active': { - color: theme.palette.primary.main, - }, - '& .ql-picker-label.ql-selected': { - color: theme.palette.primary.main, - }, - '& .ql-picker-label:hover': { - color: theme.palette.primary.main, - }, - - '& button:hover .ql-fill': { - fill: theme.palette.primary.main, - }, - '& button.ql-active .ql-fill': { - fill: theme.palette.primary.main, - }, - - '& button:hover .ql-stroke': { - stroke: theme.palette.primary.main, - }, - '& button.ql-active .ql-stroke': { - stroke: theme.palette.primary.main, - }, - '& .ql-picker-label:hover .ql-stroke': { - stroke: theme.palette.primary.main, - }, - - '& .ql-picker.ql-expanded .ql-picker-options': { - backgroundColor: theme.palette.background.paper, - borderColor: theme.palette.background.paper, - }, - - '& .ql-snow .ql-picker.ql-expanded .ql-picker-options': { - background: '#fff', - zIndex: 10, - }, - - '& .ql-picker-label': { - paddingLeft: 0, - color: theme.palette.text.primary, - }, - - '& + .ql-container.ql-snow': { - border: 0, - }, }, }, }, diff --git a/packages/ra-no-code/src/Admin.spec.tsx b/packages/ra-no-code/src/Admin.spec.tsx index aaac353fd70..6de301b7218 100644 --- a/packages/ra-no-code/src/Admin.spec.tsx +++ b/packages/ra-no-code/src/Admin.spec.tsx @@ -30,7 +30,12 @@ describe('Admin', () => { let file = new File([customers], 'customers.csv', { type: 'text/csv', }); - const { getByLabelText, getByText, getByDisplayValue } = render( + const { + getAllByText, + getByLabelText, + getByText, + getByDisplayValue, + } = render( <ApplicationContext.Provider value={{ application: { name: 'test', created_at: new Date() }, @@ -72,7 +77,9 @@ describe('Admin', () => { getByText('1-10 of 10'); }); - fireEvent.click(getByText('New resource', { selector: 'button *' })); + fireEvent.click( + getAllByText('New resource', { selector: '[role="menuitem"] *' })[0] + ); file = new File([orders1], 'orders.csv', { type: 'text/csv', @@ -101,7 +108,9 @@ describe('Admin', () => { getByText('1-5 of 5'); }); - fireEvent.click(getByText('New resource', { selector: 'button *' })); + fireEvent.click( + getAllByText('New resource', { selector: '[role="menuitem"] *' })[0] + ); file = new File([orders2], 'orders2.csv', { type: 'text/csv', diff --git a/packages/ra-no-code/src/ApplicationsDashboard/ApplicationsDashboard.tsx b/packages/ra-no-code/src/ApplicationsDashboard/ApplicationsDashboard.tsx index 0422b62d5ce..2d597603fe1 100644 --- a/packages/ra-no-code/src/ApplicationsDashboard/ApplicationsDashboard.tsx +++ b/packages/ra-no-code/src/ApplicationsDashboard/ApplicationsDashboard.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { useState } from 'react'; import { Avatar, @@ -9,18 +10,13 @@ import { ListItemAvatar, ListItemText, Typography, -} from '@material-ui/core'; -import { - createMuiTheme, - makeStyles, - ThemeProvider, - unstable_createMuiStrictModeTheme, -} from '@material-ui/core/styles'; +} from '@mui/material'; +import { createTheme, ThemeProvider } from '@mui/material/styles'; import { defaultTheme as RaDefaultTheme, RaThemeOptions, } from 'ra-ui-materialui'; -import FolderIcon from '@material-ui/icons/Folder'; +import FolderIcon from '@mui/icons-material/Folder'; import { Application } from './types'; import { NewApplicationForm } from './NewApplicationForm'; import { @@ -28,10 +24,42 @@ import { storeApplicationsInStorage, } from './applicationStorage'; -const defaultTheme = - process.env.NODE_ENV !== 'production' - ? unstable_createMuiStrictModeTheme(RaDefaultTheme) - : createMuiTheme(RaDefaultTheme); +const PREFIX = 'ApplicationsDashboard'; + +const classes = { + main: `${PREFIX}-main`, + title: `${PREFIX}-title`, + applications: `${PREFIX}-applications`, + logo: `${PREFIX}-logo`, +}; + +const StyledContainer = styled(Container)(({ theme }) => ({ + [`&.${classes.main}`]: { + width: '100vw', + height: '100vh', + display: 'flex', + paddingTop: theme.spacing(4), + flexDirection: 'column', + background: + 'linear-gradient(135deg, #00023b 0%, #00023b 50%, #313264 100%)', + }, + + [`& .${classes.title}`]: { + color: theme.palette.common.white, + marginBottom: theme.spacing(4), + textAlign: 'center', + }, + + [`& .${classes.applications}`]: { + marginTop: theme.spacing(4), + }, + + [`& .${classes.logo}`]: { + height: 100, + }, +})); + +const defaultTheme = createTheme(RaDefaultTheme); export const ApplicationsDashboard = ({ onApplicationSelected, @@ -40,13 +68,12 @@ export const ApplicationsDashboard = ({ onApplicationSelected: any; theme: RaThemeOptions; }) => ( - <ThemeProvider theme={createMuiTheme(theme)}> + <ThemeProvider theme={createTheme(theme)}> <Applications onApplicationSelected={onApplicationSelected} /> </ThemeProvider> ); const Applications = ({ onApplicationSelected }) => { - const classes = useStyles(); const [applications, setApplications] = useState<Application[]>(() => loadApplicationsFromStorage() ); @@ -60,7 +87,8 @@ const Applications = ({ onApplicationSelected }) => { }; return ( - <Container component="main" className={classes.main}> + // @ts-ignore + <StyledContainer component="main" className={classes.main}> <img className={classes.logo} src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDIzLjAuMSwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkNhbHF1ZV8xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4PSIwcHgiIHk9IjBweCIKCSB2aWV3Qm94PSIwIDAgMTMxIDEzMSIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgMTMxIDEzMTsiIHhtbDpzcGFjZT0icHJlc2VydmUiPgo8c3R5bGUgdHlwZT0idGV4dC9jc3MiPgoJLnN0MHtmaWxsOiMyMjI0NTg7fQoJLnN0MXtmaWxsOiM1MTUzN0Q7fQoJLnN0MntmaWxsOiNBM0E0Qjk7fQoJLnN0M3tmaWxsOiMwMDAyM0I7fQoJLnN0NHtmaWxsOiNGRkZGRkY7fQoJLnN0NXtlbmFibGUtYmFja2dyb3VuZDpuZXcgICAgO30KPC9zdHlsZT4KPHRpdGxlPkxvZ29fc29tYnJlX2FpPC90aXRsZT4KPGcgaWQ9IlJlY3RhbmdsZV81NiI+Cgk8Zz4KCQk8cmVjdCB4PSIxOS4xIiB5PSIxOSIgdHJhbnNmb3JtPSJtYXRyaXgoMC41IC0wLjg2NiAwLjg2NiAwLjUgLTIzLjkyMjYgODkuNTQ2KSIgY2xhc3M9InN0MCIgd2lkdGg9IjkyLjkiIGhlaWdodD0iOTIuOSIvPgoJCTxwYXRoIGNsYXNzPSJzdDEiIGQ9Ik04MywxMzAuM0wwLjgsODIuOUw0OC4yLDAuN2w4Mi4yLDQ3LjVMODMsMTMwLjN6IE0zLjUsODIuMWw3OC43LDQ1LjVsNDUuNS03OC43TDQ5LDMuNEwzLjUsODIuMXoiLz4KCTwvZz4KPC9nPgo8ZyBpZD0iUmVjdGFuZ2xlXzU2LTIiPgoJPGc+CgkJPHJlY3QgeD0iMTkiIHk9IjE5LjEiIHRyYW5zZm9ybT0ibWF0cml4KDAuODY2IC0wLjUgMC41IDAuODY2IC0yMy45Nzc3IDQxLjUyNykiIGNsYXNzPSJzdDAiIHdpZHRoPSI5Mi45IiBoZWlnaHQ9IjkyLjkiLz4KCQk8cGF0aCBjbGFzcz0ic3QyIiBkPSJNNDcuOSwxMzFMMCw0OEw4My4xLDBsNDgsODMuMUw0Ny45LDEzMXogTTQuMSw0OS4xbDQ1LDc3LjlsNzcuOS00NUw4Miw0LjFMNC4xLDQ5LjF6Ii8+Cgk8L2c+CjwvZz4KPGcgaWQ9IlJlY3RhbmdsZV81Ni0zIj4KCTxnPgoJCTxyZWN0IHg9IjE5LjEiIHk9IjE5IiBjbGFzcz0ic3QzIiB3aWR0aD0iOTIuOSIgaGVpZ2h0PSI5Mi45Ii8+CgkJPHBhdGggY2xhc3M9InN0NCIgZD0iTTExNC41LDExNC41SDE2LjZWMTYuNWg5Ny45VjExNC41eiBNMjEuNiwxMDkuNWg4Ny45VjIxLjVIMjEuNlYxMDkuNXoiLz4KCTwvZz4KPC9nPgo8ZyBpZD0iUmEiPgoJPGcgY2xhc3M9InN0NSI+CgkJPHBhdGggY2xhc3M9InN0NCIgZD0iTTU5LDg2LjdsLTYuNy0xOS4yaC0xLjJIMzguOXYxOS4yaC01LjZWMzguNWgxOC41YzMuNiwwLDYuMywwLjYsOC4xLDEuOGMxLjgsMS4yLDMsMi44LDMuNSw0LjgKCQkJYzAuNSwyLDAuOCw0LjYsMC44LDcuOGMwLDMuNS0wLjQsNi40LTEuMyw4LjdjLTAuOCwyLjMtMi42LDMuOS01LjMsNC44TDY1LDg2LjdINTl6IE01NS43LDYxLjZjMS4yLTAuNywyLTEuNywyLjQtMwoJCQljMC40LTEuMywwLjYtMy4yLDAuNi01LjZjMC0yLjUtMC4yLTQuMy0wLjUtNS42Yy0wLjMtMS4zLTEuMS0yLjItMi4zLTIuOWMtMS4yLTAuNy0zLTEtNS41LTFIMzguOXYxOS4xSDUwCgkJCUM1Mi41LDYyLjYsNTQuNCw2Mi4zLDU1LjcsNjEuNnoiLz4KCQk8cGF0aCBjbGFzcz0ic3Q0IiBkPSJNNzQuMyw4NWMtMS42LTEuNS0yLjUtNC4yLTIuNS04LjJjMC0yLjcsMC4zLTQuOCwwLjktNi4zYzAuNi0xLjUsMS42LTIuNiwzLTMuM2MxLjQtMC43LDMuNC0xLDYtMQoJCQljMS4zLDAsNS4xLDAuMSwxMS40LDAuM3YtMi40YzAtMi45LTAuMi01LTAuNy02LjJjLTAuNS0xLjItMS4zLTItMi42LTIuNGMtMS4yLTAuMy0zLjMtMC41LTYuMy0wLjVjLTEuMywwLTMsMC4xLTQuOSwwLjIKCQkJYy0yLDAuMS0zLjYsMC4zLTQuOCwwLjV2LTQuM2MzLjMtMC43LDcuMS0xLDExLjQtMWMzLjcsMCw2LjUsMC40LDguNCwxLjJjMS44LDAuOCwzLjEsMi4yLDMuOCw0LjFjMC43LDEuOSwxLDQuNywxLDguNHYyMi41aC00LjgKCQkJbC0wLjMtNWgtMC4zYy0wLjgsMi4yLTIuMiwzLjctNC4xLDQuNGMtMS45LDAuNy00LjEsMS4xLTYuNiwxLjFDNzguNiw4Ny4yLDc2LDg2LjUsNzQuMyw4NXogTTg5LjEsODJjMS4yLTAuNCwyLjItMS4yLDIuOC0yLjQKCQkJYzAuOS0xLjgsMS4zLTQuMywxLjMtNy4zdi0yaC0xMGMtMS43LDAtMywwLjItMy44LDAuNWMtMC44LDAuMy0xLjQsMC45LTEuNywxLjhjLTAuMywwLjktMC41LDIuMi0wLjUsNGMwLDEuOCwwLjIsMy4xLDAuNiwzLjkKCQkJYzAuNCwwLjgsMS4xLDEuNCwyLDEuOGMxLDAuMywyLjUsMC41LDQuNSwwLjVDODYuMiw4Mi42LDg3LjgsODIuNCw4OS4xLDgyeiIvPgoJPC9nPgo8L2c+Cjwvc3ZnPgo=" @@ -100,29 +128,6 @@ const Applications = ({ onApplicationSelected }) => { </List> </Card> )} - </Container> + </StyledContainer> ); }; - -const useStyles = makeStyles(theme => ({ - main: { - width: '100vw', - height: '100vh', - display: 'flex', - paddingTop: theme.spacing(4), - flexDirection: 'column', - background: - 'linear-gradient(135deg, #00023b 0%, #00023b 50%, #313264 100%)', - }, - title: { - color: theme.palette.common.white, - marginBottom: theme.spacing(4), - textAlign: 'center', - }, - applications: { - marginTop: theme.spacing(4), - }, - logo: { - height: 100, - }, -})); diff --git a/packages/ra-no-code/src/ApplicationsDashboard/NewApplicationForm.tsx b/packages/ra-no-code/src/ApplicationsDashboard/NewApplicationForm.tsx index 2ab6d214921..be35c699312 100644 --- a/packages/ra-no-code/src/ApplicationsDashboard/NewApplicationForm.tsx +++ b/packages/ra-no-code/src/ApplicationsDashboard/NewApplicationForm.tsx @@ -8,7 +8,7 @@ import { Snackbar, TextField, Typography, -} from '@material-ui/core'; +} from '@mui/material'; import { Application } from './types'; export const NewApplicationForm = ({ diff --git a/packages/ra-no-code/src/ResourceConfiguration/FieldConfigurationFormSection.tsx b/packages/ra-no-code/src/ResourceConfiguration/FieldConfigurationFormSection.tsx index 828b9916bbf..e2d232eb0b8 100644 --- a/packages/ra-no-code/src/ResourceConfiguration/FieldConfigurationFormSection.tsx +++ b/packages/ra-no-code/src/ResourceConfiguration/FieldConfigurationFormSection.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { getFieldLabelTranslationArgs, useTranslate } from 'ra-core'; import { TextInput } from 'ra-ui-materialui'; -import { CardContent } from '@material-ui/core'; +import { CardContent } from '@mui/material'; import { FieldTypeInput } from './FieldConfiguration/FieldTypeInput'; import { FieldViewsInput } from './FieldConfiguration/FieldViewsInput'; import { ConfigurationInputsFromFieldDefinition } from './ConfigurationInputsFromFieldDefinition'; diff --git a/packages/ra-no-code/src/ResourceConfiguration/FieldConfigurationTab.tsx b/packages/ra-no-code/src/ResourceConfiguration/FieldConfigurationTab.tsx index 7a72eb7eb7c..ec6de2781ce 100644 --- a/packages/ra-no-code/src/ResourceConfiguration/FieldConfigurationTab.tsx +++ b/packages/ra-no-code/src/ResourceConfiguration/FieldConfigurationTab.tsx @@ -1,10 +1,32 @@ import * as React from 'react'; -import { Tab } from '@material-ui/core'; -import { makeStyles } from '@material-ui/core/styles'; +import { styled } from '@mui/material/styles'; +import { Tab } from '@mui/material'; import { getFieldLabelTranslationArgs, useTranslate } from 'ra-core'; +const PREFIX = 'FieldConfigurationTab'; + +const classes = { + root: `${PREFIX}-root`, + selected: `${PREFIX}-selected`, +}; + +const StyledTab = styled(Tab)(({ theme }) => ({ + [`&.MuiTab-root`]: { + paddingTop: theme.spacing(1), + paddingBottom: theme.spacing(1), + paddingLeft: theme.spacing(2), + paddingRight: theme.spacing(2), + textTransform: 'none', + minHeight: 0, + fontWeight: 'normal', + }, + + [`& .Mui-selected`]: { + fontWeight: 'bold', + }, +})); + export const FieldConfigurationTab = ({ field, resource, ...props }) => { - const classes = useStyles(); const translate = useTranslate(); const labelArgs = getFieldLabelTranslationArgs({ source: field.props.source, @@ -13,7 +35,7 @@ export const FieldConfigurationTab = ({ field, resource, ...props }) => { }); return ( - <Tab + <StyledTab {...props} key={field.props.source} label={translate(...labelArgs)} @@ -23,18 +45,3 @@ export const FieldConfigurationTab = ({ field, resource, ...props }) => { /> ); }; - -const useStyles = makeStyles(theme => ({ - root: { - paddingTop: theme.spacing(1), - paddingBottom: theme.spacing(1), - paddingLeft: theme.spacing(2), - paddingRight: theme.spacing(2), - textTransform: 'none', - minHeight: 0, - fontWeight: 'normal', - }, - selected: { - fontWeight: 'bold', - }, -})); diff --git a/packages/ra-no-code/src/ResourceConfiguration/ResourceConfiguration.tsx b/packages/ra-no-code/src/ResourceConfiguration/ResourceConfiguration.tsx index 9a18d2317f7..3d9a19855b2 100644 --- a/packages/ra-no-code/src/ResourceConfiguration/ResourceConfiguration.tsx +++ b/packages/ra-no-code/src/ResourceConfiguration/ResourceConfiguration.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { useEffect, useState } from 'react'; import { Avatar, @@ -9,9 +10,8 @@ import { Divider, IconButton, Tabs, -} from '@material-ui/core'; -import { makeStyles } from '@material-ui/core/styles'; -import MoreVertIcon from '@material-ui/icons/MoreVert'; +} from '@mui/material'; +import MoreVertIcon from '@mui/icons-material/MoreVert'; import { FormWithRedirect, RecordContextProvider, @@ -26,6 +26,43 @@ import { useResourceConfiguration } from './useResourceConfiguration'; import { FieldConfigurationFormSection } from './FieldConfigurationFormSection'; import { FieldConfigurationTab } from './FieldConfigurationTab'; +const PREFIX = 'ResourceConfigurationPage'; + +const classes = { + fields: `${PREFIX}-fields`, + fieldList: `${PREFIX}-fieldList`, + fieldTitle: `${PREFIX}-fieldTitle`, + fieldPanel: `${PREFIX}-fieldPanel`, + actions: `${PREFIX}-actions`, +}; + +const StyledCard = styled(Card)(({ theme }) => ({ + [`& .${classes.fields}`]: { + display: 'flex', + }, + + [`& .${classes.fieldList}`]: { + backgroundColor: theme.palette.background.default, + }, + + [`& .${classes.fieldTitle}`]: { + paddingTop: theme.spacing(1), + paddingBottom: theme.spacing(1), + paddingLeft: theme.spacing(2), + paddingRight: theme.spacing(2), + textTransform: 'none', + minHeight: 0, + }, + + [`& .${classes.fieldPanel}`]: { + flexGrow: 1, + }, + + [`& .${classes.actions}`]: { + backgroundColor: theme.palette.background.default, + }, +})); + export const ResourceConfigurationPage = ({ resource, }: { @@ -33,7 +70,6 @@ export const ResourceConfigurationPage = ({ }) => { const [resourceConfiguration, actions] = useResourceConfiguration(resource); const [activeField, setActiveField] = useState<FieldConfiguration>(); - const classes = useStyles(); const save = (values: ResourceConfiguration) => { actions.update(values); @@ -68,7 +104,7 @@ export const ResourceConfigurationPage = ({ save={save} initialValues={resourceConfiguration} render={({ handleSubmitWithRedirect }) => ( - <Card> + <StyledCard> <CardHeader avatar={ <Avatar> @@ -83,7 +119,10 @@ export const ResourceConfigurationPage = ({ } action={ // TODO: Add a menu with resource related actions (delete, etc.) - <IconButton aria-label="settings"> + <IconButton + aria-label="settings" + size="large" + > <MoreVertIcon /> </IconButton> } @@ -154,33 +193,10 @@ export const ResourceConfigurationPage = ({ } /> </CardActions> - </Card> + </StyledCard> )} /> </SaveContextProvider> </RecordContextProvider> ); }; - -const useStyles = makeStyles(theme => ({ - fields: { - display: 'flex', - }, - fieldList: { - backgroundColor: theme.palette.background.default, - }, - fieldTitle: { - paddingTop: theme.spacing(1), - paddingBottom: theme.spacing(1), - paddingLeft: theme.spacing(2), - paddingRight: theme.spacing(2), - textTransform: 'none', - minHeight: 0, - }, - fieldPanel: { - flexGrow: 1, - }, - actions: { - backgroundColor: theme.palette.background.default, - }, -})); diff --git a/packages/ra-no-code/src/ui/ExitApplicationMenu.tsx b/packages/ra-no-code/src/ui/ExitApplicationMenu.tsx index 3862a103254..a6ad4f57028 100644 --- a/packages/ra-no-code/src/ui/ExitApplicationMenu.tsx +++ b/packages/ra-no-code/src/ui/ExitApplicationMenu.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { forwardRef } from 'react'; import { MenuItemLink, MenuItemLinkProps } from 'react-admin'; -import ExitToAppIcon from '@material-ui/icons/ExitToApp'; +import ExitToAppIcon from '@mui/icons-material/ExitToApp'; import { useApplication } from '../ApplicationContext'; export const ExitApplicationMenu = forwardRef<HTMLLIElement>( diff --git a/packages/ra-no-code/src/ui/ImportResourceDialog.tsx b/packages/ra-no-code/src/ui/ImportResourceDialog.tsx index 1707ef52ec3..152df75b9cb 100644 --- a/packages/ra-no-code/src/ui/ImportResourceDialog.tsx +++ b/packages/ra-no-code/src/ui/ImportResourceDialog.tsx @@ -7,9 +7,8 @@ import { DialogContent, DialogContentText, DialogTitle, - RootRef, TextField, -} from '@material-ui/core'; +} from '@mui/material'; import { useDropzone } from 'react-dropzone'; import { useNotify, useRefresh } from 'ra-core'; @@ -87,7 +86,7 @@ export const ImportResourceDialog = (props: ImportResourceDialogProps) => { </DialogContent> ) : ( <> - <RootRef rootRef={ref}> + <> <DialogContent {...rootProps}> <input aria-label="CSV File" @@ -102,7 +101,7 @@ export const ImportResourceDialog = (props: ImportResourceDialogProps) => { a local file. </DialogContentText> </DialogContent> - </RootRef> + </> {!!file && ( <DialogContent> <TextField diff --git a/packages/ra-no-code/src/ui/Menu.tsx b/packages/ra-no-code/src/ui/Menu.tsx index 5721b85bdd4..5a8588aa351 100644 --- a/packages/ra-no-code/src/ui/Menu.tsx +++ b/packages/ra-no-code/src/ui/Menu.tsx @@ -1,11 +1,11 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { ReactNode } from 'react'; import PropTypes from 'prop-types'; import { useSelector } from 'react-redux'; import lodashGet from 'lodash/get'; // @ts-ignore -import { useMediaQuery, Theme } from '@material-ui/core'; -import { makeStyles } from '@material-ui/core/styles'; +import { useMediaQuery, Theme } from '@mui/material'; import classnames from 'classnames'; import { ReduxState } from 'ra-core'; @@ -14,12 +14,43 @@ import { NewResourceMenuItem } from './NewResourceMenuItem'; import { useResourcesConfiguration } from '../ResourceConfiguration'; import { ResourceMenuItem } from './ResourceMenuItem'; +const PREFIX = 'RaMenu'; + +const classes = { + main: `${PREFIX}-main`, + open: `${PREFIX}-open`, + closed: `${PREFIX}-closed`, +}; + +// TODO jss-to-styled codemod: The Fragment root was replaced by div. Change the tag if needed. +const Root = styled('div')(({ theme }) => ({ + [`& .${classes.main}`]: { + display: 'flex', + flexDirection: 'column', + justifyContent: 'flex-start', + marginTop: '0.5em', + [theme.breakpoints.only('xs')]: { + marginTop: 0, + }, + [theme.breakpoints.up('md')]: { + marginTop: '1.5em', + }, + }, + + [`& .${classes.open}`]: { + width: lodashGet(theme, 'menu.width', MENU_WIDTH), + }, + + [`& .${classes.closed}`]: { + width: lodashGet(theme, 'menu.closedWidth', CLOSED_MENU_WIDTH), + }, +})); + export const MENU_WIDTH = 240; export const CLOSED_MENU_WIDTH = 55; export const Menu = (props: MenuProps) => { const { - classes: classesOverride, className, dense, hasDashboard, @@ -28,15 +59,14 @@ export const Menu = (props: MenuProps) => { ...rest } = props; - const classes = useStyles(props); const isXSmall = useMediaQuery((theme: Theme) => - theme.breakpoints.down('xs') + theme.breakpoints.down('sm') ); const open = useSelector((state: ReduxState) => state.admin.ui.sidebarOpen); const [resources] = useResourcesConfiguration(); return ( - <> + <Root> <div className={classnames( classes.main, @@ -71,34 +101,10 @@ export const Menu = (props: MenuProps) => { /> {isXSmall && logout} </div> - </> + </Root> ); }; -const useStyles = makeStyles( - theme => ({ - main: { - display: 'flex', - flexDirection: 'column', - justifyContent: 'flex-start', - marginTop: '0.5em', - [theme.breakpoints.only('xs')]: { - marginTop: 0, - }, - [theme.breakpoints.up('md')]: { - marginTop: '1.5em', - }, - }, - open: { - width: lodashGet(theme, 'menu.width', MENU_WIDTH), - }, - closed: { - width: lodashGet(theme, 'menu.closedWidth', CLOSED_MENU_WIDTH), - }, - }), - { name: 'RaMenu' } -); - export interface MenuProps { classes?: object; className?: string; @@ -109,7 +115,6 @@ export interface MenuProps { } Menu.propTypes = { - classes: PropTypes.object, className: PropTypes.string, dense: PropTypes.bool, hasDashboard: PropTypes.bool, diff --git a/packages/ra-no-code/src/ui/NewResourceMenuItem.tsx b/packages/ra-no-code/src/ui/NewResourceMenuItem.tsx index 2875d4eb9e3..a68795cd4ae 100644 --- a/packages/ra-no-code/src/ui/NewResourceMenuItem.tsx +++ b/packages/ra-no-code/src/ui/NewResourceMenuItem.tsx @@ -1,14 +1,35 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { MouseEvent, ReactElement, useState } from 'react'; -import AddIcon from '@material-ui/icons/Add'; +import AddIcon from '@mui/icons-material/Add'; import { ImportResourceDialog } from './ImportResourceDialog'; -import { makeStyles } from '@material-ui/core/styles'; import { ListItemIcon, + ListItemText, MenuItem, MenuItemProps, Tooltip, -} from '@material-ui/core'; +} from '@mui/material'; + +const PREFIX = 'RaMenuItemLink'; + +const classes = { + root: `${PREFIX}-root`, + active: `${PREFIX}-active`, + icon: `${PREFIX}-icon`, +}; + +const Root = styled('div')(({ theme }) => ({ + [`& .${classes.root}`]: { + color: theme.palette.text.secondary, + }, + + [`& .${classes.active}`]: { + color: theme.palette.text.primary, + }, + + [`& .${classes.icon}`]: { minWidth: theme.spacing(5) }, +})); export const NewResourceMenuItem = ( props: MenuItemProps<'li', { button?: true } & { sidebarIsOpen: boolean }> @@ -17,7 +38,6 @@ export const NewResourceMenuItem = ( const [showImportResourceDialog, setShowImportResourceDialog] = useState( false ); - const classes = useStyles(props); const handleClick = ( event: MouseEvent<HTMLAnchorElement> & MouseEvent<HTMLLIElement> @@ -35,8 +55,6 @@ export const NewResourceMenuItem = ( const renderMenuItem = (): ReactElement => ( <MenuItem className={classes.root} - // @ts-ignore - component="button" tabIndex={0} {...rest} onClick={handleClick} @@ -44,12 +62,12 @@ export const NewResourceMenuItem = ( <ListItemIcon className={classes.icon}> <AddIcon titleAccess={primaryText} /> </ListItemIcon> - {primaryText} + <ListItemText>{primaryText}</ListItemText> </MenuItem> ); return ( - <> + <Root> {sidebarIsOpen ? ( renderMenuItem() ) : ( @@ -61,19 +79,6 @@ export const NewResourceMenuItem = ( open={showImportResourceDialog} onClose={handleCloseImportNewResourceDialog} /> - </> + </Root> ); }; - -const useStyles = makeStyles( - theme => ({ - root: { - color: theme.palette.text.secondary, - }, - active: { - color: theme.palette.text.primary, - }, - icon: { minWidth: theme.spacing(5) }, - }), - { name: 'RaMenuItemLink' } -); diff --git a/packages/ra-no-code/src/ui/ResourceMenuItem.tsx b/packages/ra-no-code/src/ui/ResourceMenuItem.tsx index d8816c9988b..4f84c074e67 100644 --- a/packages/ra-no-code/src/ui/ResourceMenuItem.tsx +++ b/packages/ra-no-code/src/ui/ResourceMenuItem.tsx @@ -1,21 +1,43 @@ import React, { forwardRef } from 'react'; +import { styled } from '@mui/material/styles'; import { MenuItemLink, MenuItemLinkProps } from 'react-admin'; -import { IconButton } from '@material-ui/core'; -import { makeStyles } from '@material-ui/core/styles'; -import SettingsIcon from '@material-ui/icons/Settings'; -import DefaultIcon from '@material-ui/icons/ViewList'; +import { IconButton } from '@mui/material'; +import SettingsIcon from '@mui/icons-material/Settings'; +import DefaultIcon from '@mui/icons-material/ViewList'; import { NavLink, NavLinkProps } from 'react-router-dom'; import { ResourceConfiguration } from '../ResourceConfiguration'; +const PREFIX = 'ResourceMenuItem'; + +const classes = { + root: `${PREFIX}-root`, + resource: `${PREFIX}-resource`, + settings: `${PREFIX}-settings`, +}; + +const Root = styled('div')(({ theme }) => ({ + [`&.${classes.root}`]: { + display: 'flex', + }, + + [`& .${classes.resource}`]: { + flexGrow: 1, + }, + + [`& .${classes.settings}`]: { + marginLeft: 'auto', + }, +})); + export const ResourceMenuItem = ( props: Omit<MenuItemLinkProps, 'to' | 'resource'> & { resource: ResourceConfiguration; } ) => { const { resource, ...rest } = props; - const classes = useStyles(props); + return ( - <div className={classes.root}> + <Root className={classes.root}> <MenuItemLink key={resource.name} className={classes.resource} @@ -33,25 +55,14 @@ export const ResourceMenuItem = ( pathname: `/configure/${resource.name}`, }} className={classes.settings} + size="large" > <SettingsIcon /> </IconButton> - </div> + </Root> ); }; const NavLinkRef = forwardRef<HTMLAnchorElement, NavLinkProps>((props, ref) => ( <NavLink innerRef={ref} {...props} /> )); - -const useStyles = makeStyles(theme => ({ - root: { - display: 'flex', - }, - resource: { - flexGrow: 1, - }, - settings: { - marginLeft: 'auto', - }, -})); diff --git a/packages/ra-ui-materialui/package.json b/packages/ra-ui-materialui/package.json index 7cd83f956b1..9ff69a4b4ad 100644 --- a/packages/ra-ui-materialui/package.json +++ b/packages/ra-ui-materialui/package.json @@ -26,9 +26,8 @@ "watch": "tsc --outDir esm --module es2015 --watch" }, "devDependencies": { - "@material-ui/core": "^4.12.1", - "@material-ui/icons": "^4.11.2", - "@material-ui/styles": "^4.11.4", + "@mui/material": "^5.0.2", + "@mui/icons-material": "^5.0.1", "@testing-library/react": "^11.2.3", "@types/query-string": "5.1.0", "cross-env": "^5.2.0", @@ -50,9 +49,8 @@ "rimraf": "^2.6.3" }, "peerDependencies": { - "@material-ui/core": "^4.12.1", - "@material-ui/icons": "^4.11.2", - "@material-ui/styles": "^4.11.4", + "@mui/material": "^5.0.2", + "@mui/icons-material": "^5.0.1", "final-form": "^4.20.2", "final-form-arrays": "^3.0.2", "ra-core": "^3.14.0", diff --git a/packages/ra-ui-materialui/src/Link.tsx b/packages/ra-ui-materialui/src/Link.tsx index b5beec6313d..c55d16509ce 100644 --- a/packages/ra-ui-materialui/src/Link.tsx +++ b/packages/ra-ui-materialui/src/Link.tsx @@ -2,43 +2,36 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import { Link as RRLink, LinkProps as RRLinkProps } from 'react-router-dom'; -import { makeStyles } from '@material-ui/core/styles'; -import { ClassNameMap } from '@material-ui/styles'; +import { styled } from '@mui/material'; -const useStyles = makeStyles( - theme => ({ - link: { - textDecoration: 'none', - color: theme.palette.primary.main, - }, - }), - { name: 'RaLink' } -); +const PREFIX = 'RaLink'; -type LinkClassKey = 'link'; +const classes = { + link: `${PREFIX}-link`, +}; + +const StyledLink = styled(RRLink)(({ theme }) => ({ + [`& .${classes.link}`]: { + textDecoration: 'none', + color: theme.palette.primary.main, + }, +})); export interface LinkProps extends RRLinkProps { - classes?: Partial<ClassNameMap<LinkClassKey>>; className?: string; } const Link = (props: LinkProps) => { - const { - to, - children, - classes: classesOverride, - className, - ...rest - } = props; - const classes = useStyles(props); + const { to, children, className, ...rest } = props; + return ( - <RRLink + <StyledLink to={to} className={classNames(classes.link, className)} {...rest} > {children} - </RRLink> + </StyledLink> ); }; diff --git a/packages/ra-ui-materialui/src/auth/Login.tsx b/packages/ra-ui-materialui/src/auth/Login.tsx index 07d009f26d8..778e7fdd406 100644 --- a/packages/ra-ui-materialui/src/auth/Login.tsx +++ b/packages/ra-ui-materialui/src/auth/Login.tsx @@ -9,16 +9,14 @@ import React, { } from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; -import { Card, Avatar, Theme } from '@material-ui/core'; -import { makeStyles } from '@material-ui/core/styles'; -import { ThemeProvider } from '@material-ui/styles'; -import LockIcon from '@material-ui/icons/Lock'; +import { Card, Avatar } from '@mui/material'; +import { createTheme, styled, ThemeProvider } from '@mui/material/styles'; +import LockIcon from '@mui/icons-material/Lock'; import { StaticContext } from 'react-router'; import { useHistory } from 'react-router-dom'; import { useCheckAuth, TitleComponent } from 'ra-core'; import defaultTheme from '../defaultTheme'; -import { createMuiTheme } from '../layout'; import DefaultNotification from '../layout/Notification'; import DefaultLoginForm from './LoginForm'; @@ -34,35 +32,40 @@ export interface LoginProps title?: TitleComponent; } -const useStyles = makeStyles( - (theme: Theme) => ({ - main: { - display: 'flex', - flexDirection: 'column', - minHeight: '100vh', - height: '1px', - alignItems: 'center', - justifyContent: 'flex-start', - backgroundRepeat: 'no-repeat', - backgroundSize: 'cover', - backgroundImage: - 'radial-gradient(circle at 50% 14em, #313264 0%, #00023b 60%, #00023b 100%)', - }, - card: { - minWidth: 300, - marginTop: '6em', - }, - avatar: { - margin: '1em', - display: 'flex', - justifyContent: 'center', - }, - icon: { - backgroundColor: theme.palette.secondary[500], - }, - }), - { name: 'RaLogin' } -); +const PREFIX = 'RaLogin'; +const classes = { + main: `${PREFIX}-main`, + card: `${PREFIX}-card`, + avatar: `${PREFIX}-avatar`, + icon: `${PREFIX}-icon`, +}; + +const Root = styled('div')(({ theme }) => ({ + [`&.${classes.main}`]: { + display: 'flex', + flexDirection: 'column', + minHeight: '100vh', + height: '1px', + alignItems: 'center', + justifyContent: 'flex-start', + backgroundRepeat: 'no-repeat', + backgroundSize: 'cover', + backgroundImage: + 'radial-gradient(circle at 50% 14em, #313264 0%, #00023b 60%, #00023b 100%)', + }, + [`& .${classes.card}`]: { + minWidth: 300, + marginTop: '6em', + }, + [`& .${classes.avatar}`]: { + margin: '1em', + display: 'flex', + justifyContent: 'center', + }, + [`& .${classes.icon}`]: { + backgroundColor: theme.palette.secondary[500], + }, +})); /** * A standalone login page, to serve as authentication gate to the admin @@ -83,8 +86,17 @@ const useStyles = makeStyles( * ); */ const Login: React.FunctionComponent<LoginProps> = props => { + const { theme, ...rest } = props; + const muiTheme = useMemo(() => createTheme(theme), [theme]); + return ( + <ThemeProvider theme={muiTheme}> + <LoginContainer {...rest} /> + </ThemeProvider> + ); +}; + +const LoginContainer = props => { const { - theme, title, classes: classesOverride, className, @@ -95,8 +107,6 @@ const Login: React.FunctionComponent<LoginProps> = props => { ...rest } = props; const containerRef = useRef<HTMLDivElement>(); - const classes = useStyles(props); - const muiTheme = useMemo(() => createMuiTheme(theme), [theme]); let backgroundImageLoaded = false; const checkAuth = useCheckAuth(); const history = useHistory(); @@ -132,25 +142,22 @@ const Login: React.FunctionComponent<LoginProps> = props => { lazyLoadBackgroundImage(); } }); - return ( - <ThemeProvider theme={muiTheme}> - <div - className={classnames(classes.main, className)} - {...rest} - ref={containerRef} - > - <Card className={classes.card}> - <div className={classes.avatar}> - <Avatar className={classes.icon}> - <LockIcon /> - </Avatar> - </div> - {children} - </Card> - {notification ? createElement(notification) : null} - </div> - </ThemeProvider> + <Root + className={classnames(classes.main, className)} + {...rest} + ref={containerRef} + > + <Card className={classes.card}> + <div className={classes.avatar}> + <Avatar className={classes.icon}> + <LockIcon /> + </Avatar> + </div> + {children} + </Card> + {notification ? createElement(notification) : null} + </Root> ); }; diff --git a/packages/ra-ui-materialui/src/auth/LoginForm.tsx b/packages/ra-ui-materialui/src/auth/LoginForm.tsx index 48ff37f1c79..618a0e2badd 100644 --- a/packages/ra-ui-materialui/src/auth/LoginForm.tsx +++ b/packages/ra-ui-materialui/src/auth/LoginForm.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import PropTypes from 'prop-types'; import { Field, Form } from 'react-final-form'; import { @@ -6,10 +7,36 @@ import { CardActions, CircularProgress, TextField, -} from '@material-ui/core'; -import { makeStyles, Theme } from '@material-ui/core/styles'; +} from '@mui/material'; import { useTranslate, useLogin, useNotify, useSafeSetState } from 'ra-core'; +const PREFIX = 'RaLoginForm'; + +const classes = { + form: `${PREFIX}-form`, + input: `${PREFIX}-input`, + button: `${PREFIX}-button`, + icon: `${PREFIX}-icon`, +}; + +const Root = styled('form')(({ theme }) => ({ + [`& .${classes.form}`]: { + padding: '0 1em 1em 1em', + }, + + [`& .${classes.input}`]: { + marginTop: '1em', + }, + + [`& .${classes.button}`]: { + width: '100%', + }, + + [`& .${classes.icon}`]: { + marginRight: theme.spacing(1), + }, +})); + interface Props { redirectTo?: string; } @@ -19,24 +46,6 @@ interface FormData { password: string; } -const useStyles = makeStyles( - (theme: Theme) => ({ - form: { - padding: '0 1em 1em 1em', - }, - input: { - marginTop: '1em', - }, - button: { - width: '100%', - }, - icon: { - marginRight: theme.spacing(1), - }, - }), - { name: 'RaLoginForm' } -); - const Input = ({ meta: { touched, error }, // eslint-disable-line react/prop-types input: inputProps, // eslint-disable-line react/prop-types @@ -57,7 +66,6 @@ const LoginForm = (props: Props) => { const login = useLogin(); const translate = useTranslate(); const notify = useNotify(); - const classes = useStyles(props); const validate = (values: FormData) => { const errors = { username: undefined, password: undefined }; @@ -103,7 +111,7 @@ const LoginForm = (props: Props) => { onSubmit={submit} validate={validate} render={({ handleSubmit }) => ( - <form onSubmit={handleSubmit} noValidate> + <Root onSubmit={handleSubmit} noValidate> <div className={classes.form}> <div className={classes.input}> <Field @@ -145,7 +153,7 @@ const LoginForm = (props: Props) => { {translate('ra.auth.sign_in')} </Button> </CardActions> - </form> + </Root> )} /> ); diff --git a/packages/ra-ui-materialui/src/auth/Logout.tsx b/packages/ra-ui-materialui/src/auth/Logout.tsx index 960c7aa411e..1bd74745f32 100644 --- a/packages/ra-ui-materialui/src/auth/Logout.tsx +++ b/packages/ra-ui-materialui/src/auth/Logout.tsx @@ -1,30 +1,35 @@ import * as React from 'react'; +import { styled, Theme } from '@mui/material/styles'; import { useCallback, FunctionComponent, ReactElement } from 'react'; import PropTypes from 'prop-types'; -import { ListItemIcon, MenuItem, useMediaQuery } from '@material-ui/core'; -import { MenuItemProps } from '@material-ui/core/MenuItem'; -import { Theme, makeStyles } from '@material-ui/core/styles'; +import { ListItemIcon, MenuItem, useMediaQuery } from '@mui/material'; +import { MenuItemProps } from '@mui/material/MenuItem'; -import ExitIcon from '@material-ui/icons/PowerSettingsNew'; +import ExitIcon from '@mui/icons-material/PowerSettingsNew'; import classnames from 'classnames'; import { useTranslate, useLogout } from 'ra-core'; +const PREFIX = 'RaLogout'; + +const classes = { + menuItem: `${PREFIX}-menuItem`, + icon: `${PREFIX}-icon`, +}; + +const StyledMenuItem = styled(MenuItem)(({ theme }) => ({ + [`&.${classes.menuItem}`]: { + color: theme.palette.text.secondary, + }, + + [`& .${classes.icon}`]: { minWidth: theme.spacing(5) }, +})); + interface Props { className?: string; redirectTo?: string; icon?: ReactElement; } -const useStyles = makeStyles( - (theme: Theme) => ({ - menuItem: { - color: theme.palette.text.secondary, - }, - icon: { minWidth: theme.spacing(5) }, - }), - { name: 'RaLogout' } -); - /** * Logout button component, to be passed to the Admin component * @@ -40,9 +45,9 @@ const LogoutWithRef: FunctionComponent< icon, ...rest } = props; - const classes = useStyles(props); + const isXSmall = useMediaQuery((theme: Theme) => - theme.breakpoints.down('xs') + theme.breakpoints.down('sm') ); const translate = useTranslate(); const logout = useLogout(); @@ -52,10 +57,11 @@ const LogoutWithRef: FunctionComponent< logout, ]); return ( - <MenuItem + <StyledMenuItem className={classnames('logout', classes.menuItem, className)} onClick={handleClick} ref={ref} + // @ts-ignore component={isXSmall ? 'span' : 'li'} {...rest} > @@ -63,7 +69,7 @@ const LogoutWithRef: FunctionComponent< {icon ? icon : <ExitIcon />} </ListItemIcon> {translate('ra.auth.logout')} - </MenuItem> + </StyledMenuItem> ); }); diff --git a/packages/ra-ui-materialui/src/button/BulkDeleteWithConfirmButton.tsx b/packages/ra-ui-materialui/src/button/BulkDeleteWithConfirmButton.tsx index 77269f3f464..555d1769319 100644 --- a/packages/ra-ui-materialui/src/button/BulkDeleteWithConfirmButton.tsx +++ b/packages/ra-ui-materialui/src/button/BulkDeleteWithConfirmButton.tsx @@ -1,10 +1,9 @@ import * as React from 'react'; import { Fragment, useState, ReactElement } from 'react'; import PropTypes from 'prop-types'; -import ActionDelete from '@material-ui/icons/Delete'; -import { alpha } from '@material-ui/core/styles/colorManipulator'; +import ActionDelete from '@mui/icons-material/Delete'; import inflection from 'inflection'; -import { makeStyles } from '@material-ui/core/styles'; +import { alpha, styled } from '@mui/material/styles'; import { useTranslate, useDeleteMany, @@ -19,21 +18,24 @@ import Confirm from '../layout/Confirm'; import Button, { ButtonProps } from './Button'; import { BulkActionProps } from '../types'; -const useStyles = makeStyles( - theme => ({ - deleteButton: { - color: theme.palette.error.main, - '&:hover': { - backgroundColor: alpha(theme.palette.error.main, 0.12), - // Reset on mouse devices - '@media (hover: none)': { - backgroundColor: 'transparent', - }, +const PREFIX = 'RaBulkDeleteWithConfirmButton'; + +const classes = { + deleteButton: `${PREFIX}-deleteButton`, +}; + +const StyledButton = styled(Button)(({ theme }) => ({ + [`&.${classes.deleteButton}`]: { + color: theme.palette.error.main, + '&:hover': { + backgroundColor: alpha(theme.palette.error.main, 0.12), + // Reset on mouse devices + '@media (hover: none)': { + backgroundColor: 'transparent', }, }, - }), - { name: 'RaBulkDeleteWithConfirmButton' } -); + }, +})); const defaultIcon = <ActionDelete />; @@ -52,7 +54,6 @@ const BulkDeleteWithConfirmButton = ( ...rest } = props; const [isOpen, setOpen] = useState(false); - const classes = useStyles(props); const notify = useNotify(); const unselectAll = useUnselectAll(); const refresh = useRefresh(); @@ -105,14 +106,14 @@ const BulkDeleteWithConfirmButton = ( return ( <Fragment> - <Button + <StyledButton onClick={handleClick} label={label} className={classes.deleteButton} {...sanitizeRestProps(rest)} > {icon} - </Button> + </StyledButton> <Confirm isOpen={isOpen} loading={loading} diff --git a/packages/ra-ui-materialui/src/button/BulkDeleteWithUndoButton.tsx b/packages/ra-ui-materialui/src/button/BulkDeleteWithUndoButton.tsx index 91511473ba8..bd665ff3eef 100644 --- a/packages/ra-ui-materialui/src/button/BulkDeleteWithUndoButton.tsx +++ b/packages/ra-ui-materialui/src/button/BulkDeleteWithUndoButton.tsx @@ -1,9 +1,9 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { ReactElement } from 'react'; import PropTypes from 'prop-types'; -import ActionDelete from '@material-ui/icons/Delete'; -import { alpha } from '@material-ui/core/styles/colorManipulator'; -import { makeStyles } from '@material-ui/core/styles'; +import ActionDelete from '@mui/icons-material/Delete'; +import { alpha } from '@mui/material/styles'; import { useDeleteMany, useRefresh, @@ -17,33 +17,36 @@ import { import Button, { ButtonProps } from './Button'; import { BulkActionProps } from '../types'; -const useStyles = makeStyles( - theme => ({ - deleteButton: { - color: theme.palette.error.main, - '&:hover': { - backgroundColor: alpha(theme.palette.error.main, 0.12), - // Reset on mouse devices - '@media (hover: none)': { - backgroundColor: 'transparent', - }, +const PREFIX = 'RaBulkDeleteWithUndoButton'; + +const classes = { + deleteButton: `${PREFIX}-deleteButton`, +}; + +const StyledButton = styled(Button)(({ theme }) => ({ + [`&.${classes.deleteButton}`]: { + color: theme.palette.error.main, + '&:hover': { + backgroundColor: alpha(theme.palette.error.main, 0.12), + // Reset on mouse devices + '@media (hover: none)': { + backgroundColor: 'transparent', }, }, - }), - { name: 'RaBulkDeleteWithUndoButton' } -); + }, +})); const BulkDeleteWithUndoButton = (props: BulkDeleteWithUndoButtonProps) => { const { basePath, classes: classesOverride, - icon, - label, + label = 'ra.action.delete', + icon = defaultIcon, onClick, ...rest } = props; const { selectedIds } = useListContext(props); - const classes = useStyles(props); + const notify = useNotify(); const unselectAll = useUnselectAll(); const refresh = useRefresh(); @@ -88,7 +91,7 @@ const BulkDeleteWithUndoButton = (props: BulkDeleteWithUndoButtonProps) => { }; return ( - <Button + <StyledButton onClick={handleClick} label={label} className={classes.deleteButton} @@ -96,10 +99,12 @@ const BulkDeleteWithUndoButton = (props: BulkDeleteWithUndoButtonProps) => { {...sanitizeRestProps(rest)} > {icon} - </Button> + </StyledButton> ); }; +const defaultIcon = <ActionDelete />; + const sanitizeRestProps = ({ basePath, classes, @@ -124,9 +129,4 @@ BulkDeleteWithUndoButton.propTypes = { icon: PropTypes.element, }; -BulkDeleteWithUndoButton.defaultProps = { - label: 'ra.action.delete', - icon: <ActionDelete />, -}; - export default BulkDeleteWithUndoButton; diff --git a/packages/ra-ui-materialui/src/button/BulkExportButton.tsx b/packages/ra-ui-materialui/src/button/BulkExportButton.tsx index 216e5c76e1f..db395e6787a 100644 --- a/packages/ra-ui-materialui/src/button/BulkExportButton.tsx +++ b/packages/ra-ui-materialui/src/button/BulkExportButton.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { useCallback } from 'react'; import PropTypes from 'prop-types'; -import DownloadIcon from '@material-ui/icons/GetApp'; +import DownloadIcon from '@mui/icons-material/GetApp'; import { fetchRelatedRecords, useDataProvider, diff --git a/packages/ra-ui-materialui/src/button/BulkUpdateWithConfirmButton.tsx b/packages/ra-ui-materialui/src/button/BulkUpdateWithConfirmButton.tsx index 545ab584a5e..24e2c06043c 100644 --- a/packages/ra-ui-materialui/src/button/BulkUpdateWithConfirmButton.tsx +++ b/packages/ra-ui-materialui/src/button/BulkUpdateWithConfirmButton.tsx @@ -1,10 +1,9 @@ import * as React from 'react'; import { Fragment, useState, ReactElement } from 'react'; import PropTypes from 'prop-types'; -import ActionUpdate from '@material-ui/icons/Update'; -import { alpha } from '@material-ui/core/styles/colorManipulator'; +import ActionUpdate from '@mui/icons-material/Update'; import inflection from 'inflection'; -import { makeStyles } from '@material-ui/core/styles'; +import { alpha, styled } from '@mui/material/styles'; import { useTranslate, useUpdateMany, @@ -20,21 +19,24 @@ import Confirm from '../layout/Confirm'; import Button, { ButtonProps } from './Button'; import { BulkActionProps } from '../types'; -const useStyles = makeStyles( - theme => ({ - updateButton: { - color: theme.palette.error.main, - '&:hover': { - backgroundColor: alpha(theme.palette.error.main, 0.12), - // Reset on mouse devices - '@media (hover: none)': { - backgroundColor: 'transparent', - }, +const PREFIX = 'RaBulkUpdateWithConfirmButton'; + +const classes = { + updateButton: `${PREFIX}-updateButton`, +}; + +const StyledButton = styled(Button)(({ theme }) => ({ + [`&.${classes.updateButton}`]: { + color: theme.palette.error.main, + '&:hover': { + backgroundColor: alpha(theme.palette.error.main, 0.12), + // Reset on mouse devices + '@media (hover: none)': { + backgroundColor: 'transparent', }, }, - }), - { name: 'RaBulkUpdateWithConfirmButton' } -); + }, +})); const defaultIcon = <ActionUpdate />; @@ -46,7 +48,6 @@ const BulkUpdateWithConfirmButton = ( const translate = useTranslate(); const unselectAll = useUnselectAll(); const resource = useResourceContext(props); - const classes = useStyles(props); const [isOpen, setOpen] = useState(false); const { @@ -118,14 +119,14 @@ const BulkUpdateWithConfirmButton = ( return ( <Fragment> - <Button + <StyledButton onClick={handleClick} label={label} className={classes.updateButton} {...sanitizeRestProps(rest)} > {icon} - </Button> + </StyledButton> <Confirm isOpen={isOpen} loading={loading} diff --git a/packages/ra-ui-materialui/src/button/BulkUpdateWithUndoButton.tsx b/packages/ra-ui-materialui/src/button/BulkUpdateWithUndoButton.tsx index 404ca6c3f8e..6901d69b9be 100644 --- a/packages/ra-ui-materialui/src/button/BulkUpdateWithUndoButton.tsx +++ b/packages/ra-ui-materialui/src/button/BulkUpdateWithUndoButton.tsx @@ -1,9 +1,9 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { ReactElement } from 'react'; import PropTypes from 'prop-types'; -import ActionUpdate from '@material-ui/icons/Update'; -import { alpha } from '@material-ui/core/styles/colorManipulator'; -import { makeStyles } from '@material-ui/core/styles'; +import ActionUpdate from '@mui/icons-material/Update'; +import { alpha } from '@mui/material/styles'; import { useUpdateMany, useRefresh, @@ -17,25 +17,28 @@ import { import Button, { ButtonProps } from './Button'; import { BulkActionProps } from '../types'; -const useStyles = makeStyles( - theme => ({ - updateButton: { - color: theme.palette.error.main, - '&:hover': { - backgroundColor: alpha(theme.palette.error.main, 0.12), - // Reset on mouse devices - '@media (hover: none)': { - backgroundColor: 'transparent', - }, +const PREFIX = 'RaBulkUpdateWithUndoButton'; + +const classes = { + updateButton: `${PREFIX}-updateButton`, +}; + +const StyledButton = styled(Button)(({ theme }) => ({ + [`&.${classes.updateButton}`]: { + color: theme.palette.error.main, + '&:hover': { + backgroundColor: alpha(theme.palette.error.main, 0.12), + // Reset on mouse devices + '@media (hover: none)': { + backgroundColor: 'transparent', }, }, - }), - { name: 'RaBulkUpdateWithUndoButton' } -); + }, +})); const BulkUpdateWithUndoButton = (props: BulkUpdateWithUndoButtonProps) => { const { selectedIds } = useListContext(props); - const classes = useStyles(props); + const notify = useNotify(); const unselectAll = useUnselectAll(); const refresh = useRefresh(); @@ -45,8 +48,8 @@ const BulkUpdateWithUndoButton = (props: BulkUpdateWithUndoButtonProps) => { basePath, classes: classesOverride, data, - icon, - label, + label = 'ra.action.update', + icon = defaultIcon, onClick, onSuccess = () => { notify( @@ -98,7 +101,7 @@ const BulkUpdateWithUndoButton = (props: BulkUpdateWithUndoButtonProps) => { }; return ( - <Button + <StyledButton onClick={handleClick} label={label} className={classes.updateButton} @@ -106,10 +109,12 @@ const BulkUpdateWithUndoButton = (props: BulkUpdateWithUndoButtonProps) => { {...sanitizeRestProps(rest)} > {icon} - </Button> + </StyledButton> ); }; +const defaultIcon = <ActionUpdate />; + const sanitizeRestProps = ({ basePath, classes, @@ -140,9 +145,4 @@ BulkUpdateWithUndoButton.propTypes = { data: PropTypes.any.isRequired, }; -BulkUpdateWithUndoButton.defaultProps = { - label: 'ra.action.update', - icon: <ActionUpdate />, -}; - export default BulkUpdateWithUndoButton; diff --git a/packages/ra-ui-materialui/src/button/Button.spec.tsx b/packages/ra-ui-materialui/src/button/Button.spec.tsx index 3e2a1c31da1..4224af591e6 100644 --- a/packages/ra-ui-materialui/src/button/Button.spec.tsx +++ b/packages/ra-ui-materialui/src/button/Button.spec.tsx @@ -2,8 +2,8 @@ import { render } from '@testing-library/react'; import * as React from 'react'; import expect from 'expect'; import { TestContext } from 'ra-test'; -import { ThemeProvider } from '@material-ui/core'; -import { createTheme } from '@material-ui/core/styles'; +import { ThemeProvider } from '@mui/material'; +import { createTheme } from '@mui/material/styles'; import Button from './Button'; const theme = createTheme(); diff --git a/packages/ra-ui-materialui/src/button/Button.tsx b/packages/ra-ui-materialui/src/button/Button.tsx index cf1120c5c91..5fe7ab1b013 100644 --- a/packages/ra-ui-materialui/src/button/Button.tsx +++ b/packages/ra-ui-materialui/src/button/Button.tsx @@ -3,18 +3,51 @@ import { ReactElement, SyntheticEvent, ReactNode } from 'react'; import PropTypes from 'prop-types'; import { Button as MuiButton, + ButtonProps as MuiButtonProps, Tooltip, IconButton, useMediaQuery, PropTypes as MuiPropTypes, -} from '@material-ui/core'; -import { makeStyles } from '@material-ui/core/styles'; -import { ButtonProps as MuiButtonProps } from '@material-ui/core/Button'; -import { Theme } from '@material-ui/core'; + Theme, +} from '@mui/material'; +import { styled } from '@mui/material/styles'; import classnames from 'classnames'; import { Record, RedirectionSideEffect, useTranslate } from 'ra-core'; import { LocationDescriptor } from 'history'; +const PREFIX = 'RaButton'; + +const classes = { + button: `${PREFIX}-button`, + label: `${PREFIX}-label`, + labelRightIcon: `${PREFIX}-labelRightIcon`, + smallIcon: `${PREFIX}-smallIcon`, + mediumIcon: `${PREFIX}-mediumIcon`, + largeIcon: `${PREFIX}-largeIcon`, +}; + +const StyledButton = styled(MuiButton)({ + [`& .${classes.button}`]: { + display: 'inline-flex', + alignItems: 'center', + }, + [`& .${classes.label}`]: { + paddingLeft: '0.5em', + }, + [`& .${classes.labelRightIcon}`]: { + paddingRight: '0.5em', + }, + [`& .${classes.smallIcon}`]: { + fontSize: 20, + }, + [`& .${classes.mediumIcon}`]: { + fontSize: 22, + }, + [`& .${classes.largeIcon}`]: { + fontSize: 24, + }, +}); + /** * A generic Button with side icon. Only the icon is displayed on small screens. * @@ -34,16 +67,16 @@ const Button = (props: ButtonProps) => { children, classes: classesOverride, className, - color, disabled, label, - size, + color = 'primary', + size = 'small', ...rest } = props; const translate = useTranslate(); - const classes = useStyles(props); + const isXSmall = useMediaQuery((theme: Theme) => - theme.breakpoints.down('xs') + theme.breakpoints.down('sm') ); const restProps = sanitizeButtonRestProps(rest); @@ -55,6 +88,7 @@ const Button = (props: ButtonProps) => { className={className} color={color} {...restProps} + size="large" > {children} </IconButton> @@ -65,12 +99,13 @@ const Button = (props: ButtonProps) => { color={color} disabled={disabled} {...restProps} + size="large" > {children} </IconButton> ) ) : ( - <MuiButton + <StyledButton className={classnames(classes.button, className)} color={color} size={size} @@ -98,35 +133,10 @@ const Button = (props: ButtonProps) => { React.cloneElement(children, { className: classes[`${size}Icon`], })} - </MuiButton> + </StyledButton> ); }; -const useStyles = makeStyles( - { - button: { - display: 'inline-flex', - alignItems: 'center', - }, - label: { - paddingLeft: '0.5em', - }, - labelRightIcon: { - paddingRight: '0.5em', - }, - smallIcon: { - fontSize: 20, - }, - mediumIcon: { - fontSize: 22, - }, - largeIcon: { - fontSize: 24, - }, - }, - { name: 'RaButton' } -); - interface Props { alignIcon?: 'left' | 'right'; children?: ReactElement; @@ -185,9 +195,4 @@ Button.propTypes = { size: PropTypes.oneOf(['small', 'medium', 'large']), }; -Button.defaultProps = { - color: 'primary', - size: 'small', -}; - export default Button; diff --git a/packages/ra-ui-materialui/src/button/CloneButton.spec.tsx b/packages/ra-ui-materialui/src/button/CloneButton.spec.tsx index 52884d7edd0..9931311e9bc 100644 --- a/packages/ra-ui-materialui/src/button/CloneButton.spec.tsx +++ b/packages/ra-ui-materialui/src/button/CloneButton.spec.tsx @@ -1,13 +1,13 @@ +import * as React from 'react'; import expect from 'expect'; -import { ThemeProvider } from '@material-ui/core'; -import { createTheme } from '@material-ui/core/styles'; +import { ThemeProvider } from '@mui/material'; +import { createTheme } from '@mui/material/styles'; import { render } from '@testing-library/react'; -import * as React from 'react'; import { createMemoryHistory } from 'history'; import { Router } from 'react-router-dom'; +import { TestContext } from 'ra-test'; import { CloneButton } from './CloneButton'; -import { TestContext } from 'ra-test'; const theme = createTheme(); @@ -29,7 +29,7 @@ const invalidButtonDomProps = { describe('<CloneButton />', () => { it('should pass a clone of the record in the location state', () => { const history = createMemoryHistory(); - const { getByRole } = render( + const { getByLabelText } = render( <Router history={history}> <ThemeProvider theme={theme}> <CloneButton @@ -40,8 +40,7 @@ describe('<CloneButton />', () => { </Router> ); - const button = getByRole('button'); - expect(button.getAttribute('href')).toEqual( + expect(getByLabelText('ra.action.clone').getAttribute('href')).toEqual( '/posts/create?source=%7B%22foo%22%3A%22bar%22%7D' ); }); @@ -49,7 +48,7 @@ describe('<CloneButton />', () => { it('should render as button type with no DOM errors', () => { const spy = jest.spyOn(console, 'error').mockImplementation(() => {}); - const { getByRole } = render( + const { getByLabelText } = render( <TestContext> <ThemeProvider theme={theme}> <CloneButton {...invalidButtonDomProps} /> @@ -58,7 +57,7 @@ describe('<CloneButton />', () => { ); expect(spy).not.toHaveBeenCalled(); - expect(getByRole('button').getAttribute('href')).toEqual( + expect(getByLabelText('ra.action.clone').getAttribute('href')).toEqual( '/posts/create?source=%7B%22foo%22%3A%22bar%22%7D' ); diff --git a/packages/ra-ui-materialui/src/button/CloneButton.tsx b/packages/ra-ui-materialui/src/button/CloneButton.tsx index 95ae01781d7..39f0fa14e4d 100644 --- a/packages/ra-ui-materialui/src/button/CloneButton.tsx +++ b/packages/ra-ui-materialui/src/button/CloneButton.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { memo, ReactElement } from 'react'; import PropTypes from 'prop-types'; -import Queue from '@material-ui/icons/Queue'; +import Queue from '@mui/icons-material/Queue'; import { Link } from 'react-router-dom'; import { stringify } from 'query-string'; import { Record, useResourceContext } from 'ra-core'; diff --git a/packages/ra-ui-materialui/src/button/CreateButton.spec.tsx b/packages/ra-ui-materialui/src/button/CreateButton.spec.tsx index 9a870352786..252635683b8 100644 --- a/packages/ra-ui-materialui/src/button/CreateButton.spec.tsx +++ b/packages/ra-ui-materialui/src/button/CreateButton.spec.tsx @@ -1,9 +1,9 @@ -import { render } from '@testing-library/react'; import * as React from 'react'; +import { render } from '@testing-library/react'; import expect from 'expect'; import { TestContext } from 'ra-test'; -import { ThemeProvider } from '@material-ui/core'; -import { createTheme } from '@material-ui/core/styles'; +import { ThemeProvider } from '@mui/material'; +import { createTheme } from '@mui/material/styles'; import CreateButton from './CreateButton'; const invalidButtonDomProps = { @@ -25,13 +25,7 @@ describe('<CreateButton />', () => { it('should render a button with no DOM errors', () => { const spy = jest.spyOn(console, 'error').mockImplementation(() => {}); - const theme = createTheme({ - props: { - MuiWithWidth: { - initialWidth: 'sm', - }, - }, - }); + const theme = createTheme(); const { getByLabelText } = render( <TestContext> diff --git a/packages/ra-ui-materialui/src/button/CreateButton.tsx b/packages/ra-ui-materialui/src/button/CreateButton.tsx index e873db8f85d..9fea63bdfc0 100644 --- a/packages/ra-ui-materialui/src/button/CreateButton.tsx +++ b/packages/ra-ui-materialui/src/button/CreateButton.tsx @@ -1,15 +1,34 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { ReactElement, memo, useMemo } from 'react'; import PropTypes from 'prop-types'; -import { Fab, useMediaQuery, Theme } from '@material-ui/core'; -import { makeStyles } from '@material-ui/core/styles'; -import ContentAdd from '@material-ui/icons/Add'; +import { Fab, useMediaQuery, Theme } from '@mui/material'; +import ContentAdd from '@mui/icons-material/Add'; import classnames from 'classnames'; import { Link } from 'react-router-dom'; import { useTranslate, useResourceContext } from 'ra-core'; import Button, { ButtonProps, sanitizeButtonRestProps } from './Button'; +const PREFIX = 'RaCreateButton'; + +const classes = { + floating: `${PREFIX}-floating`, +}; + +const StyledFab = styled(Fab)(({ theme }) => ({ + [`&.${classes.floating}`]: { + color: theme.palette.getContrastText(theme.palette.primary.main), + margin: 0, + top: 'auto', + right: 20, + bottom: 60, + left: 'auto', + position: 'fixed', + zIndex: 1000, + }, +})); + /** * Opens the Create view of a given resource * @@ -34,10 +53,10 @@ const CreateButton = (props: CreateButtonProps) => { variant, ...rest } = props; - const classes = useStyles(props); + const translate = useTranslate(); const isSmall = useMediaQuery((theme: Theme) => - theme.breakpoints.down('sm') + theme.breakpoints.down('md') ); const resource = useResourceContext(); const location = useMemo( @@ -48,7 +67,7 @@ const CreateButton = (props: CreateButtonProps) => { [basePath, resource, scrollToTop] ); return isSmall ? ( - <Fab + <StyledFab component={Link} color="primary" className={classnames(classes.floating, className)} @@ -57,7 +76,7 @@ const CreateButton = (props: CreateButtonProps) => { {...sanitizeButtonRestProps(rest)} > {icon} - </Fab> + </StyledFab> ) : ( <Button component={Link} @@ -74,22 +93,6 @@ const CreateButton = (props: CreateButtonProps) => { const defaultIcon = <ContentAdd />; -const useStyles = makeStyles( - theme => ({ - floating: { - color: theme.palette.getContrastText(theme.palette.primary.main), - margin: 0, - top: 'auto', - right: 20, - bottom: 60, - left: 'auto', - position: 'fixed', - zIndex: 1000, - }, - }), - { name: 'RaCreateButton' } -); - interface Props { basePath?: string; icon?: ReactElement; diff --git a/packages/ra-ui-materialui/src/button/DeleteWithConfirmButton.spec.tsx b/packages/ra-ui-materialui/src/button/DeleteWithConfirmButton.spec.tsx index 72f0d4e6a7b..54559676608 100644 --- a/packages/ra-ui-materialui/src/button/DeleteWithConfirmButton.spec.tsx +++ b/packages/ra-ui-materialui/src/button/DeleteWithConfirmButton.spec.tsx @@ -1,9 +1,9 @@ -import { render, waitFor, fireEvent } from '@testing-library/react'; import * as React from 'react'; +import { render, waitFor, fireEvent } from '@testing-library/react'; import expect from 'expect'; import { DataProvider, DataProviderContext } from 'ra-core'; import { renderWithRedux, TestContext } from 'ra-test'; -import { createTheme, ThemeProvider } from '@material-ui/core/styles'; +import { createTheme, ThemeProvider } from '@mui/material/styles'; import { DeleteWithConfirmButton } from './DeleteWithConfirmButton'; import { Toolbar, SimpleForm } from '../form'; import { Edit } from '../detail'; diff --git a/packages/ra-ui-materialui/src/button/DeleteWithConfirmButton.tsx b/packages/ra-ui-materialui/src/button/DeleteWithConfirmButton.tsx index e7112388c9d..bb4006b7a3b 100644 --- a/packages/ra-ui-materialui/src/button/DeleteWithConfirmButton.tsx +++ b/packages/ra-ui-materialui/src/button/DeleteWithConfirmButton.tsx @@ -4,10 +4,10 @@ import React, { ReactElement, SyntheticEvent, } from 'react'; +import { styled } from '@mui/material/styles'; import PropTypes from 'prop-types'; -import { makeStyles } from '@material-ui/core/styles'; -import { alpha } from '@material-ui/core/styles/colorManipulator'; -import ActionDelete from '@material-ui/icons/Delete'; +import { alpha } from '@mui/material/styles'; +import ActionDelete from '@mui/icons-material/Delete'; import classnames from 'classnames'; import inflection from 'inflection'; import { @@ -25,6 +25,25 @@ import { import Confirm from '../layout/Confirm'; import Button, { ButtonProps } from './Button'; +const PREFIX = 'RaDeleteWithConfirmButton'; + +const classes = { + deleteButton: `${PREFIX}-deleteButton`, +}; + +const StyledButton = styled(Button)(({ theme }) => ({ + [`&.${classes.deleteButton}`]: { + color: theme.palette.error.main, + '&:hover': { + backgroundColor: alpha(theme.palette.error.main, 0.12), + // Reset on mouse devices + '@media (hover: none)': { + backgroundColor: 'transparent', + }, + }, + }, +})); + export const DeleteWithConfirmButton = ( props: DeleteWithConfirmButtonProps ) => { @@ -46,7 +65,7 @@ export const DeleteWithConfirmButton = ( ...rest } = props; const translate = useTranslate(); - const classes = useStyles(props); + const resource = useResourceContext(props); const mode = getMutationMode(mutationMode, undoable); @@ -69,7 +88,7 @@ export const DeleteWithConfirmButton = ( return ( <Fragment> - <Button + <StyledButton onClick={handleDialogOpen} label={label} className={classnames( @@ -81,7 +100,7 @@ export const DeleteWithConfirmButton = ( {...rest} > {icon} - </Button> + </StyledButton> <Confirm isOpen={open} loading={loading} @@ -109,22 +128,6 @@ export const DeleteWithConfirmButton = ( const defaultIcon = <ActionDelete />; -const useStyles = makeStyles( - theme => ({ - deleteButton: { - color: theme.palette.error.main, - '&:hover': { - backgroundColor: alpha(theme.palette.error.main, 0.12), - // Reset on mouse devices - '@media (hover: none)': { - backgroundColor: 'transparent', - }, - }, - }, - }), - { name: 'RaDeleteWithConfirmButton' } -); - interface Props { basePath?: string; classes?: object; diff --git a/packages/ra-ui-materialui/src/button/DeleteWithUndoButton.spec.tsx b/packages/ra-ui-materialui/src/button/DeleteWithUndoButton.spec.tsx index abb7a66d37e..98b35a72270 100644 --- a/packages/ra-ui-materialui/src/button/DeleteWithUndoButton.spec.tsx +++ b/packages/ra-ui-materialui/src/button/DeleteWithUndoButton.spec.tsx @@ -1,9 +1,9 @@ -import { render, waitFor, fireEvent } from '@testing-library/react'; import * as React from 'react'; +import { render, waitFor, fireEvent } from '@testing-library/react'; import expect from 'expect'; import { DataProvider, DataProviderContext } from 'ra-core'; import { renderWithRedux, TestContext } from 'ra-test'; -import { createTheme, ThemeProvider } from '@material-ui/core/styles'; +import { createTheme, ThemeProvider } from '@mui/material/styles'; import { Toolbar, SimpleForm } from '../form'; import { Edit } from '../detail'; import { TextInput } from '../input'; diff --git a/packages/ra-ui-materialui/src/button/DeleteWithUndoButton.tsx b/packages/ra-ui-materialui/src/button/DeleteWithUndoButton.tsx index 243d1e3b60d..12cbf3a1a6f 100644 --- a/packages/ra-ui-materialui/src/button/DeleteWithUndoButton.tsx +++ b/packages/ra-ui-materialui/src/button/DeleteWithUndoButton.tsx @@ -1,9 +1,9 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { ReactElement, ReactEventHandler, SyntheticEvent } from 'react'; import PropTypes from 'prop-types'; -import { makeStyles } from '@material-ui/core/styles'; -import { alpha } from '@material-ui/core/styles/colorManipulator'; -import ActionDelete from '@material-ui/icons/Delete'; +import { alpha } from '@mui/material/styles'; +import ActionDelete from '@mui/icons-material/Delete'; import classnames from 'classnames'; import { Record, @@ -16,6 +16,25 @@ import { import Button, { ButtonProps } from './Button'; +const PREFIX = 'RaDeleteWithUndoButton'; + +const classes = { + deleteButton: `${PREFIX}-deleteButton`, +}; + +const StyledButton = styled(Button)(({ theme }) => ({ + [`&.${classes.deleteButton}`]: { + color: theme.palette.error.main, + '&:hover': { + backgroundColor: alpha(theme.palette.error.main, 0.12), + // Reset on mouse devices + '@media (hover: none)': { + backgroundColor: 'transparent', + }, + }, + }, +})); + export const DeleteWithUndoButton = (props: DeleteWithUndoButtonProps) => { const { label = 'ra.action.delete', @@ -30,7 +49,7 @@ export const DeleteWithUndoButton = (props: DeleteWithUndoButtonProps) => { onFailure, ...rest } = props; - const classes = useStyles(props); + const resource = useResourceContext(props); const { loading, handleDelete } = useDeleteWithUndoController({ record, @@ -43,7 +62,7 @@ export const DeleteWithUndoButton = (props: DeleteWithUndoButtonProps) => { }); return ( - <Button + <StyledButton onClick={handleDelete} disabled={loading} label={label} @@ -56,26 +75,10 @@ export const DeleteWithUndoButton = (props: DeleteWithUndoButtonProps) => { {...rest} > {icon} - </Button> + </StyledButton> ); }; -const useStyles = makeStyles( - theme => ({ - deleteButton: { - color: theme.palette.error.main, - '&:hover': { - backgroundColor: alpha(theme.palette.error.main, 0.12), - // Reset on mouse devices - '@media (hover: none)': { - backgroundColor: 'transparent', - }, - }, - }, - }), - { name: 'RaDeleteWithUndoButton' } -); - interface Props { basePath?: string; classes?: object; diff --git a/packages/ra-ui-materialui/src/button/EditButton.tsx b/packages/ra-ui-materialui/src/button/EditButton.tsx index cfccdd1f3c9..6707681f5c3 100644 --- a/packages/ra-ui-materialui/src/button/EditButton.tsx +++ b/packages/ra-ui-materialui/src/button/EditButton.tsx @@ -1,8 +1,8 @@ import * as React from 'react'; import { ReactElement, useMemo } from 'react'; import PropTypes from 'prop-types'; -import ContentCreate from '@material-ui/icons/Create'; -import { ButtonProps as MuiButtonProps } from '@material-ui/core/Button'; +import ContentCreate from '@mui/icons-material/Create'; +import { ButtonProps as MuiButtonProps } from '@mui/material/Button'; import { Link } from 'react-router-dom'; import { linkToRecord, Record, useResourceContext } from 'ra-core'; diff --git a/packages/ra-ui-materialui/src/button/ExportButton.tsx b/packages/ra-ui-materialui/src/button/ExportButton.tsx index f36783d6fee..4f218794c02 100644 --- a/packages/ra-ui-materialui/src/button/ExportButton.tsx +++ b/packages/ra-ui-materialui/src/button/ExportButton.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { useCallback } from 'react'; import PropTypes from 'prop-types'; -import DownloadIcon from '@material-ui/icons/GetApp'; +import DownloadIcon from '@mui/icons-material/GetApp'; import { fetchRelatedRecords, useDataProvider, diff --git a/packages/ra-ui-materialui/src/button/IconButtonWithTooltip.tsx b/packages/ra-ui-materialui/src/button/IconButtonWithTooltip.tsx index 9629da1a973..e34fa2d9948 100644 --- a/packages/ra-ui-materialui/src/button/IconButtonWithTooltip.tsx +++ b/packages/ra-ui-materialui/src/button/IconButtonWithTooltip.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { MouseEvent } from 'react'; -import { IconButton, IconButtonProps, Tooltip } from '@material-ui/core'; +import { IconButton, IconButtonProps, Tooltip } from '@mui/material'; import { useTranslate } from 'ra-core'; /** @@ -41,6 +41,7 @@ export const IconButtonWithTooltip = ({ aria-label={translatedLabel} onClick={handleClick} {...props} + size="large" /> </Tooltip> ); diff --git a/packages/ra-ui-materialui/src/button/ListButton.tsx b/packages/ra-ui-materialui/src/button/ListButton.tsx index 0596c5ea7b7..671f7cda76d 100644 --- a/packages/ra-ui-materialui/src/button/ListButton.tsx +++ b/packages/ra-ui-materialui/src/button/ListButton.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { ReactElement } from 'react'; import PropTypes from 'prop-types'; -import ActionList from '@material-ui/icons/List'; +import ActionList from '@mui/icons-material/List'; import { Link } from 'react-router-dom'; import { useResourceContext } from 'ra-core'; diff --git a/packages/ra-ui-materialui/src/button/RefreshButton.tsx b/packages/ra-ui-materialui/src/button/RefreshButton.tsx index a21db20a855..fb80c899c90 100644 --- a/packages/ra-ui-materialui/src/button/RefreshButton.tsx +++ b/packages/ra-ui-materialui/src/button/RefreshButton.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { ReactElement, MouseEvent, useCallback } from 'react'; import PropTypes from 'prop-types'; import { useDispatch } from 'react-redux'; -import NavigationRefresh from '@material-ui/icons/Refresh'; +import NavigationRefresh from '@mui/icons-material/Refresh'; import { refreshView } from 'ra-core'; import Button, { ButtonProps } from './Button'; diff --git a/packages/ra-ui-materialui/src/button/RefreshIconButton.tsx b/packages/ra-ui-materialui/src/button/RefreshIconButton.tsx index 6b9c12103d9..b4e788d20b1 100644 --- a/packages/ra-ui-materialui/src/button/RefreshIconButton.tsx +++ b/packages/ra-ui-materialui/src/button/RefreshIconButton.tsx @@ -2,9 +2,9 @@ import * as React from 'react'; import { useCallback, ReactElement } from 'react'; import PropTypes from 'prop-types'; import { useDispatch } from 'react-redux'; -import Tooltip from '@material-ui/core/Tooltip'; -import IconButton, { IconButtonProps } from '@material-ui/core/IconButton'; -import NavigationRefresh from '@material-ui/icons/Refresh'; +import Tooltip from '@mui/material/Tooltip'; +import IconButton, { IconButtonProps } from '@mui/material/IconButton'; +import NavigationRefresh from '@mui/icons-material/Refresh'; import { refreshView, useTranslate } from 'ra-core'; const RefreshIconButton = (props: RefreshIconButtonProps) => { @@ -36,6 +36,7 @@ const RefreshIconButton = (props: RefreshIconButtonProps) => { color="inherit" onClick={handleClick} {...rest} + size="large" > {icon} </IconButton> diff --git a/packages/ra-ui-materialui/src/button/SaveButton.spec.tsx b/packages/ra-ui-materialui/src/button/SaveButton.spec.tsx index 64a7a95e605..1441939b68e 100644 --- a/packages/ra-ui-materialui/src/button/SaveButton.spec.tsx +++ b/packages/ra-ui-materialui/src/button/SaveButton.spec.tsx @@ -8,8 +8,7 @@ import { FormContextProvider, } from 'ra-core'; import { renderWithRedux, TestContext } from 'ra-test'; -import { ThemeProvider } from '@material-ui/core'; -import { createTheme } from '@material-ui/core/styles'; +import { createTheme, ThemeProvider } from '@mui/material/styles'; import SaveButton from './SaveButton'; import { Toolbar, SimpleForm } from '../form'; @@ -90,11 +89,13 @@ describe('<SaveButton />', () => { it('should render as submit type when submitOnEnter is true', () => { const { getByLabelText } = render( <TestContext> - <SaveContextProvider value={saveContextValue}> - <FormContextProvider value={formContextValue}> - <SaveButton submitOnEnter /> - </FormContextProvider> - </SaveContextProvider> + <ThemeProvider theme={theme}> + <SaveContextProvider value={saveContextValue}> + <FormContextProvider value={formContextValue}> + <SaveButton submitOnEnter /> + </FormContextProvider> + </SaveContextProvider> + </ThemeProvider> </TestContext> ); expect(getByLabelText('ra.action.save').getAttribute('type')).toEqual( @@ -105,11 +106,13 @@ describe('<SaveButton />', () => { it('should render as button type when submitOnEnter is false', () => { const { getByLabelText } = render( <TestContext> - <SaveContextProvider value={saveContextValue}> - <FormContextProvider value={formContextValue}> - <SaveButton submitOnEnter={false} /> - </FormContextProvider> - </SaveContextProvider> + <ThemeProvider theme={theme}> + <SaveContextProvider value={saveContextValue}> + <FormContextProvider value={formContextValue}> + <SaveButton submitOnEnter={false} /> + </FormContextProvider> + </SaveContextProvider> + </ThemeProvider> </TestContext> ); @@ -122,14 +125,16 @@ describe('<SaveButton />', () => { const onSubmit = jest.fn(); const { getByLabelText } = render( <TestContext> - <SaveContextProvider value={saveContextValue}> - <FormContextProvider value={formContextValue}> - <SaveButton - handleSubmitWithRedirect={onSubmit} - saving={false} - /> - </FormContextProvider> - </SaveContextProvider> + <ThemeProvider theme={theme}> + <SaveContextProvider value={saveContextValue}> + <FormContextProvider value={formContextValue}> + <SaveButton + handleSubmitWithRedirect={onSubmit} + saving={false} + /> + </FormContextProvider> + </SaveContextProvider> + </ThemeProvider> </TestContext> ); @@ -142,14 +147,16 @@ describe('<SaveButton />', () => { const { getByLabelText } = render( <TestContext> - <SaveContextProvider value={saveContextValue}> - <FormContextProvider value={formContextValue}> - <SaveButton - handleSubmitWithRedirect={onSubmit} - saving - /> - </FormContextProvider> - </SaveContextProvider> + <ThemeProvider theme={theme}> + <SaveContextProvider value={saveContextValue}> + <FormContextProvider value={formContextValue}> + <SaveButton + handleSubmitWithRedirect={onSubmit} + saving + /> + </FormContextProvider> + </SaveContextProvider> + </ThemeProvider> </TestContext> ); @@ -166,14 +173,16 @@ describe('<SaveButton />', () => { {({ store }) => { dispatchSpy = jest.spyOn(store, 'dispatch'); return ( - <SaveContextProvider value={saveContextValue}> - <FormContextProvider value={formContextValue}> - <SaveButton - handleSubmitWithRedirect={onSubmit} - invalid - /> - </FormContextProvider> - </SaveContextProvider> + <ThemeProvider theme={theme}> + <SaveContextProvider value={saveContextValue}> + <FormContextProvider value={formContextValue}> + <SaveButton + handleSubmitWithRedirect={onSubmit} + invalid + /> + </FormContextProvider> + </SaveContextProvider> + </ThemeProvider> ); }} </TestContext> @@ -230,13 +239,15 @@ describe('<SaveButton />', () => { getByLabelText, getByText, } = renderWithRedux( - <DataProviderContext.Provider value={dataProvider}> - <Edit {...defaultEditProps}> - <SimpleForm toolbar={<EditToolbar />}> - <TextInput source="title" /> - </SimpleForm> - </Edit> - </DataProviderContext.Provider>, + <ThemeProvider theme={theme}> + <DataProviderContext.Provider value={dataProvider}> + <Edit {...defaultEditProps}> + <SimpleForm toolbar={<EditToolbar />}> + <TextInput source="title" /> + </SimpleForm> + </Edit> + </DataProviderContext.Provider> + </ThemeProvider>, { admin: { resources: { posts: { data: {} } } } } ); // waitFor for the dataProvider.getOne() return @@ -275,13 +286,15 @@ describe('<SaveButton />', () => { getByLabelText, getByText, } = renderWithRedux( - <DataProviderContext.Provider value={dataProvider}> - <Edit {...defaultEditProps}> - <SimpleForm toolbar={<EditToolbar />}> - <TextInput source="title" /> - </SimpleForm> - </Edit> - </DataProviderContext.Provider>, + <ThemeProvider theme={theme}> + <DataProviderContext.Provider value={dataProvider}> + <Edit {...defaultEditProps}> + <SimpleForm toolbar={<EditToolbar />}> + <TextInput source="title" /> + </SimpleForm> + </Edit> + </DataProviderContext.Provider> + </ThemeProvider>, { admin: { resources: { posts: { data: {} } } } } ); // waitFor for the dataProvider.getOne() return @@ -325,13 +338,15 @@ describe('<SaveButton />', () => { getByLabelText, getByText, } = renderWithRedux( - <DataProviderContext.Provider value={dataProvider}> - <Edit {...defaultEditProps}> - <SimpleForm toolbar={<EditToolbar />}> - <TextInput source="title" /> - </SimpleForm> - </Edit> - </DataProviderContext.Provider>, + <ThemeProvider theme={theme}> + <DataProviderContext.Provider value={dataProvider}> + <Edit {...defaultEditProps}> + <SimpleForm toolbar={<EditToolbar />}> + <TextInput source="title" /> + </SimpleForm> + </Edit> + </DataProviderContext.Provider> + </ThemeProvider>, { admin: { resources: { posts: { data: {} } } } } ); // waitFor for the dataProvider.getOne() return diff --git a/packages/ra-ui-materialui/src/button/SaveButton.tsx b/packages/ra-ui-materialui/src/button/SaveButton.tsx index 7aac9e55a34..7d047d52564 100644 --- a/packages/ra-ui-materialui/src/button/SaveButton.tsx +++ b/packages/ra-ui-materialui/src/button/SaveButton.tsx @@ -1,9 +1,9 @@ import React, { cloneElement, ReactElement, SyntheticEvent } from 'react'; +import { styled } from '@mui/material/styles'; import PropTypes from 'prop-types'; -import Button, { ButtonProps } from '@material-ui/core/Button'; -import CircularProgress from '@material-ui/core/CircularProgress'; -import { makeStyles } from '@material-ui/core/styles'; -import ContentSave from '@material-ui/icons/Save'; +import Button, { ButtonProps } from '@mui/material/Button'; +import CircularProgress from '@mui/material/CircularProgress'; +import ContentSave from '@mui/icons-material/Save'; import classnames from 'classnames'; import { useTranslate, @@ -21,6 +21,28 @@ import { import { sanitizeButtonRestProps } from './Button'; import { FormRenderProps } from 'react-final-form'; +const PREFIX = 'RaSaveButton'; + +const classes = { + button: `${PREFIX}-button`, + leftIcon: `${PREFIX}-leftIcon`, + icon: `${PREFIX}-icon`, +}; + +const StyledButton = styled(Button)(({ theme }) => ({ + [`&.${classes.button}`]: { + position: 'relative', + }, + + [`& .${classes.leftIcon}`]: { + marginRight: theme.spacing(1), + }, + + [`& .${classes.icon}`]: { + fontSize: 18, + }, +})); + /** * Submit button for resource forms (Edit and Create). * @@ -61,7 +83,6 @@ import { FormRenderProps } from 'react-final-form'; const SaveButton = (props: SaveButtonProps) => { const { className, - classes: classesOverride, invalid, label = 'ra.action.save', disabled, @@ -78,7 +99,7 @@ const SaveButton = (props: SaveButtonProps) => { transform, ...rest } = props; - const classes = useStyles(props); + const notify = useNotify(); const translate = useTranslate(); const formContext = useFormContext(); @@ -146,12 +167,14 @@ const SaveButton = (props: SaveButtonProps) => { const type = submitOnEnter ? 'submit' : 'button'; const displayedLabel = label && translate(label, { _: label }); return ( - <Button + <StyledButton className={classnames(classes.button, className)} variant={variant} type={type} onClick={handleClick} - color={saving ? 'default' : 'primary'} + // TODO: find a way to display the loading state (LoadingButton from mui Lab?) + // This is because the "default" color does not exist anymore + color="primary" aria-label={displayedLabel} disabled={disabled} {...sanitizeButtonRestProps(rest)} @@ -168,27 +191,12 @@ const SaveButton = (props: SaveButtonProps) => { }) )} {displayedLabel} - </Button> + </StyledButton> ); }; const defaultIcon = <ContentSave />; -const useStyles = makeStyles( - theme => ({ - button: { - position: 'relative', - }, - leftIcon: { - marginRight: theme.spacing(1), - }, - icon: { - fontSize: 18, - }, - }), - { name: 'RaSaveButton' } -); - interface Props { classes?: object; className?: string; diff --git a/packages/ra-ui-materialui/src/button/ShowButton.tsx b/packages/ra-ui-materialui/src/button/ShowButton.tsx index 2a6a25d5c09..43f58cb2519 100644 --- a/packages/ra-ui-materialui/src/button/ShowButton.tsx +++ b/packages/ra-ui-materialui/src/button/ShowButton.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { memo, useMemo, ReactElement } from 'react'; import PropTypes from 'prop-types'; -import ImageEye from '@material-ui/icons/RemoveRedEye'; +import ImageEye from '@mui/icons-material/RemoveRedEye'; import { Link } from 'react-router-dom'; import { linkToRecord, Record, useResourceContext } from 'ra-core'; diff --git a/packages/ra-ui-materialui/src/button/SkipNavigationButton.tsx b/packages/ra-ui-materialui/src/button/SkipNavigationButton.tsx index a0e276cbfdf..34aebc1f069 100644 --- a/packages/ra-ui-materialui/src/button/SkipNavigationButton.tsx +++ b/packages/ra-ui-materialui/src/button/SkipNavigationButton.tsx @@ -1,9 +1,42 @@ import React from 'react'; -import { makeStyles } from '@material-ui/core/styles'; +import { styled } from '@mui/material/styles'; import Button from './Button'; import { useTranslate } from 'ra-core'; import classnames from 'classnames'; +const PREFIX = 'RaSkipToContentButton'; + +const classes = { + skipToContentButton: `${PREFIX}-skipToContentButton`, +}; + +const StyledButton = styled(Button)(({ theme }) => ({ + [`&.${classes.skipToContentButton}`]: { + position: 'fixed', + padding: theme.spacing(1), + backgroundColor: theme.palette.background.default, + color: theme.palette.getContrastText(theme.palette.background.default), + transition: theme.transitions.create(['top', 'opacity'], { + easing: theme.transitions.easing.easeIn, + duration: theme.transitions.duration.leavingScreen, + }), + left: theme.spacing(2), + top: theme.spacing(-10), + zIndex: 5000, + '&:hover': { + opacity: 0.8, + backgroundColor: theme.palette.background.default, + }, + '&:focus': { + top: theme.spacing(2), + transition: theme.transitions.create(['top', 'opacity'], { + easing: theme.transitions.easing.easeOut, + duration: theme.transitions.duration.enteringScreen, + }), + }, + }, +})); + const skipToContent = () => { if (typeof document === 'undefined') return; const element = document.getElementById('main-content'); @@ -24,44 +57,11 @@ const skipToContent = () => { element.removeAttribute('tabIndex'); }; -const useStyles = makeStyles( - theme => ({ - skipToContentButton: { - position: 'fixed', - padding: theme.spacing(1), - backgroundColor: theme.palette.background.default, - color: theme.palette.getContrastText( - theme.palette.background.default - ), - transition: theme.transitions.create(['top', 'opacity'], { - easing: theme.transitions.easing.easeIn, - duration: theme.transitions.duration.leavingScreen, - }), - left: theme.spacing(2), - top: theme.spacing(-10), - zIndex: 5000, - '&:hover': { - opacity: 0.8, - backgroundColor: theme.palette.background.default, - }, - '&:focus': { - top: theme.spacing(2), - transition: theme.transitions.create(['top', 'opacity'], { - easing: theme.transitions.easing.easeOut, - duration: theme.transitions.duration.enteringScreen, - }), - }, - }, - }), - { name: 'RaSkipToContentButton' } -); - const SkipNavigationButton = () => { - const classes = useStyles(); const translate = useTranslate(); return ( - <Button + <StyledButton onClick={skipToContent} className={classnames( classes.skipToContentButton, diff --git a/packages/ra-ui-materialui/src/button/SortButton.tsx b/packages/ra-ui-materialui/src/button/SortButton.tsx index fbc48019ff9..f9887876cce 100644 --- a/packages/ra-ui-materialui/src/button/SortButton.tsx +++ b/packages/ra-ui-materialui/src/button/SortButton.tsx @@ -8,9 +8,9 @@ import { IconButton, useMediaQuery, Theme, -} from '@material-ui/core'; -import SortIcon from '@material-ui/icons/Sort'; -import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown'; +} from '@mui/material'; +import SortIcon from '@mui/icons-material/Sort'; +import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown'; import { shallowEqual } from 'react-redux'; import { useListSortContext, @@ -48,7 +48,7 @@ const SortButton = (props: SortButtonProps) => { const { resource, currentSort, setSort } = useListSortContext(); const translate = useTranslate(); const isXSmall = useMediaQuery((theme: Theme) => - theme.breakpoints.down('xs') + theme.breakpoints.down('sm') ); const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null); const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => { @@ -91,6 +91,7 @@ const SortButton = (props: SortButtonProps) => { aria-label={buttonLabel} color="primary" onClick={handleClick} + size="large" > {icon} </IconButton> diff --git a/packages/ra-ui-materialui/src/defaultTheme.ts b/packages/ra-ui-materialui/src/defaultTheme.ts index 31f46694e0e..9271c823380 100644 --- a/packages/ra-ui-materialui/src/defaultTheme.ts +++ b/packages/ra-ui-materialui/src/defaultTheme.ts @@ -1,5 +1,4 @@ -import { ThemeOptions } from '@material-ui/core'; -import { Overrides } from '@material-ui/core/styles/overrides'; +import { ThemeOptions } from '@mui/material'; export default { palette: { @@ -19,54 +18,49 @@ export default { width: 240, closedWidth: 55, }, - overrides: { - MuiFilledInput: { - root: { - backgroundColor: 'rgba(0, 0, 0, 0.04)', - '&$disabled': { - backgroundColor: 'rgba(0, 0, 0, 0.04)', + components: { + MuiButtonBase: { + defaultProps: { + // disable ripple for perf reasons + disableRipple: true, + }, + styleOverrides: { + root: { + '&:hover:active::after': { + // recreate a static ripple color + // use the currentColor to make it work both for outlined and contained buttons + // but to dim the background without dimming the text, + // put another element on top with a limited opacity + content: '""', + display: 'block', + width: '100%', + height: '100%', + position: 'absolute', + top: 0, + right: 0, + backgroundColor: 'currentColor', + opacity: 0.3, + borderRadius: 'inherit', + }, }, }, }, - MuiButtonBase: { - root: { - '&:hover:active::after': { - // recreate a static ripple color - // use the currentColor to make it work both for outlined and contained buttons - // but to dim the background without dimming the text, - // put another element on top with a limited opacity - content: '""', - display: 'block', - width: '100%', - height: '100%', - position: 'absolute', - top: 0, - right: 0, - backgroundColor: 'currentColor', - opacity: 0.3, - borderRadius: 'inherit', + MuiFilledInput: { + styleOverrides: { + root: { + backgroundColor: 'rgba(0, 0, 0, 0.04)', + '&$disabled': { + backgroundColor: 'rgba(0, 0, 0, 0.04)', + }, }, }, }, }, - props: { - MuiButtonBase: { - // disable ripple for perf reasons - disableRipple: true, - }, - }, }; -// Temporary solution until we specify our components in it like MUI does -// See https://github.com/mui-org/material-ui/blob/master/packages/material-ui/src/styles/overrides.d.ts#L103 -export interface RaThemeOverrides extends Overrides { - [key: string]: any; -} - export interface RaThemeOptions extends ThemeOptions { sidebar?: { width?: number; closedWidth?: number; }; - overrides?: RaThemeOverrides; } diff --git a/packages/ra-ui-materialui/src/detail/CreateActions.tsx b/packages/ra-ui-materialui/src/detail/CreateActions.tsx index 9642f185edf..fafc3e106e3 100644 --- a/packages/ra-ui-materialui/src/detail/CreateActions.tsx +++ b/packages/ra-ui-materialui/src/detail/CreateActions.tsx @@ -13,7 +13,7 @@ import { useCreateContext, useResourceDefinition } from 'ra-core'; * use it in the `actions` prop to pass a custom component. * * @example - * import Button from '@material-ui/core/Button'; + * import Button from '@mui/material/Button'; * import { TopToolbar, Create, ListButton } from 'react-admin'; * * const PostCreateActions = ({ basePath }) => ( diff --git a/packages/ra-ui-materialui/src/detail/CreateView.tsx b/packages/ra-ui-materialui/src/detail/CreateView.tsx index 94bc0354533..36fbcc8eb24 100644 --- a/packages/ra-ui-materialui/src/detail/CreateView.tsx +++ b/packages/ra-ui-materialui/src/detail/CreateView.tsx @@ -1,27 +1,51 @@ import * as React from 'react'; import { Children, cloneElement, ReactElement } from 'react'; import PropTypes from 'prop-types'; +import { Card } from '@mui/material'; +import { styled } from '@mui/material/styles'; import { CreateControllerProps, useCreateContext } from 'ra-core'; -import { Card } from '@material-ui/core'; -import { makeStyles } from '@material-ui/core/styles'; import classnames from 'classnames'; import { CreateProps } from '../types'; import { TitleForRecord } from '../layout'; +const PREFIX = 'RaCreate'; + +const classes = { + root: `${PREFIX}-root`, + main: `${PREFIX}-main`, + noActions: `${PREFIX}-noActions`, + card: `${PREFIX}-card`, +}; + +const Root = styled('div')(({ theme }) => ({ + [`&.${classes.root}`]: {}, + + [`& .${classes.main}`]: { + display: 'flex', + }, + + [`& .${classes.noActions}`]: { + [theme.breakpoints.up('sm')]: { + marginTop: '1em', + }, + }, + + [`& .${classes.card}`]: { + flex: '1 1 auto', + }, +})); + export const CreateView = (props: CreateViewProps) => { const { actions, aside, children, - classes: classesOverride, className, component: Content, title, ...rest } = props; - const classes = useStyles(props); - const { basePath, defaultTitle, @@ -35,7 +59,7 @@ export const CreateView = (props: CreateViewProps) => { } = useCreateContext(props); return ( - <div + <Root className={classnames('create-page', classes.root, className)} {...sanitizeRestProps(rest)} > @@ -87,7 +111,7 @@ export const CreateView = (props: CreateViewProps) => { version, })} </div> - </div> + </Root> ); }; @@ -102,7 +126,6 @@ CreateView.propTypes = { aside: PropTypes.element, basePath: PropTypes.string, children: PropTypes.element, - classes: PropTypes.object, className: PropTypes.string, defaultTitle: PropTypes.any, hasList: PropTypes.bool, @@ -120,28 +143,9 @@ CreateView.propTypes = { }; CreateView.defaultProps = { - classes: {}, component: Card, }; -const useStyles = makeStyles( - theme => ({ - root: {}, - main: { - display: 'flex', - }, - noActions: { - [theme.breakpoints.up('sm')]: { - marginTop: '1em', - }, - }, - card: { - flex: '1 1 auto', - }, - }), - { name: 'RaCreate' } -); - const sanitizeRestProps = ({ basePath = null, defaultTitle = null, diff --git a/packages/ra-ui-materialui/src/detail/EditActions.tsx b/packages/ra-ui-materialui/src/detail/EditActions.tsx index 814418d99c4..1bbcace2c90 100644 --- a/packages/ra-ui-materialui/src/detail/EditActions.tsx +++ b/packages/ra-ui-materialui/src/detail/EditActions.tsx @@ -13,7 +13,7 @@ import TopToolbar from '../layout/TopToolbar'; * use it in the `actions` prop to pass a custom component. * * @example - * import Button from '@material-ui/core/Button'; + * import Button from '@mui/material/Button'; * import { TopToolbar, ShowButton, Edit } from 'react-admin'; * * const PostEditActions = ({ basePath, record, resource }) => ( diff --git a/packages/ra-ui-materialui/src/detail/EditView.tsx b/packages/ra-ui-materialui/src/detail/EditView.tsx index 1e3c632995a..dc00b354326 100644 --- a/packages/ra-ui-materialui/src/detail/EditView.tsx +++ b/packages/ra-ui-materialui/src/detail/EditView.tsx @@ -1,9 +1,8 @@ import * as React from 'react'; import { Children, cloneElement, ReactElement } from 'react'; +import { styled } from '@mui/material/styles'; import PropTypes from 'prop-types'; -import Card from '@material-ui/core/Card'; -import CardContent from '@material-ui/core/CardContent'; -import { makeStyles } from '@material-ui/core/styles'; +import { Card, CardContent } from '@mui/material'; import classnames from 'classnames'; import { EditControllerProps, @@ -15,12 +14,33 @@ import { EditActions as DefaultActions } from './EditActions'; import TitleForRecord from '../layout/TitleForRecord'; import { EditProps } from '../types'; +const PREFIX = 'RaEdit'; + +const classes = { + root: `${PREFIX}-root`, + main: `${PREFIX}-main`, + noActions: `${PREFIX}-noActions`, + card: `${PREFIX}-card`, +}; + +const Root = styled('div')({ + [`&.${classes.root}`]: {}, + [`& .${classes.main}`]: { + display: 'flex', + }, + [`& .${classes.noActions}`]: { + marginTop: '1em', + }, + [`& .${classes.card}`]: { + flex: '1 1 auto', + }, +}); + export const EditView = (props: EditViewProps) => { const { actions, aside, children, - classes: classesOverride, className, component: Content, title, @@ -29,8 +49,6 @@ export const EditView = (props: EditViewProps) => { ...rest } = props; - const classes = useStyles(props); - const { basePath, defaultTitle, @@ -54,7 +72,7 @@ export const EditView = (props: EditViewProps) => { return null; } return ( - <div + <Root className={classnames('edit-page', classes.root, className)} {...sanitizeRestProps(rest)} > @@ -114,7 +132,7 @@ export const EditView = (props: EditViewProps) => { saving, })} </div> - </div> + </Root> ); }; @@ -129,7 +147,6 @@ EditView.propTypes = { aside: PropTypes.element, basePath: PropTypes.string, children: PropTypes.element, - classes: PropTypes.object, className: PropTypes.string, component: ComponentPropType, defaultTitle: PropTypes.any, @@ -151,26 +168,9 @@ EditView.propTypes = { }; EditView.defaultProps = { - classes: {}, component: Card, }; -const useStyles = makeStyles( - { - root: {}, - main: { - display: 'flex', - }, - noActions: { - marginTop: '1em', - }, - card: { - flex: '1 1 auto', - }, - }, - { name: 'RaEdit' } -); - const sanitizeRestProps = ({ basePath = null, defaultTitle = null, diff --git a/packages/ra-ui-materialui/src/detail/ShowActions.tsx b/packages/ra-ui-materialui/src/detail/ShowActions.tsx index 928ceb1d630..78a4409713e 100644 --- a/packages/ra-ui-materialui/src/detail/ShowActions.tsx +++ b/packages/ra-ui-materialui/src/detail/ShowActions.tsx @@ -22,7 +22,7 @@ const sanitizeRestProps = ({ * use it in the `actions` prop to pass a custom component. * * @example - * import Button from '@material-ui/core/Button'; + * import Button from '@mui/material/Button'; * import { TopToolbar, EditButton, Show } from 'react-admin'; * * const PostShowActions = ({ basePath, record, resource }) => ( diff --git a/packages/ra-ui-materialui/src/detail/ShowView.tsx b/packages/ra-ui-materialui/src/detail/ShowView.tsx index 2e99756b747..a3bc23eb49b 100644 --- a/packages/ra-ui-materialui/src/detail/ShowView.tsx +++ b/packages/ra-ui-materialui/src/detail/ShowView.tsx @@ -1,8 +1,8 @@ import * as React from 'react'; import { cloneElement, Children, ReactElement } from 'react'; import PropTypes from 'prop-types'; -import Card from '@material-ui/core/Card'; -import { makeStyles } from '@material-ui/core/styles'; +import { styled } from '@mui/material/styles'; +import { Card } from '@mui/material'; import classnames from 'classnames'; import { ShowControllerProps, @@ -14,20 +14,39 @@ import { ShowActions as DefaultActions } from './ShowActions'; import TitleForRecord from '../layout/TitleForRecord'; import { ShowProps } from '../types'; +const PREFIX = 'RaShow'; + +const classes = { + root: `${PREFIX}-root`, + main: `${PREFIX}-main`, + noActions: `${PREFIX}-noActions`, + card: `${PREFIX}-card`, +}; + +const Root = styled('div')({ + [`&.${classes.root}`]: {}, + [`& .${classes.main}`]: { + display: 'flex', + }, + [`& .${classes.noActions}`]: { + marginTop: '1em', + }, + [`& .${classes.card}`]: { + flex: '1 1 auto', + }, +}); + export const ShowView = (props: ShowViewProps) => { const { actions, aside, children, - classes: classesOverride, className, component: Content, title, ...rest } = props; - const classes = useStyles(props); - const { basePath, defaultTitle, @@ -49,7 +68,7 @@ export const ShowView = (props: ShowViewProps) => { return null; } return ( - <div + <Root className={classnames('show-page', classes.root, className)} {...sanitizeRestProps(rest)} > @@ -90,7 +109,7 @@ export const ShowView = (props: ShowViewProps) => { version, })} </div> - </div> + </Root> ); }; @@ -105,7 +124,6 @@ ShowView.propTypes = { aside: PropTypes.element, basePath: PropTypes.string, children: PropTypes.element, - classes: PropTypes.object, className: PropTypes.string, defaultTitle: PropTypes.any, hasEdit: PropTypes.bool, @@ -119,26 +137,9 @@ ShowView.propTypes = { }; ShowView.defaultProps = { - classes: {}, component: Card, }; -const useStyles = makeStyles( - { - root: {}, - main: { - display: 'flex', - }, - noActions: { - marginTop: '1em', - }, - card: { - flex: '1 1 auto', - }, - }, - { name: 'RaShow' } -); - const sanitizeRestProps = ({ basePath = null, defaultTitle = null, diff --git a/packages/ra-ui-materialui/src/detail/Tab.tsx b/packages/ra-ui-materialui/src/detail/Tab.tsx index 3d75220a1a2..020bd5c0f49 100644 --- a/packages/ra-ui-materialui/src/detail/Tab.tsx +++ b/packages/ra-ui-materialui/src/detail/Tab.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { isValidElement, ReactElement, ReactNode } from 'react'; import PropTypes from 'prop-types'; import { Link, useLocation } from 'react-router-dom'; -import MuiTab, { TabProps as MuiTabProps } from '@material-ui/core/Tab'; +import { Tab as MuiTab, TabProps as MuiTabProps } from '@mui/material'; import { useTranslate, Record } from 'ra-core'; import classnames from 'classnames'; @@ -20,8 +20,8 @@ import Labeled from '../input/Labeled'; * @example * // in src/posts.js * import * as React from "react"; - * import FavoriteIcon from '@material-ui/icons/Favorite'; - * import PersonPinIcon from '@material-ui/icons/PersonPin'; + * import FavoriteIcon from '@mui/icons-material/Favorite'; + * import PersonPinIcon from '@mui/icons-material/PersonPin'; * import { Show, TabbedShowLayout, Tab, TextField } from 'react-admin'; * * export const PostShow = (props) => ( @@ -135,7 +135,7 @@ Tab.propTypes = { value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), }; -export interface TabProps extends MuiTabProps { +export interface TabProps extends Omit<MuiTabProps, 'children'> { basePath?: string; children: ReactNode; contentClassName?: string; diff --git a/packages/ra-ui-materialui/src/detail/TabbedShowLayout.tsx b/packages/ra-ui-materialui/src/detail/TabbedShowLayout.tsx index 061e970bd04..634739d6bfd 100644 --- a/packages/ra-ui-materialui/src/detail/TabbedShowLayout.tsx +++ b/packages/ra-ui-materialui/src/detail/TabbedShowLayout.tsx @@ -9,14 +9,27 @@ import { useState, } from 'react'; import PropTypes from 'prop-types'; -import Divider from '@material-ui/core/Divider'; +import { styled } from '@mui/material/styles'; +import { Divider } from '@mui/material'; import { Route } from 'react-router-dom'; -import { makeStyles } from '@material-ui/core/styles'; import { useRouteMatch } from 'react-router-dom'; import { escapePath, Record } from 'ra-core'; import { TabbedShowLayoutTabs, getTabFullPath } from './TabbedShowLayoutTabs'; -import { ClassesOverride } from '../types'; + +const PREFIX = 'RaTabbedShowLayout'; + +const classes = { + content: `${PREFIX}-content`, +}; + +const Root = styled('div')(({ theme }) => ({ + [`& .${classes.content}`]: { + paddingTop: theme.spacing(1), + paddingLeft: theme.spacing(2), + paddingRight: theme.spacing(2), + }, +})); const sanitizeRestProps = ({ children, @@ -32,17 +45,6 @@ const sanitizeRestProps = ({ ...rest }: any) => rest; -const useStyles = makeStyles( - theme => ({ - content: { - paddingTop: theme.spacing(1), - paddingLeft: theme.spacing(2), - paddingRight: theme.spacing(2), - }, - }), - { name: 'RaTabbedShowLayout' } -); - /** * Tabbed Layout for a Show view, showing fields grouped in tabs. * @@ -86,7 +88,6 @@ export const TabbedShowLayout = (props: TabbedShowLayoutProps) => { const { basePath, children, - classes: classesOverride, className, record, resource, @@ -97,7 +98,7 @@ export const TabbedShowLayout = (props: TabbedShowLayoutProps) => { ...rest } = props; const match = useRouteMatch(); - const classes = useStyles(props); + const nonNullChildren = Children.toArray(children).filter( child => child !== null ); @@ -110,7 +111,7 @@ export const TabbedShowLayout = (props: TabbedShowLayoutProps) => { }; return ( - <div className={className} key={version} {...sanitizeRestProps(rest)}> + <Root className={className} key={version} {...sanitizeRestProps(rest)}> {cloneElement( tabs, { @@ -151,14 +152,14 @@ export const TabbedShowLayout = (props: TabbedShowLayoutProps) => { ) : null )} </div> - </div> + </Root> ); }; export interface TabbedShowLayoutProps { basePath?: string; className?: string; - classes?: ClassesOverride<typeof useStyles>; + children: ReactNode; record?: Record; resource?: string; diff --git a/packages/ra-ui-materialui/src/detail/TabbedShowLayoutTabs.tsx b/packages/ra-ui-materialui/src/detail/TabbedShowLayoutTabs.tsx index 3830381c649..9cd7527d88d 100644 --- a/packages/ra-ui-materialui/src/detail/TabbedShowLayoutTabs.tsx +++ b/packages/ra-ui-materialui/src/detail/TabbedShowLayoutTabs.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { Children, cloneElement, ReactElement, isValidElement } from 'react'; import PropTypes from 'prop-types'; -import Tabs, { TabsProps } from '@material-ui/core/Tabs'; +import Tabs, { TabsProps } from '@mui/material/Tabs'; import { useLocation, useRouteMatch } from 'react-router-dom'; import { TabProps } from './Tab'; diff --git a/packages/ra-ui-materialui/src/field/BooleanField.spec.tsx b/packages/ra-ui-materialui/src/field/BooleanField.spec.tsx index 7622f965a02..ca448322251 100644 --- a/packages/ra-ui-materialui/src/field/BooleanField.spec.tsx +++ b/packages/ra-ui-materialui/src/field/BooleanField.spec.tsx @@ -13,76 +13,76 @@ const defaultProps = { describe('<BooleanField />', () => { it('should display tick and truthy text if value is true', () => { - const { queryByTitle } = render(<BooleanField {...defaultProps} />); - expect(queryByTitle('ra.boolean.true')).not.toBeNull(); + const { queryByLabelText } = render(<BooleanField {...defaultProps} />); + expect(queryByLabelText('ra.boolean.true')).not.toBeNull(); expect( - (queryByTitle('ra.boolean.true').firstChild as HTMLElement).dataset - .testid + (queryByLabelText('ra.boolean.true').firstChild as HTMLElement) + .dataset.testid ).toBe('true'); - expect(queryByTitle('ra.boolean.false')).toBeNull(); + expect(queryByLabelText('ra.boolean.false')).toBeNull(); }); it('should use record from RecordContext', () => { - const { queryByTitle } = render( + const { queryByLabelText } = render( <RecordContextProvider value={{ id: 123, published: true }}> <BooleanField source="published" /> </RecordContextProvider> ); - expect(queryByTitle('ra.boolean.true')).not.toBeNull(); + expect(queryByLabelText('ra.boolean.true')).not.toBeNull(); expect( - (queryByTitle('ra.boolean.true').firstChild as HTMLElement).dataset - .testid + (queryByLabelText('ra.boolean.true').firstChild as HTMLElement) + .dataset.testid ).toBe('true'); - expect(queryByTitle('ra.boolean.false')).toBeNull(); + expect(queryByLabelText('ra.boolean.false')).toBeNull(); }); it('should use valueLabelTrue for custom truthy text', () => { - const { queryByTitle } = render( + const { queryByLabelText } = render( <BooleanField {...defaultProps} valueLabelTrue="Has been published" /> ); - expect(queryByTitle('ra.boolean.true')).toBeNull(); - expect(queryByTitle('Has been published')).not.toBeNull(); + expect(queryByLabelText('ra.boolean.true')).toBeNull(); + expect(queryByLabelText('Has been published')).not.toBeNull(); }); it('should display cross and falsy text if value is false', () => { - const { queryByTitle } = render( + const { queryByLabelText } = render( <BooleanField {...defaultProps} record={{ id: 123, published: false }} /> ); - expect(queryByTitle('ra.boolean.true')).toBeNull(); - expect(queryByTitle('ra.boolean.false')).not.toBeNull(); + expect(queryByLabelText('ra.boolean.true')).toBeNull(); + expect(queryByLabelText('ra.boolean.false')).not.toBeNull(); expect( - (queryByTitle('ra.boolean.false').firstChild as HTMLElement).dataset - .testid + (queryByLabelText('ra.boolean.false').firstChild as HTMLElement) + .dataset.testid ).toBe('false'); }); it('should use valueLabelFalse for custom falsy text', () => { - const { queryByTitle } = render( + const { queryByLabelText } = render( <BooleanField {...defaultProps} record={{ id: 123, published: false }} valueLabelFalse="Has not been published" /> ); - expect(queryByTitle('ra.boolean.false')).toBeNull(); - expect(queryByTitle('Has not been published')).not.toBeNull(); + expect(queryByLabelText('ra.boolean.false')).toBeNull(); + expect(queryByLabelText('Has not been published')).not.toBeNull(); }); it('should not display anything if value is null', () => { - const { queryByTitle } = render( + const { queryByLabelText } = render( <BooleanField {...defaultProps} record={{ id: 123, published: null }} /> ); - expect(queryByTitle('ra.boolean.true')).toBeNull(); - expect(queryByTitle('ra.boolean.false')).toBeNull(); + expect(queryByLabelText('ra.boolean.true')).toBeNull(); + expect(queryByLabelText('ra.boolean.false')).toBeNull(); }); it('should display tick and truthy text if looseValue is true and value is truthy', () => { @@ -92,15 +92,15 @@ describe('<BooleanField />', () => { resource: 'posts', classes: {}, }; - const { queryByTitle } = render( + const { queryByLabelText } = render( <BooleanField {...defaultProps} looseValue /> ); - expect(queryByTitle('ra.boolean.true')).not.toBeNull(); + expect(queryByLabelText('ra.boolean.true')).not.toBeNull(); expect( - (queryByTitle('ra.boolean.true').firstChild as HTMLElement).dataset - .testid + (queryByLabelText('ra.boolean.true').firstChild as HTMLElement) + .dataset.testid ).toBe('true'); - expect(queryByTitle('ra.boolean.false')).toBeNull(); + expect(queryByLabelText('ra.boolean.false')).toBeNull(); }); it('should display cross and falsy text if looseValue is true and value is falsy', () => { @@ -111,29 +111,29 @@ describe('<BooleanField />', () => { classes: {}, }; - const { queryByTitle } = render( + const { queryByLabelText } = render( <BooleanField {...defaultProps} looseValue /> ); - expect(queryByTitle('ra.boolean.false')).not.toBeNull(); + expect(queryByLabelText('ra.boolean.false')).not.toBeNull(); expect( - (queryByTitle('ra.boolean.false').firstChild as HTMLElement).dataset - .testid + (queryByLabelText('ra.boolean.false').firstChild as HTMLElement) + .dataset.testid ).toBe('false'); - expect(queryByTitle('ra.boolean.true')).toBeNull(); + expect(queryByLabelText('ra.boolean.true')).toBeNull(); }); it.each([null, undefined])( 'should display the emptyText when is present and the value is %s', published => { - const { queryByTitle, queryByText } = render( + const { queryByLabelText, queryByText } = render( <BooleanField {...defaultProps} record={{ id: 123, published }} emptyText="NA" /> ); - expect(queryByTitle('ra.boolean.true')).toBeNull(); - expect(queryByTitle('ra.boolean.false')).toBeNull(); + expect(queryByLabelText('ra.boolean.true')).toBeNull(); + expect(queryByLabelText('ra.boolean.false')).toBeNull(); expect(queryByText('NA')).not.toBeNull(); } ); @@ -150,13 +150,13 @@ describe('<BooleanField />', () => { }); it('should handle deep fields', () => { - const { queryByTitle } = render( + const { queryByLabelText } = render( <BooleanField {...defaultProps} record={{ id: 123, foo: { bar: true } }} source="foo.bar" /> ); - expect(queryByTitle('ra.boolean.true')).not.toBeNull(); + expect(queryByLabelText('ra.boolean.true')).not.toBeNull(); }); }); diff --git a/packages/ra-ui-materialui/src/field/BooleanField.tsx b/packages/ra-ui-materialui/src/field/BooleanField.tsx index 13b7bace739..deb01ec08e7 100644 --- a/packages/ra-ui-materialui/src/field/BooleanField.tsx +++ b/packages/ra-ui-materialui/src/field/BooleanField.tsx @@ -1,29 +1,29 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { memo, FC } from 'react'; -import { SvgIconComponent } from '@material-ui/icons'; +import { SvgIconComponent } from '@mui/icons-material'; import PropTypes from 'prop-types'; import get from 'lodash/get'; import classnames from 'classnames'; -import DoneIcon from '@material-ui/icons/Done'; -import ClearIcon from '@material-ui/icons/Clear'; -import { Tooltip, Typography } from '@material-ui/core'; -import { makeStyles } from '@material-ui/core/styles'; -import { TypographyProps } from '@material-ui/core/Typography'; +import DoneIcon from '@mui/icons-material/Done'; +import ClearIcon from '@mui/icons-material/Clear'; +import { Tooltip, Typography, TypographyProps } from '@mui/material'; import { useTranslate, useRecordContext } from 'ra-core'; import { PublicFieldProps, InjectedFieldProps, fieldPropTypes } from './types'; import sanitizeFieldRestProps from './sanitizeFieldRestProps'; -const useStyles = makeStyles( - { - root: { - display: 'flex', - }, +const PREFIX = 'RaBooleanField'; + +const classes = { + root: `${PREFIX}-root`, +}; + +const StyledTypography = styled(Typography)({ + [`&.${classes.root}`]: { + display: 'flex', }, - { - name: 'RaBooleanField', - } -); +}); const BooleanField: FC<BooleanFieldProps> = memo(props => { const { @@ -40,7 +40,7 @@ const BooleanField: FC<BooleanFieldProps> = memo(props => { } = props; const record = useRecordContext(props); const translate = useTranslate(); - const classes = useStyles(props); + const value = get(record, source); const isTruthyValue = value === true || (looseValue && value); let ariaLabel = value ? valueLabelTrue : valueLabelFalse; @@ -51,7 +51,7 @@ const BooleanField: FC<BooleanFieldProps> = memo(props => { if (looseValue || value === false || value === true) { return ( - <Typography + <StyledTypography component="span" variant="body2" className={classnames(classes.root, className)} @@ -68,7 +68,7 @@ const BooleanField: FC<BooleanFieldProps> = memo(props => { </span> )} </Tooltip> - </Typography> + </StyledTypography> ); } @@ -107,7 +107,7 @@ BooleanField.displayName = 'BooleanField'; export interface BooleanFieldProps extends PublicFieldProps, InjectedFieldProps, - TypographyProps { + Omit<TypographyProps, 'textAlign'> { valueLabelTrue?: string; valueLabelFalse?: string; TrueIcon?: SvgIconComponent; diff --git a/packages/ra-ui-materialui/src/field/ChipField.tsx b/packages/ra-ui-materialui/src/field/ChipField.tsx index f178b8816c2..9cc2120cb96 100644 --- a/packages/ra-ui-materialui/src/field/ChipField.tsx +++ b/packages/ra-ui-materialui/src/field/ChipField.tsx @@ -1,21 +1,24 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { memo, FC } from 'react'; import get from 'lodash/get'; -import Chip, { ChipProps } from '@material-ui/core/Chip'; -import Typography from '@material-ui/core/Typography'; -import { makeStyles } from '@material-ui/core/styles'; +import Chip, { ChipProps } from '@mui/material/Chip'; +import Typography from '@mui/material/Typography'; import classnames from 'classnames'; import { useRecordContext } from 'ra-core'; import sanitizeFieldRestProps from './sanitizeFieldRestProps'; import { PublicFieldProps, InjectedFieldProps, fieldPropTypes } from './types'; -const useStyles = makeStyles( - { - chip: { margin: 4, cursor: 'inherit' }, - }, - { name: 'RaChipField' } -); +const PREFIX = 'RaChipField'; + +const classes = { + chip: `${PREFIX}-chip`, +}; + +const StyledChip = styled(Chip)({ + [`&.${classes.chip}`]: { margin: 4, cursor: 'inherit' }, +}); export const ChipField: FC<ChipFieldProps> = memo(props => { const { @@ -26,7 +29,7 @@ export const ChipField: FC<ChipFieldProps> = memo(props => { ...rest } = props; const record = useRecordContext(props); - const classes = useStyles(props); + const value = get(record, source); if (value == null && emptyText) { @@ -43,7 +46,7 @@ export const ChipField: FC<ChipFieldProps> = memo(props => { } return ( - <Chip + <StyledChip className={classnames(classes.chip, className)} label={value} {...sanitizeFieldRestProps(rest)} diff --git a/packages/ra-ui-materialui/src/field/DateField.tsx b/packages/ra-ui-materialui/src/field/DateField.tsx index 9d5f715b407..0a3d84b2ef4 100644 --- a/packages/ra-ui-materialui/src/field/DateField.tsx +++ b/packages/ra-ui-materialui/src/field/DateField.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { memo, FC } from 'react'; import PropTypes from 'prop-types'; import get from 'lodash/get'; -import Typography, { TypographyProps } from '@material-ui/core/Typography'; +import { Typography, TypographyProps } from '@mui/material'; import { useRecordContext } from 'ra-core'; import sanitizeFieldRestProps from './sanitizeFieldRestProps'; @@ -112,7 +112,7 @@ DateField.displayName = 'DateField'; export interface DateFieldProps extends PublicFieldProps, InjectedFieldProps, - TypographyProps { + Omit<TypographyProps, 'textAlign'> { locales?: string | string[]; options?: object; showTime?: boolean; diff --git a/packages/ra-ui-materialui/src/field/EmailField.tsx b/packages/ra-ui-materialui/src/field/EmailField.tsx index fe6c0e2c5bb..fb637ab2ce4 100644 --- a/packages/ra-ui-materialui/src/field/EmailField.tsx +++ b/packages/ra-ui-materialui/src/field/EmailField.tsx @@ -1,8 +1,8 @@ import * as React from 'react'; import { AnchorHTMLAttributes, memo, FC } from 'react'; import get from 'lodash/get'; -import Typography from '@material-ui/core/Typography'; -import { Link } from '@material-ui/core'; +import Typography from '@mui/material/Typography'; +import { Link } from '@mui/material'; import { useRecordContext } from 'ra-core'; import sanitizeFieldRestProps from './sanitizeFieldRestProps'; diff --git a/packages/ra-ui-materialui/src/field/FileField.tsx b/packages/ra-ui-materialui/src/field/FileField.tsx index 6dcb90bb4b0..f509e4f5e56 100644 --- a/packages/ra-ui-materialui/src/field/FileField.tsx +++ b/packages/ra-ui-materialui/src/field/FileField.tsx @@ -1,14 +1,28 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import PropTypes from 'prop-types'; import get from 'lodash/get'; -import { makeStyles } from '@material-ui/core/styles'; -import Typography from '@material-ui/core/Typography'; +import Typography from '@mui/material/Typography'; import classnames from 'classnames'; import { useRecordContext } from 'ra-core'; import sanitizeFieldRestProps from './sanitizeFieldRestProps'; import { PublicFieldProps, InjectedFieldProps, fieldPropTypes } from './types'; +const PREFIX = 'RaFileField'; + +const classes = { + root: `${PREFIX}-root`, +}; + +const Root = styled('div')({ + [`&.${classes.root}`]: { display: 'inline-block' }, +}); + +const StyledList = styled('ul')({ + [`&.${classes.root}`]: { display: 'inline-block' }, +}); + /** * Render a link to a file based on a path contained in a record field * @@ -38,7 +52,6 @@ const FileField = (props: FileFieldProps) => { } = props; const record = useRecordContext(props); const sourceValue = get(record, source); - const classes = useStyles(props); if (!sourceValue) { return emptyText ? ( @@ -51,7 +64,7 @@ const FileField = (props: FileFieldProps) => { {emptyText} </Typography> ) : ( - <div + <Root className={classnames(classes.root, className)} {...sanitizeFieldRestProps(rest)} /> @@ -60,7 +73,7 @@ const FileField = (props: FileFieldProps) => { if (Array.isArray(sourceValue)) { return ( - <ul + <StyledList className={classnames(classes.root, className)} {...sanitizeFieldRestProps(rest)} > @@ -83,14 +96,14 @@ const FileField = (props: FileFieldProps) => { </li> ); })} - </ul> + </StyledList> ); } const titleValue = get(record, title) || title; return ( - <div + <Root className={classnames(classes.root, className)} {...sanitizeFieldRestProps(rest)} > @@ -104,7 +117,7 @@ const FileField = (props: FileFieldProps) => { > {titleValue} </a> - </div> + </Root> ); }; @@ -112,13 +125,6 @@ FileField.defaultProps = { addLabel: true, }; -const useStyles = makeStyles( - { - root: { display: 'inline-block' }, - }, - { name: 'RaFileField' } -); - export interface FileFieldProps extends PublicFieldProps, InjectedFieldProps { src?: string; title?: string; diff --git a/packages/ra-ui-materialui/src/field/FunctionField.tsx b/packages/ra-ui-materialui/src/field/FunctionField.tsx index 674ee6a2335..c15497cf3aa 100644 --- a/packages/ra-ui-materialui/src/field/FunctionField.tsx +++ b/packages/ra-ui-materialui/src/field/FunctionField.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { useMemo } from 'react'; import { Record, useRecordContext } from 'ra-core'; import PropTypes from 'prop-types'; -import Typography, { TypographyProps } from '@material-ui/core/Typography'; +import Typography, { TypographyProps } from '@mui/material/Typography'; import sanitizeFieldRestProps from './sanitizeFieldRestProps'; import { PublicFieldProps, InjectedFieldProps, fieldPropTypes } from './types'; @@ -52,7 +52,7 @@ FunctionField.propTypes = { export interface FunctionFieldProps<RecordType extends Record = Record> extends PublicFieldProps, InjectedFieldProps<RecordType>, - TypographyProps { + Omit<TypographyProps, 'textAlign'> { render: (record?: RecordType, source?: string) => any; } diff --git a/packages/ra-ui-materialui/src/field/ImageField.tsx b/packages/ra-ui-materialui/src/field/ImageField.tsx index 7bf1d743512..227d7e5ea06 100644 --- a/packages/ra-ui-materialui/src/field/ImageField.tsx +++ b/packages/ra-ui-materialui/src/field/ImageField.tsx @@ -1,27 +1,31 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import PropTypes from 'prop-types'; import get from 'lodash/get'; -import { makeStyles } from '@material-ui/core/styles'; -import Typography from '@material-ui/core/Typography'; +import Typography from '@mui/material/Typography'; import classnames from 'classnames'; import { useRecordContext } from 'ra-core'; import sanitizeFieldRestProps from './sanitizeFieldRestProps'; import { PublicFieldProps, InjectedFieldProps, fieldPropTypes } from './types'; -const useStyles = makeStyles( - { - list: { - display: 'flex', - listStyleType: 'none', - }, - image: { - margin: '0.5rem', - maxHeight: '10rem', - }, +const PREFIX = 'RaImageField'; + +const classes = { + list: `${PREFIX}-list`, + image: `${PREFIX}-image`, +}; + +const List = styled('ul')({ + [`&.${classes.list}`]: { + display: 'flex', + listStyleType: 'none', }, - { name: 'RaImageField' } -); + [`& .${classes.image}`]: { + margin: '0.5rem', + maxHeight: '10rem', + }, +}); export interface ImageFieldProps extends PublicFieldProps, InjectedFieldProps { src?: string; @@ -41,7 +45,7 @@ const ImageField = (props: ImageFieldProps) => { } = props; const record = useRecordContext(props); const sourceValue = get(record, source); - const classes = useStyles(props); + if (!sourceValue) { return emptyText ? ( <Typography @@ -59,7 +63,7 @@ const ImageField = (props: ImageFieldProps) => { if (Array.isArray(sourceValue)) { return ( - <ul + <List className={classnames(classes.list, className)} {...sanitizeFieldRestProps(rest)} > @@ -78,7 +82,7 @@ const ImageField = (props: ImageFieldProps) => { </li> ); })} - </ul> + </List> ); } diff --git a/packages/ra-ui-materialui/src/field/NumberField.tsx b/packages/ra-ui-materialui/src/field/NumberField.tsx index 91a34cc5e21..ad6d730064f 100644 --- a/packages/ra-ui-materialui/src/field/NumberField.tsx +++ b/packages/ra-ui-materialui/src/field/NumberField.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { memo, FC } from 'react'; import PropTypes from 'prop-types'; import get from 'lodash/get'; -import Typography, { TypographyProps } from '@material-ui/core/Typography'; +import Typography, { TypographyProps } from '@mui/material/Typography'; import { useRecordContext } from 'ra-core'; import sanitizeFieldRestProps from './sanitizeFieldRestProps'; @@ -103,7 +103,7 @@ NumberField.propTypes = { export interface NumberFieldProps extends PublicFieldProps, InjectedFieldProps, - TypographyProps { + Omit<TypographyProps, 'textAlign'> { locales?: string | string[]; options?: object; } diff --git a/packages/ra-ui-materialui/src/field/ReferenceArrayField.spec.tsx b/packages/ra-ui-materialui/src/field/ReferenceArrayField.spec.tsx index a6d1e37234e..6a993968988 100644 --- a/packages/ra-ui-materialui/src/field/ReferenceArrayField.spec.tsx +++ b/packages/ra-ui-materialui/src/field/ReferenceArrayField.spec.tsx @@ -4,6 +4,7 @@ import { render, act, waitFor } from '@testing-library/react'; import { renderWithRedux } from 'ra-test'; import { MemoryRouter } from 'react-router-dom'; import { ListContextProvider, DataProviderContext } from 'ra-core'; +import { createTheme, ThemeProvider } from '@mui/material/styles'; import ReferenceArrayField, { ReferenceArrayFieldView, @@ -11,29 +12,33 @@ import ReferenceArrayField, { import TextField from './TextField'; import SingleFieldList from '../list/SingleFieldList'; +const theme = createTheme({}); + describe('<ReferenceArrayField />', () => { it('should render a loading indicator when related records are not yet fetched and a second has passed', async () => { const { queryAllByRole } = render( - <ListContextProvider - value={{ - resource: 'foo', - basePath: '', - data: null, - ids: [1, 2], - loaded: false, - loading: true, - }} - > - <ReferenceArrayFieldView - source="barIds" - reference="bar" - record={{ id: 123, barIds: [1, 2] }} + <ThemeProvider theme={theme}> + <ListContextProvider + value={{ + resource: 'foo', + basePath: '', + data: null, + ids: [1, 2], + loaded: false, + loading: true, + }} > - <SingleFieldList> - <TextField source="title" /> - </SingleFieldList> - </ReferenceArrayFieldView> - </ListContextProvider> + <ReferenceArrayFieldView + source="barIds" + reference="bar" + record={{ id: 123, barIds: [1, 2] }} + > + <SingleFieldList> + <TextField source="title" /> + </SingleFieldList> + </ReferenceArrayFieldView> + </ListContextProvider> + </ThemeProvider> ); await new Promise(resolve => setTimeout(resolve, 1001)); @@ -47,12 +52,45 @@ describe('<ReferenceArrayField />', () => { }; const { queryAllByRole, container, getByText } = render( <MemoryRouter> + <ThemeProvider theme={theme}> + <ListContextProvider + value={{ + resource: 'foo', + basePath: '', + data, + ids: [1, 2], + loaded: true, + loading: false, + }} + > + <ReferenceArrayFieldView + source="barIds" + record={{ id: 123, barIds: [1, 2] }} + reference="bar" + > + <SingleFieldList> + <TextField source="title" /> + </SingleFieldList> + </ReferenceArrayFieldView> + </ListContextProvider> + </ThemeProvider> + </MemoryRouter> + ); + expect(queryAllByRole('progressbar')).toHaveLength(0); + expect(container.firstChild.textContent).not.toBeUndefined(); + expect(getByText('hello')).not.toBeNull(); + expect(getByText('world')).not.toBeNull(); + }); + + it('should render nothing when there are no related records', () => { + const { queryAllByRole, container } = render( + <ThemeProvider theme={theme}> <ListContextProvider value={{ resource: 'foo', basePath: '', - data, - ids: [1, 2], + data: {}, + ids: [], loaded: true, loading: false, }} @@ -67,36 +105,7 @@ describe('<ReferenceArrayField />', () => { </SingleFieldList> </ReferenceArrayFieldView> </ListContextProvider> - </MemoryRouter> - ); - expect(queryAllByRole('progressbar')).toHaveLength(0); - expect(container.firstChild.textContent).not.toBeUndefined(); - expect(getByText('hello')).not.toBeNull(); - expect(getByText('world')).not.toBeNull(); - }); - - it('should render nothing when there are no related records', () => { - const { queryAllByRole, container } = render( - <ListContextProvider - value={{ - resource: 'foo', - basePath: '', - data: {}, - ids: [], - loaded: true, - loading: false, - }} - > - <ReferenceArrayFieldView - source="barIds" - record={{ id: 123, barIds: [1, 2] }} - reference="bar" - > - <SingleFieldList> - <TextField source="title" /> - </SingleFieldList> - </ReferenceArrayFieldView> - </ListContextProvider> + </ThemeProvider> ); expect(queryAllByRole('progressbar')).toHaveLength(0); expect(container.firstChild.textContent).toBe(''); @@ -109,26 +118,28 @@ describe('<ReferenceArrayField />', () => { }; const { queryAllByRole, container, getByText } = render( <MemoryRouter> - <ListContextProvider - value={{ - resource: 'foo', - basePath: '', - data, - ids: ['abc-1', 'abc-2'], - loaded: true, - loading: false, - }} - > - <ReferenceArrayFieldView - record={{ id: 123, barIds: ['abc-1', 'abc-2'] }} - reference="bar" - source="barIds" + <ThemeProvider theme={theme}> + <ListContextProvider + value={{ + resource: 'foo', + basePath: '', + data, + ids: ['abc-1', 'abc-2'], + loaded: true, + loading: false, + }} > - <SingleFieldList> - <TextField source="title" /> - </SingleFieldList> - </ReferenceArrayFieldView> - </ListContextProvider> + <ReferenceArrayFieldView + record={{ id: 123, barIds: ['abc-1', 'abc-2'] }} + reference="bar" + source="barIds" + > + <SingleFieldList> + <TextField source="title" /> + </SingleFieldList> + </ReferenceArrayFieldView> + </ListContextProvider> + </ThemeProvider> </MemoryRouter> ); expect(queryAllByRole('progressbar')).toHaveLength(0); @@ -144,27 +155,29 @@ describe('<ReferenceArrayField />', () => { }; const { queryAllByRole, container, getByText } = render( <MemoryRouter> - <ListContextProvider - value={{ - resource: 'foo', - basePath: '', - data, - ids: [1, 2], - loaded: true, - loading: false, - }} - > - <ReferenceArrayFieldView - record={{ id: 123, barIds: [1, 2] }} - resource="foo" - reference="bar" - source="barIds" + <ThemeProvider theme={theme}> + <ListContextProvider + value={{ + resource: 'foo', + basePath: '', + data, + ids: [1, 2], + loaded: true, + loading: false, + }} > - <SingleFieldList> - <TextField source="title" /> - </SingleFieldList> - </ReferenceArrayFieldView> - </ListContextProvider> + <ReferenceArrayFieldView + record={{ id: 123, barIds: [1, 2] }} + resource="foo" + reference="bar" + source="barIds" + > + <SingleFieldList> + <TextField source="title" /> + </SingleFieldList> + </ReferenceArrayFieldView> + </ListContextProvider> + </ThemeProvider> </MemoryRouter> ); expect(queryAllByRole('progressbar')).toHaveLength(0); @@ -180,28 +193,30 @@ describe('<ReferenceArrayField />', () => { }; const { container } = render( <MemoryRouter> - <ListContextProvider - value={{ - resource: 'foo', - basePath: '', - data, - ids: [1, 2], - loaded: true, - loading: false, - }} - > - <ReferenceArrayFieldView - record={{ id: 123, barIds: [1, 2] }} - className="myClass" - resource="foo" - reference="bar" - source="barIds" + <ThemeProvider theme={theme}> + <ListContextProvider + value={{ + resource: 'foo', + basePath: '', + data, + ids: [1, 2], + loaded: true, + loading: false, + }} > - <SingleFieldList> - <TextField source="title" /> - </SingleFieldList> - </ReferenceArrayFieldView> - </ListContextProvider> + <ReferenceArrayFieldView + record={{ id: 123, barIds: [1, 2] }} + className="myClass" + resource="foo" + reference="bar" + source="barIds" + > + <SingleFieldList> + <TextField source="title" /> + </SingleFieldList> + </ReferenceArrayFieldView> + </ListContextProvider> + </ThemeProvider> </MemoryRouter> ); expect(container.getElementsByClassName('myClass')).toHaveLength(1); @@ -224,18 +239,20 @@ describe('<ReferenceArrayField />', () => { }; const { queryByText } = renderWithRedux( <DataProviderContext.Provider value={dataProvider}> - <ReferenceArrayField - record={{ id: 123, barIds: [1, 2] }} - className="myClass" - resource="foos" - reference="bars" - source="barIds" - basePath="/foos" - > - <SingleFieldList linkType={false}> - <WeakField /> - </SingleFieldList> - </ReferenceArrayField> + <ThemeProvider theme={theme}> + <ReferenceArrayField + record={{ id: 123, barIds: [1, 2] }} + className="myClass" + resource="foos" + reference="bars" + source="barIds" + basePath="/foos" + > + <SingleFieldList linkType={false}> + <WeakField /> + </SingleFieldList> + </ReferenceArrayField> + </ThemeProvider> </DataProviderContext.Provider>, { admin: { resources: { bars: { data: {} } } } } ); @@ -284,18 +301,20 @@ describe('<ReferenceArrayField />', () => { const onError = jest.fn(); renderWithRedux( <ErrorBoundary onError={onError}> - <ReferenceArrayField - record={{ id: 123, barIds: [1, 2] }} - className="myClass" - resource="foos" - reference="bars" - source="barIds" - basePath="/foos" - > - <SingleFieldList> - <TextField source="title" /> - </SingleFieldList> - </ReferenceArrayField> + <ThemeProvider theme={theme}> + <ReferenceArrayField + record={{ id: 123, barIds: [1, 2] }} + className="myClass" + resource="foos" + reference="bars" + source="barIds" + basePath="/foos" + > + <SingleFieldList> + <TextField source="title" /> + </SingleFieldList> + </ReferenceArrayField> + </ThemeProvider> </ErrorBoundary>, { admin: { resources: { comments: { data: {} } } } } ); diff --git a/packages/ra-ui-materialui/src/field/ReferenceArrayField.tsx b/packages/ra-ui-materialui/src/field/ReferenceArrayField.tsx index a733dd7827f..52b0ca45db2 100644 --- a/packages/ra-ui-materialui/src/field/ReferenceArrayField.tsx +++ b/packages/ra-ui-materialui/src/field/ReferenceArrayField.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { Children, cloneElement, FC, memo, ReactElement } from 'react'; import PropTypes from 'prop-types'; -import { makeStyles } from '@material-ui/core/styles'; import { useSelector } from 'react-redux'; import { ListContextProvider, @@ -16,10 +16,19 @@ import { } from 'ra-core'; import { fieldPropTypes, PublicFieldProps, InjectedFieldProps } from './types'; -import { ClassesOverride } from '../types'; import sanitizeFieldRestProps from './sanitizeFieldRestProps'; import { LinearProgress } from '../layout'; +const PREFIX = 'RaReferenceArrayField'; + +const classes = { + progress: `${PREFIX}-progress`, +}; + +const Root = styled('div')(({ theme }) => ({ + [`& .${classes.progress}`]: { marginTop: theme.spacing(2) }, +})); + /** * A container component that fetches records from another resource specified * by an array of *ids* in current record. @@ -130,7 +139,6 @@ ReferenceArrayField.propTypes = { ...fieldPropTypes, addLabel: PropTypes.bool, basePath: PropTypes.string, - classes: PropTypes.object, className: PropTypes.string, children: PropTypes.element.isRequired, label: PropTypes.string, @@ -150,7 +158,7 @@ export interface ReferenceArrayFieldProps extends PublicFieldProps, InjectedFieldProps { children: ReactElement; - classes?: ClassesOverride<typeof useStyles>; + filter?: FilterPayload; page?: number; pagination?: ReactElement; @@ -160,21 +168,12 @@ export interface ReferenceArrayFieldProps sort?: SortPayload; } -const useStyles = makeStyles( - theme => ({ - progress: { marginTop: theme.spacing(2) }, - }), - { name: 'RaReferenceArrayField' } -); - export interface ReferenceArrayFieldViewProps extends Omit< ReferenceArrayFieldProps, 'basePath' | 'resource' | 'page' | 'perPage' >, - ListControllerProps { - classes?: ClassesOverride<typeof useStyles>; -} + ListControllerProps {} export const ReferenceArrayFieldView: FC<ReferenceArrayFieldViewProps> = props => { const { @@ -185,30 +184,31 @@ export const ReferenceArrayFieldView: FC<ReferenceArrayFieldViewProps> = props = reference, ...rest } = props; - const classes = useStyles(props); - const { loaded } = useListContext(props); - if (!loaded) { - return <LinearProgress className={classes.progress} />; - } + const { loaded } = useListContext(props); return ( - <> - {cloneElement(Children.only(children), { - ...sanitizeFieldRestProps(rest), - className, - resource, - })}{' '} - {pagination && - props.total !== undefined && - cloneElement(pagination, sanitizeFieldRestProps(rest))} - </> + <Root> + {!loaded ? ( + <LinearProgress className={classes.progress} /> + ) : ( + <> + {cloneElement(Children.only(children), { + ...sanitizeFieldRestProps(rest), + className, + resource, + })} + {pagination && + props.total !== undefined && + cloneElement(pagination, sanitizeFieldRestProps(rest))} + </> + )} + </Root> ); }; ReferenceArrayFieldView.propTypes = { basePath: PropTypes.string, - classes: PropTypes.any, className: PropTypes.string, data: PropTypes.any, ids: PropTypes.array, diff --git a/packages/ra-ui-materialui/src/field/ReferenceField.spec.tsx b/packages/ra-ui-materialui/src/field/ReferenceField.spec.tsx index 8aef87828d4..23a211a680b 100644 --- a/packages/ra-ui-materialui/src/field/ReferenceField.spec.tsx +++ b/packages/ra-ui-materialui/src/field/ReferenceField.spec.tsx @@ -3,10 +3,12 @@ import expect from 'expect'; import { render, waitFor } from '@testing-library/react'; import { MemoryRouter } from 'react-router-dom'; import { DataProviderContext, RecordContextProvider } from 'ra-core'; +import { createTheme, ThemeProvider } from '@mui/material/styles'; import { renderWithRedux } from 'ra-test'; import ReferenceField, { ReferenceFieldView } from './ReferenceField'; import TextField from './TextField'; +const theme = createTheme({}); describe('<ReferenceField />', () => { const record = { id: 123, postId: 123 }; @@ -14,17 +16,19 @@ describe('<ReferenceField />', () => { describe('Progress bar', () => { it("should not display a loader on mount if the reference is not in the store and a second hasn't passed yet", async () => { const { queryByRole, container } = renderWithRedux( - <ReferenceFieldView - record={record} - resource="comments" - source="postId" - reference="posts" - basePath="/comments" - loaded={false} - loading={true} - > - <TextField source="title" /> - </ReferenceFieldView> + <ThemeProvider theme={theme}> + <ReferenceFieldView + record={record} + resource="comments" + source="postId" + reference="posts" + basePath="/comments" + loaded={false} + loading={true} + > + <TextField source="title" /> + </ReferenceFieldView> + </ThemeProvider> ); await new Promise(resolve => setTimeout(resolve, 500)); expect(queryByRole('progressbar')).toBeNull(); @@ -34,17 +38,19 @@ describe('<ReferenceField />', () => { it('should display a loader on mount if the reference is not in the store and a second has passed', async () => { const { queryByRole, container } = renderWithRedux( - <ReferenceFieldView - record={record} - resource="comments" - source="postId" - reference="posts" - basePath="/comments" - loaded={false} - loading={true} - > - <TextField source="title" /> - </ReferenceFieldView> + <ThemeProvider theme={theme}> + <ReferenceFieldView + record={record} + resource="comments" + source="postId" + reference="posts" + basePath="/comments" + loaded={false} + loading={true} + > + <TextField source="title" /> + </ReferenceFieldView> + </ThemeProvider> ); await new Promise(resolve => setTimeout(resolve, 1001)); expect(queryByRole('progressbar')).not.toBeNull(); @@ -55,15 +61,17 @@ describe('<ReferenceField />', () => { it('should not display a loader on mount if the reference is in the store', () => { const { queryByRole, container } = renderWithRedux( <MemoryRouter> - <ReferenceField - record={record} - resource="comments" - source="postId" - reference="posts" - basePath="/comments" - > - <TextField source="title" /> - </ReferenceField> + <ThemeProvider theme={theme}> + <ReferenceField + record={record} + resource="comments" + source="postId" + reference="posts" + basePath="/comments" + > + <TextField source="title" /> + </ReferenceField> + </ThemeProvider> </MemoryRouter>, { admin: { @@ -90,15 +98,17 @@ describe('<ReferenceField />', () => { // @ts-ignore-line <DataProviderContext.Provider value={dataProvider}> <MemoryRouter> - <ReferenceField - record={record} - resource="comments" - source="postId" - reference="posts" - basePath="/comments" - > - <TextField source="title" /> - </ReferenceField> + <ThemeProvider theme={theme}> + <ReferenceField + record={record} + resource="comments" + source="postId" + reference="posts" + basePath="/comments" + > + <TextField source="title" /> + </ReferenceField> + </ThemeProvider> </MemoryRouter> </DataProviderContext.Provider>, { @@ -122,15 +132,17 @@ describe('<ReferenceField />', () => { const { queryByRole, container } = renderWithRedux( // @ts-ignore-line <DataProviderContext.Provider value={dataProvider}> - <ReferenceField - record={record} - resource="comments" - source="postId" - reference="posts" - basePath="/comments" - > - <TextField source="title" /> - </ReferenceField> + <ThemeProvider theme={theme}> + <ReferenceField + record={record} + resource="comments" + source="postId" + reference="posts" + basePath="/comments" + > + <TextField source="title" /> + </ReferenceField> + </ThemeProvider> </DataProviderContext.Provider>, { admin: { resources: { posts: { data: {} } } } } ); @@ -148,15 +160,17 @@ describe('<ReferenceField />', () => { const { queryByRole, container } = renderWithRedux( // @ts-ignore-line <DataProviderContext.Provider value={dataProvider}> - <ReferenceField - record={record} - resource="comments" - source="postId" - reference="posts" - basePath="/comments" - > - <TextField source="title" /> - </ReferenceField> + <ThemeProvider theme={theme}> + <ReferenceField + record={record} + resource="comments" + source="postId" + reference="posts" + basePath="/comments" + > + <TextField source="title" /> + </ReferenceField> + </ThemeProvider> </DataProviderContext.Provider>, { admin: { resources: { posts: { data: {} } } } } ); @@ -169,33 +183,37 @@ describe('<ReferenceField />', () => { it('should display the emptyText if the field is empty', () => { const { getByText } = renderWithRedux( - <ReferenceField - record={{ id: 123 }} - resource="comments" - source="postId" - reference="posts" - basePath="/comments" - emptyText="EMPTY" - > - <TextField source="title" /> - </ReferenceField>, - { admin: { resources: { posts: { data: {} } } } } - ); - expect(getByText('EMPTY')).not.toBeNull(); - }); - - it('should use the reference from the store if available', () => { - const { container, getByText } = renderWithRedux( - <MemoryRouter> + <ThemeProvider theme={theme}> <ReferenceField - record={record} + record={{ id: 123 }} resource="comments" source="postId" reference="posts" basePath="/comments" + emptyText="EMPTY" > <TextField source="title" /> </ReferenceField> + </ThemeProvider>, + { admin: { resources: { posts: { data: {} } } } } + ); + expect(getByText('EMPTY')).not.toBeNull(); + }); + + it('should use the reference from the store if available', () => { + const { container, getByText } = renderWithRedux( + <MemoryRouter> + <ThemeProvider theme={theme}> + <ReferenceField + record={record} + resource="comments" + source="postId" + reference="posts" + basePath="/comments" + > + <TextField source="title" /> + </ReferenceField> + </ThemeProvider> </MemoryRouter>, { admin: { @@ -217,14 +235,16 @@ describe('<ReferenceField />', () => { const { container, getByText } = renderWithRedux( <MemoryRouter> <RecordContextProvider value={record}> - <ReferenceField - resource="comments" - source="postId" - reference="posts" - basePath="/comments" - > - <TextField source="title" /> - </ReferenceField> + <ThemeProvider theme={theme}> + <ReferenceField + resource="comments" + source="postId" + reference="posts" + basePath="/comments" + > + <TextField source="title" /> + </ReferenceField> + </ThemeProvider> </RecordContextProvider> </MemoryRouter>, { @@ -253,15 +273,17 @@ describe('<ReferenceField />', () => { // @ts-ignore-line <DataProviderContext.Provider value={dataProvider}> <MemoryRouter> - <ReferenceField - record={record} - resource="comments" - source="postId" - reference="posts" - basePath="/comments" - > - <TextField source="title" /> - </ReferenceField> + <ThemeProvider theme={theme}> + <ReferenceField + record={record} + resource="comments" + source="postId" + reference="posts" + basePath="/comments" + > + <TextField source="title" /> + </ReferenceField> + </ThemeProvider> </MemoryRouter> </DataProviderContext.Provider>, { admin: { resources: { posts: { data: {} } } } } @@ -281,15 +303,17 @@ describe('<ReferenceField />', () => { const { queryByRole } = renderWithRedux( // @ts-ignore-line <DataProviderContext.Provider value={dataProvider}> - <ReferenceField - record={record} - resource="comments" - source="postId" - reference="posts" - basePath="/comments" - > - <TextField source="title" /> - </ReferenceField> + <ThemeProvider theme={theme}> + <ReferenceField + record={record} + resource="comments" + source="postId" + reference="posts" + basePath="/comments" + > + <TextField source="title" /> + </ReferenceField> + </ThemeProvider> </DataProviderContext.Provider>, { admin: { resources: { posts: { data: {} } } } } ); @@ -338,15 +362,17 @@ describe('<ReferenceField />', () => { const onError = jest.fn(); renderWithRedux( <ErrorBoundary onError={onError}> - <ReferenceField - record={{ id: 123 }} - resource="comments" - source="postId" - reference="posts" - basePath="/comments" - > - <TextField source="title" /> - </ReferenceField> + <ThemeProvider theme={theme}> + <ReferenceField + record={{ id: 123 }} + resource="comments" + source="postId" + reference="posts" + basePath="/comments" + > + <TextField source="title" /> + </ReferenceField> + </ThemeProvider> </ErrorBoundary>, { admin: { resources: { comments: { data: {} } } } } ); @@ -361,19 +387,21 @@ describe('<ReferenceField />', () => { it('should render a link to specified resourceLinkPath', () => { const { container } = render( <MemoryRouter> - <ReferenceFieldView - record={record} - source="postId" - referenceRecord={{ id: 123, title: 'foo' }} - reference="posts" - resource="comments" - resourceLinkPath="/posts/123" - basePath="/comments" - loaded={true} - loading={false} - > - <TextField source="title" /> - </ReferenceFieldView> + <ThemeProvider theme={theme}> + <ReferenceFieldView + record={record} + source="postId" + referenceRecord={{ id: 123, title: 'foo' }} + reference="posts" + resource="comments" + resourceLinkPath="/posts/123" + basePath="/comments" + loaded={true} + loading={false} + > + <TextField source="title" /> + </ReferenceFieldView> + </ThemeProvider> </MemoryRouter> ); const links = container.getElementsByTagName('a'); @@ -383,38 +411,42 @@ describe('<ReferenceField />', () => { it('should render no link when resourceLinkPath is not specified', () => { const { container } = render( - <ReferenceFieldView - record={record} - source="fooId" - referenceRecord={{ id: 123, title: 'foo' }} - reference="bar" - basePath="/foo" - resourceLinkPath={false} - loaded={true} - loading={false} - > - <TextField source="title" /> - </ReferenceFieldView> - ); - const links = container.getElementsByTagName('a'); - expect(links).toHaveLength(0); - }); - - it('should work without basePath', () => { - const { container } = render( - <MemoryRouter> + <ThemeProvider theme={theme}> <ReferenceFieldView record={record} - source="postId" + source="fooId" referenceRecord={{ id: 123, title: 'foo' }} - reference="posts" - resource="comments" - resourceLinkPath="/posts/123" + reference="bar" + basePath="/foo" + resourceLinkPath={false} loaded={true} loading={false} > <TextField source="title" /> </ReferenceFieldView> + </ThemeProvider> + ); + const links = container.getElementsByTagName('a'); + expect(links).toHaveLength(0); + }); + + it('should work without basePath', () => { + const { container } = render( + <MemoryRouter> + <ThemeProvider theme={theme}> + <ReferenceFieldView + record={record} + source="postId" + referenceRecord={{ id: 123, title: 'foo' }} + reference="posts" + resource="comments" + resourceLinkPath="/posts/123" + loaded={true} + loading={false} + > + <TextField source="title" /> + </ReferenceFieldView> + </ThemeProvider> </MemoryRouter> ); const links = container.getElementsByTagName('a'); diff --git a/packages/ra-ui-materialui/src/field/ReferenceField.tsx b/packages/ra-ui-materialui/src/field/ReferenceField.tsx index 54803a30098..69054305085 100644 --- a/packages/ra-ui-materialui/src/field/ReferenceField.tsx +++ b/packages/ra-ui-materialui/src/field/ReferenceField.tsx @@ -1,11 +1,11 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { Children, cloneElement, FC, memo, ReactElement } from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; import get from 'lodash/get'; -import { makeStyles } from '@material-ui/core/styles'; -import { Typography } from '@material-ui/core'; -import ErrorIcon from '@material-ui/icons/Error'; +import { Typography } from '@mui/material'; +import ErrorIcon from '@mui/icons-material/Error'; import { useSelector } from 'react-redux'; import { useReference, @@ -23,7 +23,18 @@ import LinearProgress from '../layout/LinearProgress'; import Link from '../Link'; import sanitizeFieldRestProps from './sanitizeFieldRestProps'; import { PublicFieldProps, fieldPropTypes, InjectedFieldProps } from './types'; -import { ClassesOverride } from '../types'; + +const PREFIX = 'RaReferenceField'; + +const classes = { + link: `${PREFIX}-link`, +}; + +const Root = styled('div')(({ theme }) => ({ + [`& .${classes.link}`]: { + color: theme.palette.primary.main, + }, +})); /** * Fetch reference record, and delegate rendering to child component. @@ -97,7 +108,6 @@ ReferenceField.propTypes = { addLabel: PropTypes.bool, basePath: PropTypes.string, children: PropTypes.element.isRequired, - classes: PropTypes.any, className: PropTypes.string, cellClassName: PropTypes.string, headerClassName: PropTypes.string, @@ -123,7 +133,6 @@ ReferenceField.propTypes = { ReferenceField.defaultProps = { addLabel: true, - classes: {}, link: 'edit', }; @@ -131,7 +140,6 @@ export interface ReferenceFieldProps<RecordType extends Record = Record> extends PublicFieldProps, InjectedFieldProps<RecordType> { children: ReactElement; - classes?: ClassesOverride<typeof useStyles>; reference: string; resource?: string; source: string; @@ -176,15 +184,6 @@ export const NonEmptyReferenceField: FC<Omit< ); }; -const useStyles = makeStyles( - theme => ({ - link: { - color: theme.palette.primary.main, - }, - }), - { name: 'RaReferenceField' } -); - // useful to prevent click bubbling in a datagrid with rowClick const stopPropagation = e => e.stopPropagation(); @@ -193,7 +192,6 @@ export const ReferenceFieldView: FC<ReferenceFieldViewProps> = props => { basePath, children, className, - classes: classesOverride, error, loaded, loading, @@ -207,7 +205,6 @@ export const ReferenceFieldView: FC<ReferenceFieldViewProps> = props => { translateChoice = false, ...rest } = props; - const classes = useStyles(props); if (error) { return ( @@ -230,26 +227,28 @@ export const ReferenceFieldView: FC<ReferenceFieldViewProps> = props => { if (resourceLinkPath) { return ( - <RecordContextProvider value={referenceRecord}> - <Link - to={resourceLinkPath as string} - className={className} - onClick={stopPropagation} - > - {cloneElement(Children.only(children), { - className: classnames( - children.props.className, - classes.link // force color override for Typography components - ), - record: referenceRecord, - refetch, - resource: reference, - basePath, - translateChoice, - ...sanitizeFieldRestProps(rest), - })} - </Link> - </RecordContextProvider> + <Root> + <RecordContextProvider value={referenceRecord}> + <Link + to={resourceLinkPath as string} + className={className} + onClick={stopPropagation} + > + {cloneElement(Children.only(children), { + className: classnames( + children.props.className, + classes.link // force color override for Typography components + ), + record: referenceRecord, + refetch, + resource: reference, + basePath, + translateChoice, + ...sanitizeFieldRestProps(rest), + })} + </Link> + </RecordContextProvider> + </Root> ); } @@ -270,7 +269,6 @@ ReferenceFieldView.propTypes = { basePath: PropTypes.string, children: PropTypes.element, className: PropTypes.string, - classes: PropTypes.any, loading: PropTypes.bool, record: PropTypes.any, reference: PropTypes.string, @@ -288,7 +286,6 @@ export interface ReferenceFieldViewProps extends PublicFieldProps, InjectedFieldProps, UseReferenceProps { - classes?: ClassesOverride<typeof useStyles>; reference: string; resource?: string; translateChoice?: Function | boolean; diff --git a/packages/ra-ui-materialui/src/field/ReferenceManyField.spec.tsx b/packages/ra-ui-materialui/src/field/ReferenceManyField.spec.tsx index cbf318132f6..6f2aed8fc8c 100644 --- a/packages/ra-ui-materialui/src/field/ReferenceManyField.spec.tsx +++ b/packages/ra-ui-materialui/src/field/ReferenceManyField.spec.tsx @@ -4,6 +4,7 @@ import { render, waitFor } from '@testing-library/react'; import { createMemoryHistory } from 'history'; import { Router } from 'react-router-dom'; import { renderWithRedux } from 'ra-test'; +import { createTheme, ThemeProvider } from '@mui/material/styles'; import ReferenceManyField, { ReferenceManyFieldView, @@ -11,6 +12,8 @@ import ReferenceManyField, { import TextField from './TextField'; import SingleFieldList from '../list/SingleFieldList'; +const theme = createTheme(); + describe('<ReferenceManyField />', () => { const defaultProps = { resource: 'foo', @@ -30,15 +33,17 @@ describe('<ReferenceManyField />', () => { const history = createMemoryHistory(); const { queryAllByRole } = render( <Router history={history}> - <ReferenceManyFieldView - {...defaultProps} - data={data} - ids={[1, 2]} - > - <SingleFieldList> - <TextField source="title" /> - </SingleFieldList> - </ReferenceManyFieldView> + <ThemeProvider theme={theme}> + <ReferenceManyFieldView + {...defaultProps} + data={data} + ids={[1, 2]} + > + <SingleFieldList> + <TextField source="title" /> + </SingleFieldList> + </ReferenceManyFieldView> + </ThemeProvider> </Router> ); expect(queryAllByRole('progressbar')).toHaveLength(0); @@ -52,11 +57,13 @@ describe('<ReferenceManyField />', () => { it('should render nothing when there are no related records', () => { const { queryAllByRole } = render( - <ReferenceManyFieldView {...defaultProps} data={{}} ids={[]}> - <SingleFieldList> - <TextField source="title" /> - </SingleFieldList> - </ReferenceManyFieldView> + <ThemeProvider theme={theme}> + <ReferenceManyFieldView {...defaultProps} data={{}} ids={[]}> + <SingleFieldList> + <TextField source="title" /> + </SingleFieldList> + </ReferenceManyFieldView> + </ThemeProvider> ); expect(queryAllByRole('progressbar')).toHaveLength(0); expect(queryAllByRole('link')).toHaveLength(0); @@ -70,15 +77,17 @@ describe('<ReferenceManyField />', () => { const history = createMemoryHistory(); const { queryAllByRole } = render( <Router history={history}> - <ReferenceManyFieldView - {...defaultProps} - data={data} - ids={['abc-1', 'abc-2']} - > - <SingleFieldList> - <TextField source="title" /> - </SingleFieldList> - </ReferenceManyFieldView> + <ThemeProvider theme={theme}> + <ReferenceManyFieldView + {...defaultProps} + data={data} + ids={['abc-1', 'abc-2']} + > + <SingleFieldList> + <TextField source="title" /> + </SingleFieldList> + </ReferenceManyFieldView> + </ThemeProvider> </Router> ); expect(queryAllByRole('progressbar')).toHaveLength(0); @@ -98,15 +107,17 @@ describe('<ReferenceManyField />', () => { const history = createMemoryHistory(); const { queryAllByRole } = render( <Router history={history}> - <ReferenceManyFieldView - {...defaultProps} - data={data} - ids={[1, 2]} - > - <SingleFieldList> - <TextField source="title" /> - </SingleFieldList> - </ReferenceManyFieldView> + <ThemeProvider theme={theme}> + <ReferenceManyFieldView + {...defaultProps} + data={data} + ids={[1, 2]} + > + <SingleFieldList> + <TextField source="title" /> + </SingleFieldList> + </ReferenceManyFieldView> + </ThemeProvider> </Router> ); expect(queryAllByRole('progressbar')).toHaveLength(0); @@ -156,17 +167,19 @@ describe('<ReferenceManyField />', () => { const onError = jest.fn(); renderWithRedux( <ErrorBoundary onError={onError}> - <ReferenceManyField - record={{ id: 123 }} - resource="comments" - target="postId" - reference="posts" - basePath="/comments" - > - <SingleFieldList> - <TextField source="title" /> - </SingleFieldList> - </ReferenceManyField> + <ThemeProvider theme={theme}> + <ReferenceManyField + record={{ id: 123 }} + resource="comments" + target="postId" + reference="posts" + basePath="/comments" + > + <SingleFieldList> + <TextField source="title" /> + </SingleFieldList> + </ReferenceManyField> + </ThemeProvider> </ErrorBoundary>, { admin: { resources: { comments: { data: {} } } } } ); diff --git a/packages/ra-ui-materialui/src/field/RichTextField.tsx b/packages/ra-ui-materialui/src/field/RichTextField.tsx index ce9bac06e18..c23563503dc 100644 --- a/packages/ra-ui-materialui/src/field/RichTextField.tsx +++ b/packages/ra-ui-materialui/src/field/RichTextField.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { FC, memo } from 'react'; import PropTypes from 'prop-types'; import get from 'lodash/get'; -import Typography, { TypographyProps } from '@material-ui/core/Typography'; +import Typography, { TypographyProps } from '@mui/material/Typography'; import { useRecordContext } from 'ra-core'; import sanitizeFieldRestProps from './sanitizeFieldRestProps'; @@ -51,7 +51,7 @@ RichTextField.propTypes = { export interface RichTextFieldProps extends PublicFieldProps, InjectedFieldProps, - TypographyProps { + Omit<TypographyProps, 'textAlign'> { stripTags?: boolean; } diff --git a/packages/ra-ui-materialui/src/field/SelectField.tsx b/packages/ra-ui-materialui/src/field/SelectField.tsx index 77bd12b7cfe..d6ecfbf089c 100644 --- a/packages/ra-ui-materialui/src/field/SelectField.tsx +++ b/packages/ra-ui-materialui/src/field/SelectField.tsx @@ -3,7 +3,7 @@ import { memo, FC } from 'react'; import PropTypes from 'prop-types'; import get from 'lodash/get'; import { ChoicesProps, useChoices, useRecordContext } from 'ra-core'; -import { Typography, TypographyProps } from '@material-ui/core'; +import { Typography, TypographyProps } from '@mui/material'; import sanitizeFieldRestProps from './sanitizeFieldRestProps'; import { PublicFieldProps, InjectedFieldProps, fieldPropTypes } from './types'; @@ -143,7 +143,7 @@ export interface SelectFieldProps extends ChoicesProps, PublicFieldProps, InjectedFieldProps, - TypographyProps {} + Omit<TypographyProps, 'textAlign'> {} SelectField.displayName = 'SelectField'; diff --git a/packages/ra-ui-materialui/src/field/TextField.tsx b/packages/ra-ui-materialui/src/field/TextField.tsx index fdf3b0fc11b..dc3e5548a97 100644 --- a/packages/ra-ui-materialui/src/field/TextField.tsx +++ b/packages/ra-ui-materialui/src/field/TextField.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { memo, FC, ElementType } from 'react'; import get from 'lodash/get'; -import Typography, { TypographyProps } from '@material-ui/core/Typography'; +import Typography, { TypographyProps } from '@mui/material/Typography'; import { useRecordContext } from 'ra-core'; import sanitizeFieldRestProps from './sanitizeFieldRestProps'; @@ -42,7 +42,7 @@ TextField.propTypes = { export interface TextFieldProps extends PublicFieldProps, InjectedFieldProps, - TypographyProps { + Omit<TypographyProps, 'textAlign'> { // TypographyProps do not expose the component props, see https://github.com/mui-org/material-ui/issues/19512 component?: ElementType<any>; } diff --git a/packages/ra-ui-materialui/src/field/TranslatableFields.spec.tsx b/packages/ra-ui-materialui/src/field/TranslatableFields.spec.tsx index ea47772ff8c..417aa9f5675 100644 --- a/packages/ra-ui-materialui/src/field/TranslatableFields.spec.tsx +++ b/packages/ra-ui-materialui/src/field/TranslatableFields.spec.tsx @@ -1,9 +1,12 @@ import * as React from 'react'; import expect from 'expect'; import { fireEvent, render } from '@testing-library/react'; +import { useTranslatableContext } from 'ra-core'; +import { createTheme, ThemeProvider } from '@mui/material/styles'; + import { TranslatableFields } from './TranslatableFields'; import TextField from './TextField'; -import { useTranslatableContext } from 'ra-core'; +import defaultTheme from '../defaultTheme'; const record = { id: 123, @@ -26,16 +29,18 @@ const record = { describe('<TranslatableFields />', () => { it('should render every field for every locale', () => { const { queryByText, getByLabelText, getByText } = render( - <TranslatableFields - record={record} - resource="products" - basePath="/products" - locales={['en', 'fr']} - > - <TextField source="name" /> - <TextField source="description" /> - <TextField source="nested.field" /> - </TranslatableFields> + <ThemeProvider theme={createTheme(defaultTheme)}> + <TranslatableFields + record={record} + resource="products" + basePath="/products" + locales={['en', 'fr']} + > + <TextField source="name" /> + <TextField source="description" /> + <TextField source="nested.field" /> + </TranslatableFields> + </ThemeProvider> ); expect( @@ -94,17 +99,19 @@ describe('<TranslatableFields />', () => { }; const { queryByText, getByLabelText } = render( - <TranslatableFields - record={record} - resource="products" - basePath="/products" - locales={['en', 'fr']} - selector={<Selector />} - > - <TextField source="name" /> - <TextField source="description" /> - <TextField source="nested.field" /> - </TranslatableFields> + <ThemeProvider theme={createTheme(defaultTheme)}> + <TranslatableFields + record={record} + resource="products" + basePath="/products" + locales={['en', 'fr']} + selector={<Selector />} + > + <TextField source="name" /> + <TextField source="description" /> + <TextField source="nested.field" /> + </TranslatableFields> + </ThemeProvider> ); expect(getByLabelText('en').getAttribute('hidden')).toBeNull(); diff --git a/packages/ra-ui-materialui/src/field/TranslatableFields.tsx b/packages/ra-ui-materialui/src/field/TranslatableFields.tsx index 26ff3333a38..883a4cc1c05 100644 --- a/packages/ra-ui-materialui/src/field/TranslatableFields.tsx +++ b/packages/ra-ui-materialui/src/field/TranslatableFields.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { ReactElement, ReactNode } from 'react'; import { TranslatableContextProvider, @@ -9,8 +10,20 @@ import { } from 'ra-core'; import { TranslatableFieldsTabs } from './TranslatableFieldsTabs'; import { TranslatableFieldsTabContent } from './TranslatableFieldsTabContent'; -import { makeStyles } from '@material-ui/core/styles'; -import { ClassesOverride } from '../types'; + +const PREFIX = 'RaTranslatableFields'; + +const classes = { + root: `${PREFIX}-root`, +}; + +const Root = styled('div')(({ theme }) => ({ + [`&.${classes.root}`]: { + flexGrow: 1, + marginTop: theme.spacing(1), + marginBottom: theme.spacing(0.5), + }, +})); /** * Provides a way to show multiple languages for any field passed as children. @@ -80,10 +93,9 @@ export const TranslatableFields = ( } = props; const record = useRecordContext(props); const context = useTranslatable({ defaultLocale, locales }); - const classes = useStyles(props); return ( - <div className={classes.root}> + <Root className={classes.root}> <TranslatableContextProvider value={context}> {selector} {locales.map(locale => ( @@ -99,27 +111,15 @@ export const TranslatableFields = ( </TranslatableFieldsTabContent> ))} </TranslatableContextProvider> - </div> + </Root> ); }; export interface TranslatableFieldsProps extends UseTranslatableOptions { basePath?: string; children: ReactNode; - classes?: ClassesOverride<typeof useStyles>; record?: Record; resource?: string; selector?: ReactElement; groupKey?: string; } - -const useStyles = makeStyles( - theme => ({ - root: { - flexGrow: 1, - marginTop: theme.spacing(1), - marginBottom: theme.spacing(0.5), - }, - }), - { name: 'RaTranslatableFields' } -); diff --git a/packages/ra-ui-materialui/src/field/TranslatableFieldsTab.tsx b/packages/ra-ui-materialui/src/field/TranslatableFieldsTab.tsx index d62d3f2dec3..fdd748d1333 100644 --- a/packages/ra-ui-materialui/src/field/TranslatableFieldsTab.tsx +++ b/packages/ra-ui-materialui/src/field/TranslatableFieldsTab.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import Tab, { TabProps } from '@material-ui/core/Tab'; +import Tab, { TabProps } from '@mui/material/Tab'; import { useTranslate } from 'ra-core'; import { capitalize } from 'inflection'; diff --git a/packages/ra-ui-materialui/src/field/TranslatableFieldsTabContent.tsx b/packages/ra-ui-materialui/src/field/TranslatableFieldsTabContent.tsx index 087e3ded9a8..6363691fc21 100644 --- a/packages/ra-ui-materialui/src/field/TranslatableFieldsTabContent.tsx +++ b/packages/ra-ui-materialui/src/field/TranslatableFieldsTabContent.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { Children, cloneElement, @@ -7,10 +8,26 @@ import { ReactNode, } from 'react'; import { useTranslatableContext, Record } from 'ra-core'; -import { makeStyles } from '@material-ui/core/styles'; -import { ClassesOverride } from '../types'; import { Labeled } from '../input'; +const PREFIX = 'RaTranslatableFieldsTabContent'; + +const classes = { + root: `${PREFIX}-root`, +}; + +const Root = styled('div')(({ theme }) => ({ + [`&.${classes.root}`]: { + flexGrow: 1, + padding: theme.spacing(2), + borderRadius: 0, + borderBottomLeftRadius: theme.shape.borderRadius, + borderBottomRightRadius: theme.shape.borderRadius, + border: `1px solid ${theme.palette.divider}`, + borderTop: 0, + }, +})); + /** * Default container for a group of translatable fields inside a TranslatableFields components. * @see TranslatableFields @@ -28,10 +45,9 @@ export const TranslatableFieldsTabContent = ( ...other } = props; const { selectedLocale, getLabel, getSource } = useTranslatableContext(); - const classes = useStyles(props); return ( - <div + <Root role="tabpanel" hidden={selectedLocale !== locale} id={`translatable-content-${groupKey}${locale}`} @@ -72,32 +88,16 @@ export const TranslatableFieldsTabContent = ( </div> ) : null )} - </div> + </Root> ); }; export type TranslatableFieldsTabContentProps = { basePath: string; children: ReactNode; - classes?: ClassesOverride<typeof useStyles>; formGroupKeyPrefix?: string; groupKey: string; locale: string; record: Record; resource: string; }; - -const useStyles = makeStyles( - theme => ({ - root: { - flexGrow: 1, - padding: theme.spacing(2), - borderRadius: 0, - borderBottomLeftRadius: theme.shape.borderRadius, - borderBottomRightRadius: theme.shape.borderRadius, - border: `1px solid ${theme.palette.divider}`, - borderTop: 0, - }, - }), - { name: 'RaTranslatableFieldsTabContent' } -); diff --git a/packages/ra-ui-materialui/src/field/TranslatableFieldsTabs.tsx b/packages/ra-ui-materialui/src/field/TranslatableFieldsTabs.tsx index 45ca3b64303..4e18e2cd7ce 100644 --- a/packages/ra-ui-materialui/src/field/TranslatableFieldsTabs.tsx +++ b/packages/ra-ui-materialui/src/field/TranslatableFieldsTabs.tsx @@ -1,12 +1,28 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { ReactElement } from 'react'; -import AppBar from '@material-ui/core/AppBar'; -import { makeStyles } from '@material-ui/core/styles'; -import Tabs, { TabsProps } from '@material-ui/core/Tabs'; +import AppBar from '@mui/material/AppBar'; +import Tabs, { TabsProps } from '@mui/material/Tabs'; import { useTranslatableContext } from 'ra-core'; import { TranslatableFieldsTab } from './TranslatableFieldsTab'; import { AppBarProps } from '../layout'; +const PREFIX = 'RaTranslatableFieldsTabs'; + +const classes = { + root: `${PREFIX}-root`, +}; + +const StyledAppBar = styled(AppBar)(({ theme }) => ({ + [`&.${classes.root}`]: { + boxShadow: 'none', + borderRadius: 0, + borderTopLeftRadius: theme.shape.borderRadius, + borderTopRightRadius: theme.shape.borderRadius, + border: `1px solid ${theme.palette.divider}`, + }, +})); + /** * Default locale selector for the TranslatableFields component. Generates a tab for each specified locale. * @see TranslatableFields @@ -16,14 +32,17 @@ export const TranslatableFieldsTabs = ( ): ReactElement => { const { groupKey, TabsProps: tabsProps } = props; const { locales, selectLocale, selectedLocale } = useTranslatableContext(); - const classes = useStyles(props); const handleChange = (event, newLocale): void => { selectLocale(newLocale); }; return ( - <AppBar color="default" position="static" className={classes.root}> + <StyledAppBar + color="default" + position="static" + className={classes.root} + > <Tabs value={selectedLocale} onChange={handleChange} @@ -40,7 +59,7 @@ export const TranslatableFieldsTabs = ( /> ))} </Tabs> - </AppBar> + </StyledAppBar> ); }; @@ -48,16 +67,3 @@ export interface TranslatableFieldsTabsProps { TabsProps?: TabsProps; groupKey?: string; } - -const useStyles = makeStyles( - theme => ({ - root: { - boxShadow: 'none', - borderRadius: 0, - borderTopLeftRadius: theme.shape.borderRadius, - borderTopRightRadius: theme.shape.borderRadius, - border: `1px solid ${theme.palette.divider}`, - }, - }), - { name: 'RaTranslatableFieldsTabs' } -); diff --git a/packages/ra-ui-materialui/src/field/UrlField.tsx b/packages/ra-ui-materialui/src/field/UrlField.tsx index f8edc6109d0..3dedb406bf9 100644 --- a/packages/ra-ui-materialui/src/field/UrlField.tsx +++ b/packages/ra-ui-materialui/src/field/UrlField.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { AnchorHTMLAttributes, memo, FC } from 'react'; import get from 'lodash/get'; import sanitizeFieldRestProps from './sanitizeFieldRestProps'; -import { Typography, Link } from '@material-ui/core'; +import { Typography, Link } from '@mui/material'; import { useRecordContext } from 'ra-core'; import { PublicFieldProps, InjectedFieldProps, fieldPropTypes } from './types'; diff --git a/packages/ra-ui-materialui/src/field/types.ts b/packages/ra-ui-materialui/src/field/types.ts index 36801a6acff..cdfdfb36e80 100644 --- a/packages/ra-ui-materialui/src/field/types.ts +++ b/packages/ra-ui-materialui/src/field/types.ts @@ -1,7 +1,7 @@ import { ReactElement } from 'react'; import { Record } from 'ra-core'; import PropTypes from 'prop-types'; -import { TableCellProps } from '@material-ui/core/TableCell'; +import { TableCellProps } from '@mui/material/TableCell'; type TextAlign = TableCellProps['align']; type SortOrder = 'ASC' | 'DESC'; diff --git a/packages/ra-ui-materialui/src/form/FormInput.tsx b/packages/ra-ui-materialui/src/form/FormInput.tsx index 600214f93be..bc1e80f446d 100644 --- a/packages/ra-ui-materialui/src/form/FormInput.tsx +++ b/packages/ra-ui-materialui/src/form/FormInput.tsx @@ -1,12 +1,21 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { HtmlHTMLAttributes, ReactElement } from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; -import { makeStyles } from '@material-ui/core/styles'; import { Record } from 'ra-core'; import Labeled from '../input/Labeled'; -import { ClassesOverride } from '../types'; + +const PREFIX = 'RaFormInput'; + +const classes = { + input: `${PREFIX}-input`, +}; + +const Root = styled('div')(({ theme }) => ({ + [`& .${classes.input}`]: { width: theme.spacing(32) }, +})); const sanitizeRestProps = ({ basePath, @@ -17,23 +26,16 @@ const sanitizeRestProps = ({ record?: unknown; }) => rest; -const useStyles = makeStyles( - theme => ({ - input: { width: theme.spacing(32) }, - }), - { name: 'RaFormInput' } -); - const FormInput = <RecordType extends Record | Omit<Record, 'id'> = Record>( props: FormInputProps<RecordType> ) => { - const { input, classes: classesOverride, ...rest } = props; - const classes = useStyles(props); + const { input, ...rest } = props; + const { id, className, ...inputProps } = input ? input.props : { id: undefined, className: undefined }; return input ? ( - <div + <Root className={classnames( 'ra-input', `ra-input-${input.props.source}`, @@ -71,7 +73,7 @@ const FormInput = <RecordType extends Record | Omit<Record, 'id'> = Record>( ...inputProps, }) )} - </div> + </Root> ) : null; }; @@ -85,7 +87,6 @@ export interface FormInputProps< RecordType extends Record | Omit<Record, 'id'> = Record > extends HtmlHTMLAttributes<HTMLDivElement> { basePath: string; - classes?: ClassesOverride<typeof useStyles>; input: ReactElement<{ label?: string; source?: string; diff --git a/packages/ra-ui-materialui/src/form/FormTab.spec.tsx b/packages/ra-ui-materialui/src/form/FormTab.spec.tsx index aa77a1975d7..4184c0c868a 100644 --- a/packages/ra-ui-materialui/src/form/FormTab.spec.tsx +++ b/packages/ra-ui-materialui/src/form/FormTab.spec.tsx @@ -2,10 +2,11 @@ import * as React from 'react'; import expect from 'expect'; import { SaveContextProvider, SideEffectContextProvider } from 'ra-core'; import { renderWithRedux } from 'ra-test'; - +import { createTheme, ThemeProvider } from '@mui/material/styles'; import { TabbedForm } from './TabbedForm'; import { FormTab } from './FormTab'; import TextInput from '../input/TextInput'; +import defaultTheme from '../defaultTheme'; describe('<FormTab label="foo" />', () => { const saveContextValue = { @@ -17,31 +18,35 @@ describe('<FormTab label="foo" />', () => { it('should display <Toolbar />', () => { const { queryByLabelText } = renderWithRedux( - <SaveContextProvider value={saveContextValue}> - <SideEffectContextProvider value={sideEffectValue}> - <TabbedForm> - <FormTab label="foo"> - <TextInput source="name" /> - <TextInput source="city" /> - </FormTab> - </TabbedForm> - </SideEffectContextProvider> - </SaveContextProvider> + <ThemeProvider theme={createTheme(defaultTheme)}> + <SaveContextProvider value={saveContextValue}> + <SideEffectContextProvider value={sideEffectValue}> + <TabbedForm> + <FormTab label="foo"> + <TextInput source="name" /> + <TextInput source="city" /> + </FormTab> + </TabbedForm> + </SideEffectContextProvider> + </SaveContextProvider> + </ThemeProvider> ); expect(queryByLabelText('ra.action.save')).not.toBeNull(); }); it('should not alter default margin or variant', () => { const { queryByLabelText } = renderWithRedux( - <SaveContextProvider value={saveContextValue}> - <SideEffectContextProvider value={sideEffectValue}> - <TabbedForm> - <FormTab label="foo"> - <TextInput source="name" /> - </FormTab> - </TabbedForm> - </SideEffectContextProvider> - </SaveContextProvider> + <ThemeProvider theme={createTheme(defaultTheme)}> + <SaveContextProvider value={saveContextValue}> + <SideEffectContextProvider value={sideEffectValue}> + <TabbedForm> + <FormTab label="foo"> + <TextInput source="name" /> + </FormTab> + </TabbedForm> + </SideEffectContextProvider> + </SaveContextProvider> + </ThemeProvider> ); const inputElement = queryByLabelText( 'resources.undefined.fields.name' @@ -57,42 +62,44 @@ describe('<FormTab label="foo" />', () => { const record = { id: 'gazebo', name: 'foo' }; const { container } = renderWithRedux( - <SaveContextProvider value={saveContextValue}> - <SideEffectContextProvider value={sideEffectValue}> - <TabbedForm> - <FormTab - label="First" - basePath="/posts" - resource="posts" - record={record} - margin="none" - variant="standard" - > - <TextInput source="name" /> - </FormTab> - <FormTab - label="Second" - basePath="/posts" - resource="posts" - record={record} - margin="dense" - variant="filled" - > - <TextInput source="name" /> - </FormTab> - <FormTab - label="Third" - basePath="/posts" - resource="posts" - record={record} - margin="normal" - variant="outlined" - > - <TextInput source="name" /> - </FormTab> - </TabbedForm> - </SideEffectContextProvider> - </SaveContextProvider> + <ThemeProvider theme={createTheme(defaultTheme)}> + <SaveContextProvider value={saveContextValue}> + <SideEffectContextProvider value={sideEffectValue}> + <TabbedForm> + <FormTab + label="First" + basePath="/posts" + resource="posts" + record={record} + margin="none" + variant="standard" + > + <TextInput source="name" /> + </FormTab> + <FormTab + label="Second" + basePath="/posts" + resource="posts" + record={record} + margin="dense" + variant="filled" + > + <TextInput source="name" /> + </FormTab> + <FormTab + label="Third" + basePath="/posts" + resource="posts" + record={record} + margin="normal" + variant="outlined" + > + <TextInput source="name" /> + </FormTab> + </TabbedForm> + </SideEffectContextProvider> + </SaveContextProvider> + </ThemeProvider> ); expect(spy).not.toHaveBeenCalled(); expect(container).not.toBeNull(); @@ -102,15 +109,21 @@ describe('<FormTab label="foo" />', () => { it('should pass variant and margin to child inputs', () => { const { queryByLabelText } = renderWithRedux( - <SaveContextProvider value={saveContextValue}> - <SideEffectContextProvider value={sideEffectValue}> - <TabbedForm> - <FormTab label="foo" variant="outlined" margin="normal"> - <TextInput source="name" /> - </FormTab> - </TabbedForm> - </SideEffectContextProvider> - </SaveContextProvider> + <ThemeProvider theme={createTheme(defaultTheme)}> + <SaveContextProvider value={saveContextValue}> + <SideEffectContextProvider value={sideEffectValue}> + <TabbedForm> + <FormTab + label="foo" + variant="outlined" + margin="normal" + > + <TextInput source="name" /> + </FormTab> + </TabbedForm> + </SideEffectContextProvider> + </SaveContextProvider> + </ThemeProvider> ); const inputElement = queryByLabelText( 'resources.undefined.fields.name' @@ -123,19 +136,25 @@ describe('<FormTab label="foo" />', () => { it('should allow input children to override variant and margin', () => { const { queryByLabelText } = renderWithRedux( - <SaveContextProvider value={saveContextValue}> - <SideEffectContextProvider value={sideEffectValue}> - <TabbedForm> - <FormTab label="foo" variant="standard" margin="none"> - <TextInput - source="name" - variant="outlined" - margin="normal" - /> - </FormTab> - </TabbedForm> - </SideEffectContextProvider> - </SaveContextProvider> + <ThemeProvider theme={createTheme(defaultTheme)}> + <SaveContextProvider value={saveContextValue}> + <SideEffectContextProvider value={sideEffectValue}> + <TabbedForm> + <FormTab + label="foo" + variant="standard" + margin="none" + > + <TextInput + source="name" + variant="outlined" + margin="normal" + /> + </FormTab> + </TabbedForm> + </SideEffectContextProvider> + </SaveContextProvider> + </ThemeProvider> ); const inputElement = queryByLabelText( 'resources.undefined.fields.name' diff --git a/packages/ra-ui-materialui/src/form/FormTab.tsx b/packages/ra-ui-materialui/src/form/FormTab.tsx index 291a0068eb4..0eb43a62ba2 100644 --- a/packages/ra-ui-materialui/src/form/FormTab.tsx +++ b/packages/ra-ui-materialui/src/form/FormTab.tsx @@ -12,7 +12,6 @@ export const FormTab = (props: FormTabProps) => { const { basePath, className, - classes, contentClassName, children, hidden, @@ -33,7 +32,6 @@ export const FormTab = (props: FormTabProps) => { value={value} icon={icon} className={className} - classes={classes} {...rest} /> ); diff --git a/packages/ra-ui-materialui/src/form/FormTabHeader.tsx b/packages/ra-ui-materialui/src/form/FormTabHeader.tsx index 84d1dea69a4..1441c136f10 100644 --- a/packages/ra-ui-materialui/src/form/FormTabHeader.tsx +++ b/packages/ra-ui-materialui/src/form/FormTabHeader.tsx @@ -2,15 +2,13 @@ import * as React from 'react'; import { isValidElement, ReactElement } from 'react'; import PropTypes from 'prop-types'; import { Link, useLocation } from 'react-router-dom'; -import MuiTab from '@material-ui/core/Tab'; +import MuiTab from '@mui/material/Tab'; import classnames from 'classnames'; import { useTranslate, useFormGroup } from 'ra-core'; -import { useTabbedFormViewStyles } from './TabbedFormView'; -import { ClassesOverride } from '../types'; import { useFormState } from 'react-final-form'; +import { TabbedFormClasses } from './TabbedFormView'; export const FormTabHeader = ({ - classes, label, value, icon, @@ -35,8 +33,9 @@ export const FormTabHeader = ({ value={value} icon={icon} className={classnames('form-tab', className, { - [classes.errorTabButton]: + [TabbedFormClasses.errorTabButton]: formGroup.invalid && (formGroup.touched || submitFailed), + error: formGroup.invalid && (formGroup.touched || submitFailed), })} {...(syncWithLocation ? propsForLink : {})} // to avoid TypeScript screams, see https://github.com/mui-org/material-ui/issues/9106#issuecomment-451270521 id={`tabheader-${value}`} @@ -55,7 +54,6 @@ const UseFormStateOptions = { interface FormTabHeaderProps { basePath?: string; className?: string; - classes?: ClassesOverride<typeof useTabbedFormViewStyles>; hidden?: boolean; icon?: ReactElement; intent?: 'header' | 'content'; diff --git a/packages/ra-ui-materialui/src/form/SimpleForm.spec.tsx b/packages/ra-ui-materialui/src/form/SimpleForm.spec.tsx index bf7c9ea70cd..5820aefa8f7 100644 --- a/packages/ra-ui-materialui/src/form/SimpleForm.spec.tsx +++ b/packages/ra-ui-materialui/src/form/SimpleForm.spec.tsx @@ -2,7 +2,9 @@ import * as React from 'react'; import expect from 'expect'; import { SaveContextProvider, SideEffectContextProvider } from 'ra-core'; import { renderWithRedux } from 'ra-test'; +import { createTheme, ThemeProvider } from '@mui/material/styles'; +import defaultTheme from '../defaultTheme'; import SimpleForm from './SimpleForm'; import TextInput from '../input/TextInput'; @@ -16,14 +18,16 @@ describe('<SimpleForm />', () => { it('should embed a form with given component children', () => { const { queryByLabelText } = renderWithRedux( - <SaveContextProvider value={saveContextValue}> - <SideEffectContextProvider value={sideEffects}> - <SimpleForm> - <TextInput source="name" /> - <TextInput source="city" /> - </SimpleForm> - </SideEffectContextProvider> - </SaveContextProvider> + <ThemeProvider theme={createTheme(defaultTheme)}> + <SaveContextProvider value={saveContextValue}> + <SideEffectContextProvider value={sideEffects}> + <SimpleForm> + <TextInput source="name" /> + <TextInput source="city" /> + </SimpleForm> + </SideEffectContextProvider> + </SaveContextProvider> + </ThemeProvider> ); expect( queryByLabelText('resources.undefined.fields.name') @@ -35,14 +39,16 @@ describe('<SimpleForm />', () => { it('should display <Toolbar />', () => { const { queryByLabelText } = renderWithRedux( - <SaveContextProvider value={saveContextValue}> - <SideEffectContextProvider value={sideEffects}> - <SimpleForm> - <TextInput source="name" /> - <TextInput source="city" /> - </SimpleForm> - </SideEffectContextProvider> - </SaveContextProvider> + <ThemeProvider theme={createTheme(defaultTheme)}> + <SaveContextProvider value={saveContextValue}> + <SideEffectContextProvider value={sideEffects}> + <SimpleForm> + <TextInput source="name" /> + <TextInput source="city" /> + </SimpleForm> + </SideEffectContextProvider> + </SaveContextProvider> + </ThemeProvider> ); expect(queryByLabelText('ra.action.save')).not.toBeNull(); }); @@ -53,25 +59,29 @@ describe('<SimpleForm />', () => { ); const { queryByText, rerender } = renderWithRedux( - <SaveContextProvider value={saveContextValue}> - <SideEffectContextProvider value={sideEffects}> - <SimpleForm submitOnEnter={false} toolbar={<Toolbar />}> - <TextInput source="name" /> - </SimpleForm> - </SideEffectContextProvider> - </SaveContextProvider> + <ThemeProvider theme={createTheme(defaultTheme)}> + <SaveContextProvider value={saveContextValue}> + <SideEffectContextProvider value={sideEffects}> + <SimpleForm submitOnEnter={false} toolbar={<Toolbar />}> + <TextInput source="name" /> + </SimpleForm> + </SideEffectContextProvider> + </SaveContextProvider> + </ThemeProvider> ); expect(queryByText('submitOnEnter: false')).not.toBeNull(); rerender( - <SaveContextProvider value={saveContextValue}> - <SideEffectContextProvider value={sideEffects}> - <SimpleForm submitOnEnter toolbar={<Toolbar />}> - <TextInput source="name" /> - </SimpleForm> - </SideEffectContextProvider> - </SaveContextProvider> + <ThemeProvider theme={createTheme(defaultTheme)}> + <SaveContextProvider value={saveContextValue}> + <SideEffectContextProvider value={sideEffects}> + <SimpleForm submitOnEnter toolbar={<Toolbar />}> + <TextInput source="name" /> + </SimpleForm> + </SideEffectContextProvider> + </SaveContextProvider> + </ThemeProvider> ); expect(queryByText('submitOnEnter: true')).not.toBeNull(); @@ -79,13 +89,15 @@ describe('<SimpleForm />', () => { it('should not alter default margin or variant', () => { const { queryByLabelText } = renderWithRedux( - <SaveContextProvider value={saveContextValue}> - <SideEffectContextProvider value={sideEffects}> - <SimpleForm> - <TextInput source="name" /> - </SimpleForm> - </SideEffectContextProvider> - </SaveContextProvider> + <ThemeProvider theme={createTheme(defaultTheme)}> + <SaveContextProvider value={saveContextValue}> + <SideEffectContextProvider value={sideEffects}> + <SimpleForm> + <TextInput source="name" /> + </SimpleForm> + </SideEffectContextProvider> + </SaveContextProvider> + </ThemeProvider> ); const inputElement = queryByLabelText( 'resources.undefined.fields.name' @@ -98,13 +110,15 @@ describe('<SimpleForm />', () => { it('should pass variant and margin to child inputs', () => { const { queryByLabelText } = renderWithRedux( - <SaveContextProvider value={saveContextValue}> - <SideEffectContextProvider value={sideEffects}> - <SimpleForm variant="outlined" margin="normal"> - <TextInput source="name" /> - </SimpleForm> - </SideEffectContextProvider> - </SaveContextProvider> + <ThemeProvider theme={createTheme(defaultTheme)}> + <SaveContextProvider value={saveContextValue}> + <SideEffectContextProvider value={sideEffects}> + <SimpleForm variant="outlined" margin="normal"> + <TextInput source="name" /> + </SimpleForm> + </SideEffectContextProvider> + </SaveContextProvider> + </ThemeProvider> ); const inputElement = queryByLabelText( 'resources.undefined.fields.name' @@ -117,17 +131,19 @@ describe('<SimpleForm />', () => { it('should allow input children to override variant and margin', () => { const { queryByLabelText } = renderWithRedux( - <SaveContextProvider value={saveContextValue}> - <SideEffectContextProvider value={sideEffects}> - <SimpleForm variant="standard" margin="none"> - <TextInput - source="name" - variant="outlined" - margin="normal" - /> - </SimpleForm> - </SideEffectContextProvider> - </SaveContextProvider> + <ThemeProvider theme={createTheme(defaultTheme)}> + <SaveContextProvider value={saveContextValue}> + <SideEffectContextProvider value={sideEffects}> + <SimpleForm variant="standard" margin="none"> + <TextInput + source="name" + variant="outlined" + margin="normal" + /> + </SimpleForm> + </SideEffectContextProvider> + </SaveContextProvider> + </ThemeProvider> ); const inputElement = queryByLabelText( 'resources.undefined.fields.name' diff --git a/packages/ra-ui-materialui/src/form/TabbedForm.spec.tsx b/packages/ra-ui-materialui/src/form/TabbedForm.spec.tsx index eab87e9cfb8..876f244cdfb 100644 --- a/packages/ra-ui-materialui/src/form/TabbedForm.spec.tsx +++ b/packages/ra-ui-materialui/src/form/TabbedForm.spec.tsx @@ -8,11 +8,13 @@ import { SideEffectContextProvider, } from 'ra-core'; import { renderWithRedux } from 'ra-test'; +import { createTheme, ThemeProvider } from '@mui/material/styles'; +import { fireEvent, isInaccessible } from '@testing-library/react'; +import defaultTheme from '../defaultTheme'; import { TabbedForm } from './TabbedForm'; import { FormTab } from './FormTab'; import TextInput from '../input/TextInput'; -import { fireEvent, isInaccessible } from '@testing-library/react'; describe('<TabbedForm />', () => { const saveContextValue = { @@ -25,14 +27,16 @@ describe('<TabbedForm />', () => { it('should display the tabs', () => { const { queryAllByRole } = renderWithRedux( <MemoryRouter initialEntries={['/']}> - <SaveContextProvider value={saveContextValue}> - <SideEffectContextProvider value={sideEffectValue}> - <TabbedForm> - <FormTab label="tab1" /> - <FormTab label="tab2" /> - </TabbedForm> - </SideEffectContextProvider> - </SaveContextProvider> + <ThemeProvider theme={createTheme(defaultTheme)}> + <SaveContextProvider value={saveContextValue}> + <SideEffectContextProvider value={sideEffectValue}> + <TabbedForm> + <FormTab label="tab1" /> + <FormTab label="tab2" /> + </TabbedForm> + </SideEffectContextProvider> + </SaveContextProvider> + </ThemeProvider> </MemoryRouter> ); @@ -47,14 +51,19 @@ describe('<TabbedForm />', () => { const { queryByText, rerender } = renderWithRedux( <MemoryRouter initialEntries={['/']}> - <SaveContextProvider value={saveContextValue}> - <SideEffectContextProvider value={sideEffectValue}> - <TabbedForm submitOnEnter={false} toolbar={<Toolbar />}> - <FormTab label="tab1" /> - <FormTab label="tab2" /> - </TabbedForm> - </SideEffectContextProvider> - </SaveContextProvider> + <ThemeProvider theme={createTheme(defaultTheme)}> + <SaveContextProvider value={saveContextValue}> + <SideEffectContextProvider value={sideEffectValue}> + <TabbedForm + submitOnEnter={false} + toolbar={<Toolbar />} + > + <FormTab label="tab1" /> + <FormTab label="tab2" /> + </TabbedForm> + </SideEffectContextProvider> + </SaveContextProvider> + </ThemeProvider> </MemoryRouter> ); @@ -62,14 +71,16 @@ describe('<TabbedForm />', () => { rerender( <MemoryRouter initialEntries={['/']}> - <SaveContextProvider value={saveContextValue}> - <SideEffectContextProvider value={sideEffectValue}> - <TabbedForm submitOnEnter toolbar={<Toolbar />}> - <FormTab label="tab1" /> - <FormTab label="tab2" /> - </TabbedForm> - </SideEffectContextProvider> - </SaveContextProvider> + <ThemeProvider theme={createTheme(defaultTheme)}> + <SaveContextProvider value={saveContextValue}> + <SideEffectContextProvider value={sideEffectValue}> + <TabbedForm submitOnEnter toolbar={<Toolbar />}> + <FormTab label="tab1" /> + <FormTab label="tab2" /> + </TabbedForm> + </SideEffectContextProvider> + </SaveContextProvider> + </ThemeProvider> </MemoryRouter> ); @@ -79,22 +90,27 @@ describe('<TabbedForm />', () => { it('should set the style of an inactive Tab button with errors', async () => { const { getAllByRole, getByLabelText } = renderWithRedux( <MemoryRouter initialEntries={['/posts/1']} initialIndex={0}> - <SaveContextProvider value={saveContextValue}> - <TabbedForm - classes={{ errorTabButton: 'error' }} - resource="posts" - > - <FormTab label="tab1"> - <TextInput source="title" validate={required()} /> - </FormTab> - <FormTab label="tab2"> - <TextInput - source="description" - validate={minLength(10)} - /> - </FormTab> - </TabbedForm> - </SaveContextProvider> + <ThemeProvider theme={createTheme(defaultTheme)}> + <SaveContextProvider value={saveContextValue}> + <TabbedForm + classes={{ errorTabButton: 'error' }} + resource="posts" + > + <FormTab label="tab1"> + <TextInput + source="title" + validate={required()} + /> + </FormTab> + <FormTab label="tab2"> + <TextInput + source="description" + validate={minLength(10)} + /> + </FormTab> + </TabbedForm> + </SaveContextProvider> + </ThemeProvider> </MemoryRouter> ); @@ -111,22 +127,27 @@ describe('<TabbedForm />', () => { it('should set the style of an active Tab button with errors', () => { const { getAllByRole, getByLabelText } = renderWithRedux( <MemoryRouter initialEntries={['/posts/1']} initialIndex={0}> - <SaveContextProvider value={saveContextValue}> - <TabbedForm - classes={{ errorTabButton: 'error' }} - resource="posts" - > - <FormTab label="tab1"> - <TextInput source="title" validate={required()} /> - </FormTab> - <FormTab label="tab2"> - <TextInput - source="description" - validate={required()} - /> - </FormTab> - </TabbedForm> - </SaveContextProvider> + <ThemeProvider theme={createTheme(defaultTheme)}> + <SaveContextProvider value={saveContextValue}> + <TabbedForm + classes={{ errorTabButton: 'error' }} + resource="posts" + > + <FormTab label="tab1"> + <TextInput + source="title" + validate={required()} + /> + </FormTab> + <FormTab label="tab2"> + <TextInput + source="description" + validate={required()} + /> + </FormTab> + </TabbedForm> + </SaveContextProvider> + </ThemeProvider> </MemoryRouter> ); @@ -141,22 +162,27 @@ describe('<TabbedForm />', () => { it('should set the style of any Tab button with errors on submit', () => { const { getAllByRole, getByLabelText } = renderWithRedux( <MemoryRouter initialEntries={['/posts/1']} initialIndex={0}> - <SaveContextProvider value={saveContextValue}> - <TabbedForm - classes={{ errorTabButton: 'error' }} - resource="posts" - > - <FormTab label="tab1"> - <TextInput source="title" validate={required()} /> - </FormTab> - <FormTab label="tab2"> - <TextInput - source="description" - validate={minLength(10)} - /> - </FormTab> - </TabbedForm> - </SaveContextProvider> + <ThemeProvider theme={createTheme(defaultTheme)}> + <SaveContextProvider value={saveContextValue}> + <TabbedForm + classes={{ errorTabButton: 'error' }} + resource="posts" + > + <FormTab label="tab1"> + <TextInput + source="title" + validate={required()} + /> + </FormTab> + <FormTab label="tab2"> + <TextInput + source="description" + validate={minLength(10)} + /> + </FormTab> + </TabbedForm> + </SaveContextProvider> + </ThemeProvider> </MemoryRouter> ); @@ -175,22 +201,27 @@ describe('<TabbedForm />', () => { const { getAllByRole, getByLabelText } = renderWithRedux( <Router history={history}> - <SaveContextProvider value={saveContextValue}> - <TabbedForm - classes={{ errorTabButton: 'error' }} - resource="posts" - > - <FormTab label="tab1"> - <TextInput source="title" validate={required()} /> - </FormTab> - <FormTab label="tab2"> - <TextInput - source="description" - validate={minLength(10)} - /> - </FormTab> - </TabbedForm> - </SaveContextProvider> + <ThemeProvider theme={createTheme(defaultTheme)}> + <SaveContextProvider value={saveContextValue}> + <TabbedForm + classes={{ errorTabButton: 'error' }} + resource="posts" + > + <FormTab label="tab1"> + <TextInput + source="title" + validate={required()} + /> + </FormTab> + <FormTab label="tab2"> + <TextInput + source="description" + validate={minLength(10)} + /> + </FormTab> + </TabbedForm> + </SaveContextProvider> + </ThemeProvider> </Router> ); @@ -216,23 +247,28 @@ describe('<TabbedForm />', () => { const { getAllByRole, getByLabelText } = renderWithRedux( <Router history={history}> - <SaveContextProvider value={saveContextValue}> - <TabbedForm - classes={{ errorTabButton: 'error' }} - resource="posts" - syncWithLocation={false} - > - <FormTab label="tab1"> - <TextInput source="title" validate={required()} /> - </FormTab> - <FormTab label="tab2"> - <TextInput - source="description" - validate={minLength(10)} - /> - </FormTab> - </TabbedForm> - </SaveContextProvider> + <ThemeProvider theme={createTheme(defaultTheme)}> + <SaveContextProvider value={saveContextValue}> + <TabbedForm + classes={{ errorTabButton: 'error' }} + resource="posts" + syncWithLocation={false} + > + <FormTab label="tab1"> + <TextInput + source="title" + validate={required()} + /> + </FormTab> + <FormTab label="tab2"> + <TextInput + source="description" + validate={minLength(10)} + /> + </FormTab> + </TabbedForm> + </SaveContextProvider> + </ThemeProvider> </Router> ); diff --git a/packages/ra-ui-materialui/src/form/TabbedForm.tsx b/packages/ra-ui-materialui/src/form/TabbedForm.tsx index 16b9ccacdd9..23dbf70c165 100644 --- a/packages/ra-ui-materialui/src/form/TabbedForm.tsx +++ b/packages/ra-ui-materialui/src/form/TabbedForm.tsx @@ -18,8 +18,7 @@ import { } from 'ra-core'; import get from 'lodash/get'; -import { ClassesOverride } from '../types'; -import { TabbedFormView, useTabbedFormViewStyles } from './TabbedFormView'; +import { TabbedFormView } from './TabbedFormView'; /** * Form layout where inputs are divided by tab, one input per line. @@ -123,7 +122,6 @@ export interface TabbedFormProps basePath?: string; children: ReactNode; className?: string; - classes?: ClassesOverride<typeof useTabbedFormViewStyles>; initialValues?: any; margin?: 'none' | 'normal' | 'dense'; mutationMode?: MutationMode; diff --git a/packages/ra-ui-materialui/src/form/TabbedFormTabs.tsx b/packages/ra-ui-materialui/src/form/TabbedFormTabs.tsx index 2408ebe994b..cbaaee66273 100644 --- a/packages/ra-ui-materialui/src/form/TabbedFormTabs.tsx +++ b/packages/ra-ui-materialui/src/form/TabbedFormTabs.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { Children, cloneElement, isValidElement, ReactElement } from 'react'; import PropTypes from 'prop-types'; -import Tabs, { TabsProps } from '@material-ui/core/Tabs'; +import Tabs, { TabsProps } from '@mui/material/Tabs'; import { useLocation } from 'react-router-dom'; const TabbedFormTabs = (props: TabbedFormTabsProps) => { diff --git a/packages/ra-ui-materialui/src/form/TabbedFormView.tsx b/packages/ra-ui-materialui/src/form/TabbedFormView.tsx index e9c398edf2d..8207f3d8fc2 100644 --- a/packages/ra-ui-materialui/src/form/TabbedFormView.tsx +++ b/packages/ra-ui-materialui/src/form/TabbedFormView.tsx @@ -11,8 +11,8 @@ import { import PropTypes from 'prop-types'; import classnames from 'classnames'; import { Route, useRouteMatch, useLocation } from 'react-router-dom'; -import { Divider } from '@material-ui/core'; -import { makeStyles } from '@material-ui/core/styles'; +import { Divider } from '@mui/material'; +import { styled } from '@mui/material/styles'; import { escapePath, FormWithRedirectRenderProps, @@ -21,14 +21,30 @@ import { } from 'ra-core'; import Toolbar from './Toolbar'; import TabbedFormTabs, { getTabFullPath } from './TabbedFormTabs'; -import { ClassesOverride } from '../types'; + +const PREFIX = 'RaTabbedForm'; + +export const TabbedFormClasses = { + errorTabButton: `${PREFIX}-errorTabButton`, + content: `${PREFIX}-content`, +}; + +const Root = styled('form')(({ theme }) => ({ + [`&.${TabbedFormClasses.errorTabButton}`]: { + color: theme.palette.error.main, + }, + [`&.${TabbedFormClasses.content}`]: { + paddingTop: theme.spacing(1), + paddingLeft: theme.spacing(2), + paddingRight: theme.spacing(2), + }, +})); export const TabbedFormView = (props: TabbedFormViewProps): ReactElement => { const { basePath, children, className, - classes: classesOverride, handleSubmit, handleSubmitWithRedirect, invalid, @@ -48,7 +64,6 @@ export const TabbedFormView = (props: TabbedFormViewProps): ReactElement => { validating, ...rest } = props; - const classes = useTabbedFormViewStyles(props); const match = useRouteMatch(); const location = useLocation(); const url = match ? match.url : location.pathname; @@ -61,14 +76,14 @@ export const TabbedFormView = (props: TabbedFormViewProps): ReactElement => { }; return ( - <form + <Root className={classnames('tabbed-form', className)} {...sanitizeRestProps(rest)} > {cloneElement( tabs, { - classes, + classes: TabbedFormClasses, url, syncWithLocation, onChange: handleTabChange, @@ -77,7 +92,7 @@ export const TabbedFormView = (props: TabbedFormViewProps): ReactElement => { children )} <Divider /> - <div className={classes.content}> + <div className={TabbedFormClasses.content}> {/* All tabs are rendered (not only the one in focus), to allow validation on tabs not in focus. The tabs receive a `hidden` property, which they'll use to hide the tab using CSS if it's not the one in focus. @@ -93,7 +108,7 @@ export const TabbedFormView = (props: TabbedFormViewProps): ReactElement => { isValidElement<any>(tab) ? React.cloneElement(tab, { intent: 'content', - classes, + classes: TabbedFormClasses, resource, record, basePath, @@ -129,27 +144,14 @@ export const TabbedFormView = (props: TabbedFormViewProps): ReactElement => { validating, undoable, })} - </form> + </Root> ); }; -export const useTabbedFormViewStyles = makeStyles( - theme => ({ - errorTabButton: { color: theme.palette.error.main }, - content: { - paddingTop: theme.spacing(1), - paddingLeft: theme.spacing(2), - paddingRight: theme.spacing(2), - }, - }), - { name: 'RaTabbedForm' } -); - TabbedFormView.propTypes = { basePath: PropTypes.string, children: PropTypes.node, className: PropTypes.string, - classes: PropTypes.object, defaultValue: PropTypes.oneOfType([PropTypes.object, PropTypes.func]), // @deprecated handleSubmit: PropTypes.func, // passed by react-final-form initialValues: PropTypes.oneOfType([PropTypes.object, PropTypes.func]), @@ -187,7 +189,6 @@ TabbedFormView.defaultProps = { export interface TabbedFormViewProps extends FormWithRedirectRenderProps { basePath?: string; children?: ReactNode; - classes?: ClassesOverride<typeof useTabbedFormViewStyles>; className?: string; margin?: 'none' | 'normal' | 'dense'; mutationMode?: MutationMode; diff --git a/packages/ra-ui-materialui/src/form/Toolbar.tsx b/packages/ra-ui-materialui/src/form/Toolbar.tsx index 3f4366a105c..b0dd2895f9c 100644 --- a/packages/ra-ui-materialui/src/form/Toolbar.tsx +++ b/packages/ra-ui-materialui/src/form/Toolbar.tsx @@ -1,61 +1,59 @@ import * as React from 'react'; -import { - Children, - Fragment, - isValidElement, - ReactElement, - FC, - ReactNode, -} from 'react'; +import { styled } from '@mui/material/styles'; +import { Children, isValidElement, ReactElement, FC, ReactNode } from 'react'; import PropTypes from 'prop-types'; -import MuiToolbar, { +import { + Toolbar as MuiToolbar, ToolbarProps as MuiToolbarProps, -} from '@material-ui/core/Toolbar'; -import withWidth from '@material-ui/core/withWidth'; -import { makeStyles } from '@material-ui/core/styles'; + useMediaQuery, + Theme, +} from '@mui/material'; import classnames from 'classnames'; import { Record, RedirectionSideEffect, MutationMode } from 'ra-core'; import { FormRenderProps } from 'react-final-form'; import { SaveButton, DeleteButton } from '../button'; -import { ClassesOverride } from '../types'; -import { Breakpoint } from '@material-ui/core/styles/createBreakpoints'; -const useStyles = makeStyles( - theme => ({ - toolbar: { - backgroundColor: - theme.palette.type === 'light' - ? theme.palette.grey[100] - : theme.palette.grey[900], - }, - desktopToolbar: { - marginTop: theme.spacing(2), - }, - mobileToolbar: { - position: 'fixed', - bottom: 0, - left: 0, - right: 0, - padding: '16px', - width: '100%', - boxSizing: 'border-box', - flexShrink: 0, - zIndex: 2, - }, - defaultToolbar: { - flex: 1, - display: 'flex', - justifyContent: 'space-between', - }, - spacer: { - [theme.breakpoints.down('xs')]: { - height: '5em', - }, - }, - }), - { name: 'RaToolbar' } -); +const PREFIX = 'RaToolbar'; + +const classes = { + toolbar: `${PREFIX}-toolbar`, + desktopToolbar: `${PREFIX}-desktopToolbar`, + mobileToolbar: `${PREFIX}-mobileToolbar`, + defaultToolbar: `${PREFIX}-defaultToolbar`, + spacer: `${PREFIX}-spacer`, +}; + +const StyledToolbar = styled(MuiToolbar)(({ theme }) => ({ + [`&.${classes.toolbar}`]: { + backgroundColor: + theme.palette.mode === 'light' + ? theme.palette.grey[100] + : theme.palette.grey[900], + }, + + [`&.${classes.desktopToolbar}`]: { + marginTop: theme.spacing(2), + }, + + [`&.${classes.mobileToolbar}`]: { + position: 'fixed', + bottom: 0, + left: 0, + right: 0, + padding: '16px', + width: '100%', + boxSizing: 'border-box', + flexShrink: 0, + zIndex: 2, + }, + + [`&.${classes.defaultToolbar}`]: { + flex: 1, + display: 'flex', + justifyContent: 'space-between', + }, +})); const valueOrDefault = (value, defaultValue) => typeof value === 'undefined' ? defaultValue : value; @@ -103,7 +101,6 @@ const Toolbar: FC<ToolbarProps> = props => { basePath, children, className, - classes: classesOverride, handleSubmit, handleSubmitWithRedirect, invalid, @@ -116,10 +113,10 @@ const Toolbar: FC<ToolbarProps> = props => { undoable, mutationMode, validating, - width, ...rest } = props; - const classes = useStyles(props); + + const isXs = useMediaQuery<Theme>(theme => theme.breakpoints.down('sm')); // Use form pristine and validating to enable or disable the save button // if alwaysEnableSaveButton is undefined @@ -129,84 +126,81 @@ const Toolbar: FC<ToolbarProps> = props => { ); return ( - <Fragment> - <MuiToolbar - className={classnames( - classes.toolbar, - { - [classes.mobileToolbar]: width === 'xs', - [classes.desktopToolbar]: width !== 'xs', - }, - className - )} - role="toolbar" - {...rest} - > - {Children.count(children) === 0 ? ( - <div className={classes.defaultToolbar}> - <SaveButton - handleSubmitWithRedirect={ - handleSubmitWithRedirect || handleSubmit - } - disabled={disabled} - invalid={invalid} - redirect={redirect} - saving={saving || validating} - submitOnEnter={submitOnEnter} + <StyledToolbar + className={classnames( + classes.toolbar, + { + [classes.mobileToolbar]: isXs, + [classes.desktopToolbar]: !isXs, + }, + className + )} + role="toolbar" + {...rest} + > + {Children.count(children) === 0 ? ( + <div className={classes.defaultToolbar}> + <SaveButton + handleSubmitWithRedirect={ + handleSubmitWithRedirect || handleSubmit + } + disabled={disabled} + invalid={invalid} + redirect={redirect} + saving={saving || validating} + submitOnEnter={submitOnEnter} + /> + {record && typeof record.id !== 'undefined' && ( + <DeleteButton + basePath={basePath} + record={record} + resource={resource} + undoable={undoable} + mutationMode={mutationMode} /> - {record && typeof record.id !== 'undefined' && ( - <DeleteButton - basePath={basePath} - record={record} - resource={resource} - undoable={undoable} - mutationMode={mutationMode} - /> - )} - </div> - ) : ( - Children.map(children, (button: ReactElement) => - button && isValidElement<any>(button) - ? React.cloneElement(button, { - basePath: valueOrDefault( - button.props.basePath, - basePath - ), - handleSubmit: valueOrDefault( - button.props.handleSubmit, - handleSubmit - ), - handleSubmitWithRedirect: valueOrDefault( - button.props.handleSubmitWithRedirect, - handleSubmitWithRedirect - ), - onSave: button.props.onSave, - invalid, - pristine, - record: valueOrDefault( - button.props.record, - record - ), - resource: valueOrDefault( - button.props.resource, - resource - ), - saving, - submitOnEnter: valueOrDefault( - button.props.submitOnEnter, - submitOnEnter - ), - undoable: valueOrDefault( - button.props.undoable, - undoable - ), - }) - : null - ) - )} - </MuiToolbar> - <div className={classes.spacer} /> - </Fragment> + )} + </div> + ) : ( + Children.map(children, (button: ReactElement) => + button && isValidElement<any>(button) + ? React.cloneElement(button, { + basePath: valueOrDefault( + button.props.basePath, + basePath + ), + handleSubmit: valueOrDefault( + button.props.handleSubmit, + handleSubmit + ), + handleSubmitWithRedirect: valueOrDefault( + button.props.handleSubmitWithRedirect, + handleSubmitWithRedirect + ), + onSave: button.props.onSave, + invalid, + pristine, + record: valueOrDefault( + button.props.record, + record + ), + resource: valueOrDefault( + button.props.resource, + resource + ), + saving, + submitOnEnter: valueOrDefault( + button.props.submitOnEnter, + submitOnEnter + ), + undoable: valueOrDefault( + button.props.undoable, + undoable + ), + }) + : null + ) + )} + </StyledToolbar> ); }; @@ -215,7 +209,7 @@ export interface ToolbarProps<RecordType extends Record = Record> children?: ReactNode; alwaysEnableSaveButton?: boolean; className?: string; - classes?: ClassesOverride<typeof useStyles>; + handleSubmitWithRedirect?: (redirect?: RedirectionSideEffect) => void; handleSubmit?: FormRenderProps['handleSubmit']; invalid?: boolean; @@ -230,13 +224,11 @@ export interface ToolbarProps<RecordType extends Record = Record> /** @deprecated use mutationMode: undoable instead */ undoable?: boolean; validating?: boolean; - width?: Breakpoint; } Toolbar.propTypes = { basePath: PropTypes.string, children: PropTypes.node, - classes: PropTypes.object, className: PropTypes.string, handleSubmit: PropTypes.func, handleSubmitWithRedirect: PropTypes.func, @@ -253,11 +245,10 @@ Toolbar.propTypes = { submitOnEnter: PropTypes.bool, undoable: PropTypes.bool, validating: PropTypes.bool, - width: PropTypes.oneOf(['xs', 'sm', 'md', 'lg', 'xl']), }; Toolbar.defaultProps = { submitOnEnter: true, }; -export default withWidth({ initialWidth: 'xs' })(Toolbar); +export default Toolbar; diff --git a/packages/ra-ui-materialui/src/index.ts b/packages/ra-ui-materialui/src/index.ts index 140901db2b4..dfd3249cee8 100644 --- a/packages/ra-ui-materialui/src/index.ts +++ b/packages/ra-ui-materialui/src/index.ts @@ -1,5 +1,5 @@ import Link from './Link'; -import defaultTheme, { RaThemeOptions, RaThemeOverrides } from './defaultTheme'; +import defaultTheme, { RaThemeOptions } from './defaultTheme'; export * from './auth'; export * from './button'; @@ -10,6 +10,6 @@ export * from './input'; export * from './layout'; export * from './list'; export { Link, defaultTheme }; -export type { RaThemeOptions, RaThemeOverrides }; +export type { RaThemeOptions }; export * from './types'; diff --git a/packages/ra-ui-materialui/src/input/ArrayInput/AddItemButton.tsx b/packages/ra-ui-materialui/src/input/ArrayInput/AddItemButton.tsx index 58541a465e9..bdaa7f2bde9 100644 --- a/packages/ra-ui-materialui/src/input/ArrayInput/AddItemButton.tsx +++ b/packages/ra-ui-materialui/src/input/ArrayInput/AddItemButton.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import AddIcon from '@material-ui/icons/AddCircleOutline'; +import AddIcon from '@mui/icons-material/AddCircleOutline'; import { useSimpleFormIterator } from './useSimpleFormIterator'; import { Button, ButtonProps } from '../../button'; diff --git a/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.spec.tsx b/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.spec.tsx index e6f9ea9354c..35f6e3a0563 100644 --- a/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.spec.tsx +++ b/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.spec.tsx @@ -2,8 +2,7 @@ import * as React from 'react'; import { fireEvent, render, waitFor } from '@testing-library/react'; import { Form } from 'react-final-form'; import arrayMutators from 'final-form-arrays'; -import { ThemeProvider } from '@material-ui/core'; -import { createTheme } from '@material-ui/core/styles'; +import { createTheme, ThemeProvider } from '@mui/material'; import { ArrayInput } from './ArrayInput'; import NumberInput from '../NumberInput'; diff --git a/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.tsx b/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.tsx index 0c976aa86b6..1988775ab6d 100644 --- a/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.tsx +++ b/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.tsx @@ -8,7 +8,7 @@ import { InputProps, } from 'ra-core'; import { useFieldArray } from 'react-final-form-arrays'; -import { InputLabel, FormControl, FormHelperText } from '@material-ui/core'; +import { InputLabel, FormControl, FormHelperText } from '@mui/material'; import { LinearProgress } from '../../layout'; import InputHelperText from '../InputHelperText'; diff --git a/packages/ra-ui-materialui/src/input/ArrayInput/ReOrderButtons.tsx b/packages/ra-ui-materialui/src/input/ArrayInput/ReOrderButtons.tsx index 83bd8418ebe..9b0bd5b3b90 100644 --- a/packages/ra-ui-materialui/src/input/ArrayInput/ReOrderButtons.tsx +++ b/packages/ra-ui-materialui/src/input/ArrayInput/ReOrderButtons.tsx @@ -1,8 +1,8 @@ import * as React from 'react'; import { IconButtonWithTooltip } from '../../button'; -import ArrowUpwardIcon from '@material-ui/icons/ArrowUpward'; -import ArrowDownwardIcon from '@material-ui/icons/ArrowDownward'; +import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward'; +import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward'; import { useSimpleFormIteratorItem } from './useSimpleFormIteratorItem'; export const ReOrderButtons = ({ className }: { className?: string }) => { diff --git a/packages/ra-ui-materialui/src/input/ArrayInput/RemoveItemButton.tsx b/packages/ra-ui-materialui/src/input/ArrayInput/RemoveItemButton.tsx index ab221ea46d4..b2ddac440f8 100644 --- a/packages/ra-ui-materialui/src/input/ArrayInput/RemoveItemButton.tsx +++ b/packages/ra-ui-materialui/src/input/ArrayInput/RemoveItemButton.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import CloseIcon from '@material-ui/icons/RemoveCircleOutline'; +import CloseIcon from '@mui/icons-material/RemoveCircleOutline'; import { Button, ButtonProps } from '../../button'; import { useSimpleFormIteratorItem } from './useSimpleFormIteratorItem'; diff --git a/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIterator.spec.tsx b/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIterator.spec.tsx index 63dce9d5b51..0aba2ce4e1e 100644 --- a/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIterator.spec.tsx +++ b/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIterator.spec.tsx @@ -1,16 +1,16 @@ import * as React from 'react'; -import { ThemeProvider } from '@material-ui/core'; -import { createTheme } from '@material-ui/core/styles'; import { fireEvent, getByText, waitFor } from '@testing-library/react'; import expect from 'expect'; import { SaveContextProvider, SideEffectContextProvider } from 'ra-core'; import { renderWithRedux } from 'ra-test'; +import { createTheme, ThemeProvider } from '@mui/material/styles'; + import SimpleForm from '../../form/SimpleForm'; import { ArrayInput } from './ArrayInput'; import TextInput from '../TextInput'; import { SimpleFormIterator } from './SimpleFormIterator'; -const theme = createTheme(); +const theme = createTheme({}); describe('<SimpleFormIterator />', () => { // bypass confirm leave form with unsaved changes @@ -143,17 +143,19 @@ describe('<SimpleFormIterator />', () => { it('should not display add button if disableAdd is truthy', () => { const { queryAllByText } = renderWithRedux( - <SaveContextProvider value={saveContextValue}> - <SideEffectContextProvider value={sideEffectValue}> - <SimpleForm> - <ArrayInput source="emails"> - <SimpleFormIterator disableAdd> - <TextInput source="email" /> - </SimpleFormIterator> - </ArrayInput> - </SimpleForm> - </SideEffectContextProvider> - </SaveContextProvider> + <ThemeProvider theme={theme}> + <SaveContextProvider value={saveContextValue}> + <SideEffectContextProvider value={sideEffectValue}> + <SimpleForm> + <ArrayInput source="emails"> + <SimpleFormIterator disableAdd> + <TextInput source="email" /> + </SimpleFormIterator> + </ArrayInput> + </SimpleForm> + </SideEffectContextProvider> + </SaveContextProvider> + </ThemeProvider> ); expect(queryAllByText('ra.action.add').length).toBe(0); @@ -161,17 +163,19 @@ describe('<SimpleFormIterator />', () => { it('should not display add button if disabled is truthy', () => { const { queryAllByText } = renderWithRedux( - <SaveContextProvider value={saveContextValue}> - <SideEffectContextProvider value={sideEffectValue}> - <SimpleForm> - <ArrayInput source="emails" disabled> - <SimpleFormIterator> - <TextInput source="email" /> - </SimpleFormIterator> - </ArrayInput> - </SimpleForm> - </SideEffectContextProvider> - </SaveContextProvider> + <ThemeProvider theme={theme}> + <SaveContextProvider value={saveContextValue}> + <SideEffectContextProvider value={sideEffectValue}> + <SimpleForm> + <ArrayInput source="emails" disabled> + <SimpleFormIterator> + <TextInput source="email" /> + </SimpleFormIterator> + </ArrayInput> + </SimpleForm> + </SideEffectContextProvider> + </SaveContextProvider> + </ThemeProvider> ); expect(queryAllByText('ra.action.add').length).toBe(0); @@ -509,7 +513,10 @@ describe('<SimpleFormIterator />', () => { <SaveContextProvider value={saveContextValue}> <SideEffectContextProvider value={sideEffectValue}> <SimpleForm - record={{ id: 'whatever', emails: [{ email: '' }] }} + record={{ + id: 'whatever', + emails: [{ email: '' }], + }} > <ArrayInput source="emails"> <SimpleFormIterator @@ -540,7 +547,10 @@ describe('<SimpleFormIterator />', () => { <SaveContextProvider value={saveContextValue}> <SideEffectContextProvider value={sideEffectValue}> <SimpleForm - record={{ id: 'whatever', emails: [{ email: '' }] }} + record={{ + id: 'whatever', + emails: [{ email: '' }], + }} > <ArrayInput source="emails"> <SimpleFormIterator @@ -622,7 +632,10 @@ describe('<SimpleFormIterator />', () => { <SaveContextProvider value={saveContextValue}> <SideEffectContextProvider value={sideEffectValue}> <SimpleForm - record={{ id: 'whatever', emails: [{ email: '' }] }} + record={{ + id: 'whatever', + emails: [{ email: '' }], + }} > <ArrayInput source="emails"> <SimpleFormIterator @@ -674,7 +687,10 @@ describe('<SimpleFormIterator />', () => { <SaveContextProvider value={saveContextValue}> <SideEffectContextProvider value={sideEffectValue}> <SimpleForm - record={{ id: 'whatever', emails: [{ email: '' }] }} + record={{ + id: 'whatever', + emails: [{ email: '' }], + }} > <ArrayInput source="emails"> <SimpleFormIterator diff --git a/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIterator.tsx b/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIterator.tsx index fa8b1815e64..e8c4fa55c65 100644 --- a/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIterator.tsx +++ b/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIterator.tsx @@ -9,7 +9,7 @@ import { useMemo, useRef, } from 'react'; -import { FormHelperText } from '@material-ui/core'; +import { FormHelperText, styled } from '@mui/material'; import classNames from 'classnames'; import get from 'lodash/get'; import PropTypes from 'prop-types'; @@ -18,9 +18,11 @@ import { FieldArrayRenderProps } from 'react-final-form-arrays'; import { CSSTransition, TransitionGroup } from 'react-transition-group'; import { CSSTransitionProps } from 'react-transition-group/CSSTransition'; -import { ClassesOverride } from '../../types'; import { useArrayInput } from './useArrayInput'; -import { useSimpleFormIteratorStyles } from './useSimpleFormIteratorStyles'; +import { + SimpleFormIteratorClasses, + SimpleFormIteratorPrefix, +} from './useSimpleFormIteratorStyles'; import { SimpleFormIteratorContext } from './SimpleFormIteratorContext'; import { DisableRemoveFunction, @@ -51,7 +53,6 @@ export const SimpleFormIterator = (props: SimpleFormIteratorProps) => { defaultValue, getItemLabel = DefaultLabelFn, } = props; - const classes = useSimpleFormIteratorStyles(props); const { fields, meta } = useArrayInput(props); const { error, submitFailed } = meta; const nodeRef = useRef(null); @@ -123,7 +124,12 @@ export const SimpleFormIterator = (props: SimpleFormIteratorProps) => { ); return fields ? ( <SimpleFormIteratorContext.Provider value={context}> - <ul className={classNames(classes.root, className)}> + <Root + className={classNames( + SimpleFormIteratorClasses.root, + className + )} + > {submitFailed && typeof error !== 'object' && error && ( <FormHelperText error> <ValidationError error={error as string} /> @@ -140,7 +146,6 @@ export const SimpleFormIterator = (props: SimpleFormIteratorProps) => { > <SimpleFormIteratorItem basePath={basePath} - classes={classes} disabled={disabled} disableRemove={disableRemove} disableReordering={disableReordering} @@ -165,8 +170,8 @@ export const SimpleFormIterator = (props: SimpleFormIteratorProps) => { ))} </TransitionGroup> {!disabled && !disableAdd && ( - <li className={classes.line}> - <span className={classes.action}> + <li className={SimpleFormIteratorClasses.line}> + <span className={SimpleFormIteratorClasses.action}> {cloneElement(addButton, { onClick: handleAddButtonClick( addButton.props.onClick @@ -179,7 +184,7 @@ export const SimpleFormIterator = (props: SimpleFormIteratorProps) => { </span> </li> )} - </ul> + </Root> </SimpleFormIteratorContext.Provider> ) : null; }; @@ -195,7 +200,6 @@ SimpleFormIterator.propTypes = { removeButton: PropTypes.element, basePath: PropTypes.string, children: PropTypes.node, - classes: PropTypes.object, className: PropTypes.string, // @ts-ignore fields: PropTypes.object, @@ -215,7 +219,6 @@ export interface SimpleFormIteratorProps addButton?: ReactElement; basePath?: string; children?: ReactNode; - classes?: ClassesOverride<typeof useSimpleFormIteratorStyles>; className?: string; defaultValue?: any; disabled?: boolean; @@ -238,4 +241,57 @@ export interface SimpleFormIteratorProps variant?: 'standard' | 'outlined' | 'filled'; } +const Root = styled('ul')(({ theme }) => ({ + [`${SimpleFormIteratorPrefix} .${SimpleFormIteratorClasses.root}`]: { + padding: 0, + marginBottom: 0, + '& > li:last-child': { + borderBottom: 'none', + }, + }, + [`${SimpleFormIteratorPrefix} .${SimpleFormIteratorClasses.line}`]: { + display: 'flex', + listStyleType: 'none', + borderBottom: `solid 1px ${theme.palette.divider}`, + [theme.breakpoints.down('sm')]: { display: 'block' }, + '&.fade-enter': { + opacity: 0.01, + transform: 'translateX(100vw)', + }, + '&.fade-enter-active': { + opacity: 1, + transform: 'translateX(0)', + transition: 'all 500ms ease-in', + }, + '&.fade-exit': { + opacity: 1, + transform: 'translateX(0)', + }, + '&.fade-exit-active': { + opacity: 0.01, + transform: 'translateX(100vw)', + transition: 'all 500ms ease-in', + }, + }, + [`${SimpleFormIteratorPrefix} .${SimpleFormIteratorClasses.index}`]: { + [theme.breakpoints.down('md')]: { display: 'none' }, + marginRight: theme.spacing(1), + }, + [`${SimpleFormIteratorPrefix} .${SimpleFormIteratorClasses.indexContainer}`]: { + display: 'flex', + paddingTop: '1em', + marginRight: theme.spacing(1), + alignItems: 'center', + }, + [`${SimpleFormIteratorPrefix} .${SimpleFormIteratorClasses.form}`]: { + flex: 2, + }, + [`${SimpleFormIteratorPrefix} .${SimpleFormIteratorClasses.action}`]: { + paddingTop: '0.5em', + }, + [`${SimpleFormIteratorPrefix} .${SimpleFormIteratorClasses.leftIcon}`]: { + marginRight: theme.spacing(1), + }, +})); + const DefaultLabelFn = index => index + 1; diff --git a/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIteratorItem.tsx b/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIteratorItem.tsx index 1112fb9ee3c..daf7759271f 100644 --- a/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIteratorItem.tsx +++ b/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIteratorItem.tsx @@ -9,13 +9,12 @@ import { ReactNode, useMemo, } from 'react'; -import { Typography } from '@material-ui/core'; +import { Typography } from '@mui/material'; import classNames from 'classnames'; import { Record } from 'ra-core'; -import { ClassesOverride } from '../../types'; import FormInput from '../../form/FormInput'; -import { useSimpleFormIteratorStyles } from './useSimpleFormIteratorStyles'; +import { SimpleFormIteratorClasses } from './useSimpleFormIteratorStyles'; import { useSimpleFormIterator } from './useSimpleFormIterator'; import { ArrayInputContextValue } from './ArrayInputContext'; import { @@ -27,7 +26,6 @@ export const SimpleFormIteratorItem = (props: SimpleFormIteratorItemProps) => { const { basePath, children, - classes, disabled, disableReordering, disableRemove, @@ -78,10 +76,13 @@ export const SimpleFormIteratorItem = (props: SimpleFormIteratorItemProps) => { return ( <SimpleFormIteratorItemContext.Provider value={context}> - <li className={classes.line}> + <li className={SimpleFormIteratorClasses.line}> <div> - <div className={classes.indexContainer}> - <Typography variant="body1" className={classes.index}> + <div className={SimpleFormIteratorClasses.indexContainer}> + <Typography + variant="body1" + className={SimpleFormIteratorClasses.index} + > {getItemLabel(index)} </Typography> {!disabled && @@ -97,7 +98,7 @@ export const SimpleFormIteratorItem = (props: SimpleFormIteratorItemProps) => { })} </div> </div> - <section className={classes.form}> + <section className={SimpleFormIteratorClasses.form}> {Children.map(children, (input: ReactElement, index2) => { if (!isValidElement<any>(input)) { return null; @@ -129,7 +130,7 @@ export const SimpleFormIteratorItem = (props: SimpleFormIteratorItemProps) => { })} </section> {!disabled && !disableRemoveField(record) && ( - <span className={classes.action}> + <span className={SimpleFormIteratorClasses.action}> {cloneElement(removeButton, { onClick: handleRemoveButtonClick( removeButton.props.onClick, @@ -152,7 +153,6 @@ export type DisableRemoveFunction = (record: Record) => boolean; export type SimpleFormIteratorItemProps = ArrayInputContextValue & { basePath: string; children?: ReactNode; - classes?: ClassesOverride<typeof useSimpleFormIteratorStyles>; disabled?: boolean; disableRemove?: boolean | DisableRemoveFunction; disableReordering?: boolean; diff --git a/packages/ra-ui-materialui/src/input/ArrayInput/useSimpleFormIteratorStyles.ts b/packages/ra-ui-materialui/src/input/ArrayInput/useSimpleFormIteratorStyles.ts index dd8578a28e5..0b1d0b3207c 100644 --- a/packages/ra-ui-materialui/src/input/ArrayInput/useSimpleFormIteratorStyles.ts +++ b/packages/ra-ui-materialui/src/input/ArrayInput/useSimpleFormIteratorStyles.ts @@ -1,55 +1,11 @@ -import { makeStyles } from '@material-ui/core/styles'; +export const SimpleFormIteratorPrefix = 'RaSimpleFormIterator'; -export const useSimpleFormIteratorStyles = makeStyles( - theme => ({ - root: { - padding: 0, - marginBottom: 0, - '& > li:last-child': { - borderBottom: 'none', - }, - }, - line: { - display: 'flex', - listStyleType: 'none', - borderBottom: `solid 1px ${theme.palette.divider}`, - [theme.breakpoints.down('xs')]: { display: 'block' }, - '&.fade-enter': { - opacity: 0.01, - transform: 'translateX(100vw)', - }, - '&.fade-enter-active': { - opacity: 1, - transform: 'translateX(0)', - transition: 'all 500ms ease-in', - }, - '&.fade-exit': { - opacity: 1, - transform: 'translateX(0)', - }, - '&.fade-exit-active': { - opacity: 0.01, - transform: 'translateX(100vw)', - transition: 'all 500ms ease-in', - }, - }, - index: { - [theme.breakpoints.down('sm')]: { display: 'none' }, - marginRight: theme.spacing(1), - }, - indexContainer: { - display: 'flex', - paddingTop: '1em', - marginRight: theme.spacing(1), - alignItems: 'center', - }, - form: { flex: 2 }, - action: { - paddingTop: '0.5em', - }, - leftIcon: { - marginRight: theme.spacing(1), - }, - }), - { name: 'RaSimpleFormIterator' } -); +export const SimpleFormIteratorClasses = { + root: `${SimpleFormIteratorPrefix}-root`, + line: `${SimpleFormIteratorPrefix}-line`, + index: `${SimpleFormIteratorPrefix}-index`, + indexContainer: `${SimpleFormIteratorPrefix}-indexContainer`, + form: `${SimpleFormIteratorPrefix}-form`, + action: `${SimpleFormIteratorPrefix}-action`, + leftIcon: `${SimpleFormIteratorPrefix}-leftIcon`, +}; diff --git a/packages/ra-ui-materialui/src/input/AutocompleteArrayInput.tsx b/packages/ra-ui-materialui/src/input/AutocompleteArrayInput.tsx index b1b0e01b027..fc4d49714f6 100644 --- a/packages/ra-ui-materialui/src/input/AutocompleteArrayInput.tsx +++ b/packages/ra-ui-materialui/src/input/AutocompleteArrayInput.tsx @@ -5,12 +5,12 @@ import React, { useMemo, isValidElement, } from 'react'; +import { styled } from '@mui/material/styles'; import Downshift, { DownshiftProps } from 'downshift'; import classNames from 'classnames'; import get from 'lodash/get'; -import { TextField, Chip, InputProps } from '@material-ui/core'; -import { makeStyles } from '@material-ui/core/styles'; -import { TextFieldProps } from '@material-ui/core/TextField'; +import { TextField, Chip, InputProps } from '@mui/material'; +import { TextFieldProps } from '@mui/material/TextField'; import { useInput, FieldTitle, @@ -30,6 +30,61 @@ import { useSupportCreateSuggestion, } from './useSupportCreateSuggestion'; +const PREFIX = 'RaAutocompleteArrayInput'; + +const classes = { + container: `${PREFIX}-container`, + suggestionsContainer: `${PREFIX}-suggestionsContainer`, + chip: `${PREFIX}-chip`, + chipContainerFilled: `${PREFIX}-chipContainerFilled`, + chipContainerOutlined: `${PREFIX}-chipContainerOutlined`, + inputRoot: `${PREFIX}-inputRoot`, + inputRootFilled: `${PREFIX}-inputRootFilled`, + inputInput: `${PREFIX}-inputInput`, +}; + +const Root = styled('div')(({ theme }) => ({ + [`&.${classes.container}`]: { + flexGrow: 1, + position: 'relative', + }, + + [`& .${classes.suggestionsContainer}`]: { + zIndex: theme.zIndex.modal, + }, + + [`& .${classes.chip}`]: { + margin: theme.spacing(0.5, 0.5, 0.5, 0), + }, + + [`& .${classes.chipContainerFilled}`]: { + margin: '27px 12px 10px 0', + }, + + [`& .${classes.chipContainerOutlined}`]: { + margin: '12px 12px 10px 0', + }, + + [`& .${classes.inputRoot}`]: { + flexWrap: 'wrap', + }, + + [`& .${classes.inputRootFilled}`]: { + flexWrap: 'wrap', + '& $chip': { + backgroundColor: + theme.palette.mode === 'light' + ? 'rgba(0, 0, 0, 0.09)' + : 'rgba(255, 255, 255, 0.09)', + }, + }, + + [`& .${classes.inputInput}`]: { + width: 'auto', + flexGrow: 1, + }, +})); + /** * An Input component for an autocomplete field, using an array of objects for the options * @@ -96,7 +151,6 @@ const AutocompleteArrayInput = (props: AutocompleteArrayInputProps) => { const { allowDuplicates, allowEmpty, - classes: classesOverride, choices = [], create, createLabel, @@ -161,8 +215,6 @@ const AutocompleteArrayInput = (props: AutocompleteArrayInputProps) => { `If you're not wrapping the AutocompleteArrayInput inside a ReferenceArrayInput, you must provide the choices prop` ); - const classes = useStyles(props); - let inputEl = useRef<HTMLInputElement>(); let anchorEl = useRef<any>(); @@ -385,6 +437,7 @@ const AutocompleteArrayInput = (props: AutocompleteArrayInputProps) => { getItemProps, getLabelProps, getMenuProps, + getRootProps, isOpen, inputValue: suggestionFilter, highlightedIndex, @@ -413,7 +466,7 @@ const AutocompleteArrayInput = (props: AutocompleteArrayInputProps) => { ...(onCreate || create ? [getCreateItem()] : []), ]; return ( - <div className={classes.container}> + <Root className={classes.container} {...getRootProps()}> <TextField id={id} fullWidth={fullWidth} @@ -536,7 +589,7 @@ const AutocompleteArrayInput = (props: AutocompleteArrayInputProps) => { /> ))} </AutocompleteSuggestionList> - </div> + </Root> ); }} </Downshift> @@ -547,44 +600,6 @@ const AutocompleteArrayInput = (props: AutocompleteArrayInputProps) => { const emptyArray = []; -const useStyles = makeStyles( - theme => ({ - container: { - flexGrow: 1, - position: 'relative', - }, - suggestionsContainer: { - zIndex: theme.zIndex.modal, - }, - chip: { - margin: theme.spacing(0.5, 0.5, 0.5, 0), - }, - chipContainerFilled: { - margin: '27px 12px 10px 0', - }, - chipContainerOutlined: { - margin: '12px 12px 10px 0', - }, - inputRoot: { - flexWrap: 'wrap', - }, - inputRootFilled: { - flexWrap: 'wrap', - '& $chip': { - backgroundColor: - theme.palette.type === 'light' - ? 'rgba(0, 0, 0, 0.09)' - : 'rgba(255, 255, 255, 0.09)', - }, - }, - inputInput: { - width: 'auto', - flexGrow: 1, - }, - }), - { name: 'RaAutocompleteArrayInput' } -); - const DefaultSetFilter = () => {}; interface Options { diff --git a/packages/ra-ui-materialui/src/input/AutocompleteInput.tsx b/packages/ra-ui-materialui/src/input/AutocompleteInput.tsx index f9343d9dc58..acb8766a235 100644 --- a/packages/ra-ui-materialui/src/input/AutocompleteInput.tsx +++ b/packages/ra-ui-materialui/src/input/AutocompleteInput.tsx @@ -6,6 +6,7 @@ import React, { useMemo, isValidElement, } from 'react'; +import { styled } from '@mui/material/styles'; import Downshift, { DownshiftProps } from 'downshift'; import get from 'lodash/get'; import classNames from 'classnames'; @@ -14,10 +15,9 @@ import { InputAdornment, IconButton, InputProps, -} from '@material-ui/core'; -import { makeStyles } from '@material-ui/core/styles'; -import ClearIcon from '@material-ui/icons/Clear'; -import { TextFieldProps } from '@material-ui/core/TextField'; +} from '@mui/material'; +import ClearIcon from '@mui/icons-material/Clear'; +import { TextFieldProps } from '@mui/material/TextField'; import { useInput, FieldTitle, @@ -38,6 +38,53 @@ import { useSupportCreateSuggestion, } from './useSupportCreateSuggestion'; +const PREFIX = 'RaAutocompleteInput'; + +const classes = { + container: `${PREFIX}-container`, + clearIcon: `${PREFIX}-clearIcon`, + visibleClearIcon: `${PREFIX}-visibleClearIcon`, + clearButton: `${PREFIX}-clearButton`, + selectAdornment: `${PREFIX}-selectAdornment`, + inputAdornedEnd: `${PREFIX}-inputAdornedEnd`, + suggestionsContainer: `${PREFIX}-suggestionsContainer`, +}; + +const Root = styled('div')(({ theme }) => ({ + [`&.${classes.container}`]: { + flexGrow: 1, + position: 'relative', + }, + + [`& .${classes.clearIcon}`]: { + height: 16, + width: 0, + }, + + [`& .${classes.visibleClearIcon}`]: { + width: 16, + }, + + [`& .${classes.clearButton}`]: { + height: 24, + width: 24, + padding: 0, + }, + + [`& .${classes.selectAdornment}`]: { + position: 'absolute', + right: 24, + }, + + [`& .${classes.inputAdornedEnd}`]: { + paddingRight: 0, + }, + + [`& .${classes.suggestionsContainer}`]: { + zIndex: theme.zIndex.modal, + }, +})); + /** * An Input component for an autocomplete field, using an array of objects for the options * @@ -184,7 +231,6 @@ export const AutocompleteInput = (props: AutocompleteInputProps) => { `If you're not wrapping the AutocompleteInput inside a ReferenceInput, you must provide the choices prop` ); - const classes = useStyles(props); let inputEl = useRef<HTMLInputElement>(); let anchorEl = useRef<any>(); const translate = useTranslate(); @@ -406,6 +452,7 @@ export const AutocompleteInput = (props: AutocompleteInputProps) => { title={label} disableRipple disabled={true} + size="large" > <ClearIcon className={classNames( @@ -443,6 +490,7 @@ export const AutocompleteInput = (props: AutocompleteInputProps) => { onClick={handleClickClearButton(openMenu)} onMouseDown={handleMouseDownClearButton} disabled={disabled} + size="large" > <ClearIcon className={classNames(classes.clearIcon, { @@ -471,6 +519,7 @@ export const AutocompleteInput = (props: AutocompleteInputProps) => { getItemProps, getLabelProps, getMenuProps, + getRootProps, isOpen, highlightedIndex, openMenu, @@ -498,7 +547,7 @@ export const AutocompleteInput = (props: AutocompleteInputProps) => { ]; return ( - <div className={classes.container}> + <Root className={classes.container} {...getRootProps()}> <TextField id={id} name={input.name} @@ -589,7 +638,7 @@ export const AutocompleteInput = (props: AutocompleteInputProps) => { /> ))} </AutocompleteSuggestionList> - </div> + </Root> ); }} </Downshift> @@ -602,42 +651,11 @@ const handleMouseDownClearButton = event => { event.preventDefault(); }; -const useStyles = makeStyles( - theme => ({ - container: { - flexGrow: 1, - position: 'relative', - }, - clearIcon: { - height: 16, - width: 0, - }, - visibleClearIcon: { - width: 16, - }, - clearButton: { - height: 24, - width: 24, - padding: 0, - }, - selectAdornment: { - position: 'absolute', - right: 24, - }, - inputAdornedEnd: { - paddingRight: 0, - }, - suggestionsContainer: { - zIndex: theme.zIndex.modal, - }, - }), - { name: 'RaAutocompleteInput' } -); - interface Options { InputProps?: InputProps; labelProps?: any; suggestionsContainerProps?: any; + fullWidth?: boolean; } export interface AutocompleteInputProps diff --git a/packages/ra-ui-materialui/src/input/AutocompleteInputLoader.tsx b/packages/ra-ui-materialui/src/input/AutocompleteInputLoader.tsx index 625fc3be479..61013e6e03e 100644 --- a/packages/ra-ui-materialui/src/input/AutocompleteInputLoader.tsx +++ b/packages/ra-ui-materialui/src/input/AutocompleteInputLoader.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { CircularProgress } from '@material-ui/core'; +import { CircularProgress } from '@mui/material'; import { useTimeout } from 'ra-core'; export const AutocompleteInputLoader = ({ timeout = 1000 }) => { diff --git a/packages/ra-ui-materialui/src/input/AutocompleteSuggestionItem.tsx b/packages/ra-ui-materialui/src/input/AutocompleteSuggestionItem.tsx index ee99c725dbb..e52671e4c51 100644 --- a/packages/ra-ui-materialui/src/input/AutocompleteSuggestionItem.tsx +++ b/packages/ra-ui-materialui/src/input/AutocompleteSuggestionItem.tsx @@ -1,30 +1,40 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { isValidElement, cloneElement } from 'react'; import parse from 'autosuggest-highlight/parse'; import match from 'autosuggest-highlight/match'; -import { MenuItem } from '@material-ui/core'; -import { makeStyles } from '@material-ui/core/styles'; -import { MenuItemProps } from '@material-ui/core/MenuItem'; +import { MenuItem } from '@mui/material'; +import { MenuItemProps } from '@mui/material/MenuItem'; import classnames from 'classnames'; -const useStyles = makeStyles( - theme => ({ - root: { - fontWeight: 400, - }, - selected: { - fontWeight: 500, - }, - suggestion: { - display: 'block', - fontFamily: theme.typography.fontFamily, - minHeight: 24, - }, - suggestionText: { fontWeight: 300 }, - highlightedSuggestionText: { fontWeight: 500 }, - }), - { name: 'RaAutocompleteSuggestionItem' } -); +const PREFIX = 'RaAutocompleteSuggestionItem'; + +const classes = { + root: `${PREFIX}-root`, + selected: `${PREFIX}-selected`, + suggestion: `${PREFIX}-suggestion`, + suggestionText: `${PREFIX}-suggestionText`, + highlightedSuggestionText: `${PREFIX}-highlightedSuggestionText`, +}; + +const StyledMenuItem = styled(MenuItem)(({ theme }) => ({ + [`&.${classes.root}`]: { + fontWeight: 400, + }, + + [`&.${classes.selected}`]: { + fontWeight: 500, + }, + + [`& .${classes.suggestion}`]: { + display: 'block', + fontFamily: theme.typography.fontFamily, + minHeight: 24, + }, + + [`& .${classes.suggestionText}`]: { fontWeight: 300 }, + [`& .${classes.highlightedSuggestionText}`]: { fontWeight: 500 }, +})); export interface AutocompleteSuggestionItemProps { createValue?: any; @@ -52,7 +62,7 @@ const AutocompleteSuggestionItem = ( getSuggestionText, ...rest } = props; - const classes = useStyles(props); + const isHighlighted = highlightedIndex === index; const suggestionText = 'id' in suggestion && suggestion.id === createValue @@ -67,7 +77,7 @@ const AutocompleteSuggestionItem = ( } return ( - <MenuItem + <StyledMenuItem key={suggestionText} selected={isHighlighted} className={classnames(classes.root, { @@ -98,7 +108,7 @@ const AutocompleteSuggestionItem = ( })} </div> )} - </MenuItem> + </StyledMenuItem> ); }; diff --git a/packages/ra-ui-materialui/src/input/AutocompleteSuggestionList.tsx b/packages/ra-ui-materialui/src/input/AutocompleteSuggestionList.tsx index 051fa3233fa..2e0361d21bd 100644 --- a/packages/ra-ui-materialui/src/input/AutocompleteSuggestionList.tsx +++ b/packages/ra-ui-materialui/src/input/AutocompleteSuggestionList.tsx @@ -1,21 +1,25 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { ReactNode } from 'react'; import classnames from 'classnames'; -import { Paper, Popper } from '@material-ui/core'; -import { makeStyles } from '@material-ui/core/styles'; +import { Paper, Popper } from '@mui/material'; -const useStyles = makeStyles( - { - suggestionsContainer: { - zIndex: 2, - }, - suggestionsPaper: { - maxHeight: '50vh', - overflowY: 'auto', - }, +const PREFIX = 'RaAutocompleteSuggestionList'; + +const classes = { + suggestionsContainer: `${PREFIX}-suggestionsContainer`, + suggestionsPaper: `${PREFIX}-suggestionsPaper`, +}; + +const StyledPopper = styled(Popper)({ + [`&.${classes.suggestionsContainer}`]: { + zIndex: 2, + }, + [`& .${classes.suggestionsPaper}`]: { + maxHeight: '50vh', + overflowY: 'auto', }, - { name: 'RaAutocompleteSuggestionList' } -); +}); interface Props { children: ReactNode; @@ -27,6 +31,7 @@ interface Props { suggestionsContainerProps?: any; } +const PopperModifiers = []; const AutocompleteSuggestionList = (props: Props) => { const { children, @@ -36,14 +41,13 @@ const AutocompleteSuggestionList = (props: Props) => { inputEl, suggestionsContainerProps, } = props; - const classes = useStyles(props); return ( - <Popper + <StyledPopper open={isOpen} anchorEl={inputEl} className={classnames(classes.suggestionsContainer, className)} - modifiers={{}} + modifiers={PopperModifiers} {...suggestionsContainerProps} > <div {...(isOpen ? menuProps : {})}> @@ -58,7 +62,7 @@ const AutocompleteSuggestionList = (props: Props) => { {children} </Paper> </div> - </Popper> + </StyledPopper> ); }; diff --git a/packages/ra-ui-materialui/src/input/BooleanInput.tsx b/packages/ra-ui-materialui/src/input/BooleanInput.tsx index 003bf1e4599..8abd29ebe16 100644 --- a/packages/ra-ui-materialui/src/input/BooleanInput.tsx +++ b/packages/ra-ui-materialui/src/input/BooleanInput.tsx @@ -1,10 +1,10 @@ import * as React from 'react'; import { useCallback } from 'react'; import PropTypes from 'prop-types'; -import FormControlLabel from '@material-ui/core/FormControlLabel'; -import FormHelperText from '@material-ui/core/FormHelperText'; -import FormGroup, { FormGroupProps } from '@material-ui/core/FormGroup'; -import Switch, { SwitchProps } from '@material-ui/core/Switch'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import FormHelperText from '@mui/material/FormHelperText'; +import FormGroup, { FormGroupProps } from '@mui/material/FormGroup'; +import Switch, { SwitchProps } from '@mui/material/Switch'; import { FieldTitle, useInput, InputProps } from 'ra-core'; import sanitizeInputRestProps from './sanitizeInputRestProps'; diff --git a/packages/ra-ui-materialui/src/input/CheckboxGroupInput.tsx b/packages/ra-ui-materialui/src/input/CheckboxGroupInput.tsx index 112c5806a60..08d112c0adb 100644 --- a/packages/ra-ui-materialui/src/input/CheckboxGroupInput.tsx +++ b/packages/ra-ui-materialui/src/input/CheckboxGroupInput.tsx @@ -1,13 +1,13 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { useCallback, FunctionComponent } from 'react'; import PropTypes from 'prop-types'; import get from 'lodash/get'; -import FormLabel from '@material-ui/core/FormLabel'; -import FormControl, { FormControlProps } from '@material-ui/core/FormControl'; -import FormGroup from '@material-ui/core/FormGroup'; -import FormHelperText from '@material-ui/core/FormHelperText'; -import { makeStyles } from '@material-ui/core/styles'; -import { CheckboxProps } from '@material-ui/core/Checkbox'; +import FormLabel from '@mui/material/FormLabel'; +import FormControl, { FormControlProps } from '@mui/material/FormControl'; +import FormGroup from '@mui/material/FormGroup'; +import FormHelperText from '@mui/material/FormHelperText'; +import { CheckboxProps } from '@mui/material/Checkbox'; import { FieldTitle, useInput, ChoicesInputProps, warning } from 'ra-core'; import sanitizeInputRestProps from './sanitizeInputRestProps'; @@ -16,7 +16,22 @@ import InputHelperText from './InputHelperText'; import classnames from 'classnames'; import Labeled from './Labeled'; import { LinearProgress } from '../layout'; -import { ClassesOverride } from '../types'; + +const PREFIX = 'RaCheckboxGroupInput'; + +const classes = { + root: `${PREFIX}-root`, + label: `${PREFIX}-label`, +}; + +const StyledFormControl = styled(FormControl)(({ theme }) => ({ + [`&.${classes.root}`]: {}, + + [`& .${classes.label}`]: { + transform: 'translate(0, 8px) scale(0.75)', + transformOrigin: `top ${theme.direction === 'ltr' ? 'left' : 'right'}`, + }, +})); /** * An Input component for a checkbox group, using an array of objects for the options @@ -106,7 +121,6 @@ const CheckboxGroupInput: FunctionComponent<CheckboxGroupInputProps> = props => validate, ...rest } = props; - const classes = useStyles(props); warning( source === undefined, @@ -171,7 +185,7 @@ const CheckboxGroupInput: FunctionComponent<CheckboxGroupInputProps> = props => } return ( - <FormControl + <StyledFormControl component="fieldset" margin={margin} error={touched && !!(error || submitError)} @@ -208,7 +222,7 @@ const CheckboxGroupInput: FunctionComponent<CheckboxGroupInputProps> = props => helperText={helperText} /> </FormHelperText> - </FormControl> + </StyledFormControl> ); }; @@ -222,19 +236,6 @@ const sanitizeRestProps = ({ ...rest }: any) => sanitizeInputRestProps(rest); -const useStyles = makeStyles( - theme => ({ - root: {}, - label: { - transform: 'translate(0, 8px) scale(0.75)', - transformOrigin: `top ${ - theme.direction === 'ltr' ? 'left' : 'right' - }`, - }, - }), - { name: 'RaCheckboxGroupInput' } -); - CheckboxGroupInput.propTypes = { choices: PropTypes.arrayOf(PropTypes.object), className: PropTypes.string, @@ -263,8 +264,6 @@ CheckboxGroupInput.defaultProps = { export interface CheckboxGroupInputProps extends ChoicesInputProps<CheckboxProps>, - FormControlProps { - classes?: ClassesOverride<typeof useStyles>; -} + FormControlProps {} export default CheckboxGroupInput; diff --git a/packages/ra-ui-materialui/src/input/CheckboxGroupInputItem.tsx b/packages/ra-ui-materialui/src/input/CheckboxGroupInputItem.tsx index 04215cf77cb..0903e1b5779 100644 --- a/packages/ra-ui-materialui/src/input/CheckboxGroupInputItem.tsx +++ b/packages/ra-ui-materialui/src/input/CheckboxGroupInputItem.tsx @@ -1,17 +1,20 @@ import * as React from 'react'; -import FormControlLabel from '@material-ui/core/FormControlLabel'; -import Checkbox from '@material-ui/core/Checkbox'; -import { makeStyles } from '@material-ui/core/styles'; +import { styled } from '@mui/material/styles'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import Checkbox from '@mui/material/Checkbox'; import { useChoices } from 'ra-core'; -const useStyles = makeStyles( - { - checkbox: { - height: 32, - }, +const PREFIX = 'RaCheckboxGroupInputItem'; + +const classes = { + checkbox: `${PREFIX}-checkbox`, +}; + +const StyledFormControlLabel = styled(FormControlLabel)({ + [`& .${classes.checkbox}`]: { + height: 32, }, - { name: 'RaCheckboxGroupInputItem' } -); +}); const CheckboxGroupInputItem = props => { const { @@ -26,7 +29,7 @@ const CheckboxGroupInputItem = props => { value, ...rest } = props; - const classes = useStyles(props); + const { getChoiceText, getChoiceValue } = useChoices({ optionText, optionValue, @@ -36,7 +39,7 @@ const CheckboxGroupInputItem = props => { const choiceName = getChoiceText(choice); return ( - <FormControlLabel + <StyledFormControlLabel htmlFor={`${id}_${getChoiceValue(choice)}`} key={getChoiceValue(choice)} onChange={onChange} diff --git a/packages/ra-ui-materialui/src/input/DateInput.tsx b/packages/ra-ui-materialui/src/input/DateInput.tsx index 81428f70f8c..b8fdd672da8 100644 --- a/packages/ra-ui-materialui/src/input/DateInput.tsx +++ b/packages/ra-ui-materialui/src/input/DateInput.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import PropTypes from 'prop-types'; -import TextField, { TextFieldProps } from '@material-ui/core/TextField'; +import TextField, { TextFieldProps } from '@mui/material/TextField'; import { useInput, FieldTitle, InputProps } from 'ra-core'; import sanitizeInputRestProps from './sanitizeInputRestProps'; diff --git a/packages/ra-ui-materialui/src/input/DateTimeInput.spec.tsx b/packages/ra-ui-materialui/src/input/DateTimeInput.spec.tsx index d8e8a33b20e..c2497c62465 100644 --- a/packages/ra-ui-materialui/src/input/DateTimeInput.spec.tsx +++ b/packages/ra-ui-materialui/src/input/DateTimeInput.spec.tsx @@ -5,8 +5,7 @@ import { Form } from 'react-final-form'; import { required, FormWithRedirect } from 'ra-core'; import { renderWithRedux } from 'ra-test'; import format from 'date-fns/format'; -import { ThemeProvider } from '@material-ui/core'; -import { createTheme } from '@material-ui/core/styles'; +import { createTheme, ThemeProvider } from '@mui/material'; import DateTimeInput from './DateTimeInput'; import { ArrayInput, SimpleFormIterator } from './ArrayInput'; diff --git a/packages/ra-ui-materialui/src/input/DateTimeInput.tsx b/packages/ra-ui-materialui/src/input/DateTimeInput.tsx index 6d18057b058..c9d045a53a9 100644 --- a/packages/ra-ui-materialui/src/input/DateTimeInput.tsx +++ b/packages/ra-ui-materialui/src/input/DateTimeInput.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import PropTypes from 'prop-types'; -import TextField, { TextFieldProps } from '@material-ui/core/TextField'; +import TextField, { TextFieldProps } from '@mui/material/TextField'; import { useInput, FieldTitle, InputProps } from 'ra-core'; import sanitizeInputRestProps from './sanitizeInputRestProps'; diff --git a/packages/ra-ui-materialui/src/input/FileInput.tsx b/packages/ra-ui-materialui/src/input/FileInput.tsx index 5fc2927d2a1..a620d7db54f 100644 --- a/packages/ra-ui-materialui/src/input/FileInput.tsx +++ b/packages/ra-ui-materialui/src/input/FileInput.tsx @@ -5,11 +5,11 @@ import React, { ReactElement, ReactNode, } from 'react'; +import { styled } from '@mui/material/styles'; import PropTypes from 'prop-types'; import { shallowEqual } from 'react-redux'; import { useDropzone, DropzoneOptions } from 'react-dropzone'; -import { makeStyles } from '@material-ui/core/styles'; -import FormHelperText from '@material-ui/core/FormHelperText'; +import FormHelperText from '@mui/material/FormHelperText'; import classnames from 'classnames'; import { useInput, useTranslate, InputProps } from 'ra-core'; @@ -18,23 +18,28 @@ import FileInputPreview from './FileInputPreview'; import sanitizeInputRestProps from './sanitizeInputRestProps'; import InputHelperText from './InputHelperText'; -const useStyles = makeStyles( - theme => ({ - dropZone: { - background: theme.palette.background.default, - cursor: 'pointer', - padding: theme.spacing(1), - textAlign: 'center', - color: theme.palette.getContrastText( - theme.palette.background.default - ), - }, - preview: {}, - removeButton: {}, - root: { width: '100%' }, - }), - { name: 'RaFileInput' } -); +const PREFIX = 'RaFileInput'; + +const classes = { + dropZone: `${PREFIX}-dropZone`, + preview: `${PREFIX}-preview`, + removeButton: `${PREFIX}-removeButton`, + root: `${PREFIX}-root`, +}; + +const StyledLabeled = styled(Labeled)(({ theme }) => ({ + [`& .${classes.dropZone}`]: { + background: theme.palette.background.default, + cursor: 'pointer', + padding: theme.spacing(1), + textAlign: 'center', + color: theme.palette.getContrastText(theme.palette.background.default), + }, + + [`& .${classes.preview}`]: {}, + [`& .${classes.removeButton}`]: {}, + [`&.${classes.root}`]: { width: '100%' }, +})); export interface FileInputProps { accept?: string; @@ -57,7 +62,6 @@ const FileInput = (props: FileInputProps & InputProps<FileInputOptions>) => { accept, children, className, - classes: classesOverride, format, helperText, label, @@ -78,7 +82,6 @@ const FileInput = (props: FileInputProps & InputProps<FileInputOptions>) => { ...rest } = props; const translate = useTranslate(); - const classes = useStyles(props); // turn a browser dropped file structure into expected structure const transformFile = file => { @@ -175,7 +178,7 @@ const FileInput = (props: FileInputProps & InputProps<FileInputOptions>) => { }); return ( - <Labeled + <StyledLabeled id={id} label={label} className={classnames(classes.root, className)} @@ -231,14 +234,13 @@ const FileInput = (props: FileInputProps & InputProps<FileInputOptions>) => { </div> )} </> - </Labeled> + </StyledLabeled> ); }; FileInput.propTypes = { accept: PropTypes.string, children: PropTypes.element, - classes: PropTypes.object, className: PropTypes.string, id: PropTypes.string, isRequired: PropTypes.bool, diff --git a/packages/ra-ui-materialui/src/input/FileInputPreview.tsx b/packages/ra-ui-materialui/src/input/FileInputPreview.tsx index 81d814a1ae8..892680a535d 100644 --- a/packages/ra-ui-materialui/src/input/FileInputPreview.tsx +++ b/packages/ra-ui-materialui/src/input/FileInputPreview.tsx @@ -1,20 +1,25 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { useEffect, ReactNode } from 'react'; import PropTypes from 'prop-types'; -import { makeStyles } from '@material-ui/core/styles'; -import RemoveCircle from '@material-ui/icons/RemoveCircle'; -import IconButton from '@material-ui/core/IconButton'; +import RemoveCircle from '@mui/icons-material/RemoveCircle'; +import IconButton from '@mui/material/IconButton'; import { useTranslate } from 'ra-core'; -const useStyles = makeStyles( - theme => ({ - removeButton: {}, - removeIcon: { - color: theme.palette.error.main, - }, - }), - { name: 'RaFileInputPreview' } -); +const PREFIX = 'RaFileInputPreview'; + +const classes = { + removeButton: `${PREFIX}-removeButton`, + removeIcon: `${PREFIX}-removeIcon`, +}; + +const Root = styled('div')(({ theme }) => ({ + [`& .${classes.removeButton}`]: {}, + + [`& .${classes.removeIcon}`]: { + color: theme.palette.error.main, + }, +})); interface Props { children: ReactNode; @@ -33,7 +38,7 @@ const FileInputPreview = (props: Props) => { file, ...rest } = props; - const classes = useStyles(props); + const translate = useTranslate(); useEffect(() => { @@ -47,17 +52,18 @@ const FileInputPreview = (props: Props) => { }, [file]); return ( - <div className={className} {...rest}> + <Root className={className} {...rest}> <IconButton className={classes.removeButton} onClick={onRemove} aria-label={translate('ra.action.delete')} title={translate('ra.action.delete')} + size="large" > <RemoveCircle className={classes.removeIcon} /> </IconButton> {children} - </div> + </Root> ); }; diff --git a/packages/ra-ui-materialui/src/input/ImageInput.tsx b/packages/ra-ui-materialui/src/input/ImageInput.tsx index 1061515d813..080be541a14 100644 --- a/packages/ra-ui-materialui/src/input/ImageInput.tsx +++ b/packages/ra-ui-materialui/src/input/ImageInput.tsx @@ -1,48 +1,53 @@ import * as React from 'react'; -import { makeStyles } from '@material-ui/core/styles'; +import { styled } from '@mui/material/styles'; import FileInput, { FileInputProps, FileInputOptions } from './FileInput'; import { InputProps } from 'ra-core'; -const useStyles = makeStyles( - theme => ({ - root: { width: '100%' }, - dropZone: { - background: theme.palette.background.default, - cursor: 'pointer', - padding: theme.spacing(1), - textAlign: 'center', - color: theme.palette.getContrastText( - theme.palette.background.default - ), - }, - preview: { - display: 'inline-block', +const PREFIX = 'RaImageInput'; + +const classes = { + root: `${PREFIX}-root`, + dropZone: `${PREFIX}-dropZone`, + preview: `${PREFIX}-preview`, + removeButton: `${PREFIX}-removeButton`, +}; + +const StyledFileInput = styled(FileInput)(({ theme }) => ({ + [`& .${classes.root}`]: { width: '100%' }, + + [`& .${classes.dropZone}`]: { + background: theme.palette.background.default, + cursor: 'pointer', + padding: theme.spacing(1), + textAlign: 'center', + color: theme.palette.getContrastText(theme.palette.background.default), + }, + + [`& .${classes.preview}`]: { + display: 'inline-block', + }, + + [`& .${classes.removeButton}`]: { + display: 'inline-block', + position: 'relative', + float: 'left', + '& button': { + position: 'absolute', + top: theme.spacing(1), + right: theme.spacing(1), + minWidth: theme.spacing(2), + opacity: 0, }, - removeButton: { - display: 'inline-block', - position: 'relative', - float: 'left', - '& button': { - position: 'absolute', - top: theme.spacing(1), - right: theme.spacing(1), - minWidth: theme.spacing(2), - opacity: 0, - }, - '&:hover button': { - opacity: 1, - }, + '&:hover button': { + opacity: 1, }, - }), - { name: 'RaImageInput' } -); + }, +})); const ImageInput = (props: ImageInputProps) => { - const classes = useStyles(props); - return ( - <FileInput + <StyledFileInput labelMultiple="ra.input.image.upload_several" labelSingle="ra.input.image.upload_single" classes={classes} diff --git a/packages/ra-ui-materialui/src/input/Labeled.tsx b/packages/ra-ui-materialui/src/input/Labeled.tsx index 824cb35283c..3fe2d9c6193 100644 --- a/packages/ra-ui-materialui/src/input/Labeled.tsx +++ b/packages/ra-ui-materialui/src/input/Labeled.tsx @@ -1,46 +1,37 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { ReactElement } from 'react'; import PropTypes from 'prop-types'; -import InputLabel from '@material-ui/core/InputLabel'; -import FormControl from '@material-ui/core/FormControl'; -import { makeStyles } from '@material-ui/core/styles'; +import InputLabel from '@mui/material/InputLabel'; +import FormControl from '@mui/material/FormControl'; import { FieldTitle } from 'ra-core'; -const useStyles = makeStyles( - theme => ({ - label: { - position: 'relative', - }, - value: { - fontFamily: theme.typography.fontFamily, - color: 'currentColor', - padding: `${theme.spacing(1)}px 0 ${theme.spacing(1) / 2}px`, - border: 0, - boxSizing: 'content-box', - verticalAlign: 'middle', - background: 'none', - margin: 0, // Reset for Safari - display: 'block', - width: '100%', - }, - }), - { name: 'RaLabeled' } -); +const PREFIX = 'RaLabeled'; + +const classes = { + label: `${PREFIX}-label`, + value: `${PREFIX}-value`, +}; + +const StyledFormControl = styled(FormControl)(({ theme }) => ({ + [`& .${classes.label}`]: { + position: 'relative', + }, + + [`& .${classes.value}`]: { + fontFamily: theme.typography.fontFamily, + color: 'currentColor', + padding: `calc(${theme.spacing(1)} 0 ${theme.spacing(1)} / 2)`, + border: 0, + boxSizing: 'content-box', + verticalAlign: 'middle', + background: 'none', + margin: 0, // Reset for Safari + display: 'block', + width: '100%', + }, +})); -export interface LabeledProps { - children: ReactElement; - className?: string; - classes?: object; - fullWidth?: boolean; - id?: string; - input?: any; - isRequired?: boolean; - label?: string | ReactElement; - meta?: any; - resource?: string; - source?: string; - [key: string]: any; -} /** * Use any component as read-only Input, labeled just like other Inputs. * @@ -60,7 +51,6 @@ const Labeled = (props: LabeledProps) => { const { children, className, - classes: classesOverride, fullWidth, id, input, @@ -72,7 +62,7 @@ const Labeled = (props: LabeledProps) => { source, ...rest } = props; - const classes = useStyles(props); + if (!label && !source) { // @ts-ignore const name = children && children.type && children.type.name; @@ -84,7 +74,7 @@ const Labeled = (props: LabeledProps) => { const restProps = fullWidth ? { ...rest, fullWidth } : rest; return ( - <FormControl + <StyledFormControl className={className} fullWidth={fullWidth} error={meta && meta.touched && !!(meta.error || meta.submitError)} @@ -107,7 +97,7 @@ const Labeled = (props: LabeledProps) => { }) : children} </div> - </FormControl> + </StyledFormControl> ); }; @@ -115,7 +105,6 @@ Labeled.propTypes = { basePath: PropTypes.string, children: PropTypes.element, className: PropTypes.string, - classes: PropTypes.object, fullWidth: PropTypes.bool, id: PropTypes.string, input: PropTypes.object, @@ -129,4 +118,18 @@ Labeled.propTypes = { labelStyle: PropTypes.object, }; +export interface LabeledProps { + children: ReactElement; + className?: string; + fullWidth?: boolean; + id?: string; + input?: any; + isRequired?: boolean; + label?: string | ReactElement; + meta?: any; + resource?: string; + source?: string; + [key: string]: any; +} + export default Labeled; diff --git a/packages/ra-ui-materialui/src/input/NullableBooleanInput.tsx b/packages/ra-ui-materialui/src/input/NullableBooleanInput.tsx index 3ad0973acfe..333bba79edf 100644 --- a/packages/ra-ui-materialui/src/input/NullableBooleanInput.tsx +++ b/packages/ra-ui-materialui/src/input/NullableBooleanInput.tsx @@ -1,20 +1,23 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import PropTypes from 'prop-types'; -import TextField, { TextFieldProps } from '@material-ui/core/TextField'; -import MenuItem from '@material-ui/core/MenuItem'; -import { makeStyles } from '@material-ui/core/styles'; +import TextField, { TextFieldProps } from '@mui/material/TextField'; +import MenuItem from '@mui/material/MenuItem'; import classnames from 'classnames'; import { useInput, useTranslate, FieldTitle, InputProps } from 'ra-core'; import sanitizeInputRestProps from './sanitizeInputRestProps'; import InputHelperText from './InputHelperText'; -const useStyles = makeStyles( - theme => ({ - input: { width: theme.spacing(16) }, - }), - { name: 'RaNullableBooleanInput' } -); +const PREFIX = 'RaNullableBooleanInput'; + +const classes = { + input: `${PREFIX}-input`, +}; + +const StyledTextField = styled(TextField)(({ theme }) => ({ + [`&.${classes.input}`]: { width: theme.spacing(16) }, +})); const getBooleanFromString = (value: string): boolean | null => { if (value === 'true') return true; @@ -38,7 +41,6 @@ export type NullableBooleanInputProps = InputProps<TextFieldProps> & const NullableBooleanInput = (props: NullableBooleanInputProps) => { const { className, - classes: classesOverride, format = getStringFromBoolean, helperText, label, @@ -57,7 +59,7 @@ const NullableBooleanInput = (props: NullableBooleanInputProps) => { trueLabel = 'ra.boolean.true', ...rest } = props; - const classes = useStyles(props); + const translate = useTranslate(); const { @@ -78,7 +80,7 @@ const NullableBooleanInput = (props: NullableBooleanInputProps) => { }); return ( - <TextField + <StyledTextField id={id} {...input} select @@ -107,7 +109,7 @@ const NullableBooleanInput = (props: NullableBooleanInputProps) => { <MenuItem value="">{translate(nullLabel)}</MenuItem> <MenuItem value="false">{translate(falseLabel)}</MenuItem> <MenuItem value="true">{translate(trueLabel)}</MenuItem> - </TextField> + </StyledTextField> ); }; diff --git a/packages/ra-ui-materialui/src/input/NumberInput.tsx b/packages/ra-ui-materialui/src/input/NumberInput.tsx index a1e5af31417..070b8049c48 100644 --- a/packages/ra-ui-materialui/src/input/NumberInput.tsx +++ b/packages/ra-ui-materialui/src/input/NumberInput.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { FunctionComponent } from 'react'; import PropTypes from 'prop-types'; -import TextField, { TextFieldProps } from '@material-ui/core/TextField'; +import TextField, { TextFieldProps } from '@mui/material/TextField'; import { useInput, FieldTitle, InputProps } from 'ra-core'; import InputHelperText from './InputHelperText'; diff --git a/packages/ra-ui-materialui/src/input/PasswordInput.tsx b/packages/ra-ui-materialui/src/input/PasswordInput.tsx index 7d99c9a94af..2677f2ec97f 100644 --- a/packages/ra-ui-materialui/src/input/PasswordInput.tsx +++ b/packages/ra-ui-materialui/src/input/PasswordInput.tsx @@ -1,9 +1,9 @@ import * as React from 'react'; import { useState } from 'react'; import { useTranslate } from 'ra-core'; -import { InputAdornment, IconButton } from '@material-ui/core'; -import Visibility from '@material-ui/icons/Visibility'; -import VisibilityOff from '@material-ui/icons/VisibilityOff'; +import { InputAdornment, IconButton } from '@mui/material'; +import Visibility from '@mui/icons-material/Visibility'; +import VisibilityOff from '@mui/icons-material/VisibilityOff'; import TextInput, { TextInputProps } from './TextInput'; @@ -34,6 +34,7 @@ const PasswordInput = (props: PasswordInputProps) => { : 'ra.input.password.toggle_hidden' )} onClick={handleClick} + size="large" > {visible ? <Visibility /> : <VisibilityOff />} </IconButton> diff --git a/packages/ra-ui-materialui/src/input/RadioButtonGroupInput.tsx b/packages/ra-ui-materialui/src/input/RadioButtonGroupInput.tsx index 2e39f9ce95f..9a048ff34df 100644 --- a/packages/ra-ui-materialui/src/input/RadioButtonGroupInput.tsx +++ b/packages/ra-ui-materialui/src/input/RadioButtonGroupInput.tsx @@ -1,14 +1,14 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import PropTypes from 'prop-types'; import { FormControl, FormHelperText, FormLabel, RadioGroup, -} from '@material-ui/core'; -import { makeStyles } from '@material-ui/core/styles'; -import { RadioGroupProps } from '@material-ui/core/RadioGroup'; -import { FormControlProps } from '@material-ui/core/FormControl'; +} from '@mui/material'; +import { RadioGroupProps } from '@mui/material/RadioGroup'; +import { FormControlProps } from '@mui/material/FormControl'; import get from 'lodash/get'; import { useInput, FieldTitle, ChoicesInputProps, warning } from 'ra-core'; @@ -18,17 +18,18 @@ import RadioButtonGroupInputItem from './RadioButtonGroupInputItem'; import Labeled from './Labeled'; import { LinearProgress } from '../layout'; -const useStyles = makeStyles( - theme => ({ - label: { - transform: 'translate(0, 5px) scale(0.75)', - transformOrigin: `top ${ - theme.direction === 'ltr' ? 'left' : 'right' - }`, - }, - }), - { name: 'RaRadioButtonGroupInput' } -); +const PREFIX = 'RaRadioButtonGroupInput'; + +const classes = { + label: `${PREFIX}-label`, +}; + +const StyledFormControl = styled(FormControl)(({ theme }) => ({ + [`& .${classes.label}`]: { + transform: 'translate(0, 5px) scale(0.75)', + transformOrigin: `top ${theme.direction === 'ltr' ? 'left' : 'right'}`, + }, +})); /** * An Input component for a radio button group, using an array of objects for the options @@ -90,7 +91,6 @@ const useStyles = makeStyles( const RadioButtonGroupInput = (props: RadioButtonGroupInputProps) => { const { choices = [], - classes: classesOverride, format, helperText, label, @@ -111,7 +111,6 @@ const RadioButtonGroupInput = (props: RadioButtonGroupInputProps) => { validate, ...rest } = props; - const classes = useStyles(props); warning( source === undefined, @@ -155,7 +154,7 @@ const RadioButtonGroupInput = (props: RadioButtonGroupInputProps) => { ); } return ( - <FormControl + <StyledFormControl component="fieldset" margin={margin} error={touched && !!(error || submitError)} @@ -190,7 +189,7 @@ const RadioButtonGroupInput = (props: RadioButtonGroupInputProps) => { helperText={helperText} /> </FormHelperText> - </FormControl> + </StyledFormControl> ); }; diff --git a/packages/ra-ui-materialui/src/input/RadioButtonGroupInputItem.tsx b/packages/ra-ui-materialui/src/input/RadioButtonGroupInputItem.tsx index 30b86480559..13bcfa4de31 100644 --- a/packages/ra-ui-materialui/src/input/RadioButtonGroupInputItem.tsx +++ b/packages/ra-ui-materialui/src/input/RadioButtonGroupInputItem.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { useField } from 'react-final-form'; -import FormControlLabel from '@material-ui/core/FormControlLabel'; -import Radio from '@material-ui/core/Radio'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import Radio from '@mui/material/Radio'; import { useChoices } from 'ra-core'; const RadioButtonGroupInputItem = ({ diff --git a/packages/ra-ui-materialui/src/input/ReferenceError.tsx b/packages/ra-ui-materialui/src/input/ReferenceError.tsx index 8a4dab87f8b..71fa97b554d 100644 --- a/packages/ra-ui-materialui/src/input/ReferenceError.tsx +++ b/packages/ra-ui-materialui/src/input/ReferenceError.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import PropTypes from 'prop-types'; -import TextField from '@material-ui/core/TextField'; +import TextField from '@mui/material/TextField'; const ReferenceError = ({ label, error }) => ( <TextField error disabled label={label} value={error} margin="normal" /> diff --git a/packages/ra-ui-materialui/src/input/ResettableTextField.tsx b/packages/ra-ui-materialui/src/input/ResettableTextField.tsx index ec00bb15422..eef81b7014c 100644 --- a/packages/ra-ui-materialui/src/input/ResettableTextField.tsx +++ b/packages/ra-ui-materialui/src/input/ResettableTextField.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { useCallback } from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; @@ -7,20 +8,49 @@ import { IconButton, TextField as MuiTextField, TextFieldProps, - Theme, -} from '@material-ui/core'; -import { makeStyles } from '@material-ui/core/styles'; -import ClearIcon from '@material-ui/icons/Clear'; +} from '@mui/material'; +import ClearIcon from '@mui/icons-material/Clear'; import { InputProps, useTranslate } from 'ra-core'; -import { ClassesOverride } from '../types'; -import { Styles } from '@material-ui/styles'; + +const PREFIX = 'RaResettableTextField'; + +const classes = { + clearIcon: `${PREFIX}-clearIcon`, + visibleClearIcon: `${PREFIX}-visibleClearIcon`, + clearButton: `${PREFIX}-clearButton`, + selectAdornment: `${PREFIX}-selectAdornment`, + inputAdornedEnd: `${PREFIX}-inputAdornedEnd`, +}; + +export const ResettableTextFieldStyles = { + [`& .${classes.clearIcon}`]: { + height: 16, + width: 0, + }, + [`& .${classes.visibleClearIcon}`]: { + width: 16, + }, + [`& .${classes.clearButton}`]: { + height: 24, + width: 24, + padding: 0, + }, + [`& .${classes.selectAdornment}`]: { + position: 'absolute', + right: 24, + }, + [`& .${classes.inputAdornedEnd}`]: { + paddingRight: 0, + }, +}; + +const StyledTextField = styled(MuiTextField)(ResettableTextFieldStyles); /** * An override of the default Material-UI TextField which is resettable */ const ResettableTextField = (props: ResettableTextFieldProps) => { const { - classes: classesOverride, clearAlwaysVisible, InputProps, value, @@ -30,7 +60,7 @@ const ResettableTextField = (props: ResettableTextFieldProps) => { margin = 'dense', ...rest } = props; - const classes = useStyles(props); + const translate = useTranslate(); const { onChange, onFocus, onBlur } = props; @@ -94,6 +124,7 @@ const ResettableTextField = (props: ResettableTextFieldProps) => { title={translate('ra.action.clear_input_value')} disableRipple disabled={true} + size="large" > <ClearIcon className={classNames( @@ -138,6 +169,7 @@ const ResettableTextField = (props: ResettableTextFieldProps) => { onClick={handleClickClearButton} onMouseDown={handleMouseDownClearButton} disabled={disabled} + size="large" > <ClearIcon className={classNames(clearIcon, { @@ -151,7 +183,7 @@ const ResettableTextField = (props: ResettableTextFieldProps) => { }; return ( - <MuiTextField + <StyledTextField classes={restClasses} value={value} InputProps={{ @@ -172,38 +204,13 @@ const ResettableTextField = (props: ResettableTextFieldProps) => { ); }; -export const resettableStyles: Styles<Theme, ResettableTextFieldProps> = { - clearIcon: { - height: 16, - width: 0, - }, - visibleClearIcon: { - width: 16, - }, - clearButton: { - height: 24, - width: 24, - padding: 0, - }, - selectAdornment: { - position: 'absolute', - right: 24, - }, - inputAdornedEnd: { - paddingRight: 0, - }, -}; - -const useStyles = makeStyles(resettableStyles, { - name: 'RaResettableTextField', -}); +export {}; const handleMouseDownClearButton = event => { event.preventDefault(); }; ResettableTextField.propTypes = { - classes: PropTypes.object, clearAlwaysVisible: PropTypes.bool, disabled: PropTypes.bool, InputProps: PropTypes.object, @@ -215,7 +222,6 @@ ResettableTextField.propTypes = { }; interface Props { - classes?: ClassesOverride<typeof useStyles>; clearAlwaysVisible?: boolean; resettable?: boolean; } diff --git a/packages/ra-ui-materialui/src/input/SearchInput.tsx b/packages/ra-ui-materialui/src/input/SearchInput.tsx index 7b33eab1792..2954bfc4e93 100644 --- a/packages/ra-ui-materialui/src/input/SearchInput.tsx +++ b/packages/ra-ui-materialui/src/input/SearchInput.tsx @@ -1,26 +1,27 @@ import * as React from 'react'; -import PropTypes from 'prop-types'; -import SearchIcon from '@material-ui/icons/Search'; -import { InputAdornment } from '@material-ui/core'; -import { makeStyles } from '@material-ui/core/styles'; -import { TextFieldProps } from '@material-ui/core/TextField'; +import { styled } from '@mui/material/styles'; +import SearchIcon from '@mui/icons-material/Search'; +import { InputAdornment } from '@mui/material'; +import { TextFieldProps } from '@mui/material/TextField'; import { useTranslate, InputProps } from 'ra-core'; import TextInput from './TextInput'; -const useStyles = makeStyles( - { - input: { - marginTop: 32, - }, +const PREFIX = 'RaSearchInput'; + +const classes = { + input: `${PREFIX}-input`, +}; + +const StyledTextInput = styled(TextInput)({ + [`&.${classes.input}`]: { + marginTop: 32, }, - { name: 'RaSearchInput' } -); +}); const SearchInput = (props: SearchInputProps) => { - const { classes: classesOverride, ...rest } = props; const translate = useTranslate(); - const classes = useStyles(props); + if (props.label) { throw new Error( "<SearchInput> isn't designed to be used with a label prop. Use <TextInput> if you need a label." @@ -28,7 +29,7 @@ const SearchInput = (props: SearchInputProps) => { } return ( - <TextInput + <StyledTextInput hiddenLabel label="" resettable @@ -41,15 +42,11 @@ const SearchInput = (props: SearchInputProps) => { ), }} className={classes.input} - {...rest} + {...props} /> ); }; -SearchInput.propTypes = { - classes: PropTypes.object, -}; - export type SearchInputProps = InputProps<TextFieldProps> & Omit<TextFieldProps, 'label' | 'helperText'>; diff --git a/packages/ra-ui-materialui/src/input/SelectArrayInput.spec.tsx b/packages/ra-ui-materialui/src/input/SelectArrayInput.spec.tsx index 3c60158cc24..488d1abc1e4 100644 --- a/packages/ra-ui-materialui/src/input/SelectArrayInput.spec.tsx +++ b/packages/ra-ui-materialui/src/input/SelectArrayInput.spec.tsx @@ -190,7 +190,7 @@ describe('<SelectArrayInput />', () => { const select = getByRole('button'); fireEvent.mouseDown(select); const option1 = getByText('Angular'); - expect(option1.getAttribute('aria-disabled')).toEqual('false'); + expect(option1.getAttribute('aria-disabled')).toBeNull(); const option2 = getByText('React'); expect(option2.getAttribute('aria-disabled')).toEqual('true'); diff --git a/packages/ra-ui-materialui/src/input/SelectArrayInput.tsx b/packages/ra-ui-materialui/src/input/SelectArrayInput.tsx index d6e31cc09ba..edb657737c0 100644 --- a/packages/ra-ui-materialui/src/input/SelectArrayInput.tsx +++ b/packages/ra-ui-materialui/src/input/SelectArrayInput.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; -import { useCallback, useRef, useState, useEffect } from 'react'; +import { styled } from '@mui/material/styles'; +import { useCallback, useRef } from 'react'; import PropTypes from 'prop-types'; import { Select, @@ -8,8 +9,7 @@ import { FormHelperText, FormControl, Chip, -} from '@material-ui/core'; -import { makeStyles } from '@material-ui/core/styles'; +} from '@mui/material'; import classnames from 'classnames'; import { FieldTitle, @@ -19,8 +19,8 @@ import { useChoices, } from 'ra-core'; import InputHelperText from './InputHelperText'; -import { SelectProps } from '@material-ui/core/Select'; -import { FormControlProps } from '@material-ui/core/FormControl'; +import { SelectProps } from '@mui/material/Select'; +import { FormControlProps } from '@mui/material/FormControl'; import Labeled from './Labeled'; import { LinearProgress } from '../layout'; import { @@ -28,6 +28,27 @@ import { useSupportCreateSuggestion, } from './useSupportCreateSuggestion'; +const PREFIX = 'RaSelectArrayInput'; + +const classes = { + root: `${PREFIX}-root`, + chips: `${PREFIX}-chips`, + chip: `${PREFIX}-chip`, +}; + +const StyledFormControl = styled(FormControl)(({ theme }) => ({ + [`&.${classes.root}`]: {}, + + [`& .${classes.chips}`]: { + display: 'flex', + flexWrap: 'wrap', + }, + + [`& .${classes.chip}`]: { + margin: theme.spacing(1 / 4), + }, +})); + /** * An Input component for a select box allowing multiple selections, using an array of objects for the options * @@ -83,7 +104,6 @@ import { const SelectArrayInput = (props: SelectArrayInputProps) => { const { choices = [], - classes: classesOverride, className, create, createLabel, @@ -111,16 +131,7 @@ const SelectArrayInput = (props: SelectArrayInputProps) => { ...rest } = props; - const classes = useStyles(props); const inputLabel = useRef(null); - const [labelWidth, setLabelWidth] = useState(0); - - useEffect(() => { - // Will be null while loading and we don't need this fix in that case - if (inputLabel.current) { - setLabelWidth(inputLabel.current.offsetWidth); - } - }, []); const { getChoiceText, getChoiceValue, getDisableValue } = useChoices({ optionText, @@ -210,7 +221,7 @@ const SelectArrayInput = (props: SelectArrayInputProps) => { return ( <> - <FormControl + <StyledFormControl margin={margin} className={classnames(classes.root, className)} error={touched && !!(error || submitError)} @@ -257,7 +268,6 @@ const SelectArrayInput = (props: SelectArrayInputProps) => { {...input} onChange={handleChangeWithCreateSupport} value={input.value || []} - labelWidth={labelWidth} {...options} > {finalChoices.map(renderMenuItem)} @@ -269,7 +279,7 @@ const SelectArrayInput = (props: SelectArrayInputProps) => { helperText={helperText} /> </FormHelperText> - </FormControl> + </StyledFormControl> {createElement} </> ); @@ -289,7 +299,6 @@ export interface SelectArrayInputProps SelectArrayInput.propTypes = { choices: PropTypes.arrayOf(PropTypes.object), - classes: PropTypes.object, className: PropTypes.string, children: PropTypes.node, label: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), @@ -357,18 +366,4 @@ const sanitizeRestProps = ({ ...rest }: any) => rest; -const useStyles = makeStyles( - theme => ({ - root: {}, - chips: { - display: 'flex', - flexWrap: 'wrap', - }, - chip: { - margin: theme.spacing(1 / 4), - }, - }), - { name: 'RaSelectArrayInput' } -); - export default SelectArrayInput; diff --git a/packages/ra-ui-materialui/src/input/SelectInput.spec.tsx b/packages/ra-ui-materialui/src/input/SelectInput.spec.tsx index 8da61dfebe2..3db06db39bb 100644 --- a/packages/ra-ui-materialui/src/input/SelectInput.spec.tsx +++ b/packages/ra-ui-materialui/src/input/SelectInput.spec.tsx @@ -2,11 +2,14 @@ import * as React from 'react'; import { render, fireEvent, waitFor } from '@testing-library/react'; import { Form } from 'react-final-form'; import { TestTranslationProvider } from 'ra-core'; +import { createTheme, ThemeProvider } from '@mui/material/styles'; import { SelectInput } from './SelectInput'; import { required } from 'ra-core'; import { useCreateSuggestionContext } from './useSupportCreateSuggestion'; +const theme = createTheme({}); + describe('<SelectInput />', () => { const defaultProps = { source: 'language', @@ -19,11 +22,13 @@ describe('<SelectInput />', () => { it('should use the input parameter value as the initial input value', () => { const { container } = render( - <Form - initialValues={{ language: 'ang' }} - onSubmit={jest.fn()} - render={() => <SelectInput {...defaultProps} />} - /> + <ThemeProvider theme={theme}> + <Form + initialValues={{ language: 'ang' }} + onSubmit={jest.fn()} + render={() => <SelectInput {...defaultProps} />} + /> + </ThemeProvider> ); const input = container.querySelector('input'); expect(input.value).toEqual('ang'); @@ -31,10 +36,12 @@ describe('<SelectInput />', () => { it('should render choices as mui MenuItem components', async () => { const { getByRole, getByText, queryAllByRole } = render( - <Form - onSubmit={jest.fn()} - render={() => <SelectInput {...defaultProps} />} - /> + <ThemeProvider theme={theme}> + <Form + onSubmit={jest.fn()} + render={() => <SelectInput {...defaultProps} />} + /> + </ThemeProvider> ); const select = getByRole('button'); fireEvent.mouseDown(select); @@ -50,23 +57,25 @@ describe('<SelectInput />', () => { it('should render disable choices marked so', () => { const { getByRole, getByText } = render( - <Form - onSubmit={jest.fn()} - render={() => ( - <SelectInput - {...defaultProps} - choices={[ - { id: 'ang', name: 'Angular' }, - { id: 'rea', name: 'React', disabled: true }, - ]} - /> - )} - /> + <ThemeProvider theme={theme}> + <Form + onSubmit={jest.fn()} + render={() => ( + <SelectInput + {...defaultProps} + choices={[ + { id: 'ang', name: 'Angular' }, + { id: 'rea', name: 'React', disabled: true }, + ]} + /> + )} + /> + </ThemeProvider> ); const select = getByRole('button'); fireEvent.mouseDown(select); const option1 = getByText('Angular'); - expect(option1.getAttribute('aria-disabled')).toEqual('false'); + expect(option1.getAttribute('aria-disabled')).toBeNull(); const option2 = getByText('React'); expect(option2.getAttribute('aria-disabled')).toEqual('true'); @@ -74,10 +83,12 @@ describe('<SelectInput />', () => { it('should add an empty menu when allowEmpty is true', () => { const { getByRole, queryAllByRole } = render( - <Form - onSubmit={jest.fn()} - render={() => <SelectInput {...defaultProps} allowEmpty />} - /> + <ThemeProvider theme={theme}> + <Form + onSubmit={jest.fn()} + render={() => <SelectInput {...defaultProps} allowEmpty />} + /> + </ThemeProvider> ); const select = getByRole('button'); fireEvent.mouseDown(select); @@ -91,16 +102,18 @@ describe('<SelectInput />', () => { const emptyValue = 'test'; const { getByRole, queryAllByRole } = render( - <Form - onSubmit={jest.fn()} - render={() => ( - <SelectInput - {...defaultProps} - allowEmpty - emptyValue={emptyValue} - /> - )} - /> + <ThemeProvider theme={theme}> + <Form + onSubmit={jest.fn()} + render={() => ( + <SelectInput + {...defaultProps} + allowEmpty + emptyValue={emptyValue} + /> + )} + /> + </ThemeProvider> ); const select = getByRole('button'); fireEvent.mouseDown(select); @@ -114,16 +127,18 @@ describe('<SelectInput />', () => { const emptyText = 'Default'; const { getByRole, getByText, queryAllByRole } = render( - <Form - onSubmit={jest.fn()} - render={() => ( - <SelectInput - allowEmpty - emptyText={emptyText} - {...defaultProps} - /> - )} - /> + <ThemeProvider theme={theme}> + <Form + onSubmit={jest.fn()} + render={() => ( + <SelectInput + allowEmpty + emptyText={emptyText} + {...defaultProps} + /> + )} + /> + </ThemeProvider> ); const emptyOption = getByRole('button'); fireEvent.mouseDown(emptyOption); @@ -142,16 +157,18 @@ describe('<SelectInput />', () => { ); const { getByRole, getByText, queryAllByRole } = render( - <Form - onSubmit={jest.fn()} - render={() => ( - <SelectInput - allowEmpty - emptyText={emptyText} - {...defaultProps} - /> - )} - /> + <ThemeProvider theme={theme}> + <Form + onSubmit={jest.fn()} + render={() => ( + <SelectInput + allowEmpty + emptyText={emptyText} + {...defaultProps} + /> + )} + /> + </ThemeProvider> ); const emptyOption = getByRole('button'); fireEvent.mouseDown(emptyOption); @@ -164,10 +181,12 @@ describe('<SelectInput />', () => { it('should not add a falsy (null or false) element when allowEmpty is false', () => { const { getByRole, queryAllByRole } = render( - <Form - onSubmit={jest.fn()} - render={() => <SelectInput {...defaultProps} />} - /> + <ThemeProvider theme={theme}> + <Form + onSubmit={jest.fn()} + render={() => <SelectInput {...defaultProps} />} + /> + </ThemeProvider> ); const select = getByRole('button'); fireEvent.mouseDown(select); @@ -177,19 +196,21 @@ describe('<SelectInput />', () => { it('should use optionValue as value identifier', () => { const { getByRole, getByText } = render( - <Form - onSubmit={jest.fn()} - render={() => ( - <SelectInput - {...defaultProps} - optionValue="foobar" - choices={[ - { foobar: 'ang', name: 'Angular' }, - { foobar: 'rea', name: 'React' }, - ]} - /> - )} - /> + <ThemeProvider theme={theme}> + <Form + onSubmit={jest.fn()} + render={() => ( + <SelectInput + {...defaultProps} + optionValue="foobar" + choices={[ + { foobar: 'ang', name: 'Angular' }, + { foobar: 'rea', name: 'React' }, + ]} + /> + )} + /> + </ThemeProvider> ); const select = getByRole('button'); fireEvent.mouseDown(select); @@ -200,19 +221,21 @@ describe('<SelectInput />', () => { it('should use optionValue including "." as value identifier', () => { const { getByRole, getByText } = render( - <Form - onSubmit={jest.fn()} - render={() => ( - <SelectInput - {...defaultProps} - optionValue="foobar.id" - choices={[ - { foobar: { id: 'ang' }, name: 'Angular' }, - { foobar: { id: 'rea' }, name: 'React' }, - ]} - /> - )} - /> + <ThemeProvider theme={theme}> + <Form + onSubmit={jest.fn()} + render={() => ( + <SelectInput + {...defaultProps} + optionValue="foobar.id" + choices={[ + { foobar: { id: 'ang' }, name: 'Angular' }, + { foobar: { id: 'rea' }, name: 'React' }, + ]} + /> + )} + /> + </ThemeProvider> ); const select = getByRole('button'); fireEvent.mouseDown(select); @@ -223,19 +246,21 @@ describe('<SelectInput />', () => { it('should use optionText with a string value as text identifier', () => { const { getByRole, getByText } = render( - <Form - onSubmit={jest.fn()} - render={() => ( - <SelectInput - {...defaultProps} - optionText="foobar" - choices={[ - { id: 'ang', foobar: 'Angular' }, - { id: 'rea', foobar: 'React' }, - ]} - /> - )} - /> + <ThemeProvider theme={theme}> + <Form + onSubmit={jest.fn()} + render={() => ( + <SelectInput + {...defaultProps} + optionText="foobar" + choices={[ + { id: 'ang', foobar: 'Angular' }, + { id: 'rea', foobar: 'React' }, + ]} + /> + )} + /> + </ThemeProvider> ); const select = getByRole('button'); fireEvent.mouseDown(select); @@ -246,19 +271,21 @@ describe('<SelectInput />', () => { it('should use optionText with a string value including "." as text identifier', () => { const { getByRole, getByText } = render( - <Form - onSubmit={jest.fn()} - render={() => ( - <SelectInput - {...defaultProps} - optionText="foobar.name" - choices={[ - { id: 'ang', foobar: { name: 'Angular' } }, - { id: 'rea', foobar: { name: 'React' } }, - ]} - /> - )} - /> + <ThemeProvider theme={theme}> + <Form + onSubmit={jest.fn()} + render={() => ( + <SelectInput + {...defaultProps} + optionText="foobar.name" + choices={[ + { id: 'ang', foobar: { name: 'Angular' } }, + { id: 'rea', foobar: { name: 'React' } }, + ]} + /> + )} + /> + </ThemeProvider> ); const select = getByRole('button'); fireEvent.mouseDown(select); @@ -269,19 +296,21 @@ describe('<SelectInput />', () => { it('should use optionText with a function value as text identifier', () => { const { getByRole, getByText } = render( - <Form - onSubmit={jest.fn()} - render={() => ( - <SelectInput - {...defaultProps} - optionText={choice => choice.foobar} - choices={[ - { id: 'ang', foobar: 'Angular' }, - { id: 'rea', foobar: 'React' }, - ]} - /> - )} - /> + <ThemeProvider theme={theme}> + <Form + onSubmit={jest.fn()} + render={() => ( + <SelectInput + {...defaultProps} + optionText={choice => choice.foobar} + choices={[ + { id: 'ang', foobar: 'Angular' }, + { id: 'rea', foobar: 'React' }, + ]} + /> + )} + /> + </ThemeProvider> ); const select = getByRole('button'); fireEvent.mouseDown(select); @@ -296,19 +325,21 @@ describe('<SelectInput />', () => { ); const { getByRole, getByLabelText } = render( - <Form - onSubmit={jest.fn()} - render={() => ( - <SelectInput - {...defaultProps} - optionText={<Foobar />} - choices={[ - { id: 'ang', foobar: 'Angular' }, - { id: 'rea', foobar: 'React' }, - ]} - /> - )} - /> + <ThemeProvider theme={theme}> + <Form + onSubmit={jest.fn()} + render={() => ( + <SelectInput + {...defaultProps} + optionText={<Foobar />} + choices={[ + { id: 'ang', foobar: 'Angular' }, + { id: 'rea', foobar: 'React' }, + ]} + /> + )} + /> + </ThemeProvider> ); const select = getByRole('button'); fireEvent.mouseDown(select); @@ -319,12 +350,14 @@ describe('<SelectInput />', () => { it('should translate the choices by default', () => { const { getByRole, getByText, queryAllByRole } = render( - <TestTranslationProvider translate={x => `**${x}**`}> - <Form - onSubmit={jest.fn()} - render={() => <SelectInput {...defaultProps} />} - /> - </TestTranslationProvider> + <ThemeProvider theme={theme}> + <TestTranslationProvider translate={x => `**${x}**`}> + <Form + onSubmit={jest.fn()} + render={() => <SelectInput {...defaultProps} />} + /> + </TestTranslationProvider> + </ThemeProvider> ); const select = getByRole('button'); fireEvent.mouseDown(select); @@ -340,17 +373,19 @@ describe('<SelectInput />', () => { it('should not translate the choices if translateChoice is false', () => { const { getByRole, getByText, queryAllByRole } = render( - <TestTranslationProvider translate={x => `**${x}**`}> - <Form - onSubmit={jest.fn()} - render={() => ( - <SelectInput - {...defaultProps} - translateChoice={false} - /> - )} - /> - </TestTranslationProvider> + <ThemeProvider theme={theme}> + <TestTranslationProvider translate={x => `**${x}**`}> + <Form + onSubmit={jest.fn()} + render={() => ( + <SelectInput + {...defaultProps} + translateChoice={false} + /> + )} + /> + </TestTranslationProvider> + </ThemeProvider> ); const select = getByRole('button'); fireEvent.mouseDown(select); @@ -366,15 +401,17 @@ describe('<SelectInput />', () => { it('should display helperText if prop is present', () => { const { getByText } = render( - <Form - onSubmit={jest.fn()} - render={() => ( - <SelectInput - {...defaultProps} - helperText="Can I help you?" - /> - )} - /> + <ThemeProvider theme={theme}> + <Form + onSubmit={jest.fn()} + render={() => ( + <SelectInput + {...defaultProps} + helperText="Can I help you?" + /> + )} + /> + </ThemeProvider> ); const helperText = getByText('Can I help you?'); expect(helperText).not.toBeNull(); @@ -383,12 +420,17 @@ describe('<SelectInput />', () => { describe('error message', () => { it('should not be displayed if field is pristine', () => { const { queryAllByText } = render( - <Form - onSubmit={jest.fn()} - render={() => ( - <SelectInput {...defaultProps} validate={required()} /> - )} - /> + <ThemeProvider theme={theme}> + <Form + onSubmit={jest.fn()} + render={() => ( + <SelectInput + {...defaultProps} + validate={required()} + /> + )} + /> + </ThemeProvider> ); const error = queryAllByText('ra.validation.required'); expect(error.length).toEqual(0); @@ -396,14 +438,19 @@ describe('<SelectInput />', () => { it('should not be displayed if field has been touched but is valid', () => { const { getByLabelText, queryAllByText } = render( - <Form - validateOnBlur - initialValues={{ language: 'ang' }} - onSubmit={jest.fn()} - render={() => ( - <SelectInput {...defaultProps} validate={required()} /> - )} - /> + <ThemeProvider theme={theme}> + <Form + validateOnBlur + initialValues={{ language: 'ang' }} + onSubmit={jest.fn()} + render={() => ( + <SelectInput + {...defaultProps} + validate={required()} + /> + )} + /> + </ThemeProvider> ); const input = getByLabelText('resources.posts.fields.language *'); input.focus(); @@ -415,18 +462,20 @@ describe('<SelectInput />', () => { it('should be displayed if field has been touched and is invalid', () => { const { getByLabelText, getByRole, getByText } = render( - <Form - validateOnBlur - onSubmit={jest.fn()} - render={() => ( - <SelectInput - {...defaultProps} - allowEmpty - emptyText="Empty" - validate={required()} - /> - )} - /> + <ThemeProvider theme={theme}> + <Form + validateOnBlur + onSubmit={jest.fn()} + render={() => ( + <SelectInput + {...defaultProps} + allowEmpty + emptyText="Empty" + validate={required()} + /> + )} + /> + </ThemeProvider> ); const input = getByLabelText('resources.posts.fields.language *'); input.focus(); @@ -451,19 +500,21 @@ describe('<SelectInput />', () => { it('should not render a LinearProgress if loading is true and a second has not passed yet', () => { const { queryByRole } = render( - <Form - validateOnBlur - onSubmit={jest.fn()} - render={() => ( - <SelectInput - {...{ - ...defaultProps, - loaded: true, - loading: true, - }} - /> - )} - /> + <ThemeProvider theme={theme}> + <Form + validateOnBlur + onSubmit={jest.fn()} + render={() => ( + <SelectInput + {...{ + ...defaultProps, + loaded: true, + loading: true, + }} + /> + )} + /> + </ThemeProvider> ); expect(queryByRole('progressbar')).toBeNull(); @@ -471,19 +522,21 @@ describe('<SelectInput />', () => { it('should render a LinearProgress if loading is true and a second has passed', async () => { const { queryByRole } = render( - <Form - validateOnBlur - onSubmit={jest.fn()} - render={() => ( - <SelectInput - {...{ - ...defaultProps, - loaded: true, - loading: true, - }} - /> - )} - /> + <ThemeProvider theme={theme}> + <Form + validateOnBlur + onSubmit={jest.fn()} + render={() => ( + <SelectInput + {...{ + ...defaultProps, + loaded: true, + loading: true, + }} + /> + )} + /> + </ThemeProvider> ); await new Promise(resolve => setTimeout(resolve, 1001)); @@ -493,11 +546,13 @@ describe('<SelectInput />', () => { it('should not render a LinearProgress if loading is false', () => { const { queryByRole } = render( - <Form - validateOnBlur - onSubmit={jest.fn()} - render={() => <SelectInput {...defaultProps} />} - /> + <ThemeProvider theme={theme}> + <Form + validateOnBlur + onSubmit={jest.fn()} + render={() => <SelectInput {...defaultProps} />} + /> + </ThemeProvider> ); expect(queryByRole('progressbar')).toBeNull(); @@ -508,20 +563,22 @@ describe('<SelectInput />', () => { const newChoice = { id: 'js_fatigue', name: 'New Kid On The Block' }; const { getByLabelText, getByRole, getByText, queryByText } = render( - <Form - validateOnBlur - onSubmit={jest.fn()} - render={() => ( - <SelectInput - {...defaultProps} - choices={choices} - onCreate={() => { - choices.push(newChoice); - return newChoice; - }} - /> - )} - /> + <ThemeProvider theme={theme}> + <Form + validateOnBlur + onSubmit={jest.fn()} + render={() => ( + <SelectInput + {...defaultProps} + choices={choices} + onCreate={() => { + choices.push(newChoice); + return newChoice; + }} + /> + )} + /> + </ThemeProvider> ); const input = getByLabelText( @@ -546,24 +603,26 @@ describe('<SelectInput />', () => { const newChoice = { id: 'js_fatigue', name: 'New Kid On The Block' }; const { getByLabelText, getByRole, getByText, queryByText } = render( - <Form - validateOnBlur - onSubmit={jest.fn()} - render={() => ( - <SelectInput - {...defaultProps} - choices={choices} - onCreate={() => { - return new Promise(resolve => { - setTimeout(() => { - choices.push(newChoice); - resolve(newChoice); - }, 200); - }); - }} - /> - )} - /> + <ThemeProvider theme={theme}> + <Form + validateOnBlur + onSubmit={jest.fn()} + render={() => ( + <SelectInput + {...defaultProps} + choices={choices} + onCreate={() => { + return new Promise(resolve => { + setTimeout(() => { + choices.push(newChoice); + resolve(newChoice); + }, 200); + }); + }} + /> + )} + /> + </ThemeProvider> ); const input = getByLabelText( @@ -600,17 +659,19 @@ describe('<SelectInput />', () => { }; const { getByLabelText, getByRole, getByText, queryByText } = render( - <Form - validateOnBlur - onSubmit={jest.fn()} - render={() => ( - <SelectInput - {...defaultProps} - choices={choices} - create={<Create />} - /> - )} - /> + <ThemeProvider theme={theme}> + <Form + validateOnBlur + onSubmit={jest.fn()} + render={() => ( + <SelectInput + {...defaultProps} + choices={choices} + create={<Create />} + /> + )} + /> + </ThemeProvider> ); const input = getByLabelText( diff --git a/packages/ra-ui-materialui/src/input/SelectInput.tsx b/packages/ra-ui-materialui/src/input/SelectInput.tsx index 7db3505e958..90d48dc9d45 100644 --- a/packages/ra-ui-materialui/src/input/SelectInput.tsx +++ b/packages/ra-ui-materialui/src/input/SelectInput.tsx @@ -2,9 +2,9 @@ import * as React from 'react'; import { useCallback } from 'react'; import PropTypes from 'prop-types'; import get from 'lodash/get'; -import MenuItem from '@material-ui/core/MenuItem'; -import { TextFieldProps } from '@material-ui/core/TextField'; -import { makeStyles } from '@material-ui/core/styles'; +import MenuItem from '@mui/material/MenuItem'; +import { TextFieldProps } from '@mui/material/TextField'; +import { styled } from '@mui/material/styles'; import { useInput, FieldTitle, @@ -14,7 +14,9 @@ import { warning, } from 'ra-core'; -import ResettableTextField, { resettableStyles } from './ResettableTextField'; +import ResettableTextField, { + ResettableTextFieldStyles, +} from './ResettableTextField'; import InputHelperText from './InputHelperText'; import sanitizeInputRestProps from './sanitizeInputRestProps'; import Labeled from './Labeled'; @@ -23,7 +25,6 @@ import { useSupportCreateSuggestion, SupportCreateSuggestionOptions, } from './useSupportCreateSuggestion'; -import { ClassesOverride } from '../types'; /** * An Input component for a select box, using an array of objects for the options @@ -132,7 +133,6 @@ export const SelectInput = (props: SelectInputProps) => { ...rest } = props; const translate = useTranslate(); - const classes = useStyles(props); warning( source === undefined, @@ -233,7 +233,7 @@ export const SelectInput = (props: SelectInputProps) => { return ( <> - <ResettableTextField + <StyledResettableTextField id={id} {...input} onChange={handleChangeWithCreateSupport} @@ -249,7 +249,7 @@ export const SelectInput = (props: SelectInputProps) => { /> ) } - className={`${classes.input} ${className}`} + className={className} clearAlwaysVisible error={!!(touched && (error || submitError))} helperText={ @@ -283,7 +283,7 @@ export const SelectInput = (props: SelectInputProps) => { </MenuItem> ))} {renderCreateItem()} - </ResettableTextField> + </StyledResettableTextField> {createElement} </> ); @@ -294,7 +294,6 @@ SelectInput.propTypes = { emptyText: PropTypes.oneOfType([PropTypes.string, PropTypes.element]), emptyValue: PropTypes.any, choices: PropTypes.arrayOf(PropTypes.object), - classes: PropTypes.object, className: PropTypes.string, label: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), options: PropTypes.object, @@ -356,19 +355,19 @@ const sanitizeRestProps = ({ ...rest }: any) => sanitizeInputRestProps(rest); -const useStyles = makeStyles( - theme => ({ - ...resettableStyles, - input: { - minWidth: theme.spacing(20), - }, - }), - { name: 'RaSelectInput' } -); +const PREFIX = 'RaSelectInput'; + +const classes = { + input: `${PREFIX}-inputEnd`, +}; + +const StyledResettableTextField = styled(ResettableTextField)(({ theme }) => ({ + ...ResettableTextFieldStyles, + [classes.input]: { + minWidth: theme.spacing(20), + }, +})); -export interface SelectInputProps - extends ChoicesInputProps<TextFieldProps>, - Omit<SupportCreateSuggestionOptions, 'handleChange'>, - Omit<TextFieldProps, 'label' | 'helperText' | 'classes'> { - classes?: ClassesOverride<typeof useStyles>; -} +export type SelectInputProps = ChoicesInputProps<TextFieldProps> & + Omit<SupportCreateSuggestionOptions, 'handleChange'> & + Omit<TextFieldProps, 'label' | 'helperText' | 'classes'>; diff --git a/packages/ra-ui-materialui/src/input/TextInput.tsx b/packages/ra-ui-materialui/src/input/TextInput.tsx index 9c6cc706ae4..8a7635af4e5 100644 --- a/packages/ra-ui-materialui/src/input/TextInput.tsx +++ b/packages/ra-ui-materialui/src/input/TextInput.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import { useInput, FieldTitle, InputProps } from 'ra-core'; -import { TextFieldProps } from '@material-ui/core/TextField'; +import { TextFieldProps } from '@mui/material/TextField'; import ResettableTextField from './ResettableTextField'; import InputHelperText from './InputHelperText'; diff --git a/packages/ra-ui-materialui/src/input/TranslatableInputs.spec.tsx b/packages/ra-ui-materialui/src/input/TranslatableInputs.spec.tsx index c8d827875a7..0459d4e436d 100644 --- a/packages/ra-ui-materialui/src/input/TranslatableInputs.spec.tsx +++ b/packages/ra-ui-materialui/src/input/TranslatableInputs.spec.tsx @@ -6,7 +6,7 @@ import TextInput from './TextInput'; import { FormWithRedirect, required, useTranslatableContext } from 'ra-core'; import { renderWithRedux } from 'ra-test'; import { TranslatableInputsTab } from './TranslatableInputsTab'; -import { Tabs } from '@material-ui/core'; +import { Tabs } from '@mui/material'; const record = { id: 123, @@ -92,7 +92,6 @@ describe('<TranslatableInputs />', () => { key={locale} value={locale} locale={locale} - classes={{ error: 'error' }} /> ))} </Tabs> @@ -143,7 +142,9 @@ describe('<TranslatableInputs />', () => { fireEvent.click(getByText('ra.locales.en')); const tabs = getAllByRole('tab'); expect(tabs[1].getAttribute('id')).toEqual('translatable-header-fr'); - expect(tabs[1].classList.contains('error')).toEqual(true); + expect( + tabs[1].classList.contains('RaTranslatableInputsTab-error') + ).toEqual(true); }); it('should allow to update any input for any locale', () => { diff --git a/packages/ra-ui-materialui/src/input/TranslatableInputs.tsx b/packages/ra-ui-materialui/src/input/TranslatableInputs.tsx index 16cde03ef41..ad1ecec9f15 100644 --- a/packages/ra-ui-materialui/src/input/TranslatableInputs.tsx +++ b/packages/ra-ui-materialui/src/input/TranslatableInputs.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { ReactElement, ReactNode } from 'react'; import { TranslatableContextProvider, @@ -7,7 +8,20 @@ import { } from 'ra-core'; import { TranslatableInputsTabs } from './TranslatableInputsTabs'; import { TranslatableInputsTabContent } from './TranslatableInputsTabContent'; -import { makeStyles } from '@material-ui/core/styles'; + +const PREFIX = 'RaTranslatableInputs'; + +const classes = { + root: `${PREFIX}-root`, +}; + +const Root = styled('div')(({ theme }) => ({ + [`&.${classes.root}`]: { + flexGrow: 1, + marginTop: theme.spacing(1), + marginBottom: theme.spacing(0.5), + }, +})); /** * Provides a way to edit multiple languages for any input passed as children. @@ -75,10 +89,9 @@ export const TranslatableInputs = ( margin, } = props; const context = useTranslatable({ defaultLocale, locales }); - const classes = useStyles(props); return ( - <div className={classes.root}> + <Root className={classes.root}> <TranslatableContextProvider value={context}> {selector} {locales.map(locale => ( @@ -93,7 +106,7 @@ export const TranslatableInputs = ( </TranslatableInputsTabContent> ))} </TranslatableContextProvider> - </div> + </Root> ); }; @@ -104,14 +117,3 @@ export interface TranslatableInputsProps extends UseTranslatableOptions { margin?: 'none' | 'normal' | 'dense'; variant?: 'standard' | 'outlined' | 'filled'; } - -const useStyles = makeStyles( - theme => ({ - root: { - flexGrow: 1, - marginTop: theme.spacing(1), - marginBottom: theme.spacing(0.5), - }, - }), - { name: 'RaTranslatableInputs' } -); diff --git a/packages/ra-ui-materialui/src/input/TranslatableInputsTab.tsx b/packages/ra-ui-materialui/src/input/TranslatableInputsTab.tsx index fe27899fb3d..fcee084a946 100644 --- a/packages/ra-ui-materialui/src/input/TranslatableInputsTab.tsx +++ b/packages/ra-ui-materialui/src/input/TranslatableInputsTab.tsx @@ -1,9 +1,26 @@ import React from 'react'; -import Tab, { TabProps } from '@material-ui/core/Tab'; +import { styled } from '@mui/material/styles'; +import Tab, { TabProps } from '@mui/material/Tab'; import { useFormGroup, useTranslate } from 'ra-core'; -import { makeStyles } from '@material-ui/core/styles'; -import { ClassesOverride } from '../types'; import { capitalize } from 'inflection'; +import classnames from 'classnames'; + +const PREFIX = 'RaTranslatableInputsTab'; + +const classes = { + root: `${PREFIX}-root`, + error: `${PREFIX}-error`, +}; + +const StyledTab = styled(Tab)(({ theme }) => ({ + [`&.${classes.root}`]: { + fontSize: '0.8em', + minHeight: theme.spacing(3), + minWidth: theme.spacing(6), + }, + + [`& .${classes.error}`]: { color: theme.palette.error.main }, +})); /** * Single tab that selects a locale in a TranslatableInputs component. @@ -12,40 +29,26 @@ import { capitalize } from 'inflection'; export const TranslatableInputsTab = ( props: TranslatableInputsTabProps & TabProps ) => { - const { groupKey = '', locale, classes: classesOverride, ...rest } = props; + const { groupKey = '', locale, ...rest } = props; const { invalid, touched } = useFormGroup(`${groupKey}${locale}`); - const classes = useStyles(props); const translate = useTranslate(); return ( - <Tab + <StyledTab id={`translatable-header-${groupKey}${locale}`} label={translate(`ra.locales.${locale}`, { _: capitalize(locale), })} - className={`${classes.root} ${ - invalid && touched ? classes.error : '' - }`} + className={classnames(classes.root, { + [classes.error]: invalid && touched, + })} {...rest} /> ); }; -const useStyles = makeStyles( - theme => ({ - root: { - fontSize: '0.8em', - minHeight: theme.spacing(3), - minWidth: theme.spacing(6), - }, - error: { color: theme.palette.error.main }, - }), - { name: 'RaTranslatableInputsTab' } -); - interface TranslatableInputsTabProps { - classes?: ClassesOverride<typeof useStyles>; groupKey?: string; locale: string; } diff --git a/packages/ra-ui-materialui/src/input/TranslatableInputsTabContent.tsx b/packages/ra-ui-materialui/src/input/TranslatableInputsTabContent.tsx index d5ca649dede..5fff7766ff7 100644 --- a/packages/ra-ui-materialui/src/input/TranslatableInputsTabContent.tsx +++ b/packages/ra-ui-materialui/src/input/TranslatableInputsTabContent.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { Children, cloneElement, @@ -12,11 +13,30 @@ import { useRecordContext, useTranslatableContext, } from 'ra-core'; -import { makeStyles } from '@material-ui/core/styles'; -import { ClassesOverride } from '../types'; import { FormInput } from '../form'; import { useResourceContext } from 'ra-core'; +const PREFIX = 'RaTranslatableInputsTabContent'; + +const classes = { + root: `${PREFIX}-root`, +}; + +const Root = styled('div')(({ theme }) => ({ + [`&.${classes.root}`]: { + flexGrow: 1, + paddingLeft: theme.spacing(2), + paddingRight: theme.spacing(2), + paddingTop: theme.spacing(1), + paddingBottom: theme.spacing(1), + borderRadius: 0, + borderBottomLeftRadius: theme.shape.borderRadius, + borderBottomRightRadius: theme.shape.borderRadius, + border: `1px solid ${theme.palette.divider}`, + borderTop: 0, + }, +})); + /** * Default container for a group of translatable inputs inside a TranslatableInputs component. * @see TranslatableInputs @@ -34,13 +54,13 @@ export const TranslatableInputsTabContent = ( ...other } = props; const { selectedLocale, getLabel, getSource } = useTranslatableContext(); - const classes = useStyles(props); + const record = useRecordContext(props); const resource = useResourceContext(props); return ( <FormGroupContextProvider name={`${groupKey}${locale}`}> - <div + <Root role="tabpanel" hidden={selectedLocale !== locale} id={`translatable-content-${groupKey}${locale}`} @@ -67,7 +87,7 @@ export const TranslatableInputsTabContent = ( /> ) : null )} - </div> + </Root> </FormGroupContextProvider> ); }; @@ -77,7 +97,6 @@ export type TranslatableInputsTabContentProps< > = { basePath?: string; children: ReactNode; - classes?: ClassesOverride<typeof useStyles>; groupKey?: string; locale: string; record?: RecordType; @@ -85,21 +104,3 @@ export type TranslatableInputsTabContentProps< margin?: 'none' | 'normal' | 'dense'; variant?: 'standard' | 'outlined' | 'filled'; }; - -const useStyles = makeStyles( - theme => ({ - root: { - flexGrow: 1, - paddingLeft: theme.spacing(2), - paddingRight: theme.spacing(2), - paddingTop: theme.spacing(1), - paddingBottom: theme.spacing(1), - borderRadius: 0, - borderBottomLeftRadius: theme.shape.borderRadius, - borderBottomRightRadius: theme.shape.borderRadius, - border: `1px solid ${theme.palette.divider}`, - borderTop: 0, - }, - }), - { name: 'RaTranslatableInputsTabContent' } -); diff --git a/packages/ra-ui-materialui/src/input/TranslatableInputsTabs.tsx b/packages/ra-ui-materialui/src/input/TranslatableInputsTabs.tsx index 6c05f096dad..6c09bda920b 100644 --- a/packages/ra-ui-materialui/src/input/TranslatableInputsTabs.tsx +++ b/packages/ra-ui-materialui/src/input/TranslatableInputsTabs.tsx @@ -1,11 +1,32 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { ReactElement } from 'react'; -import { AppBar, Tabs, TabsProps } from '@material-ui/core'; -import { makeStyles } from '@material-ui/core/styles'; +import { AppBar, Tabs, TabsProps } from '@mui/material'; import { useTranslatableContext } from 'ra-core'; import { TranslatableInputsTab } from './TranslatableInputsTab'; import { AppBarProps } from '../layout'; +const PREFIX = 'RaTranslatableInputsTabs'; + +const classes = { + root: `${PREFIX}-root`, + tabs: `${PREFIX}-tabs`, +}; + +const StyledAppBar = styled(AppBar)(({ theme }) => ({ + [`&.${classes.root}`]: { + boxShadow: 'none', + borderRadius: 0, + borderTopLeftRadius: theme.shape.borderRadius, + borderTopRightRadius: theme.shape.borderRadius, + border: `1px solid ${theme.palette.divider}`, + }, + + [`& .${classes.tabs}`]: { + minHeight: theme.spacing(3), + }, +})); + /** * Default locale selector for the TranslatableInputs component. Generates a tab for each specified locale. * @see TranslatableInputs @@ -15,14 +36,17 @@ export const TranslatableInputsTabs = ( ): ReactElement => { const { groupKey, TabsProps: tabsProps } = props; const { locales, selectLocale, selectedLocale } = useTranslatableContext(); - const classes = useStyles(props); const handleChange = (event, newLocale): void => { selectLocale(newLocale); }; return ( - <AppBar color="default" position="static" className={classes.root}> + <StyledAppBar + color="default" + position="static" + className={classes.root} + > <Tabs value={selectedLocale} onChange={handleChange} @@ -40,7 +64,7 @@ export const TranslatableInputsTabs = ( /> ))} </Tabs> - </AppBar> + </StyledAppBar> ); }; @@ -48,19 +72,3 @@ export interface TranslatableInputsTabsProps { groupKey?: string; TabsProps?: TabsProps; } - -const useStyles = makeStyles( - theme => ({ - root: { - boxShadow: 'none', - borderRadius: 0, - borderTopLeftRadius: theme.shape.borderRadius, - borderTopRightRadius: theme.shape.borderRadius, - border: `1px solid ${theme.palette.divider}`, - }, - tabs: { - minHeight: theme.spacing(3), - }, - }), - { name: 'RaTranslatableInputsTabs' } -); diff --git a/packages/ra-ui-materialui/src/layout/AppBar.tsx b/packages/ra-ui-materialui/src/layout/AppBar.tsx index c2e58ad8f30..628b66bf7ab 100644 --- a/packages/ra-ui-materialui/src/layout/AppBar.tsx +++ b/packages/ra-ui-materialui/src/layout/AppBar.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { Children, cloneElement, memo } from 'react'; import PropTypes from 'prop-types'; import { @@ -8,15 +9,41 @@ import { Typography, useMediaQuery, Theme, -} from '@material-ui/core'; -import { makeStyles } from '@material-ui/core/styles'; +} from '@mui/material'; import { ComponentPropType } from 'ra-core'; import { SidebarToggleButton } from './SidebarToggleButton'; import LoadingIndicator from './LoadingIndicator'; import DefaultUserMenu from './UserMenu'; import HideOnScroll from './HideOnScroll'; -import { ClassesOverride } from '../types'; + +const PREFIX = 'RaAppBar'; + +const classes = { + toolbar: `${PREFIX}-toolbar`, + menuButton: `${PREFIX}-menuButton`, + menuButtonIconClosed: `${PREFIX}-menuButtonIconClosed`, + menuButtonIconOpen: `${PREFIX}-menuButtonIconOpen`, + title: `${PREFIX}-title`, +}; + +const StyledAppBar = styled(MuiAppBar)(({ theme }) => ({ + [`& .${classes.toolbar}`]: { + paddingRight: 24, + }, + + [`& .${classes.menuButton}`]: { + marginLeft: '0.2em', + marginRight: '0.2em', + }, + + [`& .${classes.title}`]: { + flex: 1, + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + overflow: 'hidden', + }, +})); /** * The AppBar component renders a custom MuiAppBar. @@ -33,7 +60,7 @@ import { ClassesOverride } from '../types'; * @example * * const MyAppBar = props => { - * const classes = useStyles(); + * return ( * <AppBar {...props}> * <Typography @@ -49,7 +76,7 @@ import { ClassesOverride } from '../types'; * @example Without a user menu * * const MyAppBar = props => { - * const classes = useStyles(); + * return ( * <AppBar {...props} userMenu={false} /> * ); @@ -58,7 +85,6 @@ import { ClassesOverride } from '../types'; const AppBar = (props: AppBarProps): JSX.Element => { const { children, - classes: classesOverride, className, color = 'secondary', logout, @@ -68,27 +94,20 @@ const AppBar = (props: AppBarProps): JSX.Element => { container: Container, ...rest } = props; - const classes = useStyles(props); - const sidebarToggleButtonClasses = { - menuButtonIconClosed: classes.menuButtonIconClosed, - menuButtonIconOpen: classes.menuButtonIconOpen, - }; + const isXSmall = useMediaQuery<Theme>(theme => - theme.breakpoints.down('xs') + theme.breakpoints.down('sm') ); return ( <Container> - <MuiAppBar className={className} color={color} {...rest}> + <StyledAppBar className={className} color={color} {...rest}> <Toolbar disableGutters variant={isXSmall ? 'regular' : 'dense'} className={classes.toolbar} > - <SidebarToggleButton - className={classes.menuButton} - classes={sidebarToggleButtonClasses} - /> + <SidebarToggleButton className={classes.menuButton} /> {Children.count(children) === 0 ? ( <Typography variant="h6" @@ -108,15 +127,13 @@ const AppBar = (props: AppBarProps): JSX.Element => { cloneElement(userMenu, { logout }) )} </Toolbar> - </MuiAppBar> + </StyledAppBar> </Container> ); }; AppBar.propTypes = { children: PropTypes.node, - // @ts-ignore - classes: PropTypes.object, className: PropTypes.string, color: PropTypes.oneOf([ 'default', @@ -137,29 +154,7 @@ AppBar.defaultProps = { container: HideOnScroll, }; -const useStyles = makeStyles( - theme => ({ - toolbar: { - paddingRight: 24, - }, - menuButton: { - marginLeft: '0.2em', - marginRight: '0.2em', - }, - menuButtonIconClosed: {}, - menuButtonIconOpen: {}, - title: { - flex: 1, - textOverflow: 'ellipsis', - whiteSpace: 'nowrap', - overflow: 'hidden', - }, - }), - { name: 'RaAppBar' } -); - -export interface AppBarProps extends Omit<MuiAppBarProps, 'title' | 'classes'> { - classes?: ClassesOverride<typeof useStyles>; +export interface AppBarProps extends Omit<MuiAppBarProps, 'title'> { container?: React.ElementType<any>; logout?: React.ReactNode; // @deprecated diff --git a/packages/ra-ui-materialui/src/layout/CardActions.tsx b/packages/ra-ui-materialui/src/layout/CardActions.tsx index 3f72da18c73..09b64175143 100644 --- a/packages/ra-ui-materialui/src/layout/CardActions.tsx +++ b/packages/ra-ui-materialui/src/layout/CardActions.tsx @@ -1,34 +1,37 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import PropTypes from 'prop-types'; import { warning } from 'ra-core'; -import { makeStyles } from '@material-ui/core/styles'; import classnames from 'classnames'; -const useStyles = makeStyles( - { - cardActions: { - zIndex: 2, - display: 'flex', - alignItems: 'flex-start', - justifyContent: 'flex-end', - flexWrap: 'wrap', - padding: 0, - }, +const PREFIX = 'RaCardActions'; + +const classes = { + cardActions: `${PREFIX}-cardActions`, +}; + +const Root = styled('div')({ + [`&.${classes.cardActions}`]: { + zIndex: 2, + display: 'flex', + alignItems: 'flex-start', + justifyContent: 'flex-end', + flexWrap: 'wrap', + padding: 0, }, - { name: 'RaCardActions' } -); +}); const CardActions = props => { - const { classes: classesOverride, className, children, ...rest } = props; + const { className, children, ...rest } = props; warning( true, '<CardActions> is deprecated. Please use the <TopToolbar> component instead to wrap your action buttons' ); - const classes = useStyles(props); + return ( - <div className={classnames(classes.cardActions, className)} {...rest}> + <Root className={classnames(classes.cardActions, className)} {...rest}> {children} - </div> + </Root> ); }; diff --git a/packages/ra-ui-materialui/src/layout/CardContentInner.tsx b/packages/ra-ui-materialui/src/layout/CardContentInner.tsx index e61cfc71c5d..43b63fb7422 100644 --- a/packages/ra-ui-materialui/src/layout/CardContentInner.tsx +++ b/packages/ra-ui-materialui/src/layout/CardContentInner.tsx @@ -1,30 +1,31 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { ReactNode } from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; -import CardContent from '@material-ui/core/CardContent'; -import { makeStyles } from '@material-ui/core/styles'; - -import { ClassesOverride } from '../types'; - -const useStyles = makeStyles( - theme => ({ - root: { - paddingTop: 0, - paddingBottom: 0, - '&:first-child': { - paddingTop: 16, - }, - '&:last-child': { - paddingBottom: 16, - [theme.breakpoints.only('xs')]: { - paddingBottom: 70, - }, +import CardContent from '@mui/material/CardContent'; + +const PREFIX = 'RaCardContentInner'; + +const classes = { + root: `${PREFIX}-root`, +}; + +const Root = styled(CardContent)(({ theme }) => ({ + [`&.${classes.root}`]: { + paddingTop: 0, + paddingBottom: 0, + '&:first-of-type': { + paddingTop: 16, + }, + '&:last-child': { + paddingBottom: 16, + [theme.breakpoints.only('xs')]: { + paddingBottom: 70, }, }, - }), - { name: 'RaCardContentInner' } -); + }, +})); /** * Overrides material-ui CardContent to allow inner content @@ -35,11 +36,9 @@ const useStyles = makeStyles( */ const CardContentInner = (props: CardContentInnerProps): JSX.Element => { const { className, children } = props; - const classes = useStyles(props); + return ( - <CardContent className={classnames(classes.root, className)}> - {children} - </CardContent> + <Root className={classnames(classes.root, className)}>{children}</Root> ); }; @@ -52,7 +51,6 @@ CardContentInner.propTypes = { export interface CardContentInnerProps { className?: string; children: ReactNode; - classes?: ClassesOverride<typeof useStyles>; } export default CardContentInner; diff --git a/packages/ra-ui-materialui/src/layout/Confirm.tsx b/packages/ra-ui-materialui/src/layout/Confirm.tsx index c5e0caa2ff6..d974d7244da 100644 --- a/packages/ra-ui-materialui/src/layout/Confirm.tsx +++ b/packages/ra-ui-materialui/src/layout/Confirm.tsx @@ -1,40 +1,47 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { useCallback, MouseEventHandler } from 'react'; import PropTypes, { ReactComponentLike } from 'prop-types'; -import Dialog from '@material-ui/core/Dialog'; -import DialogActions from '@material-ui/core/DialogActions'; -import DialogContent from '@material-ui/core/DialogContent'; -import DialogContentText from '@material-ui/core/DialogContentText'; -import DialogTitle from '@material-ui/core/DialogTitle'; -import Button from '@material-ui/core/Button'; -import { makeStyles } from '@material-ui/core/styles'; -import { alpha } from '@material-ui/core/styles/colorManipulator'; -import ActionCheck from '@material-ui/icons/CheckCircle'; -import AlertError from '@material-ui/icons/ErrorOutline'; +import Dialog from '@mui/material/Dialog'; +import DialogActions from '@mui/material/DialogActions'; +import DialogContent from '@mui/material/DialogContent'; +import DialogContentText from '@mui/material/DialogContentText'; +import DialogTitle from '@mui/material/DialogTitle'; +import Button from '@mui/material/Button'; +import { alpha } from '@mui/material/styles'; +import ActionCheck from '@mui/icons-material/CheckCircle'; +import AlertError from '@mui/icons-material/ErrorOutline'; import classnames from 'classnames'; import { useTranslate } from 'ra-core'; -const useStyles = makeStyles( - theme => ({ - confirmPrimary: { - color: theme.palette.primary.main, - }, - confirmWarning: { - color: theme.palette.error.main, - '&:hover': { - backgroundColor: alpha(theme.palette.error.main, 0.12), - // Reset on mouse devices - '@media (hover: none)': { - backgroundColor: 'transparent', - }, +const PREFIX = 'RaConfirm'; + +const classes = { + confirmPrimary: `${PREFIX}-confirmPrimary`, + confirmWarning: `${PREFIX}-confirmWarning`, + iconPaddingStyle: `${PREFIX}-iconPaddingStyle`, +}; + +const StyledDialog = styled(Dialog)(({ theme }) => ({ + [`& .${classes.confirmPrimary}`]: { + color: theme.palette.primary.main, + }, + + [`& .${classes.confirmWarning}`]: { + color: theme.palette.error.main, + '&:hover': { + backgroundColor: alpha(theme.palette.error.main, 0.12), + // Reset on mouse devices + '@media (hover: none)': { + backgroundColor: 'transparent', }, }, - iconPaddingStyle: { - paddingRight: '0.5em', - }, - }), - { name: 'RaConfirm' } -); + }, + + [`& .${classes.iconPaddingStyle}`]: { + paddingRight: '0.5em', + }, +})); /** * Confirmation dialog @@ -68,7 +75,7 @@ const Confirm = (props: ConfirmProps) => { onConfirm, translateOptions = {}, } = props; - const classes = useStyles(props); + const translate = useTranslate(); const handleConfirm = useCallback( @@ -84,7 +91,7 @@ const Confirm = (props: ConfirmProps) => { }, []); return ( - <Dialog + <StyledDialog open={isOpen} onClose={onClose} onClick={handleClick} @@ -123,13 +130,12 @@ const Confirm = (props: ConfirmProps) => { {translate(confirm, { _: confirm })} </Button> </DialogActions> - </Dialog> + </StyledDialog> ); }; export interface ConfirmProps { cancel?: string; - classes?: object; confirm?: string; confirmColor?: string; ConfirmIcon?: ReactComponentLike; @@ -145,7 +151,6 @@ export interface ConfirmProps { Confirm.propTypes = { cancel: PropTypes.string, - classes: PropTypes.object, confirm: PropTypes.string, confirmColor: PropTypes.string, ConfirmIcon: PropTypes.elementType, @@ -160,7 +165,6 @@ Confirm.propTypes = { Confirm.defaultProps = { cancel: 'ra.action.cancel', - classes: {}, confirm: 'ra.action.confirm', confirmColor: 'primary', ConfirmIcon: ActionCheck, diff --git a/packages/ra-ui-materialui/src/layout/DashboardMenuItem.tsx b/packages/ra-ui-materialui/src/layout/DashboardMenuItem.tsx index d04ce8b90a4..c068a925a24 100644 --- a/packages/ra-ui-materialui/src/layout/DashboardMenuItem.tsx +++ b/packages/ra-ui-materialui/src/layout/DashboardMenuItem.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import PropTypes from 'prop-types'; -import DashboardIcon from '@material-ui/icons/Dashboard'; +import DashboardIcon from '@mui/icons-material/Dashboard'; import { useTranslate } from 'ra-core'; import MenuItemLink from './MenuItemLink'; diff --git a/packages/ra-ui-materialui/src/layout/DeviceTestWrapper.tsx b/packages/ra-ui-materialui/src/layout/DeviceTestWrapper.tsx index 2bec02be56c..45798e1ac54 100644 --- a/packages/ra-ui-materialui/src/layout/DeviceTestWrapper.tsx +++ b/packages/ra-ui-materialui/src/layout/DeviceTestWrapper.tsx @@ -1,7 +1,6 @@ import * as React from 'react'; import mediaQuery from 'css-mediaquery'; -import { ThemeProvider } from '@material-ui/styles'; -import { createTheme } from '@material-ui/core/styles'; +import { createTheme, ThemeProvider } from '@mui/material/styles'; /** * Test utility to simulate a device form factor for server-side mediaQueries @@ -19,7 +18,6 @@ const DeviceTestWrapper = ({ children, }: DeviceTestWrapperProps): JSX.Element => { const theme = createTheme(); - // Use https://github.com/ericf/css-mediaquery as polyfill. const ssrMatchMedia = query => ({ matches: mediaQuery.match(query, { @@ -27,13 +25,23 @@ const DeviceTestWrapper = ({ // For the sake of this demo, we are using a fixed value. // In production, you can look into client-hint https://caniuse.com/#search=client%20hint // or user-agent resolution. - width: theme.breakpoints.width(width), + width: theme.breakpoints.values[width], }), }); return ( <ThemeProvider - theme={{ ...theme, props: { MuiUseMediaQuery: { ssrMatchMedia } } }} + theme={{ + ...theme, + components: { + MuiUseMediaQuery: { + defaultProps: { + ssrMatchMedia, + matchMedia: ssrMatchMedia, + }, + }, + }, + }} > {children} </ThemeProvider> diff --git a/packages/ra-ui-materialui/src/layout/Error.tsx b/packages/ra-ui-materialui/src/layout/Error.tsx index feb733aab53..724bddf11c3 100644 --- a/packages/ra-ui-materialui/src/layout/Error.tsx +++ b/packages/ra-ui-materialui/src/layout/Error.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { Fragment, HtmlHTMLAttributes, ErrorInfo } from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; @@ -8,75 +9,84 @@ import { AccordionDetails, AccordionSummary, Typography, -} from '@material-ui/core'; -import { makeStyles } from '@material-ui/core/styles'; -import ErrorIcon from '@material-ui/icons/Report'; -import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; -import History from '@material-ui/icons/History'; +} from '@mui/material'; +import ErrorIcon from '@mui/icons-material/Report'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import History from '@mui/icons-material/History'; import { useTranslate } from 'ra-core'; import Title, { TitlePropType } from './Title'; -import { ClassesOverride } from '../types'; - -const useStyles = makeStyles( - theme => ({ - container: { - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - justifyContent: 'center', - [theme.breakpoints.down('sm')]: { - padding: '1em', - }, - fontFamily: 'Roboto, sans-serif', - opacity: 0.5, - }, - title: { - display: 'flex', - alignItems: 'center', - }, - icon: { - width: '2em', - height: '2em', - marginRight: '0.5em', - }, - panel: { - marginTop: '1em', - maxWidth: '60em', - }, - panelDetails: { - whiteSpace: 'pre-wrap', - }, - toolbar: { - marginTop: '2em', - }, - advice: { - marginTop: '2em', + +const PREFIX = 'RaError'; + +const classes = { + container: `${PREFIX}-container`, + title: `${PREFIX}-title`, + icon: `${PREFIX}-icon`, + panel: `${PREFIX}-panel`, + panelDetails: `${PREFIX}-panelDetails`, + toolbar: `${PREFIX}-toolbar`, + advice: `${PREFIX}-advice`, +}; + +const Root = styled('div')(({ theme }) => ({ + [`&.${classes.container}`]: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + [theme.breakpoints.down('md')]: { + padding: '1em', }, - }), - { name: 'RaError' } -); + fontFamily: 'Roboto, sans-serif', + opacity: 0.5, + }, + + [`& .${classes.title}`]: { + display: 'flex', + alignItems: 'center', + }, + + [`& .${classes.icon}`]: { + width: '2em', + height: '2em', + marginRight: '0.5em', + }, + + [`& .${classes.panel}`]: { + marginTop: '1em', + maxWidth: '60em', + }, + + [`& .${classes.panelDetails}`]: { + whiteSpace: 'pre-wrap', + }, + + [`& .${classes.toolbar}`]: { + marginTop: '2em', + }, + + [`& .${classes.advice}`]: { + marginTop: '2em', + }, +})); function goBack() { window.history.go(-1); } const Error = (props: ErrorProps): JSX.Element => { - const { - error, - errorInfo, - classes: classesOverride, - className, - title, - ...rest - } = props; - const classes = useStyles(props); + const { error, errorInfo, className, title, ...rest } = props; + const translate = useTranslate(); return ( <Fragment> {title && <Title defaultTitle={title} />} - <div className={classnames(classes.container, className)} {...rest}> + <Root + className={classnames(classes.container, className)} + {...rest} + > <h1 className={classes.title} role="alert"> <ErrorIcon className={classes.icon} /> {translate('ra.page.error')} @@ -138,13 +148,12 @@ const Error = (props: ErrorProps): JSX.Element => { {translate('ra.action.back')} </Button> </div> - </div> + </Root> </Fragment> ); }; Error.propTypes = { - classes: PropTypes.object, className: PropTypes.string, error: PropTypes.object.isRequired, errorInfo: PropTypes.object, @@ -152,7 +161,6 @@ Error.propTypes = { }; export interface ErrorProps extends HtmlHTMLAttributes<HTMLDivElement> { - classes?: ClassesOverride<typeof useStyles>; className?: string; error: Error; errorInfo?: ErrorInfo; diff --git a/packages/ra-ui-materialui/src/layout/HideOnScroll.tsx b/packages/ra-ui-materialui/src/layout/HideOnScroll.tsx index 6a7e43999f3..93ed7d58599 100644 --- a/packages/ra-ui-materialui/src/layout/HideOnScroll.tsx +++ b/packages/ra-ui-materialui/src/layout/HideOnScroll.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import PropTypes from 'prop-types'; -import useScrollTrigger from '@material-ui/core/useScrollTrigger'; -import Slide from '@material-ui/core/Slide'; +import useScrollTrigger from '@mui/material/useScrollTrigger'; +import Slide from '@mui/material/Slide'; function HideOnScroll(props: HideOnScrollProps) { const { children } = props; diff --git a/packages/ra-ui-materialui/src/layout/Layout.tsx b/packages/ra-ui-materialui/src/layout/Layout.tsx index d656fc36189..815a70612c5 100644 --- a/packages/ra-ui-materialui/src/layout/Layout.tsx +++ b/packages/ra-ui-materialui/src/layout/Layout.tsx @@ -13,9 +13,8 @@ import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import classnames from 'classnames'; import { RouteComponentProps, withRouter } from 'react-router-dom'; -import { withStyles, createStyles } from '@material-ui/core/styles'; -import { ThemeProvider } from '@material-ui/styles'; -import { ThemeOptions } from '@material-ui/core'; +import { createTheme, styled, ThemeProvider } from '@mui/material/styles'; +import { DeprecatedThemeOptions } from '@mui/material'; import { ComponentPropType, CoreLayoutProps } from 'ra-core'; import compose from 'lodash/flowRight'; @@ -26,54 +25,58 @@ import DefaultNotification from './Notification'; import DefaultError from './Error'; import defaultTheme from '../defaultTheme'; import SkipNavigationButton from '../button/SkipNavigationButton'; -import { createMuiTheme } from './createMuiTheme'; - -const styles = theme => - createStyles({ - root: { - display: 'flex', - flexDirection: 'column', - zIndex: 1, - minHeight: '100vh', - backgroundColor: theme.palette.background.default, - position: 'relative', - minWidth: 'fit-content', - width: '100%', - color: theme.palette.getContrastText( - theme.palette.background.default - ), + +const PREFIX = 'RaLayout'; +const classes = { + root: `${PREFIX}-root`, + appFrame: `${PREFIX}-appFrame`, + contentWithSidebar: `${PREFIX}-contentWithSidebar`, + content: `${PREFIX}-content`, +}; + +const StyledLayout = styled('div')(({ theme }) => ({ + [`&.${classes.root}`]: { + display: 'flex', + flexDirection: 'column', + zIndex: 1, + minHeight: '100vh', + backgroundColor: theme.palette.background.default, + position: 'relative', + minWidth: 'fit-content', + width: '100%', + color: theme.palette.getContrastText(theme.palette.background.default), + }, + [`& .${classes.appFrame}`]: { + display: 'flex', + flexDirection: 'column', + flexGrow: 1, + [theme.breakpoints.up('xs')]: { + marginTop: theme.spacing(6), }, - appFrame: { - display: 'flex', - flexDirection: 'column', - flexGrow: 1, - [theme.breakpoints.up('xs')]: { - marginTop: theme.spacing(6), - }, - [theme.breakpoints.down('xs')]: { - marginTop: theme.spacing(7), - }, + [theme.breakpoints.down('sm')]: { + marginTop: theme.spacing(7), }, - contentWithSidebar: { - display: 'flex', - flexGrow: 1, + }, + [`& .${classes.contentWithSidebar}`]: { + display: 'flex', + flexGrow: 1, + }, + [`& .${classes.content}`]: { + display: 'flex', + flexDirection: 'column', + flexGrow: 1, + flexBasis: 0, + padding: theme.spacing(3), + paddingTop: theme.spacing(1), + paddingLeft: 0, + [theme.breakpoints.up('xs')]: { + paddingLeft: 5, }, - content: { - display: 'flex', - flexDirection: 'column', - flexGrow: 1, - flexBasis: 0, - padding: theme.spacing(3), - paddingTop: theme.spacing(1), - paddingLeft: 0, - [theme.breakpoints.up('xs')]: { - paddingLeft: 5, - }, - [theme.breakpoints.down('sm')]: { - padding: 0, - }, + [theme.breakpoints.down('md')]: { + padding: 0, }, - }); + }, +})); class LayoutWithoutTheme extends Component< LayoutWithoutThemeProps, @@ -100,60 +103,7 @@ class LayoutWithoutTheme extends Component< } render() { - const { - appBar, - children, - classes, - className, - error: ErrorComponent, - dashboard, - logout, - menu, - notification, - open, - sidebar, - title, - // sanitize react-router props - match, - location, - history, - staticContext, - ...props - } = this.props; - const { hasError, error, errorInfo } = this.state; - return ( - <> - <div - className={classnames('layout', classes.root, className)} - {...props} - > - <SkipNavigationButton /> - <div className={classes.appFrame}> - {createElement(appBar, { title, open, logout })} - <main className={classes.contentWithSidebar}> - {createElement(sidebar, { - children: createElement(menu, { - logout, - hasDashboard: !!dashboard, - }), - })} - <div id="main-content" className={classes.content}> - {hasError ? ( - <ErrorComponent - error={error} - errorInfo={errorInfo} - title={title} - /> - ) : ( - children - )} - </div> - </main> - </div> - </div> - {createElement(notification)} - </> - ); + return <LayoutContainer {...this.props} {...this.state} />; } static propTypes = { @@ -181,6 +131,65 @@ class LayoutWithoutTheme extends Component< }; } +const LayoutContainer = props => { + const { + appBar, + children, + className, + error: ErrorComponent, + dashboard, + error, + errorInfo, + hasError, + logout, + menu, + notification, + open, + sidebar, + title, + // sanitize react-router props + match, + location, + history, + staticContext, + ...rest + } = props; + + return ( + <> + <StyledLayout + className={classnames('layout', classes.root, className)} + {...rest} + > + <SkipNavigationButton /> + <div className={classes.appFrame}> + {createElement(appBar, { title, open, logout })} + <main className={classes.contentWithSidebar}> + {createElement(sidebar, { + children: createElement(menu, { + logout, + hasDashboard: !!dashboard, + }), + })} + <div id="main-content" className={classes.content}> + {hasError ? ( + <ErrorComponent + error={error} + errorInfo={errorInfo} + title={title} + /> + ) : ( + children + )} + </div> + </main> + </div> + </StyledLayout> + {createElement(notification)} + </> + ); +}; + export interface LayoutProps extends CoreLayoutProps, Omit<HtmlHTMLAttributes<HTMLDivElement>, 'title'> { @@ -195,7 +204,7 @@ export interface LayoutProps menu?: ComponentType<MenuProps>; notification?: ComponentType; sidebar?: ComponentType<{ children: JSX.Element }>; - theme?: ThemeOptions; + theme?: DeprecatedThemeOptions; } export interface LayoutState { @@ -219,8 +228,7 @@ const EnhancedLayout = compose( mapStateToProps, {} // Avoid connect passing dispatch in props ), - withRouter, - withStyles(styles, { name: 'RaLayout' }) + withRouter )(LayoutWithoutTheme); const Layout = ({ @@ -228,12 +236,12 @@ const Layout = ({ ...props }: LayoutProps): JSX.Element => { const themeProp = useRef(themeOverride); - const [theme, setTheme] = useState(() => createMuiTheme(themeOverride)); + const [theme, setTheme] = useState(() => createTheme(themeOverride)); useEffect(() => { if (themeProp.current !== themeOverride) { themeProp.current = themeOverride; - setTheme(createMuiTheme(themeOverride)); + setTheme(createTheme(themeOverride)); } }, [themeOverride, themeProp, theme, setTheme]); diff --git a/packages/ra-ui-materialui/src/layout/LinearProgress.tsx b/packages/ra-ui-materialui/src/layout/LinearProgress.tsx index d80570e7cb7..dc38e800669 100644 --- a/packages/ra-ui-materialui/src/layout/LinearProgress.tsx +++ b/packages/ra-ui-materialui/src/layout/LinearProgress.tsx @@ -1,21 +1,24 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import Progress, { LinearProgressProps as ProgressProps, -} from '@material-ui/core/LinearProgress'; +} from '@mui/material/LinearProgress'; import PropTypes from 'prop-types'; -import { makeStyles } from '@material-ui/core/styles'; import classnames from 'classnames'; import { useTimeout } from 'ra-core'; -const useStyles = makeStyles( - theme => ({ - root: { - margin: `${theme.spacing(1)}px 0`, - width: `${theme.spacing(20)}px`, - }, - }), - { name: 'RaLinearProgress' } -); +const PREFIX = 'RaLinearProgress'; + +const classes = { + root: `${PREFIX}-root`, +}; + +const StyledProgress = styled(Progress)(({ theme }) => ({ + [`&.${classes.root}`]: { + margin: `${theme.spacing(1)} 0`, + width: theme.spacing(20), + }, +})); /** * Progress bar formatted to replace an input or a field in a form layout @@ -33,17 +36,19 @@ const useStyles = makeStyles( * @param {Props} props */ const LinearProgress = ({ timeout = 1000, ...props }: LinearProgressProps) => { - const { classes: classesOverride, className, ...rest } = props; - const classes = useStyles(props); + const { className, ...rest } = props; + const oneSecondHasPassed = useTimeout(timeout); return oneSecondHasPassed ? ( - <Progress className={classnames(classes.root, className)} {...rest} /> + <StyledProgress + className={classnames(classes.root, className)} + {...rest} + /> ) : null; }; LinearProgress.propTypes = { - classes: PropTypes.object, className: PropTypes.string, timeout: PropTypes.number, }; diff --git a/packages/ra-ui-materialui/src/layout/Loading.tsx b/packages/ra-ui-materialui/src/layout/Loading.tsx index f5443a65b0d..b91bd69187f 100644 --- a/packages/ra-ui-materialui/src/layout/Loading.tsx +++ b/packages/ra-ui-materialui/src/layout/Loading.tsx @@ -1,37 +1,44 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import PropTypes from 'prop-types'; -import { makeStyles } from '@material-ui/core/styles'; import classnames from 'classnames'; -import CircularProgress from '@material-ui/core/CircularProgress'; +import CircularProgress from '@mui/material/CircularProgress'; import { useTranslate } from 'ra-core'; -const useStyles = makeStyles( - theme => ({ - container: { - display: 'flex', - flexDirection: 'column', - justifyContent: 'center', - [theme.breakpoints.up('md')]: { - height: '100%', - }, - [theme.breakpoints.down('lg')]: { - height: '100vh', - marginTop: '-3em', - }, - }, - icon: { - width: '9em', - height: '9em', +const PREFIX = 'RaLoading'; + +const classes = { + container: `${PREFIX}-container`, + icon: `${PREFIX}-icon`, + message: `${PREFIX}-message`, +}; + +const Root = styled('div')(({ theme }) => ({ + [`&.${classes.container}`]: { + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + [theme.breakpoints.up('md')]: { + height: '100%', }, - message: { - textAlign: 'center', - fontFamily: 'Roboto, sans-serif', - opacity: 0.5, - margin: '0 1em', + [theme.breakpoints.down('xl')]: { + height: '100vh', + marginTop: '-3em', }, - }), - { name: 'RaLoading' } -); + }, + + [`& .${classes.icon}`]: { + width: '9em', + height: '9em', + }, + + [`& .${classes.message}`]: { + textAlign: 'center', + fontFamily: 'Roboto, sans-serif', + opacity: 0.5, + margin: '0 1em', + }, +})); const Loading = props => { const { @@ -39,21 +46,20 @@ const Loading = props => { loadingPrimary = 'ra.page.loading', loadingSecondary = 'ra.message.loading', } = props; - const classes = useStyles(props); + const translate = useTranslate(); return ( - <div className={classnames(classes.container, className)}> + <Root className={classnames(classes.container, className)}> <div className={classes.message}> <CircularProgress className={classes.icon} color="primary" /> <h1>{translate(loadingPrimary)}</h1> <div>{translate(loadingSecondary)}.</div> </div> - </div> + </Root> ); }; Loading.propTypes = { - classes: PropTypes.object, className: PropTypes.string, loadingPrimary: PropTypes.string, loadingSecondary: PropTypes.string, diff --git a/packages/ra-ui-materialui/src/layout/LoadingIndicator.tsx b/packages/ra-ui-materialui/src/layout/LoadingIndicator.tsx index c9383b795f0..84eebb217af 100644 --- a/packages/ra-ui-materialui/src/layout/LoadingIndicator.tsx +++ b/packages/ra-ui-materialui/src/layout/LoadingIndicator.tsx @@ -1,43 +1,57 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import { useSelector } from 'react-redux'; -import { makeStyles, useTheme } from '@material-ui/core/styles'; -import CircularProgress from '@material-ui/core/CircularProgress'; +import { useTheme } from '@mui/material/styles'; +import CircularProgress from '@mui/material/CircularProgress'; import { ReduxState, useRefreshWhenVisible } from 'ra-core'; import RefreshIconButton from '../button/RefreshIconButton'; -import { ClassesOverride } from '../types'; + +const PREFIX = 'RaLoadingIndicator'; + +const classes = { + loader: `${PREFIX}-loader`, + loadedIcon: `${PREFIX}-loadedIcon`, +}; + +const Root = styled('div')(({ theme }) => ({ + [`& .${classes.loader}`]: { + marginLeft: theme.spacing(2), + marginRight: theme.spacing(2), + }, + + [`& .${classes.loadedIcon}`]: {}, +})); const LoadingIndicator = (props: LoadingIndicatorProps) => { - const { classes: classesOverride, className, ...rest } = props; + const { className, ...rest } = props; useRefreshWhenVisible(); const loading = useSelector<ReduxState>(state => state.admin.loading > 0); - const classes = useStyles(props); + const theme = useTheme(); - return loading ? ( - <CircularProgress - className={classNames('app-loader', classes.loader, className)} - color="inherit" - size={theme.spacing(2)} - thickness={6} - {...rest} - /> - ) : ( - <RefreshIconButton className={classes.loadedIcon} /> + return ( + <Root> + {loading ? ( + <CircularProgress + className={classNames( + 'app-loader', + classes.loader, + className + )} + color="inherit" + size={theme.spacing(2)} + thickness={6} + {...rest} + /> + ) : ( + <RefreshIconButton className={classes.loadedIcon} /> + )} + </Root> ); }; -const useStyles = makeStyles( - theme => ({ - loader: { - margin: theme.spacing(2), - }, - loadedIcon: {}, - }), - { name: 'RaLoadingIndicator' } -); - LoadingIndicator.propTypes = { classes: PropTypes.object, className: PropTypes.string, @@ -46,7 +60,6 @@ LoadingIndicator.propTypes = { interface LoadingIndicatorProps { className?: string; - classes?: ClassesOverride<typeof useStyles>; } export default LoadingIndicator; diff --git a/packages/ra-ui-materialui/src/layout/LoadingPage.tsx b/packages/ra-ui-materialui/src/layout/LoadingPage.tsx index 2beea21316d..92added5591 100644 --- a/packages/ra-ui-materialui/src/layout/LoadingPage.tsx +++ b/packages/ra-ui-materialui/src/layout/LoadingPage.tsx @@ -1,8 +1,7 @@ import * as React from 'react'; +import { createTheme, ThemeProvider } from '@mui/material/styles'; import PropTypes from 'prop-types'; -import { ThemeProvider } from '@material-ui/styles'; -import { createMuiTheme } from './createMuiTheme'; import Loading from './Loading'; const LoadingPage = ({ theme, ...props }) => ( @@ -20,7 +19,7 @@ LoadingPage.propTypes = { }; LoadingPage.defaultProps = { - theme: createMuiTheme({}), + theme: createTheme({}), loadingPrimary: 'ra.page.loading', loadingSecondary: 'ra.message.loading', }; diff --git a/packages/ra-ui-materialui/src/layout/Menu.tsx b/packages/ra-ui-materialui/src/layout/Menu.tsx index 33deff5f7bd..e5a45b1c79b 100644 --- a/packages/ra-ui-materialui/src/layout/Menu.tsx +++ b/packages/ra-ui-materialui/src/layout/Menu.tsx @@ -1,45 +1,51 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { ReactNode } from 'react'; import PropTypes from 'prop-types'; import { shallowEqual, useSelector } from 'react-redux'; import lodashGet from 'lodash/get'; -// @ts-ignore -import { makeStyles } from '@material-ui/core/styles'; -import DefaultIcon from '@material-ui/icons/ViewList'; +import DefaultIcon from '@mui/icons-material/ViewList'; import classnames from 'classnames'; import { useGetResourceLabel, getResources, ReduxState } from 'ra-core'; import DashboardMenuItem from './DashboardMenuItem'; import MenuItemLink from './MenuItemLink'; -export const MENU_WIDTH = 240; -export const CLOSED_MENU_WIDTH = 55; +const PREFIX = 'RaMenu'; -const useStyles = makeStyles( - theme => ({ - main: { - display: 'flex', - flexDirection: 'column', - justifyContent: 'flex-start', - marginTop: '0.5em', - marginBottom: '1em', - [theme.breakpoints.only('xs')]: { - marginTop: 0, - }, - transition: theme.transitions.create('width', { - easing: theme.transitions.easing.sharp, - duration: theme.transitions.duration.leavingScreen, - }), - }, - open: { - width: lodashGet(theme, 'menu.width', MENU_WIDTH), - }, - closed: { - width: lodashGet(theme, 'menu.closedWidth', CLOSED_MENU_WIDTH), +const classes = { + main: `${PREFIX}-main`, + open: `${PREFIX}-open`, + closed: `${PREFIX}-closed`, +}; + +const Root = styled('div')(({ theme }) => ({ + [`&.${classes.main}`]: { + display: 'flex', + flexDirection: 'column', + justifyContent: 'flex-start', + marginTop: '0.5em', + marginBottom: '1em', + [theme.breakpoints.only('xs')]: { + marginTop: 0, }, - }), - { name: 'RaMenu' } -); + transition: theme.transitions.create('width', { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.leavingScreen, + }), + }, + + [`&.${classes.open}`]: { + width: lodashGet(theme, 'menu.width', MENU_WIDTH), + }, + + [`&.${classes.closed}`]: { + width: lodashGet(theme, 'menu.closedWidth', CLOSED_MENU_WIDTH), + }, +})); + +export const MENU_WIDTH = 240; +export const CLOSED_MENU_WIDTH = 55; const Menu = (props: MenuProps) => { const resources = useSelector(getResources, shallowEqual) as Array<any>; @@ -72,17 +78,16 @@ const Menu = (props: MenuProps) => { ))} </> ), - classes: classesOverride, className, onMenuClick, logout, ...rest } = props; - const classes = useStyles(props); + const open = useSelector((state: ReduxState) => state.admin.ui.sidebarOpen); return ( - <div + <Root className={classnames( classes.main, { @@ -94,13 +99,12 @@ const Menu = (props: MenuProps) => { {...rest} > {children} - </div> + </Root> ); }; export interface MenuProps { children?: ReactNode; - classes?: object; className?: string; dense?: boolean; hasDashboard?: boolean; @@ -115,7 +119,6 @@ export interface MenuProps { } Menu.propTypes = { - classes: PropTypes.object, className: PropTypes.string, dense: PropTypes.bool, hasDashboard: PropTypes.bool, diff --git a/packages/ra-ui-materialui/src/layout/MenuItemLink.tsx b/packages/ra-ui-materialui/src/layout/MenuItemLink.tsx index 5c46042ee22..7c1b94b4351 100644 --- a/packages/ra-ui-materialui/src/layout/MenuItemLink.tsx +++ b/packages/ra-ui-materialui/src/layout/MenuItemLink.tsx @@ -5,6 +5,7 @@ import React, { ReactElement, ReactNode, } from 'react'; +import { styled } from '@mui/material/styles'; import PropTypes from 'prop-types'; import classnames from 'classnames'; import { useDispatch, useSelector } from 'react-redux'; @@ -19,26 +20,32 @@ import { TooltipProps, useMediaQuery, Theme, -} from '@material-ui/core'; -import { makeStyles } from '@material-ui/core/styles'; +} from '@mui/material'; + +const PREFIX = 'RaMenuItemLink'; + +const classes = { + root: `${PREFIX}-root`, + active: `${PREFIX}-active`, + icon: `${PREFIX}-icon`, +}; + +const StyledMenuItem = styled(MenuItem)(({ theme }) => ({ + [`&.${classes.root}`]: { + color: theme.palette.text.secondary, + }, + + [`& .${classes.active}`]: { + color: theme.palette.text.primary, + }, + + [`& .${classes.icon}`]: { minWidth: theme.spacing(5) }, +})); const NavLinkRef = forwardRef<HTMLAnchorElement, NavLinkProps>((props, ref) => ( <NavLink innerRef={ref} {...props} /> )); -const useStyles = makeStyles( - theme => ({ - root: { - color: theme.palette.text.secondary, - }, - active: { - color: theme.palette.text.primary, - }, - icon: { minWidth: theme.spacing(5) }, - }), - { name: 'RaMenuItemLink' } -); - /** * Displays a menu item with a label and an icon - or only the icon with a tooltip when the sidebar is minimized. * It also handles the automatic closing of the menu on tap on mobile. @@ -56,10 +63,10 @@ const useStyles = makeStyles( * // in src/Menu.js * import * as React from 'react'; * import { DashboardMenuItem, MenuItemLink } from 'react-admin'; - * import BookIcon from '@material-ui/icons/Book'; - * import ChatBubbleIcon from '@material-ui/icons/ChatBubble'; - * import PeopleIcon from '@material-ui/icons/People'; - * import LabelIcon from '@material-ui/icons/Label'; + * import BookIcon from '@mui/icons-material/Book'; + * import ChatBubbleIcon from '@mui/icons-material/ChatBubble'; + * import PeopleIcon from '@mui/icons-material/People'; + * import LabelIcon from '@mui/icons-material/Label'; * * export const Menu = () => ( * <div> @@ -90,7 +97,6 @@ const useStyles = makeStyles( */ const MenuItemLink = forwardRef((props: MenuItemLinkProps, ref) => { const { - classes: classesOverride, className, primaryText, leftIcon, @@ -99,9 +105,9 @@ const MenuItemLink = forwardRef((props: MenuItemLinkProps, ref) => { tooltipProps, ...rest } = props; - const classes = useStyles(props); + const dispatch = useDispatch(); - const isSmall = useMediaQuery<Theme>(theme => theme.breakpoints.down('sm')); + const isSmall = useMediaQuery<Theme>(theme => theme.breakpoints.down('md')); const open = useSelector((state: ReduxState) => state.admin.ui.sidebarOpen); const handleMenuTap = useCallback( e => { @@ -115,10 +121,11 @@ const MenuItemLink = forwardRef((props: MenuItemLinkProps, ref) => { const renderMenuItem = () => { return ( - <MenuItem + <StyledMenuItem className={classnames(classes.root, className)} activeClassName={classes.active} component={NavLinkRef} + // @ts-ignore ref={ref} tabIndex={0} {...rest} @@ -132,7 +139,7 @@ const MenuItemLink = forwardRef((props: MenuItemLinkProps, ref) => { </ListItemIcon> )} {primaryText} - </MenuItem> + </StyledMenuItem> ); }; diff --git a/packages/ra-ui-materialui/src/layout/NotFound.tsx b/packages/ra-ui-materialui/src/layout/NotFound.tsx index f5d350e4ef6..61d4a02d56c 100644 --- a/packages/ra-ui-materialui/src/layout/NotFound.tsx +++ b/packages/ra-ui-materialui/src/layout/NotFound.tsx @@ -1,57 +1,66 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import PropTypes from 'prop-types'; -import Button from '@material-ui/core/Button'; -import { makeStyles } from '@material-ui/core/styles'; -import HotTub from '@material-ui/icons/HotTub'; -import History from '@material-ui/icons/History'; +import Button from '@mui/material/Button'; +import HotTub from '@mui/icons-material/HotTub'; +import History from '@mui/icons-material/History'; import classnames from 'classnames'; import { useAuthenticated, useTranslate } from 'ra-core'; import Title from './Title'; -const useStyles = makeStyles( - theme => ({ - container: { - display: 'flex', - flexDirection: 'column', - justifyContent: 'center', - [theme.breakpoints.up('md')]: { - height: '100%', - }, - [theme.breakpoints.down('sm')]: { - height: '100vh', - marginTop: '-3em', - }, - }, - icon: { - width: '9em', - height: '9em', - }, - message: { - textAlign: 'center', - fontFamily: 'Roboto, sans-serif', - opacity: 0.5, - margin: '0 1em', +const PREFIX = 'RaNotFound'; + +const classes = { + container: `${PREFIX}-container`, + icon: `${PREFIX}-icon`, + message: `${PREFIX}-message`, + toolbar: `${PREFIX}-toolbar`, +}; + +const Root = styled('div')(({ theme }) => ({ + [`&.${classes.container}`]: { + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + [theme.breakpoints.up('md')]: { + height: '100%', }, - toolbar: { - textAlign: 'center', - marginTop: '2em', + [theme.breakpoints.down('md')]: { + height: '100vh', + marginTop: '-3em', }, - }), - { name: 'RaNotFound' } -); + }, + + [`& .${classes.icon}`]: { + width: '9em', + height: '9em', + }, + + [`& .${classes.message}`]: { + textAlign: 'center', + fontFamily: 'Roboto, sans-serif', + opacity: 0.5, + margin: '0 1em', + }, + + [`& .${classes.toolbar}`]: { + textAlign: 'center', + marginTop: '2em', + }, +})); function goBack() { window.history.go(-1); } const NotFound = props => { - const { className, classes: classesOverride, title, ...rest } = props; - const classes = useStyles(props); + const { className, title, ...rest } = props; + const translate = useTranslate(); useAuthenticated(); return ( - <div + <Root className={classnames(classes.container, className)} {...sanitizeRestProps(rest)} > @@ -70,7 +79,7 @@ const NotFound = props => { {translate('ra.action.back')} </Button> </div> - </div> + </Root> ); }; @@ -84,7 +93,6 @@ const sanitizeRestProps = ({ NotFound.propTypes = { className: PropTypes.string, - classes: PropTypes.object, title: PropTypes.string, location: PropTypes.object, }; diff --git a/packages/ra-ui-materialui/src/layout/Notification.tsx b/packages/ra-ui-materialui/src/layout/Notification.tsx index ad33f782b70..13beb6f6d97 100644 --- a/packages/ra-ui-materialui/src/layout/Notification.tsx +++ b/packages/ra-ui-materialui/src/layout/Notification.tsx @@ -1,10 +1,9 @@ import * as React from 'react'; +import { styled, Theme } from '@mui/material/styles'; import { useState, useEffect, useCallback } from 'react'; import PropTypes from 'prop-types'; import { useSelector, useDispatch } from 'react-redux'; -import Snackbar, { SnackbarProps } from '@material-ui/core/Snackbar'; -import Button from '@material-ui/core/Button'; -import { makeStyles, Theme } from '@material-ui/core/styles'; +import { Button, Snackbar, SnackbarProps } from '@mui/material'; import classnames from 'classnames'; import { @@ -16,35 +15,48 @@ import { useTranslate, } from 'ra-core'; -interface Props { - type?: string; -} +const PREFIX = 'RaNotification'; + +const classes = { + success: `${PREFIX}-success`, + error: `${PREFIX}-error`, + warning: `${PREFIX}-warning`, + undo: `${PREFIX}-undo`, +}; -const useStyles = makeStyles( - (theme: Theme) => ({ - success: { +const StyledButton = styled(Button)( + ({ theme, type }: NotificationProps & { theme?: Theme }) => ({ + [`& .${classes.success}`]: { backgroundColor: theme.palette.success.main, color: theme.palette.success.contrastText, }, - error: { + + [`& .${classes.error}`]: { backgroundColor: theme.palette.error.dark, color: theme.palette.error.contrastText, }, - warning: { + + [`& .${classes.warning}`]: { backgroundColor: theme.palette.error.light, color: theme.palette.error.contrastText, }, - undo: (props: Props & Omit<SnackbarProps, 'open'>) => ({ + + [`& .${classes.undo}`]: { color: - props.type === 'success' + type === 'success' ? theme.palette.success.contrastText : theme.palette.primary.light, - }), - }), - { name: 'RaNotification' } + }, + }) ); -const Notification = (props: Props & Omit<SnackbarProps, 'open'>) => { +export interface NotificationProps { + type?: string; +} + +const Notification = ( + props: NotificationProps & Omit<SnackbarProps, 'open'> +) => { const { classes: classesOverride, type, @@ -56,7 +68,6 @@ const Notification = (props: Props & Omit<SnackbarProps, 'open'>) => { const notification = useSelector(getNotification); const dispatch = useDispatch(); const translate = useTranslate(); - const styles = useStyles(props); useEffect(() => { setOpen(!!notification); @@ -96,20 +107,20 @@ const Notification = (props: Props & Omit<SnackbarProps, 'open'>) => { onClose={handleRequestClose} ContentProps={{ className: classnames( - styles[(notification && notification.type) || type], + classes[(notification && notification.type) || type], className ), }} action={ notification && notification.undoable ? ( - <Button + <StyledButton color="primary" - className={styles.undo} + className={classes.undo} size="small" onClick={handleUndo} > - {translate('ra.action.undo')} - </Button> + <>{translate('ra.action.undo')}</> + </StyledButton> ) : null } {...rest} diff --git a/packages/ra-ui-materialui/src/layout/Responsive.spec.tsx b/packages/ra-ui-materialui/src/layout/Responsive.spec.tsx deleted file mode 100644 index 6d5d483b53a..00000000000 --- a/packages/ra-ui-materialui/src/layout/Responsive.spec.tsx +++ /dev/null @@ -1,135 +0,0 @@ -import { render } from '@testing-library/react'; -import * as React from 'react'; - -import { Responsive } from './Responsive'; - -describe('<Responsive>', () => { - const Small = () => <div>Small</div>; - const Medium = () => <div>Medium</div>; - const Large = () => <div>Large</div>; - - it('should render the small component on small screens', () => { - const { queryByText } = render( - <Responsive - small={<Small />} - medium={<Medium />} - large={<Large />} - width="xs" - /> - ); - expect(queryByText('Small')).not.toBeNull(); - expect(queryByText('Medium')).toBeNull(); - expect(queryByText('Large')).toBeNull(); - }); - - it('should render the medium component on small screens and small is null', () => { - const { queryByText } = render( - <Responsive medium={<Medium />} large={<Large />} width="xs" /> - ); - expect(queryByText('Small')).toBeNull(); - expect(queryByText('Medium')).not.toBeNull(); - expect(queryByText('Large')).toBeNull(); - }); - - it('should render the medium component on medium screens', () => { - const { queryByText } = render( - <Responsive - small={<Small />} - medium={<Medium />} - large={<Large />} - width="md" - /> - ); - expect(queryByText('Small')).toBeNull(); - expect(queryByText('Medium')).not.toBeNull(); - expect(queryByText('Large')).toBeNull(); - }); - - it('should render the large component on medium screens and medium is null', () => { - const { queryByText } = render( - <Responsive small={<Small />} large={<Large />} width="md" /> - ); - expect(queryByText('Small')).toBeNull(); - expect(queryByText('Medium')).toBeNull(); - expect(queryByText('Large')).not.toBeNull(); - }); - - it('should render the large component on large screens', () => { - const { queryByText } = render( - <Responsive - small={<Small />} - medium={<Medium />} - large={<Large />} - width="lg" - /> - ); - expect(queryByText('Small')).toBeNull(); - expect(queryByText('Medium')).toBeNull(); - expect(queryByText('Large')).not.toBeNull(); - }); - - it('should render the medium component on large screens and large is null', () => { - const { queryByText } = render( - <Responsive small={<Small />} medium={<Medium />} width="lg" /> - ); - expect(queryByText('Small')).toBeNull(); - expect(queryByText('Medium')).not.toBeNull(); - expect(queryByText('Large')).toBeNull(); - }); - - ['xs', 'sm', 'lg'].forEach(width => { - it(`should render the small component on ${width} screens when no other component is passed`, () => { - const { queryByText } = render( - <Responsive small={<Small />} width={width} /> - ); - expect(queryByText('Small')).not.toBeNull(); - expect(queryByText('Medium')).toBeNull(); - expect(queryByText('Large')).toBeNull(); - }); - - it(`should render the medium component on ${width} screens when no other component is passed`, () => { - const { queryByText } = render( - <Responsive medium={<Medium />} width={width} /> - ); - expect(queryByText('Small')).toBeNull(); - expect(queryByText('Medium')).not.toBeNull(); - expect(queryByText('Large')).toBeNull(); - }); - - it(`should render the large component on ${width} screens when no other component is passed`, () => { - const { queryByText } = render( - <Responsive large={<Large />} width={width} /> - ); - expect(queryByText('Small')).toBeNull(); - expect(queryByText('Medium')).toBeNull(); - expect(queryByText('Large')).not.toBeNull(); - }); - }); - - it('should fallback to the large component on medium screens', () => { - const { queryByText } = render( - <Responsive small={<Small />} large={<Large />} width="md" /> - ); - expect(queryByText('Small')).toBeNull(); - expect(queryByText('Medium')).toBeNull(); - expect(queryByText('Large')).not.toBeNull(); - }); - - it('should fallback to the medium component on small screens', () => { - const { queryByText } = render( - <Responsive medium={<Medium />} large={<Large />} width="sm" /> - ); - expect(queryByText('Small')).toBeNull(); - expect(queryByText('Medium')).not.toBeNull(); - expect(queryByText('Large')).toBeNull(); - }); - - it('should fallback to the medium component on large screens', () => { - const { queryByText } = render( - <Responsive small={<Small />} medium={<Medium />} width="lg" /> - ); - expect(queryByText('Small')).toBeNull(); - expect(queryByText('Medium')).not.toBeNull(); - expect(queryByText('Large')).toBeNull(); - }); -}); diff --git a/packages/ra-ui-materialui/src/layout/Responsive.tsx b/packages/ra-ui-materialui/src/layout/Responsive.tsx deleted file mode 100644 index 0cdfb9eab42..00000000000 --- a/packages/ra-ui-materialui/src/layout/Responsive.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import * as React from 'react'; -import PropTypes from 'prop-types'; -import withWidth from '@material-ui/core/withWidth'; - -/** - * @deprecated use useMediaQuery instead - */ -export const Responsive = ({ - xsmall, - small, - medium, - large, - width, - ...rest -}: ResponsiveProps): JSX.Element => { - let element; - switch (width) { - case 'xs': - element = - typeof xsmall !== 'undefined' - ? xsmall - : typeof small !== 'undefined' - ? small - : typeof medium !== 'undefined' - ? medium - : large; - break; - case 'sm': - element = - typeof small !== 'undefined' - ? small - : typeof medium !== 'undefined' - ? medium - : large; - break; - case 'md': - element = - typeof medium !== 'undefined' - ? medium - : typeof large !== 'undefined' - ? large - : small; - break; - case 'lg': - case 'xl': - element = - typeof large !== 'undefined' - ? large - : typeof medium !== 'undefined' - ? medium - : small; - break; - default: - throw new Error(`Unknown width ${width}`); - } - - return element ? React.cloneElement(element, rest) : null; -}; - -export interface ResponsiveProps { - xsmall?: JSX.Element; - small?: JSX.Element; - medium?: JSX.Element; - large?: JSX.Element; - width?: string; -} - -Responsive.propTypes = { - xsmall: PropTypes.element, - small: PropTypes.element, - medium: PropTypes.element, - large: PropTypes.element, - width: PropTypes.string, -}; - -export default withWidth({ initialWidth: 'xs' })(Responsive); diff --git a/packages/ra-ui-materialui/src/layout/Sidebar.tsx b/packages/ra-ui-materialui/src/layout/Sidebar.tsx index fd563ac8806..5aff2b8b028 100644 --- a/packages/ra-ui-materialui/src/layout/Sidebar.tsx +++ b/packages/ra-ui-materialui/src/layout/Sidebar.tsx @@ -1,78 +1,129 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { ReactElement } from 'react'; import PropTypes from 'prop-types'; import { useDispatch, useSelector } from 'react-redux'; -import { Drawer, DrawerProps, useMediaQuery, Theme } from '@material-ui/core'; -import { makeStyles } from '@material-ui/core/styles'; +import { Drawer, DrawerProps, useMediaQuery, Theme } from '@mui/material'; import lodashGet from 'lodash/get'; import { setSidebarVisibility, ReduxState, useLocale } from 'ra-core'; -import { ClassesOverride } from '../types'; + +const PREFIX = 'RaSidebar'; + +const classes = { + root: `${PREFIX}-root`, + docked: `${PREFIX}-docked`, + paper: `${PREFIX}-paper`, + paperAnchorLeft: `${PREFIX}-paperAnchorLeft`, + paperAnchorRight: `${PREFIX}-paperAnchorRight`, + paperAnchorTop: `${PREFIX}-paperAnchorTop`, + paperAnchorBottom: `${PREFIX}-paperAnchorBottom`, + paperAnchorDockedLeft: `${PREFIX}-paperAnchorDockedLeft`, + paperAnchorDockedTop: `${PREFIX}-paperAnchorDockedTop`, + paperAnchorDockedRight: `${PREFIX}-paperAnchorDockedRight`, + paperAnchorDockedBottom: `${PREFIX}-paperAnchorDockedBottom`, + modal: `${PREFIX}-modal`, + fixed: `${PREFIX}-fixed`, +}; + +const StyledDrawer = styled(Drawer)(({ open, theme }) => ({ + [`&.${classes.root}`]: { + height: 'calc(100vh - 3em)', + }, + + [`& .${classes.docked}`]: {}, + [`& .${classes.paper}`]: {}, + [`& .${classes.paperAnchorLeft}`]: {}, + [`& .${classes.paperAnchorRight}`]: {}, + [`& .${classes.paperAnchorTop}`]: {}, + [`& .${classes.paperAnchorBottom}`]: {}, + [`& .${classes.paperAnchorDockedLeft}`]: {}, + [`& .${classes.paperAnchorDockedTop}`]: {}, + [`& .${classes.paperAnchorDockedRight}`]: {}, + [`& .${classes.paperAnchorDockedBottom}`]: {}, + [`& .${classes.modal}`]: {}, + + [`& .${classes.fixed}`]: { + position: 'fixed', + height: 'calc(100vh - 3em)', + overflowX: 'hidden', + // hide scrollbar + scrollbarWidth: 'none', + msOverflowStyle: 'none', + '&::-webkit-scrollbar': { + display: 'none', + }, + }, + + [`& .MuiPaper-root`]: { + position: 'relative', + width: open + ? lodashGet(theme, 'sidebar.width', DRAWER_WIDTH) + : lodashGet(theme, 'sidebar.closedWidth', CLOSED_DRAWER_WIDTH), + transition: theme.transitions.create('width', { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.leavingScreen, + }), + backgroundColor: 'transparent', + borderRight: 'none', + [theme.breakpoints.only('xs')]: { + marginTop: 0, + height: '100vh', + position: 'inherit', + backgroundColor: theme.palette.background.default, + }, + [theme.breakpoints.up('md')]: { + border: 'none', + }, + zIndex: 'inherit', + }, +})); export const DRAWER_WIDTH = 240; export const CLOSED_DRAWER_WIDTH = 55; const Sidebar = (props: SidebarProps) => { - const { - children, - closedSize, - size, - classes: classesOverride, - ...rest - } = props; + const { children, closedSize, size, ...rest } = props; const dispatch = useDispatch(); const isXSmall = useMediaQuery<Theme>(theme => - theme.breakpoints.down('xs') + theme.breakpoints.down('sm') ); - const isSmall = useMediaQuery<Theme>(theme => theme.breakpoints.down('sm')); + const isSmall = useMediaQuery<Theme>(theme => theme.breakpoints.down('md')); const open = useSelector<ReduxState, boolean>( state => state.admin.ui.sidebarOpen ); useLocale(); // force redraw on locale change const toggleSidebar = () => dispatch(setSidebarVisibility(!open)); - const { drawerPaper, fixed, ...classes } = useStyles({ - ...props, - open, - }); return isXSmall ? ( - <Drawer + <StyledDrawer variant="temporary" open={open} - PaperProps={{ - className: drawerPaper, - }} onClose={toggleSidebar} classes={classes} {...rest} > {children} - </Drawer> + </StyledDrawer> ) : isSmall ? ( - <Drawer + <StyledDrawer variant="permanent" open={open} - PaperProps={{ - className: drawerPaper, - }} onClose={toggleSidebar} classes={classes} {...rest} > - <div className={fixed}>{children}</div> - </Drawer> + <div className={classes.fixed}>{children}</div> + </StyledDrawer> ) : ( - <Drawer + <StyledDrawer variant="permanent" open={open} - PaperProps={{ - className: drawerPaper, - }} onClose={toggleSidebar} classes={classes} {...rest} > - <div className={fixed}>{children}</div> - </Drawer> + <div className={classes.fixed}>{children}</div> + </StyledDrawer> ); }; @@ -80,68 +131,10 @@ Sidebar.propTypes = { children: PropTypes.node.isRequired, }; -const useStyles = makeStyles( - theme => ({ - root: { - height: 'calc(100vh - 3em)', - }, - docked: {}, - paper: {}, - paperAnchorLeft: {}, - paperAnchorRight: {}, - paperAnchorTop: {}, - paperAnchorBottom: {}, - paperAnchorDockedLeft: {}, - paperAnchorDockedTop: {}, - paperAnchorDockedRight: {}, - paperAnchorDockedBottom: {}, - modal: {}, - fixed: { - position: 'fixed', - height: 'calc(100vh - 3em)', - overflowX: 'hidden', - // hide scrollbar - scrollbarWidth: 'none', - msOverflowStyle: 'none', - '&::-webkit-scrollbar': { - display: 'none', - }, - }, - drawerPaper: { - position: 'relative', - width: (props: { open?: boolean }) => - props.open - ? lodashGet(theme, 'sidebar.width', DRAWER_WIDTH) - : lodashGet( - theme, - 'sidebar.closedWidth', - CLOSED_DRAWER_WIDTH - ), - transition: theme.transitions.create('width', { - easing: theme.transitions.easing.sharp, - duration: theme.transitions.duration.leavingScreen, - }), - backgroundColor: 'transparent', - borderRight: 'none', - [theme.breakpoints.only('xs')]: { - marginTop: 0, - height: '100vh', - position: 'inherit', - backgroundColor: theme.palette.background.default, - }, - [theme.breakpoints.up('md')]: { - border: 'none', - }, - zIndex: 'inherit', - }, - }), - { name: 'RaSidebar' } -); - export interface SidebarProps extends DrawerProps { children: ReactElement; closedSize?: number; - classes: ClassesOverride<typeof useStyles>; + size?: number; } diff --git a/packages/ra-ui-materialui/src/layout/SidebarToggleButton.tsx b/packages/ra-ui-materialui/src/layout/SidebarToggleButton.tsx index be13bf162e1..70507a17845 100644 --- a/packages/ra-ui-materialui/src/layout/SidebarToggleButton.tsx +++ b/packages/ra-ui-materialui/src/layout/SidebarToggleButton.tsx @@ -1,20 +1,44 @@ import * as React from 'react'; -import { IconButton, Tooltip } from '@material-ui/core'; -import { makeStyles } from '@material-ui/core/styles'; -import MenuIcon from '@material-ui/icons/Menu'; +import { styled } from '@mui/material/styles'; +import { IconButton, Tooltip } from '@mui/material'; +import MenuIcon from '@mui/icons-material/Menu'; import { useTranslate } from 'ra-core'; -import { ClassesOverride } from '../types'; import { useToggleSidebar } from './useToggleSidebar'; +const PREFIX = 'RaSidebarToggleButton'; + +const classes = { + menuButtonIconClosed: `${PREFIX}-menuButtonIconClosed`, + menuButtonIconOpen: `${PREFIX}-menuButtonIconOpen`, +}; + +const StyledIconButton = styled(IconButton)(({ theme }) => ({ + [`& .${classes.menuButtonIconClosed}`]: { + transition: theme.transitions.create(['transform'], { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.leavingScreen, + }), + transform: 'rotate(0deg)', + }, + + [`& .${classes.menuButtonIconOpen}`]: { + transition: theme.transitions.create(['transform'], { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.leavingScreen, + }), + transform: 'rotate(180deg)', + }, +})); + /** * A button that toggles the sidebar. Used by default in the <AppBar>. * @param props The component props * @param {String} props.className An optional class name to apply to the button - * @param {ClassesOverride<typeof useStyles>} props.classes An object containing styles. + */ export const SidebarToggleButton = (props: SidebarToggleButtonProps) => { const translate = useTranslate(); - const classes = useStyles(props); + const { className } = props; const [open, toggleSidebar] = useToggleSidebar(); @@ -28,10 +52,11 @@ export const SidebarToggleButton = (props: SidebarToggleButtonProps) => { )} enterDelay={500} > - <IconButton + <StyledIconButton color="inherit" onClick={() => toggleSidebar()} className={className} + size="large" > <MenuIcon classes={{ @@ -40,32 +65,11 @@ export const SidebarToggleButton = (props: SidebarToggleButtonProps) => { : classes.menuButtonIconClosed, }} /> - </IconButton> + </StyledIconButton> </Tooltip> ); }; -const useStyles = makeStyles( - theme => ({ - menuButtonIconClosed: { - transition: theme.transitions.create(['transform'], { - easing: theme.transitions.easing.sharp, - duration: theme.transitions.duration.leavingScreen, - }), - transform: 'rotate(0deg)', - }, - menuButtonIconOpen: { - transition: theme.transitions.create(['transform'], { - easing: theme.transitions.easing.sharp, - duration: theme.transitions.duration.leavingScreen, - }), - transform: 'rotate(180deg)', - }, - }), - { name: 'RaSidebarToggleButton' } -); - export type SidebarToggleButtonProps = { className?: string; - classes?: ClassesOverride<typeof useStyles>; }; diff --git a/packages/ra-ui-materialui/src/layout/TopToolbar.tsx b/packages/ra-ui-materialui/src/layout/TopToolbar.tsx index 232ab54b7ae..a367730e301 100644 --- a/packages/ra-ui-materialui/src/layout/TopToolbar.tsx +++ b/packages/ra-ui-materialui/src/layout/TopToolbar.tsx @@ -1,41 +1,47 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import PropTypes from 'prop-types'; -import Toolbar from '@material-ui/core/Toolbar'; -import { makeStyles } from '@material-ui/core/styles'; +import Toolbar from '@mui/material/Toolbar'; import classnames from 'classnames'; -const useStyles = makeStyles( - theme => ({ - root: { - display: 'flex', - justifyContent: 'flex-end', - alignItems: 'flex-start', - paddingTop: theme.spacing(3), - paddingBottom: theme.spacing(1), - minHeight: theme.spacing(5), - [theme.breakpoints.up('xs')]: { - paddingLeft: 0, - paddingRight: 0, - }, - [theme.breakpoints.down('sm')]: { - paddingRight: theme.spacing(2), - }, - [theme.breakpoints.down('xs')]: { - padding: theme.spacing(1), - backgroundColor: theme.palette.background.paper, - }, +const PREFIX = 'RaTopToolbar'; + +const classes = { + root: `${PREFIX}-root`, +}; + +const StyledToolbar = styled(Toolbar)(({ theme }) => ({ + [`&.${classes.root}`]: { + display: 'flex', + justifyContent: 'flex-end', + alignItems: 'flex-start', + paddingTop: theme.spacing(3), + paddingBottom: theme.spacing(1), + minHeight: theme.spacing(5), + [theme.breakpoints.up('xs')]: { + paddingLeft: 0, + paddingRight: 0, }, - }), - { name: 'RaTopToolbar' } -); + [theme.breakpoints.down('md')]: { + paddingRight: theme.spacing(2), + }, + [theme.breakpoints.down('sm')]: { + padding: theme.spacing(1), + backgroundColor: theme.palette.background.paper, + }, + }, +})); const TopToolbar = props => { const { className, children, ...rest } = props; - const classes = useStyles(props); + return ( - <Toolbar className={classnames(classes.root, className)} {...rest}> + <StyledToolbar + className={classnames(classes.root, className)} + {...rest} + > {children} - </Toolbar> + </StyledToolbar> ); }; diff --git a/packages/ra-ui-materialui/src/layout/UserMenu.tsx b/packages/ra-ui-materialui/src/layout/UserMenu.tsx index 9fe0d8743bc..128e3869fe2 100644 --- a/packages/ra-ui-materialui/src/layout/UserMenu.tsx +++ b/packages/ra-ui-materialui/src/layout/UserMenu.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { Children, cloneElement, isValidElement, useState } from 'react'; import PropTypes from 'prop-types'; import { useTranslate, useGetIdentity } from 'ra-core'; @@ -9,27 +10,31 @@ import { Button, Avatar, PopoverOrigin, -} from '@material-ui/core'; -import { makeStyles } from '@material-ui/core/styles'; -import AccountCircle from '@material-ui/icons/AccountCircle'; +} from '@mui/material'; +import AccountCircle from '@mui/icons-material/AccountCircle'; -import { ClassesOverride } from '../types'; +const PREFIX = 'RaUserMenu'; -const defaultIcon = <AccountCircle />; +const classes = { + user: `${PREFIX}-user`, + userButton: `${PREFIX}-userButton`, + avatar: `${PREFIX}-avatar`, +}; + +const Root = styled('div')(({ theme }) => ({ + [`&.${classes.user}`]: {}, + + [`& .${classes.userButton}`]: { + textTransform: 'none', + }, -const useStyles = makeStyles( - theme => ({ - user: {}, - userButton: { - textTransform: 'none', - }, - avatar: { - width: theme.spacing(4), - height: theme.spacing(4), - }, - }), - { name: 'RaUserMenu' } -); + [`& .${classes.avatar}`]: { + width: theme.spacing(4), + height: theme.spacing(4), + }, +})); + +const defaultIcon = <AccountCircle />; const AnchorOrigin: PopoverOrigin = { vertical: 'bottom', @@ -45,7 +50,6 @@ const UserMenu = (props: UserMenuProps) => { const [anchorEl, setAnchorEl] = useState(null); const translate = useTranslate(); const { loaded, identity } = useGetIdentity(); - const classes = useStyles(props); const { children, @@ -61,7 +65,7 @@ const UserMenu = (props: UserMenuProps) => { const handleClose = () => setAnchorEl(null); return ( - <div className={classes.user}> + <Root className={classes.user}> {loaded && identity?.fullName ? ( <Button aria-label={label && translate(label, { _: label })} @@ -90,6 +94,7 @@ const UserMenu = (props: UserMenuProps) => { aria-haspopup={true} color="inherit" onClick={handleMenu} + size="large" > {icon} </IconButton> @@ -101,9 +106,6 @@ const UserMenu = (props: UserMenuProps) => { anchorEl={anchorEl} anchorOrigin={AnchorOrigin} transformOrigin={TransformOrigin} - // Make sure the menu is display under the button and not over the appbar - // See https://material-ui.com/components/menus/#customized-menus - getContentAnchorEl={null} open={open} onClose={handleClose} > @@ -116,7 +118,7 @@ const UserMenu = (props: UserMenuProps) => { )} {logout} </Menu> - </div> + </Root> ); }; @@ -130,7 +132,7 @@ UserMenu.propTypes = { export interface UserMenuProps { children?: React.ReactNode; - classes?: ClassesOverride<typeof useStyles>; + label?: string; logout?: React.ReactNode; icon?: React.ReactNode; diff --git a/packages/ra-ui-materialui/src/layout/createMuiTheme.ts b/packages/ra-ui-materialui/src/layout/createMuiTheme.ts deleted file mode 100644 index df668ea724d..00000000000 --- a/packages/ra-ui-materialui/src/layout/createMuiTheme.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { - createTheme as createLegacyModeTheme, - unstable_createMuiStrictModeTheme as createStrictModeTheme, -} from '@material-ui/core/styles'; - -/** - * Alternative to MUI's createMuiTheme that doesn't log findDomNode warnings in StrictMode - * - * @see https://github.com/mui-org/material-ui/issues/13394 - */ -export const createMuiTheme = - process.env.NODE_ENV === 'production' - ? createLegacyModeTheme - : createStrictModeTheme; diff --git a/packages/ra-ui-materialui/src/layout/index.ts b/packages/ra-ui-materialui/src/layout/index.ts index ac59fab3933..e7e53f3249a 100644 --- a/packages/ra-ui-materialui/src/layout/index.ts +++ b/packages/ra-ui-materialui/src/layout/index.ts @@ -15,13 +15,11 @@ import Menu, { MenuProps } from './Menu'; import MenuItemLink, { MenuItemLinkProps } from './MenuItemLink'; import NotFound from './NotFound'; import Notification from './Notification'; -import Responsive, { ResponsiveProps } from './Responsive'; import Sidebar, { SidebarProps } from './Sidebar'; import Title, { TitleProps, TitlePropType } from './Title'; import TitleForRecord from './TitleForRecord'; import TopToolbar from './TopToolbar'; import UserMenu, { UserMenuProps } from './UserMenu'; -export * from './createMuiTheme'; export { AppBar, @@ -41,7 +39,6 @@ export { MenuItemLink, NotFound, Notification, - Responsive, Sidebar, Title, TitleForRecord, @@ -61,7 +58,6 @@ export type { LinearProgressProps, MenuItemLinkProps, MenuProps, - ResponsiveProps, SidebarProps, TitleProps, UserMenuProps, diff --git a/packages/ra-ui-materialui/src/list/BulkActionsToolbar.tsx b/packages/ra-ui-materialui/src/list/BulkActionsToolbar.tsx index 8369fa13448..2c75bf5bb5d 100644 --- a/packages/ra-ui-materialui/src/list/BulkActionsToolbar.tsx +++ b/packages/ra-ui-materialui/src/list/BulkActionsToolbar.tsx @@ -1,65 +1,72 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { Children, ReactNode, cloneElement, isValidElement } from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; -import Toolbar from '@material-ui/core/Toolbar'; -import Typography from '@material-ui/core/Typography'; -import { makeStyles } from '@material-ui/core/styles'; -import { lighten } from '@material-ui/core/styles/colorManipulator'; -import IconButton from '@material-ui/core/IconButton'; -import CloseIcon from '@material-ui/icons/Close'; +import Toolbar from '@mui/material/Toolbar'; +import Typography from '@mui/material/Typography'; +import { alpha } from '@mui/material/styles'; +import IconButton from '@mui/material/IconButton'; +import CloseIcon from '@mui/icons-material/Close'; import { useTranslate, sanitizeListRestProps, useListContext } from 'ra-core'; -import { ClassesOverride } from '../types'; import TopToolbar from '../layout/TopToolbar'; -const useStyles = makeStyles( - theme => ({ - toolbar: { - zIndex: 3, - color: - theme.palette.type === 'light' - ? theme.palette.primary.main - : theme.palette.text.primary, - justifyContent: 'space-between', - backgroundColor: - theme.palette.type === 'light' - ? lighten(theme.palette.primary.light, 0.85) - : theme.palette.primary.dark, - minHeight: theme.spacing(8), - height: theme.spacing(8), - transition: `${theme.transitions.create( - 'height' - )}, ${theme.transitions.create('min-height')}`, - }, - topToolbar: { - paddingTop: theme.spacing(2), - }, - buttons: {}, - collapsed: { - minHeight: 0, - height: 0, - overflowY: 'hidden', - }, - title: { - display: 'flex', - flex: '0 0 auto', - }, - icon: { - marginLeft: '-0.5em', - marginRight: '0.5em', - }, - }), - { name: 'RaBulkActionsToolbar' } -); +const PREFIX = 'RaBulkActionsToolbar'; + +const classes = { + toolbar: `${PREFIX}-toolbar`, + topToolbar: `${PREFIX}-topToolbar`, + buttons: `${PREFIX}-buttons`, + collapsed: `${PREFIX}-collapsed`, + title: `${PREFIX}-title`, + icon: `${PREFIX}-icon`, +}; + +const Root = styled(Toolbar)(({ theme }) => ({ + [`&.${classes.toolbar}`]: { + zIndex: 3, + color: + theme.palette.mode === 'light' + ? theme.palette.primary.main + : theme.palette.text.primary, + justifyContent: 'space-between', + backgroundColor: + theme.palette.mode === 'light' + ? alpha(theme.palette.primary.light, 0.85) + : theme.palette.primary.dark, + minHeight: theme.spacing(8), + height: theme.spacing(8), + transition: `${theme.transitions.create( + 'height' + )}, ${theme.transitions.create('min-height')}`, + }, + + [`& .${classes.topToolbar}`]: { + paddingTop: theme.spacing(2), + }, + + [`& .${classes.buttons}`]: {}, + + [`&.${classes.toolbar}.${classes.collapsed}`]: { + minHeight: 0, + height: 0, + overflowY: 'hidden', + }, + + [`& .${classes.title}`]: { + display: 'flex', + flex: '0 0 auto', + }, + + [`& .${classes.icon}`]: { + marginLeft: '-0.5em', + marginRight: '0.5em', + }, +})); const BulkActionsToolbar = (props: BulkActionsToolbarProps) => { - const { - classes: classesOverride, - label = 'ra.action.bulk_actions', - children, - ...rest - } = props; + const { label = 'ra.action.bulk_actions', children, ...rest } = props; const { basePath, filterValues, @@ -67,11 +74,11 @@ const BulkActionsToolbar = (props: BulkActionsToolbarProps) => { selectedIds, onUnselectItems, } = useListContext(props); - const classes = useStyles(props); + const translate = useTranslate(); return ( - <Toolbar + <Root data-test="bulk-actions-toolbar" className={classnames(classes.toolbar, { [classes.collapsed]: selectedIds.length === 0, @@ -107,19 +114,18 @@ const BulkActionsToolbar = (props: BulkActionsToolbarProps) => { : null )} </TopToolbar> - </Toolbar> + </Root> ); }; BulkActionsToolbar.propTypes = { children: PropTypes.node, - classes: PropTypes.object, label: PropTypes.string, }; export interface BulkActionsToolbarProps { children?: ReactNode; - classes?: ClassesOverride<typeof useStyles>; + label?: string; } diff --git a/packages/ra-ui-materialui/src/list/Empty.tsx b/packages/ra-ui-materialui/src/list/Empty.tsx index 3d78df9e38e..45f2b3997d3 100644 --- a/packages/ra-ui-materialui/src/list/Empty.tsx +++ b/packages/ra-ui-materialui/src/list/Empty.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; -import { Typography } from '@material-ui/core'; -import { makeStyles } from '@material-ui/core/styles'; -import Inbox from '@material-ui/icons/Inbox'; +import { styled } from '@mui/material/styles'; +import { Typography } from '@mui/material'; +import Inbox from '@mui/icons-material/Inbox'; import { useTranslate, useListContext, @@ -9,13 +9,42 @@ import { useGetResourceLabel, } from 'ra-core'; -import { ClassesOverride } from '../types'; import { CreateButton } from '../button'; +const PREFIX = 'RaEmpty'; + +const classes = { + message: `${PREFIX}-message`, + icon: `${PREFIX}-icon`, + toolbar: `${PREFIX}-toolbar`, +}; + +const Root = styled('span')(({ theme }) => ({ + [`& .${classes.message}`]: { + textAlign: 'center', + opacity: theme.palette.mode === 'light' ? 0.5 : 0.8, + margin: '0 1em', + color: + theme.palette.mode === 'light' + ? 'inherit' + : theme.palette.text.primary, + }, + + [`& .${classes.icon}`]: { + width: '9em', + height: '9em', + }, + + [`& .${classes.toolbar}`]: { + textAlign: 'center', + marginTop: '2em', + }, +})); + export const Empty = (props: EmptyProps) => { const { basePath, hasCreate } = useListContext(props); const resource = useResourceContext(props); - const classes = useStyles(props); + const translate = useTranslate(); const getResourceLabel = useGetResourceLabel(); @@ -28,7 +57,7 @@ export const Empty = (props: EmptyProps) => { const inviteMessage = translate('ra.page.invite'); return ( - <> + <Root> <div className={classes.message}> <Inbox className={classes.icon} /> <Typography variant="h4" paragraph> @@ -49,34 +78,10 @@ export const Empty = (props: EmptyProps) => { <CreateButton variant="contained" basePath={basePath} /> </div> )} - </> + </Root> ); }; export interface EmptyProps { - classes?: ClassesOverride<typeof useStyles>; resource?: string; } - -const useStyles = makeStyles( - theme => ({ - message: { - textAlign: 'center', - opacity: theme.palette.type === 'light' ? 0.5 : 0.8, - margin: '0 1em', - color: - theme.palette.type === 'light' - ? 'inherit' - : theme.palette.text.primary, - }, - icon: { - width: '9em', - height: '9em', - }, - toolbar: { - textAlign: 'center', - marginTop: '2em', - }, - }), - { name: 'RaEmpty' } -); diff --git a/packages/ra-ui-materialui/src/list/List.spec.tsx b/packages/ra-ui-materialui/src/list/List.spec.tsx index 0d35543b10a..aff2b5df7b6 100644 --- a/packages/ra-ui-materialui/src/list/List.spec.tsx +++ b/packages/ra-ui-materialui/src/list/List.spec.tsx @@ -3,8 +3,7 @@ import expect from 'expect'; import { waitFor, fireEvent } from '@testing-library/react'; import { DataProviderContext, ResourceContextProvider } from 'ra-core'; import { renderWithRedux } from 'ra-test'; -import { ThemeProvider } from '@material-ui/styles'; -import { createTheme } from '@material-ui/core/styles'; +import { createTheme, ThemeProvider } from '@mui/material/styles'; import { MemoryRouter } from 'react-router-dom'; import defaultTheme from '../defaultTheme'; diff --git a/packages/ra-ui-materialui/src/list/ListActions.tsx b/packages/ra-ui-materialui/src/list/ListActions.tsx index f71ba87a230..39aecfed6dd 100644 --- a/packages/ra-ui-materialui/src/list/ListActions.tsx +++ b/packages/ra-ui-materialui/src/list/ListActions.tsx @@ -10,7 +10,7 @@ import { useResourceContext, useResourceDefinition, } from 'ra-core'; -import { ToolbarProps } from '@material-ui/core'; +import { ToolbarProps } from '@mui/material'; import TopToolbar from '../layout/TopToolbar'; import { CreateButton, ExportButton } from '../button'; @@ -26,7 +26,7 @@ import FilterButton from './filter/FilterButton'; * * @example * import { cloneElement } from 'react'; - * import Button from '@material-ui/core/Button'; + * import Button from '@mui/material/Button'; * import { TopToolbar, List, CreateButton, ExportButton } from 'react-admin'; * * const PostListActions = ({ basePath, filters }) => ( diff --git a/packages/ra-ui-materialui/src/list/ListToolbar.tsx b/packages/ra-ui-materialui/src/list/ListToolbar.tsx index ba94cef855a..51417c45391 100644 --- a/packages/ra-ui-materialui/src/list/ListToolbar.tsx +++ b/packages/ra-ui-materialui/src/list/ListToolbar.tsx @@ -1,47 +1,50 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { ReactElement } from 'react'; import PropTypes from 'prop-types'; -import { Toolbar, ToolbarProps } from '@material-ui/core'; -import { makeStyles } from '@material-ui/core/styles'; +import { Toolbar, ToolbarProps } from '@mui/material'; import { Exporter } from 'ra-core'; -import { ClassesOverride } from '../types'; import { FilterForm } from './filter'; import { FilterContext } from './FilterContext'; -const useStyles = makeStyles( - theme => ({ - toolbar: { - justifyContent: 'space-between', - alignItems: 'flex-start', - paddingRight: 0, - [theme.breakpoints.up('xs')]: { - paddingLeft: 0, - }, - [theme.breakpoints.down('xs')]: { - paddingLeft: theme.spacing(2), - backgroundColor: theme.palette.background.paper, - }, +const PREFIX = 'RaListToolbar'; + +const classes = { + toolbar: `${PREFIX}-toolbar`, + actions: `${PREFIX}-actions`, +}; + +const Root = styled(Toolbar)(({ theme }) => ({ + [`&.${classes.toolbar}`]: { + justifyContent: 'space-between', + alignItems: 'flex-start', + paddingRight: 0, + [theme.breakpoints.up('xs')]: { + paddingLeft: 0, }, - actions: { - paddingTop: theme.spacing(3), - minHeight: theme.spacing(5), - [theme.breakpoints.down('xs')]: { - padding: theme.spacing(1), - backgroundColor: theme.palette.background.paper, - }, + [theme.breakpoints.down('sm')]: { + paddingLeft: theme.spacing(2), + backgroundColor: theme.palette.background.paper, + }, + }, + + [`& .${classes.actions}`]: { + paddingTop: theme.spacing(3), + minHeight: theme.spacing(5), + [theme.breakpoints.down('sm')]: { + padding: theme.spacing(1), + backgroundColor: theme.palette.background.paper, }, - }), - { name: 'RaListToolbar' } -); + }, +})); const ListToolbar = (props: ListToolbarProps) => { - const { classes: classesOverride, filters, actions, ...rest } = props; - const classes = useStyles(props); + const { filters, actions, ...rest } = props; return Array.isArray(filters) ? ( <FilterContext.Provider value={filters}> - <Toolbar className={classes.toolbar}> + <Root className={classes.toolbar}> <FilterForm /> <span /> {actions && @@ -50,10 +53,10 @@ const ListToolbar = (props: ListToolbarProps) => { className: classes.actions, ...actions.props, })} - </Toolbar> + </Root> </FilterContext.Provider> ) : ( - <Toolbar className={classes.toolbar}> + <Root className={classes.toolbar}> {filters && React.cloneElement(filters, { ...rest, @@ -67,12 +70,11 @@ const ListToolbar = (props: ListToolbarProps) => { filters, ...actions.props, })} - </Toolbar> + </Root> ); }; ListToolbar.propTypes = { - classes: PropTypes.object, filters: PropTypes.oneOfType([ PropTypes.element, PropTypes.arrayOf(PropTypes.element), @@ -86,7 +88,6 @@ ListToolbar.propTypes = { export interface ListToolbarProps extends Omit<ToolbarProps, 'classes' | 'onSelect'> { actions?: ReactElement | false; - classes?: ClassesOverride<typeof useStyles>; filters?: ReactElement | ReactElement[]; exporter?: Exporter | false; } diff --git a/packages/ra-ui-materialui/src/list/ListView.tsx b/packages/ra-ui-materialui/src/list/ListView.tsx index c7b9edee0e1..62da9012d28 100644 --- a/packages/ra-ui-materialui/src/list/ListView.tsx +++ b/packages/ra-ui-materialui/src/list/ListView.tsx @@ -1,9 +1,9 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { Children, cloneElement, ReactElement } from 'react'; import PropTypes from 'prop-types'; -import Card from '@material-ui/core/Card'; +import Card from '@mui/material/Card'; import classnames from 'classnames'; -import { makeStyles } from '@material-ui/core/styles'; import { ComponentPropType, defaultExporter, @@ -22,6 +22,50 @@ import DefaultActions from './ListActions'; import { Empty } from './Empty'; import { ListProps } from '../types'; +const PREFIX = 'RaList'; + +const classes = { + root: `${PREFIX}-root`, + main: `${PREFIX}-main`, + content: `${PREFIX}-content`, + bulkActionsDisplayed: `${PREFIX}-bulkActionsDisplayed`, + actions: `${PREFIX}-actions`, + noResults: `${PREFIX}-noResults`, +}; + +const Root = styled('div')(({ theme }) => ({ + [`&.${classes.root}`]: {}, + + [`& .${classes.main}`]: { + display: 'flex', + }, + + [`& .${classes.content}`]: { + marginTop: 0, + transition: theme.transitions.create('margin-top'), + position: 'relative', + flex: '1 1 auto', + [theme.breakpoints.down('sm')]: { + boxShadow: 'none', + }, + overflow: 'inherit', + }, + + [`& .${classes.bulkActionsDisplayed}`]: { + marginTop: -theme.spacing(8), + transition: theme.transitions.create('margin-top'), + }, + + [`& .${classes.actions}`]: { + zIndex: 2, + display: 'flex', + justifyContent: 'flex-end', + flexWrap: 'wrap', + }, + + [`& .${classes.noResults}`]: { padding: 20 }, +})); + export const ListView = (props: ListViewProps) => { const { actions, @@ -31,7 +75,6 @@ export const ListView = (props: ListViewProps) => { pagination, children, className, - classes: classesOverride, component: Content, exporter = defaultExporter, title, @@ -40,7 +83,7 @@ export const ListView = (props: ListViewProps) => { } = props; const controllerProps = getListControllerProps(props); // deprecated, to be removed in v4 const listContext = useListContext(props); - const classes = useStyles(props); + const { defaultTitle, total, @@ -90,7 +133,7 @@ export const ListView = (props: ListViewProps) => { loaded && !loading && total === 0 && !Object.keys(filterValues).length; return ( - <div + <Root className={classnames('list-page', classes.root, className)} {...sanitizeRestProps(rest)} > @@ -98,7 +141,7 @@ export const ListView = (props: ListViewProps) => { {shouldRenderEmptyPage && empty !== false ? cloneElement(empty, listContext) : renderList()} - </div> + </Root> ); }; @@ -111,7 +154,6 @@ ListView.propTypes = { bulkActionButtons: PropTypes.oneOfType([PropTypes.bool, PropTypes.element]), children: PropTypes.element, className: PropTypes.string, - classes: PropTypes.object, component: ComponentPropType, // @ts-ignore-line currentSort: PropTypes.shape({ @@ -157,44 +199,12 @@ const DefaultBulkActionButtons = props => <BulkDeleteButton {...props} />; ListView.defaultProps = { actions: <DefaultActions />, - classes: {}, component: Card, bulkActionButtons: <DefaultBulkActionButtons />, pagination: <DefaultPagination />, empty: <Empty />, }; -const useStyles = makeStyles( - theme => ({ - root: {}, - main: { - display: 'flex', - }, - content: { - marginTop: 0, - transition: theme.transitions.create('margin-top'), - position: 'relative', - flex: '1 1 auto', - [theme.breakpoints.down('xs')]: { - boxShadow: 'none', - }, - overflow: 'inherit', - }, - bulkActionsDisplayed: { - marginTop: -theme.spacing(8), - transition: theme.transitions.create('margin-top'), - }, - actions: { - zIndex: 2, - display: 'flex', - justifyContent: 'flex-end', - flexWrap: 'wrap', - }, - noResults: { padding: 20 }, - }), - { name: 'RaList' } -); - export interface ListViewProps extends Omit<ListProps, 'basePath' | 'hasCreate' | 'perPage' | 'resource'>, // Partial because we now get those props via context diff --git a/packages/ra-ui-materialui/src/list/Placeholder.tsx b/packages/ra-ui-materialui/src/list/Placeholder.tsx index 227638fc584..68f7c31db72 100644 --- a/packages/ra-ui-materialui/src/list/Placeholder.tsx +++ b/packages/ra-ui-materialui/src/list/Placeholder.tsx @@ -1,28 +1,29 @@ import * as React from 'react'; -import { makeStyles } from '@material-ui/core/styles'; +import { styled } from '@mui/material/styles'; import classnames from 'classnames'; -const useStyles = makeStyles( - theme => ({ - root: { - backgroundColor: theme.palette.grey[300], - display: 'flex', - }, - }), - { name: 'RaPlaceholder' } -); +const PREFIX = 'RaPlaceholder'; + +const classes = { + root: `${PREFIX}-root`, +}; + +const Root = styled('span')(({ theme }) => ({ + [`&.${classes.root}`]: { + backgroundColor: theme.palette.grey[300], + display: 'flex', + }, +})); interface Props { className?: string; - classes?: Record<'root', string>; } const Placeholder = (props: Props) => { - const classes = useStyles(props); return ( - <span className={classnames(classes.root, props.className)}> + <Root className={classnames(classes.root, props.className)}>   - </span> + </Root> ); }; diff --git a/packages/ra-ui-materialui/src/list/SimpleList.tsx b/packages/ra-ui-materialui/src/list/SimpleList.tsx index e34cfbbaebd..feb5bde6164 100644 --- a/packages/ra-ui-materialui/src/list/SimpleList.tsx +++ b/packages/ra-ui-materialui/src/list/SimpleList.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { isValidElement, ReactNode, ReactElement } from 'react'; import PropTypes from 'prop-types'; import { @@ -7,12 +8,12 @@ import { ListProps, ListItem, ListItemAvatar, + ListItemButton, ListItemIcon, ListItemProps, ListItemSecondaryAction, ListItemText, -} from '@material-ui/core'; -import { makeStyles } from '@material-ui/core/styles'; +} from '@mui/material'; import { Link } from 'react-router-dom'; import { linkToRecord, @@ -25,14 +26,16 @@ import { } from 'ra-core'; import SimpleListLoading from './SimpleListLoading'; -import { ClassesOverride } from '../types'; -const useStyles = makeStyles( - { - tertiary: { float: 'right', opacity: 0.541176 }, - }, - { name: 'RaSimpleList' } -); +const PREFIX = 'RaSimpleList'; + +const classes = { + tertiary: `${PREFIX}-tertiary`, +}; + +const Root = styled(List)({ + [`& .${classes.tertiary}`]: { float: 'right', opacity: 0.541176 }, +}); /** * The <SimpleList> component renders a list of records as a material-ui <List>. @@ -73,7 +76,6 @@ const SimpleList = <RecordType extends Record = Record>( ) => { const { className, - classes: classesOverride, hasBulkActions, leftAvatar, leftIcon, @@ -89,12 +91,10 @@ const SimpleList = <RecordType extends Record = Record>( const { basePath, data, ids, loaded, total } = useListContext<RecordType>( props ); - const classes = useStyles(props); if (loaded === false) { return ( <SimpleListLoading - classes={classes} className={className} hasLeftAvatarOrIcon={!!leftIcon || !!leftAvatar} hasRightAvatarOrIcon={!!rightIcon || !!rightAvatar} @@ -119,92 +119,82 @@ const SimpleList = <RecordType extends Record = Record>( } }; - return ( - total > 0 && ( - <List className={className} {...sanitizeListRestProps(rest)}> - {ids.map((id, rowIndex) => ( - <RecordContextProvider key={id} value={data[id]}> - <li> - <LinkOrNot - linkType={linkType} - basePath={basePath} - id={id} - record={data[id]} - style={ - rowStyle - ? rowStyle(data[id], rowIndex) - : undefined - } - > - {leftIcon && ( - <ListItemIcon> - {leftIcon(data[id], id)} - </ListItemIcon> - )} - {leftAvatar && ( - <ListItemAvatar> - {renderAvatar(id, leftAvatar)} - </ListItemAvatar> - )} - <ListItemText - primary={ - <div> - {isValidElement(primaryText) - ? primaryText - : primaryText(data[id], id)} + return total > 0 ? ( + <Root className={className} {...sanitizeListRestProps(rest)}> + {ids.map((id, rowIndex) => ( + <RecordContextProvider key={id} value={data[id]}> + <ListItem> + <LinkOrNot + linkType={linkType} + basePath={basePath} + id={id} + record={data[id]} + style={ + rowStyle + ? rowStyle(data[id], rowIndex) + : undefined + } + > + {leftIcon && ( + <ListItemIcon> + {leftIcon(data[id], id)} + </ListItemIcon> + )} + {leftAvatar && ( + <ListItemAvatar> + {renderAvatar(id, leftAvatar)} + </ListItemAvatar> + )} + <ListItemText + primary={ + <div> + {isValidElement(primaryText) + ? primaryText + : primaryText(data[id], id)} - {!!tertiaryText && - (isValidElement( - tertiaryText - ) ? ( - tertiaryText - ) : ( - <span - className={ - classes.tertiary - } - > - {tertiaryText( - data[id], - id - )} - </span> - ))} - </div> - } - secondary={ - !!secondaryText && - (isValidElement(secondaryText) - ? secondaryText - : secondaryText(data[id], id)) - } - /> - {(rightAvatar || rightIcon) && ( - <ListItemSecondaryAction> - {rightAvatar && ( - <Avatar> - {renderAvatar(id, rightAvatar)} - </Avatar> - )} - {rightIcon && ( - <ListItemIcon> - {rightIcon(data[id], id)} - </ListItemIcon> - )} - </ListItemSecondaryAction> - )} - </LinkOrNot> - </li> - </RecordContextProvider> - ))} - </List> - ) - ); + {!!tertiaryText && + (isValidElement(tertiaryText) ? ( + tertiaryText + ) : ( + <span + className={classes.tertiary} + > + {tertiaryText(data[id], id)} + </span> + ))} + </div> + } + secondary={ + !!secondaryText && + (isValidElement(secondaryText) + ? secondaryText + : secondaryText(data[id], id)) + } + /> + {(rightAvatar || rightIcon) && ( + <ListItemSecondaryAction> + {rightAvatar && ( + <Avatar> + {renderAvatar(id, rightAvatar)} + </Avatar> + )} + {rightIcon && ( + <ListItemIcon> + {rightIcon(data[id], id)} + </ListItemIcon> + )} + </ListItemSecondaryAction> + )} + </LinkOrNot> + </ListItem> + </RecordContextProvider> + ))} + </Root> + ) : null; }; SimpleList.propTypes = { className: PropTypes.string, - classes: PropTypes.object, leftAvatar: PropTypes.func, leftIcon: PropTypes.func, linkType: PropTypes.oneOfType([ @@ -228,7 +218,6 @@ export type FunctionToElement<RecordType extends Record = Record> = ( export interface SimpleListProps<RecordType extends Record = Record> extends Omit<ListProps, 'classes'> { className?: string; - classes?: ClassesOverride<typeof useStyles>; hasBulkActions?: boolean; leftAvatar?: FunctionToElement<RecordType>; leftIcon?: FunctionToElement<RecordType>; @@ -247,13 +236,6 @@ export interface SimpleListProps<RecordType extends Record = Record> total?: number; } -const useLinkOrNotStyles = makeStyles( - { - link: {}, - }, - { name: 'RaLinkOrNot' } -); - const LinkOrNot = ( props: LinkOrNotProps & Omit<ListItemProps, 'button' | 'component' | 'id'> ) => { @@ -266,58 +248,46 @@ const LinkOrNot = ( record, ...rest } = props; - const classes = useLinkOrNotStyles({ classes: classesOverride }); const link = typeof linkType === 'function' ? linkType(record, id) : linkType; return link === 'edit' || link === true ? ( - <ListItem - button - // @ts-ignore + // @ts-ignore + <ListItemButton component={Link} to={linkToRecord(basePath, id)} - className={classes.link} {...rest} > {children} - </ListItem> + </ListItemButton> ) : link === 'show' ? ( - <ListItem - button - // @ts-ignore + // @ts-ignore + <ListItemButton component={Link} to={`${linkToRecord(basePath, id)}/show`} - className={classes.link} {...rest} > {children} - </ListItem> + </ListItemButton> ) : link !== false ? ( - <ListItem - button - // @ts-ignore - component={Link} - to={link} - className={classes.link} - {...rest} - > + // @ts-ignore + <ListItemButton component={Link} to={link} {...rest}> {children} - </ListItem> + </ListItemButton> ) : ( - <ListItem + <ListItemText // @ts-ignore component="div" {...rest} > {children} - </ListItem> + </ListItemText> ); }; export type FunctionLinkType = (record: Record, id: Identifier) => string; export interface LinkOrNotProps { - classes?: ClassesOverride<typeof useLinkOrNotStyles>; linkType?: string | FunctionLinkType | boolean; basePath: string; id: Identifier; diff --git a/packages/ra-ui-materialui/src/list/SimpleListLoading.tsx b/packages/ra-ui-materialui/src/list/SimpleListLoading.tsx index 85c4fdda7f6..88de5f75244 100644 --- a/packages/ra-ui-materialui/src/list/SimpleListLoading.tsx +++ b/packages/ra-ui-materialui/src/list/SimpleListLoading.tsx @@ -1,27 +1,37 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import PropTypes from 'prop-types'; -import Avatar from '@material-ui/core/Avatar'; -import List, { ListProps } from '@material-ui/core/List'; -import ListItem from '@material-ui/core/ListItem'; -import ListItemAvatar from '@material-ui/core/ListItemAvatar'; -import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction'; -import ListItemText from '@material-ui/core/ListItemText'; -import { makeStyles } from '@material-ui/core/styles'; +import Avatar from '@mui/material/Avatar'; +import List, { ListProps } from '@mui/material/List'; +import ListItem from '@mui/material/ListItem'; +import ListItemAvatar from '@mui/material/ListItemAvatar'; +import ListItemSecondaryAction from '@mui/material/ListItemSecondaryAction'; +import ListItemText from '@mui/material/ListItemText'; import Placeholder from './Placeholder'; import { useTimeout } from 'ra-core'; -const useStyles = makeStyles( - theme => ({ - primary: { - width: '30vw', - display: 'inline-block', - marginBottom: theme.spacing(), - }, - tertiary: { float: 'right', opacity: 0.541176, minWidth: '10vw' }, - }), - { name: 'RaSimpleListLoading' } -); +const PREFIX = 'RaSimpleListLoading'; + +const classes = { + primary: `${PREFIX}-primary`, + tertiary: `${PREFIX}-tertiary`, +}; + +const StyledList = styled(List)(({ theme }) => ({ + [`& .${classes.primary}`]: { + width: '30vw', + display: 'inline-block', + marginBottom: theme.spacing(), + }, + + [`& .${classes.tertiary}`]: { + float: 'right', + opacity: 0.541176, + minWidth: '10vw', + }, +})); + const times = (nbChildren, fn) => Array.from({ length: nbChildren }, (_, key) => fn(key)); @@ -46,11 +56,11 @@ const SimpleListLoading = (props: Props & ListProps) => { nbFakeLines = 5, ...rest } = props; - const classes = useStyles(props); + const oneSecondHasPassed = useTimeout(1000); return oneSecondHasPassed ? ( - <List className={className} {...rest}> + <StyledList className={className} {...rest}> {times(nbFakeLines, key => ( <ListItem key={key}> {hasLeftAvatarOrIcon && ( @@ -80,7 +90,7 @@ const SimpleListLoading = (props: Props & ListProps) => { )} </ListItem> ))} - </List> + </StyledList> ) : null; }; diff --git a/packages/ra-ui-materialui/src/list/SingleFieldList.spec.tsx b/packages/ra-ui-materialui/src/list/SingleFieldList.spec.tsx index e0ff5c93b02..2ac499d5080 100644 --- a/packages/ra-ui-materialui/src/list/SingleFieldList.spec.tsx +++ b/packages/ra-ui-materialui/src/list/SingleFieldList.spec.tsx @@ -3,10 +3,13 @@ import { render } from '@testing-library/react'; import { createMemoryHistory } from 'history'; import { Router } from 'react-router-dom'; import { ListContext } from 'ra-core'; +import { createTheme, ThemeProvider } from '@mui/material/styles'; import SingleFieldList from './SingleFieldList'; import ChipField from '../field/ChipField'; +const theme = createTheme(); + const renderWithRouter = children => { const history = createMemoryHistory(); @@ -19,21 +22,23 @@ const renderWithRouter = children => { describe('<SingleFieldList />', () => { it('should render a link to the Edit page of the related record by default', () => { const { queryAllByRole } = renderWithRouter( - <ListContext.Provider - value={{ - ids: [1, 2], - data: { - 1: { id: 1, title: 'foo' }, - 2: { id: 2, title: 'bar' }, - }, - resource: 'posts', - basePath: '/posts', - }} - > - <SingleFieldList> - <ChipField source="title" /> - </SingleFieldList> - </ListContext.Provider> + <ThemeProvider theme={theme}> + <ListContext.Provider + value={{ + ids: [1, 2], + data: { + 1: { id: 1, title: 'foo' }, + 2: { id: 2, title: 'bar' }, + }, + resource: 'posts', + basePath: '/posts', + }} + > + <SingleFieldList> + <ChipField source="title" /> + </SingleFieldList> + </ListContext.Provider> + </ThemeProvider> ); const linkElements = queryAllByRole('link'); expect(linkElements).toHaveLength(2); @@ -45,33 +50,7 @@ describe('<SingleFieldList />', () => { it('should render a link to the Edit page of the related record when the resource contains slashes', () => { const { queryAllByRole } = renderWithRouter( - <ListContext.Provider - value={{ - ids: [1, 2], - data: { - 1: { id: 1, title: 'foo' }, - 2: { id: 2, title: 'bar' }, - }, - resource: 'posts/foo', - basePath: '/posts/foo', - }} - > - <SingleFieldList> - <ChipField source="title" /> - </SingleFieldList> - </ListContext.Provider> - ); - const linkElements = queryAllByRole('link'); - expect(linkElements).toHaveLength(2); - expect(linkElements.map(link => link.getAttribute('href'))).toEqual([ - '/posts/foo/1', - '/posts/foo/2', - ]); - }); - - ['edit', 'show'].forEach(action => { - it(`should render a link to the Edit page of the related record when the resource is named ${action}`, () => { - const { queryAllByRole } = renderWithRouter( + <ThemeProvider theme={theme}> <ListContext.Provider value={{ ids: [1, 2], @@ -79,14 +58,44 @@ describe('<SingleFieldList />', () => { 1: { id: 1, title: 'foo' }, 2: { id: 2, title: 'bar' }, }, - resource: action, - basePath: `/${action}`, + resource: 'posts/foo', + basePath: '/posts/foo', }} > <SingleFieldList> <ChipField source="title" /> </SingleFieldList> </ListContext.Provider> + </ThemeProvider> + ); + const linkElements = queryAllByRole('link'); + expect(linkElements).toHaveLength(2); + expect(linkElements.map(link => link.getAttribute('href'))).toEqual([ + '/posts/foo/1', + '/posts/foo/2', + ]); + }); + + ['edit', 'show'].forEach(action => { + it(`should render a link to the Edit page of the related record when the resource is named ${action}`, () => { + const { queryAllByRole } = renderWithRouter( + <ThemeProvider theme={theme}> + <ListContext.Provider + value={{ + ids: [1, 2], + data: { + 1: { id: 1, title: 'foo' }, + 2: { id: 2, title: 'bar' }, + }, + resource: action, + basePath: `/${action}`, + }} + > + <SingleFieldList> + <ChipField source="title" /> + </SingleFieldList> + </ListContext.Provider> + </ThemeProvider> ); const linkElements = queryAllByRole('link'); expect(linkElements).toHaveLength(2); @@ -98,21 +107,23 @@ describe('<SingleFieldList />', () => { it('should render a link to the Show page of the related record when the linkType is show', () => { const { queryAllByRole } = renderWithRouter( - <ListContext.Provider - value={{ - ids: [1, 2], - data: { - 1: { id: 1, title: 'foo' }, - 2: { id: 2, title: 'bar' }, - }, - resource: 'prefix/bar', - basePath: '/prefix/bar', - }} - > - <SingleFieldList linkType="show"> - <ChipField source="title" /> - </SingleFieldList> - </ListContext.Provider> + <ThemeProvider theme={theme}> + <ListContext.Provider + value={{ + ids: [1, 2], + data: { + 1: { id: 1, title: 'foo' }, + 2: { id: 2, title: 'bar' }, + }, + resource: 'prefix/bar', + basePath: '/prefix/bar', + }} + > + <SingleFieldList linkType="show"> + <ChipField source="title" /> + </SingleFieldList> + </ListContext.Provider> + </ThemeProvider> ); const linkElements = queryAllByRole('link'); @@ -126,6 +137,35 @@ describe('<SingleFieldList />', () => { ['edit', 'show'].forEach(action => { it(`should render a link to the Edit page of the related record when the resource is named ${action} and linkType is show`, () => { const { queryAllByRole } = renderWithRouter( + <ThemeProvider theme={theme}> + <ListContext.Provider + value={{ + ids: [1, 2], + data: { + 1: { id: 1, title: 'foo' }, + 2: { id: 2, title: 'bar' }, + }, + resource: action, + basePath: `/${action}`, + }} + > + <SingleFieldList linkType="show"> + <ChipField source="title" /> + </SingleFieldList> + </ListContext.Provider> + </ThemeProvider> + ); + const linkElements = queryAllByRole('link'); + expect(linkElements).toHaveLength(2); + expect( + linkElements.map(link => link.getAttribute('href')) + ).toEqual([`/${action}/1/show`, `/${action}/2/show`]); + }); + }); + + it('should render no link when the linkType is false', () => { + const { queryAllByRole, queryByText } = renderWithRouter( + <ThemeProvider theme={theme}> <ListContext.Provider value={{ ids: [1, 2], @@ -133,40 +173,15 @@ describe('<SingleFieldList />', () => { 1: { id: 1, title: 'foo' }, 2: { id: 2, title: 'bar' }, }, - resource: action, - basePath: `/${action}`, + resource: 'bar', + basePath: '/bar', }} > - <SingleFieldList linkType="show"> + <SingleFieldList linkType={false}> <ChipField source="title" /> </SingleFieldList> </ListContext.Provider> - ); - const linkElements = queryAllByRole('link'); - expect(linkElements).toHaveLength(2); - expect( - linkElements.map(link => link.getAttribute('href')) - ).toEqual([`/${action}/1/show`, `/${action}/2/show`]); - }); - }); - - it('should render no link when the linkType is false', () => { - const { queryAllByRole, queryByText } = renderWithRouter( - <ListContext.Provider - value={{ - ids: [1, 2], - data: { - 1: { id: 1, title: 'foo' }, - 2: { id: 2, title: 'bar' }, - }, - resource: 'bar', - basePath: '/bar', - }} - > - <SingleFieldList linkType={false}> - <ChipField source="title" /> - </SingleFieldList> - </ListContext.Provider> + </ThemeProvider> ); const linkElements = queryAllByRole('link'); diff --git a/packages/ra-ui-materialui/src/list/SingleFieldList.tsx b/packages/ra-ui-materialui/src/list/SingleFieldList.tsx index 3727fa44e0e..3e878840c83 100644 --- a/packages/ra-ui-materialui/src/list/SingleFieldList.tsx +++ b/packages/ra-ui-materialui/src/list/SingleFieldList.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { cloneElement, Children, @@ -7,8 +8,7 @@ import { } from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; -import LinearProgress from '@material-ui/core/LinearProgress'; -import { makeStyles } from '@material-ui/core/styles'; +import LinearProgress from '@mui/material/LinearProgress'; import { linkToRecord, sanitizeListRestProps, @@ -22,20 +22,24 @@ import { } from 'ra-core'; import Link from '../Link'; -import { ClassesOverride } from '../types'; - -const useStyles = makeStyles( - theme => ({ - root: { - display: 'flex', - flexWrap: 'wrap', - marginTop: -theme.spacing(1), - marginBottom: -theme.spacing(1), - }, - link: {}, - }), - { name: 'RaSingleFieldList' } -); + +const PREFIX = 'RaSingleFieldList'; + +const classes = { + root: `${PREFIX}-root`, + link: `${PREFIX}-link`, +}; + +const Root = styled('div')(({ theme }) => ({ + [`& .${classes.root}`]: { + display: 'flex', + flexWrap: 'wrap', + marginTop: -theme.spacing(1), + marginBottom: -theme.spacing(1), + }, + + [`& .${classes.link}`]: {}, +})); // useful to prevent click bubbling in a datagrid with rowClick const stopPropagation = e => e.stopPropagation(); @@ -79,17 +83,15 @@ const handleClick = () => {}; */ const SingleFieldList = (props: SingleFieldListProps) => { const { - classes: classesOverride, className, children, linkType = 'edit', - component = 'div', + component = Root, ...rest } = props; const { ids, data, loaded, basePath } = useListContext(props); const resource = useResourceContext(props); - const classes = useStyles(props); const Component = component; if (loaded === false) { @@ -158,7 +160,7 @@ SingleFieldList.propTypes = { export interface SingleFieldListProps<RecordType extends Record = Record> extends HtmlHTMLAttributes<HTMLDivElement> { className?: string; - classes?: ClassesOverride<typeof useStyles>; + component?: string | ComponentType<any>; linkType?: string | false; children: React.ReactElement; diff --git a/packages/ra-ui-materialui/src/list/datagrid/Datagrid.tsx b/packages/ra-ui-materialui/src/list/datagrid/Datagrid.tsx index b932f953edb..876358561f3 100644 --- a/packages/ra-ui-materialui/src/list/datagrid/Datagrid.tsx +++ b/packages/ra-ui-materialui/src/list/datagrid/Datagrid.tsx @@ -21,7 +21,7 @@ import { RecordMap, SortPayload, } from 'ra-core'; -import { Table, TableProps } from '@material-ui/core'; +import { TableProps } from '@mui/material'; import classnames from 'classnames'; import union from 'lodash/union'; import difference from 'lodash/difference'; @@ -29,10 +29,9 @@ import difference from 'lodash/difference'; import { DatagridHeader } from './DatagridHeader'; import DatagridLoading from './DatagridLoading'; import DatagridBody, { PureDatagridBody } from './DatagridBody'; -import useDatagridStyles from './useDatagridStyles'; -import { ClassesOverride } from '../../types'; import { RowClickFunction } from './DatagridRow'; import DatagridContextProvider from './DatagridContextProvider'; +import { DatagridClasses, StyledTable } from './useDatagridStyles'; /** * The Datagrid component renders a list of records as a table. @@ -104,13 +103,11 @@ import DatagridContextProvider from './DatagridContextProvider'; * } */ const Datagrid: FC<DatagridProps> = React.forwardRef((props, ref) => { - const classes = useDatagridStyles(props); const { optimized = false, body = optimized ? PureDatagridBody : DatagridBody, header = DatagridHeader, children, - classes: classesOverride, className, empty, expand, @@ -189,7 +186,6 @@ const Datagrid: FC<DatagridProps> = React.forwardRef((props, ref) => { if (loaded === false) { return ( <DatagridLoading - classes={classes} className={className} expand={expand} hasBulkActions={hasBulkActions} @@ -219,18 +215,16 @@ const Datagrid: FC<DatagridProps> = React.forwardRef((props, ref) => { */ return ( <DatagridContextProvider value={contextValue}> - <Table + <StyledTable ref={ref} - className={classnames(classes.table, className)} + className={classnames(DatagridClasses.table, className)} size={size} - {...sanitizeListRestProps(rest)} + {...sanitizeRestProps(rest)} > {createOrCloneElement( header, { children, - classes, - className, currentSort, data, hasExpand: !!expand, @@ -248,8 +242,6 @@ const Datagrid: FC<DatagridProps> = React.forwardRef((props, ref) => { body, { basePath, - className: classes.tbody, - classes, expand, rowClick, data, @@ -265,7 +257,7 @@ const Datagrid: FC<DatagridProps> = React.forwardRef((props, ref) => { }, children )} - </Table> + </StyledTable> </DatagridContextProvider> ); }); @@ -280,7 +272,6 @@ Datagrid.propTypes = { // @ts-ignore body: PropTypes.oneOfType([PropTypes.element, PropTypes.elementType]), children: PropTypes.node.isRequired, - classes: PropTypes.object, className: PropTypes.string, currentSort: PropTypes.exact({ field: PropTypes.string, @@ -312,7 +303,6 @@ Datagrid.propTypes = { export interface DatagridProps<RecordType extends Record = Record> extends Omit<TableProps, 'size' | 'classes' | 'onSelect'> { body?: ReactElement | ComponentType; - classes?: ClassesOverride<typeof useDatagridStyles>; className?: string; expand?: | ReactElement @@ -345,6 +335,20 @@ export interface DatagridProps<RecordType extends Record = Record> total?: number; } +export const injectedProps = [ + 'allowEmpty', + 'isRequired', + 'setFilter', + 'setPagination', + 'limitChoicesToValue', + 'translateChoice', +]; + +const sanitizeRestProps = props => + Object.keys(sanitizeListRestProps(props)) + .filter(propName => !injectedProps.includes(propName)) + .reduce((acc, key) => ({ ...acc, [key]: props[key] }), {}); + Datagrid.displayName = 'Datagrid'; export default Datagrid; diff --git a/packages/ra-ui-materialui/src/list/datagrid/DatagridBody.tsx b/packages/ra-ui-materialui/src/list/datagrid/DatagridBody.tsx index 8eb22059ebb..1b2adbad9b1 100644 --- a/packages/ra-ui-materialui/src/list/datagrid/DatagridBody.tsx +++ b/packages/ra-ui-materialui/src/list/datagrid/DatagridBody.tsx @@ -1,20 +1,19 @@ import * as React from 'react'; import { cloneElement, memo, FC, ReactElement } from 'react'; import PropTypes from 'prop-types'; -import { TableBody, TableBodyProps } from '@material-ui/core'; +import { TableBody, TableBodyProps } from '@mui/material'; import classnames from 'classnames'; import { shallowEqual } from 'react-redux'; import { Identifier, Record, RecordMap } from 'ra-core'; +import { DatagridClasses } from './useDatagridStyles'; import DatagridRow, { PureDatagridRow, RowClickFunction } from './DatagridRow'; -import useDatagridStyles from './useDatagridStyles'; const DatagridBody: FC<DatagridBodyProps> = React.forwardRef( ( { basePath, children, - classes, className, data, expand, @@ -34,7 +33,11 @@ const DatagridBody: FC<DatagridBodyProps> = React.forwardRef( ) => ( <TableBody ref={ref} - className={classnames('datagrid-body', className)} + className={classnames( + 'datagrid-body', + className, + DatagridClasses.tbody + )} {...rest} > {ids.map((id, rowIndex) => @@ -42,11 +45,10 @@ const DatagridBody: FC<DatagridBodyProps> = React.forwardRef( row, { basePath, - classes, - className: classnames(classes.row, { - [classes.rowEven]: rowIndex % 2 === 0, - [classes.rowOdd]: rowIndex % 2 !== 0, - [classes.clickableRow]: rowClick, + className: classnames(DatagridClasses.row, { + [DatagridClasses.rowEven]: rowIndex % 2 === 0, + [DatagridClasses.rowOdd]: rowIndex % 2 !== 0, + [DatagridClasses.clickableRow]: rowClick, }), expand, hasBulkActions: hasBulkActions && !!selectedIds, @@ -71,7 +73,6 @@ const DatagridBody: FC<DatagridBodyProps> = React.forwardRef( DatagridBody.propTypes = { basePath: PropTypes.string, - classes: PropTypes.any, className: PropTypes.string, children: PropTypes.node, // @ts-ignore @@ -100,7 +101,6 @@ DatagridBody.defaultProps = { export interface DatagridBodyProps extends Omit<TableBodyProps, 'classes'> { basePath?: string; - classes?: ReturnType<typeof useDatagridStyles>; className?: string; data?: RecordMap; expand?: diff --git a/packages/ra-ui-materialui/src/list/datagrid/DatagridCell.spec.tsx b/packages/ra-ui-materialui/src/list/datagrid/DatagridCell.spec.tsx index 6c64caa47e2..cc2c9712497 100644 --- a/packages/ra-ui-materialui/src/list/datagrid/DatagridCell.spec.tsx +++ b/packages/ra-ui-materialui/src/list/datagrid/DatagridCell.spec.tsx @@ -28,7 +28,7 @@ describe('<DatagridCell />', () => { const { getByRole } = renderWithTable( <DatagridCell field={<Field />} /> ); - expect(getByRole('cell').className).toEqual('MuiTableCell-root'); + expect(getByRole('cell').className).toContain('MuiTableCell-root'); }); it('should pass the Datagrid basePath by default', () => { diff --git a/packages/ra-ui-materialui/src/list/datagrid/DatagridCell.tsx b/packages/ra-ui-materialui/src/list/datagrid/DatagridCell.tsx index 9fbd7385111..dfc6d0d04b1 100644 --- a/packages/ra-ui-materialui/src/list/datagrid/DatagridCell.tsx +++ b/packages/ra-ui-materialui/src/list/datagrid/DatagridCell.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import PropTypes from 'prop-types'; -import TableCell, { TableCellProps } from '@material-ui/core/TableCell'; +import TableCell, { TableCellProps } from '@mui/material/TableCell'; import classnames from 'classnames'; import { Record } from 'ra-core'; diff --git a/packages/ra-ui-materialui/src/list/datagrid/DatagridHeader.tsx b/packages/ra-ui-materialui/src/list/datagrid/DatagridHeader.tsx index a1ac0d6d0f8..95e57c50b36 100644 --- a/packages/ra-ui-materialui/src/list/datagrid/DatagridHeader.tsx +++ b/packages/ra-ui-materialui/src/list/datagrid/DatagridHeader.tsx @@ -10,12 +10,11 @@ import { SortPayload, useTranslate, } from 'ra-core'; -import { Checkbox, TableCell, TableHead, TableRow } from '@material-ui/core'; +import { Checkbox, TableCell, TableHead, TableRow } from '@mui/material'; import classnames from 'classnames'; import DatagridHeaderCell from './DatagridHeaderCell'; -import useDatagridStyles from './useDatagridStyles'; -import { ClassesOverride } from '../../types'; +import { DatagridClasses } from './useDatagridStyles'; /** * The default Datagrid Header component. @@ -25,7 +24,6 @@ import { ClassesOverride } from '../../types'; export const DatagridHeader = (props: DatagridHeaderProps) => { const { children, - classes, className, hasExpand = false, hasBulkActions = false, @@ -83,21 +81,26 @@ export const DatagridHeader = (props: DatagridHeaderProps) => { : ids; return ( - <TableHead className={classnames(className, classes.thead)}> - <TableRow className={classnames(classes.row, classes.headerRow)}> + <TableHead className={classnames(className, DatagridClasses.thead)}> + <TableRow + className={classnames( + DatagridClasses.row, + DatagridClasses.headerRow + )} + > {hasExpand && ( <TableCell padding="none" className={classnames( - classes.headerCell, - classes.expandHeader + DatagridClasses.headerCell, + DatagridClasses.expandHeader )} /> )} {hasBulkActions && selectedIds && ( <TableCell padding="checkbox" - className={classes.headerCell} + className={DatagridClasses.headerCell} > <Checkbox aria-label={translate('ra.action.select_all', { @@ -119,7 +122,7 @@ export const DatagridHeader = (props: DatagridHeaderProps) => { {Children.map(children, (field, index) => isValidElement(field) ? ( <DatagridHeaderCell - className={classes.headerCell} + className={DatagridClasses.headerCell} currentSort={currentSort} field={field} isSorting={ @@ -140,7 +143,6 @@ export const DatagridHeader = (props: DatagridHeaderProps) => { DatagridHeader.propTypes = { children: PropTypes.node, - classes: PropTypes.object, className: PropTypes.string, currentSort: PropTypes.exact({ field: PropTypes.string, @@ -161,7 +163,6 @@ DatagridHeader.propTypes = { export interface DatagridHeaderProps<RecordType extends Record = Record> { children?: React.ReactNode; - classes?: ClassesOverride<typeof useDatagridStyles>; className?: string; hasExpand?: boolean; hasBulkActions?: boolean; diff --git a/packages/ra-ui-materialui/src/list/datagrid/DatagridHeaderCell.spec.tsx b/packages/ra-ui-materialui/src/list/datagrid/DatagridHeaderCell.spec.tsx index 6c07c8c9d2b..4a5bb0ba70d 100644 --- a/packages/ra-ui-materialui/src/list/datagrid/DatagridHeaderCell.spec.tsx +++ b/packages/ra-ui-materialui/src/list/datagrid/DatagridHeaderCell.spec.tsx @@ -7,13 +7,14 @@ import { DatagridHeaderCell } from './DatagridHeaderCell'; describe('<DatagridHeaderCell />', () => { it('should accept a React element as Field label', () => { const Label = () => <>Label</>; - const Field = () => <div />; + const Field = ({ source, label }) => <div />; const { getByText } = render( <table> <tbody> <tr> <DatagridHeaderCell - currentSort={{}} + resource="posts" + currentSort={{ field: 'title', order: 'ASC' }} field={<Field source="title" label={<Label />} />} updateSort={() => true} /> @@ -25,19 +26,26 @@ describe('<DatagridHeaderCell />', () => { }); describe('sorting on a column', () => { - const Field = () => <div />; + const Field = (props: { + source?: string; + sortBy?: string; + sortByOrder?: string; + label?: string; + sortable?: boolean; + }) => <div />; Field.defaultProps = { type: 'foo', updateSort: () => true, }; it('should be enabled when field has a source', () => { - const { getByTitle } = render( + const { getByLabelText } = render( <table> <tbody> <tr> <DatagridHeaderCell - currentSort={{}} + resource="posts" + currentSort={{ field: 'title', order: 'ASC' }} field={<Field source="title" />} updateSort={() => true} /> @@ -45,16 +53,19 @@ describe('<DatagridHeaderCell />', () => { </tbody> </table> ); - expect(getByTitle('ra.action.sort').dataset.field).toBe('title'); + expect(getByLabelText('ra.action.sort').dataset.field).toBe( + 'title' + ); }); it('should be enabled when field has a sortBy props', () => { - const { getByTitle } = render( + const { getByLabelText } = render( <table> <tbody> <tr> <DatagridHeaderCell - currentSort={{}} + resource="posts" + currentSort={{ field: 'title', order: 'ASC' }} field={<Field sortBy="title" />} updateSort={() => true} /> @@ -62,16 +73,19 @@ describe('<DatagridHeaderCell />', () => { </tbody> </table> ); - expect(getByTitle('ra.action.sort').dataset.field).toBe('title'); + expect(getByLabelText('ra.action.sort').dataset.field).toBe( + 'title' + ); }); it('should be change order when field has a sortByOrder props', () => { - const { getByTitle } = render( + const { getByLabelText } = render( <table> <tbody> <tr> <DatagridHeaderCell - currentSort={{}} + resource="posts" + currentSort={{ field: 'title', order: 'ASC' }} field={ <Field sortBy="title" sortByOrder="DESC" /> } @@ -81,16 +95,17 @@ describe('<DatagridHeaderCell />', () => { </tbody> </table> ); - expect(getByTitle('ra.action.sort').dataset.order).toBe('DESC'); + expect(getByLabelText('ra.action.sort').dataset.order).toBe('DESC'); }); it('should be keep ASC order when field has not sortByOrder props', () => { - const { getByTitle } = render( + const { getByLabelText } = render( <table> <tbody> <tr> <DatagridHeaderCell - currentSort={{}} + resource="posts" + currentSort={{ field: 'title', order: 'ASC' }} field={<Field source="title" />} updateSort={() => true} /> @@ -98,16 +113,17 @@ describe('<DatagridHeaderCell />', () => { </tbody> </table> ); - expect(getByTitle('ra.action.sort').dataset.order).toBe('ASC'); + expect(getByLabelText('ra.action.sort').dataset.order).toBe('ASC'); }); it('should be disabled when field has no sortBy and no source', () => { - const { queryAllByTitle } = render( + const { queryAllByLabelText } = render( <table> <tbody> <tr> <DatagridHeaderCell - currentSort={{}} + resource="posts" + currentSort={{ field: 'title', order: 'ASC' }} field={<Field />} updateSort={() => true} /> @@ -115,16 +131,17 @@ describe('<DatagridHeaderCell />', () => { </tbody> </table> ); - expect(queryAllByTitle('ra.action.sort')).toHaveLength(0); + expect(queryAllByLabelText('ra.action.sort')).toHaveLength(0); }); it('should be disabled when sortable prop is explicitly set to false', () => { - const { queryAllByTitle } = render( + const { queryAllByLabelText } = render( <table> <tbody> <tr> <DatagridHeaderCell - currentSort={{}} + resource="posts" + currentSort={{ field: 'title', order: 'ASC' }} field={ <Field source="title" sortable={false} /> } @@ -134,7 +151,7 @@ describe('<DatagridHeaderCell />', () => { </tbody> </table> ); - expect(queryAllByTitle('ra.action.sort')).toHaveLength(0); + expect(queryAllByLabelText('ra.action.sort')).toHaveLength(0); }); it('should use cell className if specified', () => { @@ -143,7 +160,8 @@ describe('<DatagridHeaderCell />', () => { <tbody> <tr> <DatagridHeaderCell - currentSort={{}} + resource="posts" + currentSort={{ field: 'title', order: 'ASC' }} updateSort={() => true} field={<Field />} className="blue" diff --git a/packages/ra-ui-materialui/src/list/datagrid/DatagridHeaderCell.tsx b/packages/ra-ui-materialui/src/list/datagrid/DatagridHeaderCell.tsx index 1465e562f31..50c25e0884d 100644 --- a/packages/ra-ui-materialui/src/list/datagrid/DatagridHeaderCell.tsx +++ b/packages/ra-ui-materialui/src/list/datagrid/DatagridHeaderCell.tsx @@ -1,10 +1,10 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { memo } from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; -import { TableCell, TableSortLabel, Tooltip } from '@material-ui/core'; -import { TableCellProps } from '@material-ui/core/TableCell'; -import { makeStyles } from '@material-ui/core/styles'; +import { TableCell, TableSortLabel, Tooltip } from '@mui/material'; +import { TableCellProps } from '@mui/material/TableCell'; import { FieldTitle, useTranslate, @@ -12,29 +12,26 @@ import { useResourceContext, } from 'ra-core'; -import { ClassesOverride } from '../../types'; +const PREFIX = 'RaDatagridHeaderCell'; -// remove the sort icons when not active -const useStyles = makeStyles( - { - icon: { - display: 'none', - }, - active: { - '& $icon': { - display: 'inline', - }, - }, +const classes = { + icon: `${PREFIX}-icon`, +}; + +const StyledTableCell = styled(TableCell)(({ theme }) => ({ + [`& .MuiSvgIcon-root`]: { + display: 'none', }, - { name: 'RaDatagridHeaderCell' } -); + [`& .Mui-active .MuiSvgIcon-root`]: { + display: 'inline', + }, +})); export const DatagridHeaderCell = ( props: DatagridHeaderCellProps ): JSX.Element => { const { className, - classes: classesOverride, field, currentSort, updateSort, @@ -42,11 +39,11 @@ export const DatagridHeaderCell = ( ...rest } = props; const resource = useResourceContext(props); - const classes = useStyles(props); + const translate = useTranslate(); return ( - <TableCell + <StyledTableCell className={classnames(className, field.props.headerClassName)} align={field.props.textAlign} variant="head" @@ -90,13 +87,12 @@ export const DatagridHeaderCell = ( resource={resource} /> )} - </TableCell> + </StyledTableCell> ); }; DatagridHeaderCell.propTypes = { className: PropTypes.string, - classes: PropTypes.object, field: PropTypes.element, currentSort: PropTypes.shape({ sort: PropTypes.string, @@ -110,7 +106,6 @@ DatagridHeaderCell.propTypes = { export interface DatagridHeaderCellProps extends Omit<TableCellProps, 'classes'> { className?: string; - classes?: ClassesOverride<typeof useStyles>; field?: JSX.Element; isSorting?: boolean; resource: string; diff --git a/packages/ra-ui-materialui/src/list/datagrid/DatagridLoading.tsx b/packages/ra-ui-materialui/src/list/datagrid/DatagridLoading.tsx index 705293e22bf..5d99e43eb7c 100644 --- a/packages/ra-ui-materialui/src/list/datagrid/DatagridLoading.tsx +++ b/packages/ra-ui-materialui/src/list/datagrid/DatagridLoading.tsx @@ -2,27 +2,24 @@ import * as React from 'react'; import { ReactElement, FC, memo } from 'react'; import PropTypes from 'prop-types'; import { - Table, TableCell, TableHead, TableRow, TableBody, IconButton, Checkbox, -} from '@material-ui/core'; -import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; +} from '@mui/material'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import classnames from 'classnames'; import { useTimeout, Identifier, Record } from 'ra-core'; -import useDatagridStyles from './useDatagridStyles'; +import { DatagridClasses, StyledTable } from './useDatagridStyles'; import Placeholder from '../Placeholder'; -import { ClassesOverride } from '../../types'; const times = (nbChildren, fn) => Array.from({ length: nbChildren }, (_, key) => fn(key)); const DatagridLoading = ({ - classes, className, expand, hasBulkActions, @@ -33,19 +30,22 @@ const DatagridLoading = ({ const oneSecondHasPassed = useTimeout(1000); return oneSecondHasPassed ? ( - <Table className={classnames(classes.table, className)} size={size}> + <StyledTable + className={classnames(DatagridClasses.table, className)} + size={size} + > <TableHead> - <TableRow className={classes.row}> + <TableRow className={DatagridClasses.row}> {expand && ( <TableCell padding="none" - className={classes.expandHeader} + className={DatagridClasses.expandHeader} /> )} {hasBulkActions && ( <TableCell padding="checkbox" - className={classes.expandIconCell} + className={DatagridClasses.expandIconCell} > <Checkbox className="select-all" @@ -57,7 +57,7 @@ const DatagridLoading = ({ {times(nbChildren, key => ( <TableCell variant="head" - className={classes.headerCell} + className={DatagridClasses.headerCell} key={key} > <Placeholder /> @@ -71,12 +71,13 @@ const DatagridLoading = ({ {expand && ( <TableCell padding="none" - className={classes.expandIconCell} + className={DatagridClasses.expandIconCell} > <IconButton - className={classes.expandIcon} + className={DatagridClasses.expandIcon} component="div" aria-hidden="true" + size="large" > <ExpandMoreIcon /> </IconButton> @@ -85,7 +86,7 @@ const DatagridLoading = ({ {hasBulkActions && ( <TableCell padding="checkbox" - className={classes.expandIconCell} + className={DatagridClasses.expandIconCell} > <Checkbox className="select-all" @@ -95,19 +96,21 @@ const DatagridLoading = ({ </TableCell> )} {times(nbChildren, key2 => ( - <TableCell className={classes.rowCell} key={key2}> + <TableCell + className={DatagridClasses.rowCell} + key={key2} + > <Placeholder /> </TableCell> ))} </TableRow> ))} </TableBody> - </Table> + </StyledTable> ) : null; }; DatagridLoading.propTypes = { - classes: PropTypes.object, className: PropTypes.string, expand: PropTypes.oneOfType([PropTypes.element, PropTypes.elementType]), hasBulkActions: PropTypes.bool, @@ -118,7 +121,6 @@ DatagridLoading.propTypes = { export interface DatagridLoadingProps { className?: string; - classes?: ClassesOverride<typeof useDatagridStyles>; expand?: | ReactElement | FC<{ diff --git a/packages/ra-ui-materialui/src/list/datagrid/DatagridRow.tsx b/packages/ra-ui-materialui/src/list/datagrid/DatagridRow.tsx index b48804d6d29..b3345369add 100644 --- a/packages/ra-ui-materialui/src/list/datagrid/DatagridRow.tsx +++ b/packages/ra-ui-materialui/src/list/datagrid/DatagridRow.tsx @@ -11,12 +11,7 @@ import React, { } from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; -import { - TableCell, - TableRow, - TableRowProps, - Checkbox, -} from '@material-ui/core'; +import { TableCell, TableRow, TableRowProps, Checkbox } from '@mui/material'; import { Identifier, linkToRecord, @@ -28,10 +23,11 @@ import { } from 'ra-core'; import { shallowEqual } from 'react-redux'; import { useHistory } from 'react-router-dom'; +import classNames from 'classnames'; import DatagridCell from './DatagridCell'; import ExpandRowButton from './ExpandRowButton'; -import useDatagridStyles from './useDatagridStyles'; +import { DatagridClasses } from './useDatagridStyles'; import { useDatagridContext } from './useDatagridContext'; const computeNbColumns = (expand, children, hasBulkActions) => @@ -41,13 +37,10 @@ const computeNbColumns = (expand, children, hasBulkActions) => React.Children.toArray(children).filter(child => !!child).length // non-null children : 0; // we don't need to compute columns if there is no expand panel; -const defaultClasses = { expandIconCell: '', checkbox: '', rowCell: '' }; - const DatagridRow: FC<DatagridRowProps> = React.forwardRef((props, ref) => { const { basePath, children, - classes = defaultClasses, className, expand, hasBulkActions, @@ -160,11 +153,16 @@ const DatagridRow: FC<DatagridRowProps> = React.forwardRef((props, ref) => { {expand && ( <TableCell padding="none" - className={classes.expandIconCell} + className={DatagridClasses.expandIconCell} > {expandable && ( <ExpandRowButton - classes={classes} + className={classNames( + DatagridClasses.expandIcon, + { + [DatagridClasses.expanded]: expanded, + } + )} expanded={expanded} onClick={handleToggleExpand} expandContentId={`${id}-expand`} @@ -180,7 +178,7 @@ const DatagridRow: FC<DatagridRowProps> = React.forwardRef((props, ref) => { _: 'Select this row', })} color="primary" - className={`select-item ${classes.checkbox}`} + className={`select-item ${DatagridClasses.checkbox}`} checked={selected} onClick={handleToggleSelection} /> @@ -195,7 +193,7 @@ const DatagridRow: FC<DatagridRowProps> = React.forwardRef((props, ref) => { }`} className={classnames( `column-${(field.props as any).source}`, - classes.rowCell + DatagridClasses.rowCell )} record={record} {...{ field, basePath, resource }} @@ -230,7 +228,6 @@ const DatagridRow: FC<DatagridRowProps> = React.forwardRef((props, ref) => { DatagridRow.propTypes = { basePath: PropTypes.string, children: PropTypes.node, - classes: PropTypes.any, className: PropTypes.string, // @ts-ignore expand: PropTypes.oneOfType([PropTypes.element, PropTypes.elementType]), @@ -257,7 +254,6 @@ DatagridRow.defaultProps = { export interface DatagridRowProps extends Omit<TableRowProps, 'id' | 'classes'> { - classes?: ReturnType<typeof useDatagridStyles>; basePath?: string; className?: string; expand?: diff --git a/packages/ra-ui-materialui/src/list/datagrid/ExpandRowButton.tsx b/packages/ra-ui-materialui/src/list/datagrid/ExpandRowButton.tsx index d819f84b3a3..66f59a75f10 100644 --- a/packages/ra-ui-materialui/src/list/datagrid/ExpandRowButton.tsx +++ b/packages/ra-ui-materialui/src/list/datagrid/ExpandRowButton.tsx @@ -1,12 +1,10 @@ import * as React from 'react'; import { ElementType, memo } from 'react'; -import IconButton, { IconButtonProps } from '@material-ui/core/IconButton'; -import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; -import classNames from 'classnames'; +import IconButton, { IconButtonProps } from '@mui/material/IconButton'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import { useTranslate } from 'ra-core'; const ExpandRowButton = ({ - classes, expanded, expandContentId, ...props @@ -19,13 +17,11 @@ const ExpandRowButton = ({ )} aria-expanded={expanded} aria-controls={expandContentId} - className={classNames(classes.expandIcon, { - [classes.expanded]: expanded, - })} tabIndex={-1} aria-hidden="true" component="div" {...props} + size="large" > <ExpandMoreIcon /> </IconButton> @@ -33,7 +29,6 @@ const ExpandRowButton = ({ }; export interface ExpandRowButtonProps extends IconButtonProps { - classes?: any; component?: ElementType; expanded: boolean; expandContentId?: string; diff --git a/packages/ra-ui-materialui/src/list/datagrid/index.ts b/packages/ra-ui-materialui/src/list/datagrid/index.ts index 7eb6e18ce3b..1447d24e1d4 100644 --- a/packages/ra-ui-materialui/src/list/datagrid/index.ts +++ b/packages/ra-ui-materialui/src/list/datagrid/index.ts @@ -14,7 +14,6 @@ import DatagridRow, { RowClickFunction, } from './DatagridRow'; import ExpandRowButton, { ExpandRowButtonProps } from './ExpandRowButton'; -import useDatagridStyles from './useDatagridStyles'; export * from './DatagridHeader'; @@ -28,7 +27,6 @@ export { ExpandRowButton, PureDatagridBody, PureDatagridRow, - useDatagridStyles, }; export type { diff --git a/packages/ra-ui-materialui/src/list/datagrid/useDatagridStyles.tsx b/packages/ra-ui-materialui/src/list/datagrid/useDatagridStyles.tsx index 13f7f8302e2..859e8008134 100644 --- a/packages/ra-ui-materialui/src/list/datagrid/useDatagridStyles.tsx +++ b/packages/ra-ui-materialui/src/list/datagrid/useDatagridStyles.tsx @@ -1,52 +1,67 @@ -import { makeStyles } from '@material-ui/core/styles'; +import { Table, styled } from '@mui/material'; -const useDatagridStyles = makeStyles( - theme => ({ - table: { - tableLayout: 'auto', - }, - thead: {}, - tbody: {}, - headerRow: {}, - headerCell: { - position: 'sticky', - top: 0, - zIndex: 2, - backgroundColor: theme.palette.background.paper, - '&:first-child': { - borderTopLeftRadius: theme.shape.borderRadius, - }, - '&:last-child': { - borderTopRightRadius: theme.shape.borderRadius, - }, - }, - checkbox: {}, - row: {}, - clickableRow: { - cursor: 'pointer', - }, - rowEven: {}, - rowOdd: {}, - rowCell: {}, - expandHeader: { - padding: 0, - width: theme.spacing(6), - }, - expandIconCell: { - width: theme.spacing(6), - }, - expandIcon: { - padding: theme.spacing(1), - transform: 'rotate(-90deg)', - transition: theme.transitions.create('transform', { - duration: theme.transitions.duration.shortest, - }), +const PREFIX = 'RaDatagrid'; + +export const DatagridClasses = { + table: `${PREFIX}-table`, + thead: `${PREFIX}-thead`, + tbody: `${PREFIX}-tbody`, + headerRow: `${PREFIX}-headerRow`, + headerCell: `${PREFIX}-headerCell`, + checkbox: `${PREFIX}-checkbox`, + row: `${PREFIX}-row`, + clickableRow: `${PREFIX}-clickableRow`, + rowEven: `${PREFIX}-rowEven`, + rowOdd: `${PREFIX}-rowOdd`, + rowCell: `${PREFIX}-rowCell`, + expandHeader: `${PREFIX}-expandHeader`, + expandIconCell: `${PREFIX}-expandIconCell`, + expandIcon: `${PREFIX}-expandIcon`, + expanded: `${PREFIX}-expanded`, +}; + +export const StyledTable = styled(Table)(({ theme }) => ({ + [`&.${DatagridClasses.table}`]: { + tableLayout: 'auto', + }, + [`& .${DatagridClasses.thead}`]: {}, + [`& .${DatagridClasses.tbody}`]: {}, + [`& .${DatagridClasses.headerRow}`]: {}, + [`& .${DatagridClasses.headerCell}`]: { + position: 'sticky', + top: 0, + zIndex: 2, + backgroundColor: theme.palette.background.paper, + '&:first-of-type': { + borderTopLeftRadius: theme.shape.borderRadius, }, - expanded: { - transform: 'rotate(0deg)', + '&:last-child': { + borderTopRightRadius: theme.shape.borderRadius, }, - }), - { name: 'RaDatagrid' } -); - -export default useDatagridStyles; + }, + [`& .${DatagridClasses.checkbox}`]: {}, + [`& .${DatagridClasses.row}`]: {}, + [`& .${DatagridClasses.clickableRow}`]: { + cursor: 'pointer', + }, + [`& .${DatagridClasses.rowEven}`]: {}, + [`& .${DatagridClasses.rowOdd}`]: {}, + [`& .${DatagridClasses.rowCell}`]: {}, + [`& .${DatagridClasses.expandHeader}`]: { + padding: 0, + width: theme.spacing(6), + }, + [`& .${DatagridClasses.expandIconCell}`]: { + width: theme.spacing(6), + }, + [`& .${DatagridClasses.expandIcon}`]: { + padding: theme.spacing(1), + transform: 'rotate(-90deg)', + transition: theme.transitions.create('transform', { + duration: theme.transitions.duration.shortest, + }), + }, + [`& .${DatagridClasses.expandIcon}.${DatagridClasses.expanded}`]: { + transform: 'rotate(0deg)', + }, +})); diff --git a/packages/ra-ui-materialui/src/list/filter/Filter.tsx b/packages/ra-ui-materialui/src/list/filter/Filter.tsx index c15fa368fb1..271254f9d8d 100644 --- a/packages/ra-ui-materialui/src/list/filter/Filter.tsx +++ b/packages/ra-ui-materialui/src/list/filter/Filter.tsx @@ -1,24 +1,20 @@ import * as React from 'react'; import { ReactNode } from 'react'; import PropTypes from 'prop-types'; -import { makeStyles } from '@material-ui/core/styles'; import { sanitizeListRestProps, useListContext } from 'ra-core'; import FilterForm from './FilterForm'; import FilterButton from './FilterButton'; -import { ClassesOverride } from '../../types'; -const useStyles = makeStyles( - { - button: {}, - form: {}, - }, - { name: 'RaFilter' } -); +const PREFIX = 'RaFilter'; + +const classes = { + button: `${PREFIX}-button`, + form: `${PREFIX}-form`, +}; export interface FilterProps { children: ReactNode; - classes?: ClassesOverride<typeof useStyles>; context?: 'form' | 'button'; variant?: string; } @@ -43,7 +39,6 @@ export interface FilterProps { * */ const Filter = (props: FilterProps) => { - const classes = useStyles(props); const { resource, showFilter, @@ -53,13 +48,7 @@ const Filter = (props: FilterProps) => { filterValues, } = useListContext(props); const renderButton = () => { - const { - classes: classesOverride, - context, - children, - variant, - ...rest - } = props; + const { context, children, variant, ...rest } = props; return ( <FilterButton @@ -75,7 +64,7 @@ const Filter = (props: FilterProps) => { }; const renderForm = () => { - const { classes: classesOverride, context, children, ...rest } = props; + const { context, children, ...rest } = props; return ( <FilterForm diff --git a/packages/ra-ui-materialui/src/list/filter/FilterButton.spec.tsx b/packages/ra-ui-materialui/src/list/filter/FilterButton.spec.tsx index ef19fa4c0fd..146bb6040d5 100644 --- a/packages/ra-ui-materialui/src/list/filter/FilterButton.spec.tsx +++ b/packages/ra-ui-materialui/src/list/filter/FilterButton.spec.tsx @@ -1,8 +1,7 @@ import * as React from 'react'; import expect from 'expect'; import { render, fireEvent } from '@testing-library/react'; -import { ThemeProvider } from '@material-ui/styles'; -import { createTheme } from '@material-ui/core/styles'; +import { createTheme, ThemeProvider } from '@mui/material/styles'; import FilterButton from './FilterButton'; import TextInput from '../../input/TextInput'; diff --git a/packages/ra-ui-materialui/src/list/filter/FilterButton.tsx b/packages/ra-ui-materialui/src/list/filter/FilterButton.tsx index aa7c43e33a0..3c8b36e2414 100644 --- a/packages/ra-ui-materialui/src/list/filter/FilterButton.tsx +++ b/packages/ra-ui-materialui/src/list/filter/FilterButton.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { useState, useCallback, @@ -8,32 +9,28 @@ import { useContext, } from 'react'; import PropTypes from 'prop-types'; -import Menu from '@material-ui/core/Menu'; -import { makeStyles } from '@material-ui/core/styles'; -import ContentFilter from '@material-ui/icons/FilterList'; +import Menu from '@mui/material/Menu'; +import ContentFilter from '@mui/icons-material/FilterList'; import classnames from 'classnames'; import lodashGet from 'lodash/get'; import { useListContext, useResourceContext } from 'ra-core'; import { FilterButtonMenuItem } from './FilterButtonMenuItem'; import Button from '../../button/Button'; -import { ClassesOverride } from '../../types'; import { FilterContext } from '../FilterContext'; -const useStyles = makeStyles( - { - root: { display: 'inline-block' }, - }, - { name: 'RaFilterButton' } -); +const PREFIX = 'RaFilterButton'; + +const classes = { + root: `${PREFIX}-root`, +}; + +const Root = styled('div')(({ theme }) => ({ + [`&.${classes.root}`]: { display: 'inline-block' }, +})); const FilterButton = (props: FilterButtonProps): JSX.Element => { - const { - filters: filtersProp, - classes: classesOverride, - className, - ...rest - } = props; + const { filters: filtersProp, className, ...rest } = props; const filters = useContext(FilterContext) || filtersProp; const resource = useResourceContext(props); const { displayedFilters = {}, filterValues, showFilter } = useListContext( @@ -41,7 +38,6 @@ const FilterButton = (props: FilterButtonProps): JSX.Element => { ); const [open, setOpen] = useState(false); const anchorEl = useRef(); - const classes = useStyles(props); const hiddenFilters = filters.filter( (filterElement: JSX.Element) => @@ -75,7 +71,7 @@ const FilterButton = (props: FilterButtonProps): JSX.Element => { if (hiddenFilters.length === 0) return null; return ( - <div + <Root className={classnames(classes.root, className)} {...sanitizeRestProps(rest)} > @@ -100,7 +96,7 @@ const FilterButton = (props: FilterButtonProps): JSX.Element => { /> ))} </Menu> - </div> + </Root> ); }; @@ -117,12 +113,10 @@ FilterButton.propTypes = { displayedFilters: PropTypes.object, filterValues: PropTypes.object, showFilter: PropTypes.func, - classes: PropTypes.object, className: PropTypes.string, }; export interface FilterButtonProps extends HtmlHTMLAttributes<HTMLDivElement> { - classes?: ClassesOverride<typeof useStyles>; className?: string; resource?: string; filterValues?: any; diff --git a/packages/ra-ui-materialui/src/list/filter/FilterButtonMenuItem.tsx b/packages/ra-ui-materialui/src/list/filter/FilterButtonMenuItem.tsx index 1677efc6a42..cfae58ba8a2 100644 --- a/packages/ra-ui-materialui/src/list/filter/FilterButtonMenuItem.tsx +++ b/packages/ra-ui-materialui/src/list/filter/FilterButtonMenuItem.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { forwardRef, useCallback } from 'react'; import PropTypes from 'prop-types'; -import MenuItem from '@material-ui/core/MenuItem'; +import MenuItem from '@mui/material/MenuItem'; import { FieldTitle, useResourceContext } from 'ra-core'; export const FilterButtonMenuItem = forwardRef<any, FilterButtonMenuItemProps>( diff --git a/packages/ra-ui-materialui/src/list/filter/FilterForm.spec.tsx b/packages/ra-ui-materialui/src/list/filter/FilterForm.spec.tsx index 3381353b830..eb9954a5057 100644 --- a/packages/ra-ui-materialui/src/list/filter/FilterForm.spec.tsx +++ b/packages/ra-ui-materialui/src/list/filter/FilterForm.spec.tsx @@ -3,11 +3,14 @@ import { fireEvent } from '@testing-library/react'; import * as React from 'react'; import { renderWithRedux } from 'ra-test'; import { minLength } from 'ra-core'; +import { createTheme, ThemeProvider } from '@mui/material/styles'; import FilterForm, { mergeInitialValuesWithDefaultValues } from './FilterForm'; import TextInput from '../../input/TextInput'; import { SelectInput } from '../../input/SelectInput'; +const theme = createTheme({}); + describe('<FilterForm />', () => { const defaultProps = { resource: 'post', @@ -29,11 +32,13 @@ describe('<FilterForm />', () => { }; const { queryAllByLabelText } = renderWithRedux( - <FilterForm - {...defaultProps} - filters={filters} - displayedFilters={displayedFilters} - /> + <ThemeProvider theme={theme}> + <FilterForm + {...defaultProps} + filters={filters} + displayedFilters={displayedFilters} + /> + </ThemeProvider> ); expect(queryAllByLabelText('Title')).toHaveLength(1); expect(queryAllByLabelText('Name')).toHaveLength(1); @@ -47,12 +52,14 @@ describe('<FilterForm />', () => { const setFilters = jest.fn(); const { queryByLabelText } = renderWithRedux( - <FilterForm - {...defaultProps} - filters={filters} - displayedFilters={displayedFilters} - setFilters={setFilters} - /> + <ThemeProvider theme={theme}> + <FilterForm + {...defaultProps} + filters={filters} + displayedFilters={displayedFilters} + setFilters={setFilters} + /> + </ThemeProvider> ); fireEvent.change(queryByLabelText('Title'), { target: { value: 'foo' }, @@ -77,12 +84,14 @@ describe('<FilterForm />', () => { const setFilters = jest.fn(); const { queryByLabelText } = renderWithRedux( - <FilterForm - {...defaultProps} - filters={filters} - displayedFilters={displayedFilters} - setFilters={setFilters} - /> + <ThemeProvider theme={theme}> + <FilterForm + {...defaultProps} + filters={filters} + displayedFilters={displayedFilters} + setFilters={setFilters} + /> + </ThemeProvider> ); fireEvent.change(queryByLabelText('Title'), { target: { value: 'foo' }, @@ -108,11 +117,13 @@ describe('<FilterForm />', () => { }; const { queryAllByRole, queryByLabelText } = renderWithRedux( - <FilterForm - {...defaultProps} - filters={filters} - displayedFilters={displayedFilters} - /> + <ThemeProvider theme={theme}> + <FilterForm + {...defaultProps} + filters={filters} + displayedFilters={displayedFilters} + /> + </ThemeProvider> ); const select = queryByLabelText('SelectWithUndefinedAllowEmpty'); @@ -139,11 +150,13 @@ describe('<FilterForm />', () => { }; const { queryAllByRole, queryByLabelText } = renderWithRedux( - <FilterForm - {...defaultProps} - filters={filters} - displayedFilters={displayedFilters} - /> + <ThemeProvider theme={theme}> + <FilterForm + {...defaultProps} + filters={filters} + displayedFilters={displayedFilters} + /> + </ThemeProvider> ); const select = queryByLabelText('SelectWithFalseAllowEmpty'); fireEvent.mouseDown(select); @@ -169,11 +182,13 @@ describe('<FilterForm />', () => { }; const { queryAllByRole, queryByLabelText } = renderWithRedux( - <FilterForm - {...defaultProps} - filters={filters} - displayedFilters={displayedFilters} - /> + <ThemeProvider theme={theme}> + <FilterForm + {...defaultProps} + filters={filters} + displayedFilters={displayedFilters} + /> + </ThemeProvider> ); const select = queryByLabelText('SelectWithTrueAllowEmpty'); fireEvent.mouseDown(select); diff --git a/packages/ra-ui-materialui/src/list/filter/FilterForm.tsx b/packages/ra-ui-materialui/src/list/filter/FilterForm.tsx index 5a81947faca..7d11a7fe3dc 100644 --- a/packages/ra-ui-materialui/src/list/filter/FilterForm.tsx +++ b/packages/ra-ui-materialui/src/list/filter/FilterForm.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { useEffect, useCallback, @@ -11,17 +12,35 @@ import { useListContext, useResourceContext } from 'ra-core'; import { Form, FormRenderProps, FormSpy } from 'react-final-form'; import arrayMutators from 'final-form-arrays'; import classnames from 'classnames'; -import { makeStyles } from '@material-ui/core/styles'; import lodashSet from 'lodash/set'; import lodashGet from 'lodash/get'; import FilterFormInput from './FilterFormInput'; -import { ClassesOverride } from '../../types'; import { FilterContext } from '../FilterContext'; +const PREFIX = 'RaFilterForm'; + +const classes = { + form: `${PREFIX}-form`, + clearFix: `${PREFIX}-clearFix`, +}; + +const StyledForm = styled('form')(({ theme }) => ({ + [`&.${classes.form}`]: { + marginTop: -theme.spacing(2), + paddingTop: 0, + display: 'flex', + alignItems: 'flex-end', + flexWrap: 'wrap', + minHeight: theme.spacing(10), + pointerEvents: 'none', + }, + + [`& .${classes.clearFix}`]: { clear: 'right' }, +})); + export const FilterForm = (props: FilterFormProps) => { const { - classes = {}, className, margin, filters, @@ -56,7 +75,7 @@ export const FilterForm = (props: FilterFormProps) => { ); return ( - <form + <StyledForm className={classnames(className, classes.form)} {...sanitizeRestProps(rest)} onSubmit={handleSubmit} @@ -72,7 +91,7 @@ export const FilterForm = (props: FilterFormProps) => { /> ))} <div className={classes.clearFix} /> - </form> + </StyledForm> ); }; @@ -87,26 +106,9 @@ FilterForm.propTypes = { displayedFilters: PropTypes.object, hideFilter: PropTypes.func, initialValues: PropTypes.object, - classes: PropTypes.object, className: PropTypes.string, }; -const useStyles = makeStyles( - theme => ({ - form: { - marginTop: -theme.spacing(2), - paddingTop: 0, - display: 'flex', - alignItems: 'flex-end', - flexWrap: 'wrap', - minHeight: theme.spacing(10), - pointerEvents: 'none', - }, - clearFix: { clear: 'right' }, - }), - { name: 'RaFilterForm' } -); - const sanitizeRestProps = ({ active, dirty, @@ -144,7 +146,6 @@ const sanitizeRestProps = ({ export interface FilterFormProps extends Omit<FormRenderProps, 'initialValues'>, Omit<HtmlHTMLAttributes<HTMLFormElement>, 'children'> { - classes?: ClassesOverride<typeof useStyles>; className?: string; resource?: string; filterValues: any; @@ -185,7 +186,7 @@ const EnhancedFilterForm = props => { initialValues, ...rest } = props; - const classes = useStyles(props); + const { setFilters, displayedFilters, filterValues } = useListContext( props ); @@ -212,12 +213,7 @@ const EnhancedFilterForm = props => { setFilters(values, displayedFilters); }} /> - <FilterForm - classes={classes} - {...formProps} - {...rest} - filters={filters} - /> + <FilterForm {...formProps} {...rest} filters={filters} /> </> )} /> diff --git a/packages/ra-ui-materialui/src/list/filter/FilterFormInput.tsx b/packages/ra-ui-materialui/src/list/filter/FilterFormInput.tsx index 30f08c5c5c4..e47bb36fbeb 100644 --- a/packages/ra-ui-materialui/src/list/filter/FilterFormInput.tsx +++ b/packages/ra-ui-materialui/src/list/filter/FilterFormInput.tsx @@ -1,33 +1,39 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import PropTypes from 'prop-types'; -import IconButton from '@material-ui/core/IconButton'; -import ActionHide from '@material-ui/icons/HighlightOff'; -import { makeStyles } from '@material-ui/core/styles'; +import IconButton from '@mui/material/IconButton'; +import ActionHide from '@mui/icons-material/HighlightOff'; import classnames from 'classnames'; import { useResourceContext, useTranslate } from 'ra-core'; -const emptyRecord = {}; +const PREFIX = 'RaFilterFormInput'; + +const classes = { + body: `${PREFIX}-body`, + spacer: `${PREFIX}-spacer`, + hideButton: `${PREFIX}-hideButton`, +}; + +const Root = styled('div')(({ theme }) => ({ + [`&.${classes.body}`]: { + display: 'flex', + alignItems: 'flex-end', + pointerEvents: 'auto', + }, -const useStyles = makeStyles( - theme => ({ - body: { - display: 'flex', - alignItems: 'flex-end', - pointerEvents: 'auto', - }, - spacer: { width: theme.spacing(2) }, - hideButton: {}, - }), - { name: 'RaFilterFormInput' } -); + [`& .${classes.spacer}`]: { width: theme.spacing(2) }, + [`& .${classes.hideButton}`]: {}, +})); + +const emptyRecord = {}; const FilterFormInput = props => { const { filterElement, handleHide, variant, margin } = props; const resource = useResourceContext(props); const translate = useTranslate(); - const classes = useStyles(props); + return ( - <div + <Root data-source={filterElement.props.source} className={classnames('filter-field', classes.body)} > @@ -37,6 +43,7 @@ const FilterFormInput = props => { onClick={handleHide} data-key={filterElement.props.source} title={translate('ra.action.remove_filter')} + size="large" > <ActionHide /> </IconButton> @@ -55,14 +62,13 @@ const FilterFormInput = props => { defaultValue: undefined, })} <div className={classes.spacer}> </div> - </div> + </Root> ); }; FilterFormInput.propTypes = { filterElement: PropTypes.node, handleHide: PropTypes.func, - classes: PropTypes.object, resource: PropTypes.string, margin: PropTypes.string, variant: PropTypes.string, diff --git a/packages/ra-ui-materialui/src/list/filter/FilterList.tsx b/packages/ra-ui-materialui/src/list/filter/FilterList.tsx index a00942a7ede..05b86e06343 100644 --- a/packages/ra-ui-materialui/src/list/filter/FilterList.tsx +++ b/packages/ra-ui-materialui/src/list/filter/FilterList.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { ReactNode } from 'react'; -import { Box, Typography, List } from '@material-ui/core'; +import { Box, Typography, List } from '@mui/material'; import { useTranslate } from 'ra-core'; /** @@ -16,8 +16,8 @@ import { useTranslate } from 'ra-core'; * @example * * import * as React from 'react'; - * import { Card, CardContent } from '@material-ui/core'; - * import MailIcon from '@material-ui/icons/MailOutline'; + * import { Card, CardContent } from '@mui/material'; + * import MailIcon from '@mui/icons-material/MailOutline'; * import { FilterList, FilterListItem } from 'react-admin'; * * const FilterSidebar = () => ( diff --git a/packages/ra-ui-materialui/src/list/filter/FilterListItem.tsx b/packages/ra-ui-materialui/src/list/filter/FilterListItem.tsx index bea2c063fbe..e243989feba 100644 --- a/packages/ra-ui-materialui/src/list/filter/FilterListItem.tsx +++ b/packages/ra-ui-materialui/src/list/filter/FilterListItem.tsx @@ -1,23 +1,32 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import { memo, isValidElement, ReactElement } from 'react'; import { IconButton, ListItem, + ListItemButton, ListItemText, ListItemSecondaryAction, -} from '@material-ui/core'; -import { makeStyles } from '@material-ui/core/styles'; -import CancelIcon from '@material-ui/icons/CancelOutlined'; +} from '@mui/material'; +import CancelIcon from '@mui/icons-material/CancelOutlined'; import { useTranslate, useListFilterContext } from 'ra-core'; import { shallowEqual } from 'react-redux'; import matches from 'lodash/matches'; import pickBy from 'lodash/pickBy'; -const useStyles = makeStyles(theme => ({ - listItem: { +const PREFIX = 'FilterListItem'; + +const classes = { + listItem: `${PREFIX}-listItem`, + listItemText: `${PREFIX}-listItemText`, +}; + +const StyledListItem = styled(ListItem)(({ theme }) => ({ + [`&.${classes.listItem}`]: { paddingLeft: '2em', }, - listItemText: { + + [`& .${classes.listItemText}`]: { margin: 0, }, })); @@ -35,8 +44,8 @@ const useStyles = makeStyles(theme => ({ * @example * * import * as React from 'react'; - * import { Card, CardContent } from '@material-ui/core'; - * import MailIcon from '@material-ui/icons/MailOutline'; + * import { Card, CardContent } from '@mui/material'; + * import MailIcon from '@mui/icons-material/MailOutline'; * import { FilterList, FilterListItem } from 'react-admin'; * * const FilterSidebar = () => ( @@ -69,8 +78,8 @@ const useStyles = makeStyles(theme => ({ * startOfMonth, * subMonths, * } from 'date-fns'; - * import { Card, CardContent } from '@material-ui/core'; - * import AccessTimeIcon from '@material-ui/icons/AccessTime'; + * import { Card, CardContent } from '@mui/material'; + * import AccessTimeIcon from '@mui/icons-material/AccessTime'; * import { FilterList, FilterListItem } from 'react-admin'; * * const FilterSidebar = () => ( @@ -151,7 +160,6 @@ const FilterListItem = (props: { const { label, value } = props; const { filterValues, setFilters } = useListFilterContext(); const translate = useTranslate(); - const classes = useStyles(props); const isSelected = matches( pickBy(value, val => typeof val !== 'undefined') @@ -177,29 +185,30 @@ const FilterListItem = (props: { const toggleFilter = () => (isSelected ? removeFilter() : addFilter()); return ( - <ListItem - button + <StyledListItem onClick={toggleFilter} selected={isSelected} className={classes.listItem} > - <ListItemText - primary={ - isValidElement(label) - ? label - : translate(label, { _: label }) - } - className={classes.listItemText} - data-selected={isSelected ? 'true' : 'false'} - /> - {isSelected && ( - <ListItemSecondaryAction> - <IconButton size="small" onClick={toggleFilter}> - <CancelIcon /> - </IconButton> - </ListItemSecondaryAction> - )} - </ListItem> + <ListItemButton> + <ListItemText + primary={ + isValidElement(label) + ? label + : translate(label, { _: label }) + } + className={classes.listItemText} + data-selected={isSelected ? 'true' : 'false'} + /> + {isSelected && ( + <ListItemSecondaryAction> + <IconButton size="small" onClick={toggleFilter}> + <CancelIcon /> + </IconButton> + </ListItemSecondaryAction> + )} + </ListItemButton> + </StyledListItem> ); }; diff --git a/packages/ra-ui-materialui/src/list/filter/FilterLiveSearch.tsx b/packages/ra-ui-materialui/src/list/filter/FilterLiveSearch.tsx index fbff7c3aa56..92c7bf865f5 100644 --- a/packages/ra-ui-materialui/src/list/filter/FilterLiveSearch.tsx +++ b/packages/ra-ui-materialui/src/list/filter/FilterLiveSearch.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { ChangeEvent, memo, useMemo } from 'react'; -import { InputAdornment } from '@material-ui/core'; -import SearchIcon from '@material-ui/icons/Search'; +import { InputAdornment } from '@mui/material'; +import SearchIcon from '@mui/icons-material/Search'; import { Form } from 'react-final-form'; import { useTranslate, useListFilterContext } from 'ra-core'; diff --git a/packages/ra-ui-materialui/src/list/pagination/Pagination.spec.tsx b/packages/ra-ui-materialui/src/list/pagination/Pagination.spec.tsx index ee8d2442345..c12d152aaf3 100644 --- a/packages/ra-ui-materialui/src/list/pagination/Pagination.spec.tsx +++ b/packages/ra-ui-materialui/src/list/pagination/Pagination.spec.tsx @@ -1,8 +1,7 @@ import * as React from 'react'; import expect from 'expect'; import { render } from '@testing-library/react'; -import { ThemeProvider } from '@material-ui/styles'; -import { createTheme } from '@material-ui/core/styles'; +import { createTheme, ThemeProvider } from '@mui/material/styles'; import { ListPaginationContext } from 'ra-core'; import Pagination from './Pagination'; diff --git a/packages/ra-ui-materialui/src/list/pagination/Pagination.tsx b/packages/ra-ui-materialui/src/list/pagination/Pagination.tsx index f79520c6f53..e35691ad2ec 100644 --- a/packages/ra-ui-materialui/src/list/pagination/Pagination.tsx +++ b/packages/ra-ui-materialui/src/list/pagination/Pagination.tsx @@ -7,7 +7,7 @@ import { Toolbar, useMediaQuery, Theme, -} from '@material-ui/core'; +} from '@mui/material'; import { useTranslate, useListPaginationContext, @@ -32,7 +32,7 @@ const Pagination = (props: PaginationProps) => { } = useListPaginationContext(props); const translate = useTranslate(); const isSmall = useMediaQuery((theme: Theme) => - theme.breakpoints.down('sm') + theme.breakpoints.down('md') ); const totalPages = useMemo(() => { diff --git a/packages/ra-ui-materialui/src/list/pagination/PaginationActions.tsx b/packages/ra-ui-materialui/src/list/pagination/PaginationActions.tsx index 469ff1824af..a90f6e45fda 100644 --- a/packages/ra-ui-materialui/src/list/pagination/PaginationActions.tsx +++ b/packages/ra-ui-materialui/src/list/pagination/PaginationActions.tsx @@ -1,29 +1,37 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import PropTypes from 'prop-types'; -import Button from '@material-ui/core/Button'; -import { makeStyles, useTheme } from '@material-ui/core/styles'; -import ChevronLeft from '@material-ui/icons/ChevronLeft'; -import ChevronRight from '@material-ui/icons/ChevronRight'; +import Button from '@mui/material/Button'; +import { useTheme } from '@mui/material/styles'; +import ChevronLeft from '@mui/icons-material/ChevronLeft'; +import ChevronRight from '@mui/icons-material/ChevronRight'; import { useTranslate } from 'ra-core'; import classnames from 'classnames'; -const useStyles = makeStyles( - theme => ({ - actions: { - flexShrink: 0, - color: theme.palette.text.secondary, - marginLeft: 20, - }, - button: {}, - currentPageButton: {}, - hellip: { padding: '1.2em' }, - }), - { name: 'RaPaginationActions' } -); +const PREFIX = 'RaPaginationActions'; + +const classes = { + actions: `${PREFIX}-actions`, + button: `${PREFIX}-button`, + currentPageButton: `${PREFIX}-currentPageButton`, + hellip: `${PREFIX}-hellip`, +}; + +const Root = styled('div')(({ theme }) => ({ + [`&.${classes.actions}`]: { + flexShrink: 0, + color: theme.palette.text.secondary, + marginLeft: 20, + }, + + [`& .${classes.button}`]: {}, + [`& .${classes.currentPageButton}`]: {}, + [`& .${classes.hellip}`]: { padding: '1.2em' }, +})); const PaginationActions = props => { const { page, rowsPerPage, count, onPageChange, color, size } = props; - const classes = useStyles(props); + const translate = useTranslate(); const theme = useTheme(); /** @@ -105,7 +113,8 @@ const PaginationActions = props => { className={classnames('page-number', classes.button, { [classes.currentPageButton]: pageNum === page + 1, })} - color={pageNum === page + 1 ? 'default' : color} + color={color} + variant={pageNum === page + 1 ? 'outlined' : 'text'} key={pageNum} data-page={pageNum - 1} onClick={gotoPage} @@ -119,11 +128,11 @@ const PaginationActions = props => { const nbPages = getNbPages(); if (nbPages === 1) { - return <div className={classes.actions} />; + return <Root className={classes.actions} />; } return ( - <div className={classes.actions}> + <Root className={classes.actions}> {page > 0 && ( <Button color={color} @@ -157,7 +166,7 @@ const PaginationActions = props => { )} </Button> )} - </div> + </Root> ); }; @@ -170,7 +179,6 @@ const PaginationActions = props => { PaginationActions.propTypes = { backIconButtonProps: PropTypes.object, count: PropTypes.number.isRequired, - classes: PropTypes.object, nextIconButtonProps: PropTypes.object, onPageChange: PropTypes.func.isRequired, page: PropTypes.number.isRequired, diff --git a/packages/ra-ui-materialui/src/list/pagination/PaginationLimit.tsx b/packages/ra-ui-materialui/src/list/pagination/PaginationLimit.tsx index 6284e7f7ca4..5014605381c 100644 --- a/packages/ra-ui-materialui/src/list/pagination/PaginationLimit.tsx +++ b/packages/ra-ui-materialui/src/list/pagination/PaginationLimit.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { memo } from 'react'; -import CardContent from '@material-ui/core/CardContent'; -import Typography from '@material-ui/core/Typography'; +import CardContent from '@mui/material/CardContent'; +import Typography from '@mui/material/Typography'; import { useTranslate } from 'ra-core'; const PaginationLimit = () => { diff --git a/packages/react-admin/package.json b/packages/react-admin/package.json index 1287abf736e..89082667282 100644 --- a/packages/react-admin/package.json +++ b/packages/react-admin/package.json @@ -34,9 +34,10 @@ "react-dom": "^16.9.0 || ^17.0.0" }, "dependencies": { - "@material-ui/core": "^4.12.1", - "@material-ui/icons": "^4.11.2", - "@material-ui/styles": "^4.11.2", + "@emotion/react": "^11.4.1", + "@emotion/styled": "^11.3.0", + "@mui/material": "^5.0.2", + "@mui/icons-material": "^5.0.1", "connected-react-router": "^6.5.2", "final-form": "^4.20.2", "final-form-arrays": "^3.0.2", diff --git a/test-setup.js b/test-setup.js index f98772e9692..235c2f41df5 100644 --- a/test-setup.js +++ b/test-setup.js @@ -6,44 +6,6 @@ require('raf/polyfill'); */ require('mutationobserver-shim'); -/** - * Mock PopperJS - * - * When using mount(), material-ui calls Popper.js, which is not compatible with JSDom - * And causes UnhandledPromiseRejectionWarning: TypeError: document.createRange is not a function - * - * @see https://github.com/FezVrasta/popper.js/issues/478 - */ -jest.mock('popper.js', () => { - class Popper { - constructor() { - return { - destroy: () => {}, - scheduleUpdate: () => {}, - update: () => {}, - }; - } - } - Popper.placements = [ - 'auto', - 'auto-end', - 'auto-start', - 'bottom', - 'bottom-end', - 'bottom-start', - 'left', - 'left-end', - 'left-start', - 'right', - 'right-end', - 'right-start', - 'top', - 'top-end', - 'top-start', - ]; - return Popper; -}); - // Ignore warnings about act() // See https://github.com/testing-library/react-testing-library/issues/281, // https://github.com/facebook/react/issues/14769 diff --git a/yarn.lock b/yarn.lock index 19c10319dc1..fea177e8ff8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1248,7 +1248,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-syntax-jsx@^7.14.5": +"@babel/plugin-syntax-jsx@^7.12.13", "@babel/plugin-syntax-jsx@^7.14.5": version "7.14.5" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.14.5.tgz#000e2e25d8673cce49300517a3eda44c263e4201" integrity sha512-ohuFIsOMXJnbOMRfX7/w7LocdR6R7whhuRD4ax8IipLcLPlZGJKkBxgHp++U4N/vKyU16/YDQr2f5seajD3jIw== @@ -2213,7 +2213,7 @@ dependencies: regenerator-runtime "^0.13.4" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.0", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2": version "7.8.4" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.8.4.tgz#d79f5a2040f7caa24d53e563aad49cbc05581308" integrity sha512-neAp3zt80trRVBI1x0azq6c57aNBqYZH8KhMm3TaB7wEI5Q4A2SHfBHE8w9gOhI/lrqxtEbXZgQIrHP+wvSGwQ== @@ -2248,7 +2248,7 @@ dependencies: regenerator-runtime "^0.13.4" -"@babel/runtime@^7.8.3": +"@babel/runtime@^7.13.10", "@babel/runtime@^7.15.4": version "7.15.4" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.15.4.tgz#fd17d16bfdf878e6dd02d19753a39fa8a8d9c84a" integrity sha512-99catp6bHCaxr4sJ/DbTGgHS4+Rs2RVd2g7iOap6SLGPDknRK9ztKNsE/Fg6QhSeh1FGE5f6gHGQmvvn3I3xhw== @@ -2518,11 +2518,107 @@ debug "^3.1.0" lodash.once "^4.1.1" +"@emotion/babel-plugin@^11.3.0": + version "11.3.0" + resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.3.0.tgz#3a16850ba04d8d9651f07f3fb674b3436a4fb9d7" + integrity sha512-UZKwBV2rADuhRp+ZOGgNWg2eYgbzKzQXfQPtJbu/PLy8onurxlNCLvxMQEvlr1/GudguPI5IU9qIY1+2z1M5bA== + dependencies: + "@babel/helper-module-imports" "^7.12.13" + "@babel/plugin-syntax-jsx" "^7.12.13" + "@babel/runtime" "^7.13.10" + "@emotion/hash" "^0.8.0" + "@emotion/memoize" "^0.7.5" + "@emotion/serialize" "^1.0.2" + babel-plugin-macros "^2.6.1" + convert-source-map "^1.5.0" + escape-string-regexp "^4.0.0" + find-root "^1.1.0" + source-map "^0.5.7" + stylis "^4.0.3" + +"@emotion/cache@^11.4.0": + version "11.4.0" + resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.4.0.tgz#293fc9d9a7a38b9aad8e9337e5014366c3b09ac0" + integrity sha512-Zx70bjE7LErRO9OaZrhf22Qye1y4F7iDl+ITjet0J+i+B88PrAOBkKvaAWhxsZf72tDLajwCgfCjJ2dvH77C3g== + dependencies: + "@emotion/memoize" "^0.7.4" + "@emotion/sheet" "^1.0.0" + "@emotion/utils" "^1.0.0" + "@emotion/weak-memoize" "^0.2.5" + stylis "^4.0.3" + "@emotion/hash@^0.8.0": version "0.8.0" resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413" integrity sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow== +"@emotion/is-prop-valid@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-1.1.0.tgz#29ef6be1e946fb4739f9707def860f316f668cde" + integrity sha512-9RkilvXAufQHsSsjQ3PIzSns+pxuX4EW8EbGeSPjZMHuMx6z/MOzb9LpqNieQX4F3mre3NWS2+X3JNRHTQztUQ== + dependencies: + "@emotion/memoize" "^0.7.4" + +"@emotion/memoize@^0.7.4", "@emotion/memoize@^0.7.5": + version "0.7.5" + resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.5.tgz#2c40f81449a4e554e9fc6396910ed4843ec2be50" + integrity sha512-igX9a37DR2ZPGYtV6suZ6whr8pTFtyHL3K/oLUotxpSVO2ASaprmAe2Dkq7tBo7CRY7MMDrAa9nuQP9/YG8FxQ== + +"@emotion/react@^11.4.1": + version "11.4.1" + resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.4.1.tgz#a1b0b767b5bad57515ffb0cad9349614d27f4d57" + integrity sha512-pRegcsuGYj4FCdZN6j5vqCALkNytdrKw3TZMekTzNXixRg4wkLsU5QEaBG5LC6l01Vppxlp7FE3aTHpIG5phLg== + dependencies: + "@babel/runtime" "^7.13.10" + "@emotion/cache" "^11.4.0" + "@emotion/serialize" "^1.0.2" + "@emotion/sheet" "^1.0.2" + "@emotion/utils" "^1.0.0" + "@emotion/weak-memoize" "^0.2.5" + hoist-non-react-statics "^3.3.1" + +"@emotion/serialize@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-1.0.2.tgz#77cb21a0571c9f68eb66087754a65fa97bfcd965" + integrity sha512-95MgNJ9+/ajxU7QIAruiOAdYNjxZX7G2mhgrtDWswA21VviYIRP1R5QilZ/bDY42xiKsaktP4egJb3QdYQZi1A== + dependencies: + "@emotion/hash" "^0.8.0" + "@emotion/memoize" "^0.7.4" + "@emotion/unitless" "^0.7.5" + "@emotion/utils" "^1.0.0" + csstype "^3.0.2" + +"@emotion/sheet@^1.0.0", "@emotion/sheet@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.0.2.tgz#1d9ffde531714ba28e62dac6a996a8b1089719d0" + integrity sha512-QQPB1B70JEVUHuNtzjHftMGv6eC3Y9wqavyarj4x4lg47RACkeSfNo5pxIOKizwS9AEFLohsqoaxGQj4p0vSIw== + +"@emotion/styled@^11.3.0": + version "11.3.0" + resolved "https://registry.yarnpkg.com/@emotion/styled/-/styled-11.3.0.tgz#d63ee00537dfb6ff612e31b0e915c5cf9925a207" + integrity sha512-fUoLcN3BfMiLlRhJ8CuPUMEyKkLEoM+n+UyAbnqGEsCd5IzKQ7VQFLtzpJOaCD2/VR2+1hXQTnSZXVJeiTNltA== + dependencies: + "@babel/runtime" "^7.13.10" + "@emotion/babel-plugin" "^11.3.0" + "@emotion/is-prop-valid" "^1.1.0" + "@emotion/serialize" "^1.0.2" + "@emotion/utils" "^1.0.0" + +"@emotion/unitless@^0.7.5": + version "0.7.5" + resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.7.5.tgz#77211291c1900a700b8a78cfafda3160d76949ed" + integrity sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg== + +"@emotion/utils@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-1.0.0.tgz#abe06a83160b10570816c913990245813a2fd6af" + integrity sha512-mQC2b3XLDs6QCW+pDQDiyO/EdGZYOygE8s5N5rrzjSI4M3IejPE/JPndCBwRT9z982aqQNi6beWs1UeayrQxxA== + +"@emotion/weak-memoize@^0.2.5": + version "0.2.5" + resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz#8eed982e2ee6f7f4e44c253e12962980791efd46" + integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA== + "@eslint/eslintrc@^0.2.1": version "0.2.1" resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.2.1.tgz#f72069c330461a06684d119384435e12a5d76e3c" @@ -2793,84 +2889,99 @@ "@types/yargs" "^15.0.0" chalk "^4.0.0" -"@material-ui/core@^4.12.1": - version "4.12.3" - resolved "https://registry.yarnpkg.com/@material-ui/core/-/core-4.12.3.tgz#80d665caf0f1f034e52355c5450c0e38b099d3ca" - integrity sha512-sdpgI/PL56QVsEJldwEe4FFaFTLUqN+rd7sSZiRCdx2E/C7z5yK0y/khAWVBH24tXwto7I1hCzNWfJGZIYJKnw== - dependencies: - "@babel/runtime" "^7.4.4" - "@material-ui/styles" "^4.11.4" - "@material-ui/system" "^4.12.1" - "@material-ui/types" "5.1.0" - "@material-ui/utils" "^4.11.2" - "@types/react-transition-group" "^4.2.0" - clsx "^1.0.4" - hoist-non-react-statics "^3.3.2" - popper.js "1.16.1-lts" - prop-types "^15.7.2" - react-is "^16.8.0 || ^17.0.0" - react-transition-group "^4.4.0" +"@mrmlnc/readdir-enhanced@^2.2.1": + version "2.2.1" + resolved "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde" + integrity sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g== + dependencies: + call-me-maybe "^1.0.1" + glob-to-regexp "^0.3.0" -"@material-ui/icons@^4.11.2": - version "4.11.2" - resolved "https://registry.yarnpkg.com/@material-ui/icons/-/icons-4.11.2.tgz#b3a7353266519cd743b6461ae9fdfcb1b25eb4c5" - integrity sha512-fQNsKX2TxBmqIGJCSi3tGTO/gZ+eJgWmMJkgDiOfyNaunNaxcklJQFaFogYcFl0qFuaEz1qaXYXboa/bUXVSOQ== +"@mui/core@5.0.0-alpha.49": + version "5.0.0-alpha.49" + resolved "https://registry.yarnpkg.com/@mui/core/-/core-5.0.0-alpha.49.tgz#e74d6ec7f83f85b55d48aa05ea6b7cefff88ce1b" + integrity sha512-bZ7UgH84AuKf/IT0U+knHEelDxLV0lNVFg7rKkkDfXEwUpTtAZEtZPFJjNngapSB/4MuFjaFsttex+0DGC5Z1Q== dependencies: - "@babel/runtime" "^7.4.4" + "@babel/runtime" "^7.15.4" + "@emotion/is-prop-valid" "^1.1.0" + "@mui/utils" "^5.0.1" + clsx "^1.1.1" + prop-types "^15.7.2" + react-is "^17.0.2" -"@material-ui/styles@^4.11.2", "@material-ui/styles@^4.11.4": - version "4.11.4" - resolved "https://registry.yarnpkg.com/@material-ui/styles/-/styles-4.11.4.tgz#eb9dfccfcc2d208243d986457dff025497afa00d" - integrity sha512-KNTIZcnj/zprG5LW0Sao7zw+yG3O35pviHzejMdcSGCdWbiO8qzRgOYL8JAxAsWBKOKYwVZxXtHWaB5T2Kvxew== +"@mui/icons-material@^5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@mui/icons-material/-/icons-material-5.0.1.tgz#fb7ffeba0b3604aab4a9b91644d2fc1aabb3b4f1" + integrity sha512-AZehR/Uvi9VodsNPk9ae1lENKrf1evqx9suiP6VIqu7NxjZOlw/m/yA2gRAMmLEmIGr7EChfi/wcXuq6BpM9vw== dependencies: - "@babel/runtime" "^7.4.4" - "@emotion/hash" "^0.8.0" - "@material-ui/types" "5.1.0" - "@material-ui/utils" "^4.11.2" - clsx "^1.0.4" - csstype "^2.5.2" + "@babel/runtime" "^7.15.4" + +"@mui/material@^5.0.2": + version "5.0.2" + resolved "https://registry.yarnpkg.com/@mui/material/-/material-5.0.2.tgz#380cf0ef42c538a68158b4da19c317178b22d10f" + integrity sha512-LD2xHSjTLmbN0UoCuKTu09L/7JjpEzg+Cophf+dVJOTNoK7VI0Eqv3bmpF/9pDIk5dVKmeU9Eh4t2lW1ZifM6A== + dependencies: + "@babel/runtime" "^7.15.4" + "@mui/core" "5.0.0-alpha.49" + "@mui/system" "^5.0.2" + "@mui/types" "^7.0.0" + "@mui/utils" "^5.0.1" + "@popperjs/core" "^2.4.4" + "@types/react-transition-group" "^4.4.3" + clsx "^1.1.1" + csstype "^3.0.9" hoist-non-react-statics "^3.3.2" - jss "^10.5.1" - jss-plugin-camel-case "^10.5.1" - jss-plugin-default-unit "^10.5.1" - jss-plugin-global "^10.5.1" - jss-plugin-nested "^10.5.1" - jss-plugin-props-sort "^10.5.1" - jss-plugin-rule-value-function "^10.5.1" - jss-plugin-vendor-prefixer "^10.5.1" prop-types "^15.7.2" + react-is "^17.0.2" + react-transition-group "^4.4.2" -"@material-ui/system@^4.12.1": - version "4.12.1" - resolved "https://registry.yarnpkg.com/@material-ui/system/-/system-4.12.1.tgz#2dd96c243f8c0a331b2bb6d46efd7771a399707c" - integrity sha512-lUdzs4q9kEXZGhbN7BptyiS1rLNHe6kG9o8Y307HCvF4sQxbCgpL2qi+gUk+yI8a2DNk48gISEQxoxpgph0xIw== +"@mui/private-theming@^5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@mui/private-theming/-/private-theming-5.0.1.tgz#50a0ea6ad5a8d1d78072859c4bdaaa6b6584d986" + integrity sha512-R8Cf2+32cG1OXFAqTighA5Mx9R5BQ57cN1ZVaNgfgdbI87Yig2fVMdFSPrw3txcjKlnwsvFJF8AdwQMqq1tJ3Q== dependencies: - "@babel/runtime" "^7.4.4" - "@material-ui/utils" "^4.11.2" - csstype "^2.5.2" + "@babel/runtime" "^7.15.4" + "@mui/utils" "^5.0.1" prop-types "^15.7.2" -"@material-ui/types@5.1.0": - version "5.1.0" - resolved "https://registry.yarnpkg.com/@material-ui/types/-/types-5.1.0.tgz#efa1c7a0b0eaa4c7c87ac0390445f0f88b0d88f2" - integrity sha512-7cqRjrY50b8QzRSYyhSpx4WRw2YuO0KKIGQEVk5J8uoz2BanawykgZGoWEqKm7pVIbzFDN0SpPcVV4IhOFkl8A== - -"@material-ui/utils@^4.11.2": - version "4.11.2" - resolved "https://registry.yarnpkg.com/@material-ui/utils/-/utils-4.11.2.tgz#f1aefa7e7dff2ebcb97d31de51aecab1bb57540a" - integrity sha512-Uul8w38u+PICe2Fg2pDKCaIG7kOyhowZ9vjiC1FsVwPABTW8vPPKfF6OvxRq3IiBaI1faOJmgdvMG7rMJARBhA== +"@mui/styled-engine@^5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@mui/styled-engine/-/styled-engine-5.0.1.tgz#401e3e0ff846ad1b1e7e097c8050b36d7b68343e" + integrity sha512-j40nCbaKr1HAZYqpX61XvZYsadYskjo3u6+pRFFaewSViAkkD1rjjbubpnh15nqVfYmijtHMZJ9/l1x1hamvfQ== dependencies: - "@babel/runtime" "^7.4.4" + "@babel/runtime" "^7.15.4" + "@emotion/cache" "^11.4.0" prop-types "^15.7.2" - react-is "^16.8.0 || ^17.0.0" -"@mrmlnc/readdir-enhanced@^2.2.1": - version "2.2.1" - resolved "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde" - integrity sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g== +"@mui/system@^5.0.2": + version "5.0.2" + resolved "https://registry.yarnpkg.com/@mui/system/-/system-5.0.2.tgz#9999ab61801810ea01c44588fd0dcc1f64dfcedc" + integrity sha512-K6wMbiSEYSMeYUw7zmZ2/50JFthqtuTz4OADyKc4ic2RP8ubAf/duH/nkJ4gtsKcewU4RIub0HQHl5F77WVp4Q== + dependencies: + "@babel/runtime" "^7.15.4" + "@mui/private-theming" "^5.0.1" + "@mui/styled-engine" "^5.0.1" + "@mui/types" "^7.0.0" + "@mui/utils" "^5.0.1" + clsx "^1.1.1" + csstype "^3.0.9" + prop-types "^15.7.2" + +"@mui/types@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@mui/types/-/types-7.0.0.tgz#a7398502bc9c508875aafcbe28aea599b2c3d203" + integrity sha512-M/tkF2pZ4uoPhZ8pnNhlVnOFtz6F3dnYKIsnj8MuXKT6d26IE2u0UjA8B0275ggN74dR9rlHG5xJt5jgDx/Ung== + +"@mui/utils@^5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-5.0.1.tgz#d4f0f41b82db6ac273920a1b5b6a4de7879271f5" + integrity sha512-GWO104N+o9KG5fKiTEYnAg7kONKEg3vLN+VROAU0f3it6lFGLCVPcQYex/1gJ4QAy96u6Ez8/Hmmhi1+3cX0tQ== dependencies: - call-me-maybe "^1.0.1" - glob-to-regexp "^0.3.0" + "@babel/runtime" "^7.15.4" + "@types/prop-types" "^15.7.4" + "@types/react-is" "^16.7.1 || ^17.0.0" + prop-types "^15.7.2" + react-is "^17.0.2" "@nivo/annotations@0.67.0": version "0.67.0" @@ -3019,6 +3130,11 @@ schema-utils "^2.6.5" source-map "^0.7.3" +"@popperjs/core@^2.4.4": + version "2.10.2" + resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.10.2.tgz#0798c03351f0dea1a5a4cabddf26a55a7cbee590" + integrity sha512-IXf3XA7+XyN7CP9gGh/XB0UxVMlvARGEgGXLubFICsUMGz6Q+DU+i4gGlpOxTjKvXjkJDJC8YdqdKkDj9qZHEQ== + "@react-spring/animated@9.0.0-rc.3": version "9.0.0-rc.3" resolved "https://registry.yarnpkg.com/@react-spring/animated/-/animated-9.0.0-rc.3.tgz#e792cb76aacecfc78db2be6020ac11ce96503eb5" @@ -3699,6 +3815,11 @@ resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7" integrity sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw== +"@types/prop-types@^15.7.4": + version "15.7.4" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.4.tgz#fcf7205c25dff795ee79af1e30da2c9790808f11" + integrity sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ== + "@types/q@^1.5.1": version "1.5.2" resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.2.tgz#690a1475b84f2a884fd07cd797c00f5f31356ea8" @@ -3730,6 +3851,13 @@ dependencies: "@types/react" "*" +"@types/react-is@^16.7.1 || ^17.0.0": + version "17.0.2" + resolved "https://registry.yarnpkg.com/@types/react-is/-/react-is-17.0.2.tgz#abc4d910bff5b0bc6b3e1bec57575f6b63fd4e05" + integrity sha512-2+L0ilcAEG8udkDnvx8B0upwXFBbNnVwOsSCTxW3SDOkmar9NyEeLG0ZLa3uOEw9zyYf/fQapcnfXAVmDKlyHw== + dependencies: + "@types/react" "*" + "@types/react-redux@^7.1.1": version "7.1.7" resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.7.tgz#12a0c529aba660696947384a059c5c6e08185c7a" @@ -3767,10 +3895,10 @@ "@types/history" "*" "@types/react" "*" -"@types/react-transition-group@^4.2.0": - version "4.2.3" - resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.2.3.tgz#4924133f7268694058e415bf7aea2d4c21131470" - integrity sha512-Hk8jiuT7iLOHrcjKP/ZVSyCNXK73wJAUz60xm0mVhiRujrdiI++j4duLiL282VGxwAgxetHQFfqA29LgEeSkFA== +"@types/react-transition-group@^4.4.3": + version "4.4.3" + resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.3.tgz#b0994da0a7023d67dbb4a8910a62112bc00d5688" + integrity sha512-fUx5muOWSYP8Bw2BUQ9M9RK9+W1XBK/7FLJ8PTQpnpTEkn0ccyMffyEQvan4C3h53gHdx7KE5Qrxi/LnUGQtdg== dependencies: "@types/react" "*" @@ -5105,7 +5233,7 @@ babel-plugin-jest-hoist@^26.6.2: "@types/babel__core" "^7.0.0" "@types/babel__traverse" "^7.0.6" -babel-plugin-macros@2.8.0: +babel-plugin-macros@2.8.0, babel-plugin-macros@^2.6.1: version "2.8.0" resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-2.8.0.tgz#0f958a7cc6556b1e65344465d99111a1e5e10138" integrity sha512-SEP5kJpfGYqYKpBrj5XU3ahw5p5GOHJ0U5ssOSQ/WBVdwkD2Dzlce95exQTs3jOVWPPKLBN2rlEWkCK7dSmLvg== @@ -6677,10 +6805,10 @@ cloneable-readable@^1.0.0: process-nextick-args "^2.0.0" readable-stream "^2.3.5" -clsx@^1.0.4: - version "1.1.0" - resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.0.tgz#62937c6adfea771247c34b54d320fb99624f5702" - integrity sha512-3avwM37fSK5oP6M5rQ9CNe99lwxhXDOeSWVPAOYF6OazUTgZCMb0yWlJpmdD74REy1gkEaFiub2ULv4fq9GUhA== +clsx@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188" + integrity sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA== cmd-shim@^2.0.2: version "2.1.0" @@ -7144,6 +7272,13 @@ convert-source-map@^0.3.3: resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-0.3.5.tgz#f1d802950af7dd2631a1febe0596550c86ab3190" integrity sha1-8dgClQr33SYxof6+BZZVDIarMZA= +convert-source-map@^1.5.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.8.0.tgz#f3373c32d21b4d780dd8004514684fb791ca4369" + integrity sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA== + dependencies: + safe-buffer "~5.1.1" + cookie-signature@1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" @@ -7466,14 +7601,6 @@ css-unit-converter@^1.1.1: resolved "https://registry.yarnpkg.com/css-unit-converter/-/css-unit-converter-1.1.1.tgz#d9b9281adcfd8ced935bdbaba83786897f64e996" integrity sha1-2bkoGtz9jO2TW9urqDeGiX9k6ZY= -css-vendor@^2.0.8: - version "2.0.8" - resolved "https://registry.yarnpkg.com/css-vendor/-/css-vendor-2.0.8.tgz#e47f91d3bd3117d49180a3c935e62e3d9f7f449d" - integrity sha512-x9Aq0XTInxrkuFeHKbYC7zWY8ai7qJ04Kxd9MnvbC1uO5DagxoHQjm4JvG+vCdXOoFtCjbL2XSZfxmoYa9uQVQ== - dependencies: - "@babel/runtime" "^7.8.3" - is-in-browser "^1.0.2" - css-what@2.1: version "2.1.3" resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.3.tgz#a6d7604573365fe74686c3f311c56513d88285f2" @@ -7615,7 +7742,7 @@ cssstyle@^2.2.0: dependencies: cssom "~0.3.6" -csstype@^2.5.2, csstype@^2.6.7: +csstype@^2.6.7: version "2.6.8" resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.8.tgz#0fb6fc2417ffd2816a418c9336da74d7f07db431" integrity sha512-msVS9qTuMT5zwAGCVm4mxfrZ18BNc6Csd0oJAtiFMZ1FAx1CCvy2+5MDmYoix63LM/6NDbNtodCiGYGmFgO0dA== @@ -7625,6 +7752,11 @@ csstype@^3.0.2: resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.4.tgz#b156d7be03b84ff425c9a0a4b1e5f4da9c5ca888" integrity sha512-xc8DUsCLmjvCfoD7LTGE0ou2MIWLx0K9RCZwSHMOdynqRsP4MtUcLeqh1HcQ2dInwDTqn+3CE0/FZh1et+p4jA== +csstype@^3.0.9: + version "3.0.9" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.9.tgz#6410af31b26bd0520933d02cbc64fce9ce3fbf0b" + integrity sha512-rpw6JPxK6Rfg1zLOYCSwle2GFOOsnjmDYDaBwEcwoOg4qlsIVCN789VkBZDJAGi4T07gI4YSutR43t9Zz4Lzuw== + currently-unhandled@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" @@ -8752,6 +8884,11 @@ escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.4, escape-string-regexp@^ resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + escodegen@^1.14.1: version "1.14.3" resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.14.3.tgz#4e7b81fba61581dc97582ed78cab7f0e8d63f503" @@ -9925,6 +10062,11 @@ find-cache-dir@^3.3.1: make-dir "^3.0.2" pkg-dir "^4.1.0" +find-root@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4" + integrity sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng== + find-up@4.1.0, find-up@^4.0.0, find-up@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" @@ -10879,7 +11021,7 @@ hoist-non-react-statics@^2.3.1: resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47" integrity sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw== -hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2: +hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== @@ -11187,11 +11329,6 @@ husky@^2.3.0: run-node "^1.0.0" slash "^3.0.0" -hyphenate-style-name@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.3.tgz#097bb7fa0b8f1a9cf0bd5c734cf95899981a9b48" - integrity sha512-EcuixamT82oplpoJ2XU4pDtKGWQ7b00CD9f1ug9IaQ3p1bkHMiKCZ9ut9QDI6qsa6cpUuB+A/I+zLtdNK4n2DQ== - iconv-lite@0.4.23: version "0.4.23" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.23.tgz#297871f63be507adcfbfca715d0cd0eed84e9a63" @@ -11788,11 +11925,6 @@ is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1: dependencies: is-extglob "^2.1.1" -is-in-browser@^1.0.2, is-in-browser@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/is-in-browser/-/is-in-browser-1.1.3.tgz#56ff4db683a078c6082eb95dad7dc62e1d04f835" - integrity sha1-Vv9NtoOgeMYILrldrX3GLh0E+DU= - is-installed-globally@^0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.3.2.tgz#fd3efa79ee670d1187233182d5b0a1dd00313141" @@ -12834,76 +12966,6 @@ jsprim@^1.2.2: json-schema "0.2.3" verror "1.10.0" -jss-plugin-camel-case@^10.5.1: - version "10.7.1" - resolved "https://registry.yarnpkg.com/jss-plugin-camel-case/-/jss-plugin-camel-case-10.7.1.tgz#e7f7097cf97e9deec599cef3275e213452318b93" - integrity sha512-+ioIyWvmAfgDCWXsQcW1NMnLBvRinOVFkSYJUgewQ6TynOcSj5F1bSU23B7z0p1iqK0PPHIU62xY1iNJD33WGA== - dependencies: - "@babel/runtime" "^7.3.1" - hyphenate-style-name "^1.0.3" - jss "10.7.1" - -jss-plugin-default-unit@^10.5.1: - version "10.7.1" - resolved "https://registry.yarnpkg.com/jss-plugin-default-unit/-/jss-plugin-default-unit-10.7.1.tgz#826270e2ee38d7024a281ac67c30d6944f124786" - integrity sha512-tW+dfYVNARBQb/ONzBwd8uyImigyzMiAEDai+AbH5rcHg5h3TtqhAkxx06iuZiT/dZUiFdSKlbe3q9jZGAPIwA== - dependencies: - "@babel/runtime" "^7.3.1" - jss "10.7.1" - -jss-plugin-global@^10.5.1: - version "10.7.1" - resolved "https://registry.yarnpkg.com/jss-plugin-global/-/jss-plugin-global-10.7.1.tgz#9725c46d662aac2e596a0a8741944c060e2b90a1" - integrity sha512-FbxCnu44IkK/bw8X3CwZKmcAnJqjAb9LujlAc/aP0bMSdVa3/MugKQRyeQSu00uGL44feJJDoeXXiHOakBr/Zw== - dependencies: - "@babel/runtime" "^7.3.1" - jss "10.7.1" - -jss-plugin-nested@^10.5.1: - version "10.7.1" - resolved "https://registry.yarnpkg.com/jss-plugin-nested/-/jss-plugin-nested-10.7.1.tgz#35563a7a710a45307fd6b9742ffada1d72a62eb7" - integrity sha512-RNbICk7FlYKaJyv9tkMl7s6FFfeLA3ubNIFKvPqaWtADK0KUaPsPXVYBkAu4x1ItgsWx67xvReMrkcKA0jSXfA== - dependencies: - "@babel/runtime" "^7.3.1" - jss "10.7.1" - tiny-warning "^1.0.2" - -jss-plugin-props-sort@^10.5.1: - version "10.7.1" - resolved "https://registry.yarnpkg.com/jss-plugin-props-sort/-/jss-plugin-props-sort-10.7.1.tgz#1d12b26048541ed3a2ed1b69f7fc231605728362" - integrity sha512-eyd5FhA+J0QrpqXxO7YNF/HMSXXl4pB0EmUdY4vSJI4QG22F59vQ6AHtP6fSwhmBdQ98Qd9gjfO+RMxcE39P1A== - dependencies: - "@babel/runtime" "^7.3.1" - jss "10.7.1" - -jss-plugin-rule-value-function@^10.5.1: - version "10.7.1" - resolved "https://registry.yarnpkg.com/jss-plugin-rule-value-function/-/jss-plugin-rule-value-function-10.7.1.tgz#123eb796eb9982f8efa7a7e362daddd90c0c69fe" - integrity sha512-fGAAImlbaHD3fXAHI3ooX6aRESOl5iBt3LjpVjxs9II5u9tzam7pqFUmgTcrip9VpRqYHn8J3gA7kCtm8xKwHg== - dependencies: - "@babel/runtime" "^7.3.1" - jss "10.7.1" - tiny-warning "^1.0.2" - -jss-plugin-vendor-prefixer@^10.5.1: - version "10.7.1" - resolved "https://registry.yarnpkg.com/jss-plugin-vendor-prefixer/-/jss-plugin-vendor-prefixer-10.7.1.tgz#217821be2d6dacee31d2d464886760ba7742e19a" - integrity sha512-1UHFmBn7hZNsHXTkLLOL8abRl8vi+D1EVzWD4WmLFj55vawHZfnH1oEz6TUf5Y61XHv0smdHabdXds6BgOXe3A== - dependencies: - "@babel/runtime" "^7.3.1" - css-vendor "^2.0.8" - jss "10.7.1" - -jss@10.7.1, jss@^10.5.1: - version "10.7.1" - resolved "https://registry.yarnpkg.com/jss/-/jss-10.7.1.tgz#16d846e1a22fb42e857b99f9c6a0c5a27341c804" - integrity sha512-5QN8JSVZR6cxpZNeGfzIjqPEP+ZJwJJfZbXmeABNdxiExyO+eJJDy6WDtqTf8SDKnbL5kZllEpAP71E/Lt7PXg== - dependencies: - "@babel/runtime" "^7.3.1" - csstype "^3.0.2" - is-in-browser "^1.1.3" - tiny-warning "^1.0.2" - jsx-ast-utils@^2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-2.4.1.tgz#1114a4c1209481db06c690c2b4f488cc665f657e" @@ -15311,11 +15373,6 @@ pnp-webpack-plugin@1.6.4: dependencies: ts-pnp "^1.1.6" -popper.js@1.16.1-lts: - version "1.16.1-lts" - resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.16.1-lts.tgz#cf6847b807da3799d80ee3d6d2f90df8a3f50b05" - integrity sha512-Kjw8nKRl1m+VrSFCoVGPph93W/qrSO7ZkqPpTf7F4bk/sqcfWK019dWBUpE/fBOsOQY1dks/Bmcbfn1heM/IsA== - portfinder@^1.0.26, portfinder@^1.0.9: version "1.0.28" resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.28.tgz#67c4622852bd5374dd1dd900f779f53462fac778" @@ -16557,7 +16614,7 @@ react-final-form@^6.5.2: dependencies: "@babel/runtime" "^7.12.1" -"react-is@^16.12.0 || ^17.0.0", "react-is@^16.8.0 || ^17.0.0", react-is@^17.0.1: +"react-is@^16.12.0 || ^17.0.0", react-is@^17.0.1: version "17.0.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.1.tgz#5b3531bd76a645a4c9fb6e693ed36419e3301339" integrity sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA== @@ -16572,6 +16629,11 @@ react-is@^16.5.2, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1, react-is resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.12.0.tgz#2cc0fe0fba742d97fd527c42a13bec4eeb06241c" integrity sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q== +react-is@^17.0.2: + version "17.0.2" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" + integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== + react-layout-effect@^1.0.1: version "1.0.5" resolved "https://registry.yarnpkg.com/react-layout-effect/-/react-layout-effect-1.0.5.tgz#0dc4e24452aee5de66c93c166f0ec512dfb1be80" @@ -16857,7 +16919,7 @@ react-transition-group@^2.5.0: prop-types "^15.6.2" react-lifecycles-compat "^3.0.4" -react-transition-group@^4.4.0, react-transition-group@^4.4.1: +react-transition-group@^4.4.1: version "4.4.1" resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.1.tgz#63868f9325a38ea5ee9535d828327f85773345c9" integrity sha512-Djqr7OQ2aPUiYurhPalTrVy9ddmFCCzwhqQmtN+J3+3DzLO209Fdr70QrN8Z3DsglWql6iY1lDWAfpFiBtuKGw== @@ -16867,6 +16929,16 @@ react-transition-group@^4.4.0, react-transition-group@^4.4.1: loose-envify "^1.4.0" prop-types "^15.6.2" +react-transition-group@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.2.tgz#8b59a56f09ced7b55cbd53c36768b922890d5470" + integrity sha512-/RNYfRAMlZwDSr6z4zNKV6xu53/e2BuaBbGhbyYIXTrmgu/bGHzmqOs7mJSJBHy9Ud+ApHx3QjrkKSp1pxvlFg== + dependencies: + "@babel/runtime" "^7.5.5" + dom-helpers "^5.0.1" + loose-envify "^1.4.0" + prop-types "^15.6.2" + react@^17.0.0: version "17.0.1" resolved "https://registry.yarnpkg.com/react/-/react-17.0.1.tgz#6e0600416bd57574e3f86d92edba3d9008726127" @@ -18976,6 +19048,11 @@ stylehacks@^4.0.0: postcss "^7.0.0" postcss-selector-parser "^3.0.0" +stylis@^4.0.3: + version "4.0.10" + resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.0.10.tgz#446512d1097197ab3f02fb3c258358c3f7a14240" + integrity sha512-m3k+dk7QeJw660eIKRRn3xPF6uuvHs/FFzjX3HQ5ove0qYsiygoAhwn5a3IYKaZPo5LrYD0rfVmtv1gNY1uYwg== + supports-color@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7"