diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js index 2055932c8d4..439975e0152 100644 --- a/cypress/plugins/index.js +++ b/cypress/plugins/index.js @@ -6,14 +6,15 @@ module.exports = on => { webpackOptions: require('../webpack.config'), }; on('before:browser:launch', (browser = {}, launchOptions) => { + // Fix for Cypress 4: + // https://docs.cypress.io/api/plugins/browser-launch-api.html#Usage if (browser.name === 'chrome') { - return [ - ...launchOptions.args.filter( - arg => arg !== '--disable-blink-features=RootLayerScrolling' - ), - '--disable-gpu', - '--proxy-bypass-list=<-loopback>', - ]; + launchOptions.args.push( + '--disable-blink-features=RootLayerScrolling' + ); + launchOptions.args.push('--disable-gpu'); + launchOptions.args.push('--proxy-bypass-list=<-loopback>'); + return launchOptions; } }); on('file:preprocessor', wp(options)); diff --git a/cypress/support/CreatePage.js b/cypress/support/CreatePage.js index 26006e68550..af310e223f8 100644 --- a/cypress/support/CreatePage.js +++ b/cypress/support/CreatePage.js @@ -96,7 +96,7 @@ export default url => ({ }, gotoTab(index) { - cy.get(this.elements.tab(index)).click(); + cy.get(this.elements.tab(index)).click({ force: true }); }, logout() { diff --git a/cypress/support/EditPage.js b/cypress/support/EditPage.js index c6084bfed06..21700c8bdd1 100644 --- a/cypress/support/EditPage.js +++ b/cypress/support/EditPage.js @@ -45,7 +45,7 @@ export default url => ({ }, gotoTab(index) { - cy.get(this.elements.tab(index)).click(); + cy.get(this.elements.tab(index)).click({ force: true }); }, submit() { diff --git a/docs/CreateEdit.md b/docs/CreateEdit.md index e6245ac06d7..a759e5b52bb 100644 --- a/docs/CreateEdit.md +++ b/docs/CreateEdit.md @@ -719,6 +719,7 @@ Here are all the props accepted by the `` component: * `save`: The function invoked when the form is submitted. This is passed automatically by `react-admin` when the form component is used inside `Create` and `Edit` components. * `saving`: A boolean indicating whether a save operation is ongoing. This is passed automatically by `react-admin` when the form component is used inside `Create` and `Edit` components. * [`warnWhenUnsavedChanges`](#warning-about-unsaved-changes) +* `scrollable`: A boolean, `true` by default. `` use Material UI scrollable buttons when there are too many tabs to display. If you prefer the screen to expand with the tabs, set it to `false`. {% raw %} ```jsx @@ -1802,4 +1803,4 @@ const SaveWithNoteButton = props => { }; ``` -The `onSave` value should be a function expecting 2 arguments: the form values to save, and the redirection to perform. \ No newline at end of file +The `onSave` value should be a function expecting 2 arguments: the form values to save, and the redirection to perform. diff --git a/docs/Show.md b/docs/Show.md index 5fb34387e23..bc29e8e091b 100644 --- a/docs/Show.md +++ b/docs/Show.md @@ -314,6 +314,8 @@ export const PostShow = (props) => ( ``` {% endraw %} +**Tip**: By default, `` use Material UI scrollable buttons when there are too many tabs to display. If you prefer the screen to expand with the tabs, add this prop: `scrollable={false}`. + To style the tabs, the `` component accepts two props: - `className` is passed to the tab *header* diff --git a/packages/ra-ui-materialui/src/detail/TabbedShowLayout.js b/packages/ra-ui-materialui/src/detail/TabbedShowLayout.js index 00501cd3e15..19c44c39a4f 100644 --- a/packages/ra-ui-materialui/src/detail/TabbedShowLayout.js +++ b/packages/ra-ui-materialui/src/detail/TabbedShowLayout.js @@ -4,6 +4,7 @@ import PropTypes from 'prop-types'; import Divider from '@material-ui/core/Divider'; import { Route } from 'react-router-dom'; import { makeStyles } from '@material-ui/core/styles'; +import classnames from 'classnames'; import { useRouteMatch } from 'react-router-dom'; import { escapePath } from 'ra-core'; @@ -15,6 +16,7 @@ const sanitizeRestProps = ({ record, resource, basePath, + scrollable, version, initialValues, staticContext, @@ -26,10 +28,23 @@ const sanitizeRestProps = ({ const useStyles = makeStyles( theme => ({ content: { - paddingTop: theme.spacing(1), + paddingTop: props => + props.scrollable ? theme.spacing(7) : theme.spacing(1), // When using scrollable tabs, leave enough height for the tab content paddingLeft: theme.spacing(2), paddingRight: theme.spacing(2), }, + scrollableDivider: { + marginTop: theme.spacing(6), + position: 'absolute', + width: '100%', + }, + scrollableTabs: { + position: 'absolute', + width: '100%', + }, + formRelative: { + position: 'relative', + }, }), { name: 'RaTabbedShowLayout' } ); @@ -81,6 +96,7 @@ const TabbedShowLayout = props => { className, record, resource, + scrollable, version, value, tabs, @@ -89,11 +105,24 @@ const TabbedShowLayout = props => { const match = useRouteMatch(); const classes = useStyles(props); + + const scrollableProps = scrollable + ? { scrollable: true, scrollButtons: 'on', variant: 'scrollable' } + : {}; + return ( -
- {cloneElement(tabs, {}, children)} +
+ {cloneElement(tabs, { classes, ...scrollableProps }, children)} - +
{Children.map(children, (tab, index) => tab && isValidElement(tab) ? ( @@ -126,6 +155,7 @@ TabbedShowLayout.propTypes = { record: PropTypes.object, resource: PropTypes.string, basePath: PropTypes.string, + scrollable: PropTypes.bool, value: PropTypes.number, version: PropTypes.number, tabs: PropTypes.element, @@ -133,6 +163,7 @@ TabbedShowLayout.propTypes = { TabbedShowLayout.defaultProps = { tabs: , + scrollable: true, }; export default TabbedShowLayout; diff --git a/packages/ra-ui-materialui/src/detail/TabbedShowLayoutTabs.js b/packages/ra-ui-materialui/src/detail/TabbedShowLayoutTabs.js index 610427e3e98..d5aec3f76be 100644 --- a/packages/ra-ui-materialui/src/detail/TabbedShowLayoutTabs.js +++ b/packages/ra-ui-materialui/src/detail/TabbedShowLayoutTabs.js @@ -9,7 +9,7 @@ export const getTabFullPath = (tab, index, baseUrl) => tab.props.path ? `/${tab.props.path}` : index > 0 ? `/${index}` : '' }`; -const TabbedShowLayoutTabs = ({ children, ...rest }) => { +const TabbedShowLayoutTabs = ({ classes, children, scrollable, ...rest }) => { const location = useLocation(); const match = useRouteMatch(); @@ -17,8 +17,17 @@ const TabbedShowLayoutTabs = ({ children, ...rest }) => { // so we can use it as a way to determine the current tab const value = location.pathname; + const scrollableProps = scrollable + ? { className: classes.scrollableTabs } + : {}; + return ( - + {Children.map(children, (tab, index) => { if (!tab || !isValidElement(tab)) return null; // Builds the full tab tab which is the concatenation of the last matched route in the @@ -37,6 +46,7 @@ const TabbedShowLayoutTabs = ({ children, ...rest }) => { }; TabbedShowLayoutTabs.propTypes = { + classes: PropTypes.object, children: PropTypes.node, }; diff --git a/packages/ra-ui-materialui/src/form/TabbedForm.js b/packages/ra-ui-materialui/src/form/TabbedForm.js index fada212c0dc..5aaa6d2d7a4 100644 --- a/packages/ra-ui-materialui/src/form/TabbedForm.js +++ b/packages/ra-ui-materialui/src/form/TabbedForm.js @@ -74,6 +74,7 @@ import TabbedFormTabs, { getTabFullPath } from './TabbedFormTabs'; * @prop {ReactElement} toolbar The element displayed at the bottom of the form, containing the SaveButton * @prop {string} variant Apply variant to all inputs. Possible values are 'standard', 'outlined', and 'filled' (default) * @prop {string} margin Apply variant to all inputs. Possible values are 'none', 'normal', and 'dense' (default) + * @prop {boolean} scrollable The tabs become scrollable when they extend beyond the witdh of the form * * @param {Prop} props */ @@ -99,16 +100,30 @@ TabbedForm.propTypes = { submitOnEnter: PropTypes.bool, undoable: PropTypes.bool, validate: PropTypes.func, + scrollable: PropTypes.bool, }; const useStyles = makeStyles( theme => ({ errorTabButton: { color: theme.palette.error.main }, content: { - paddingTop: theme.spacing(1), + paddingTop: props => + props.scrollable ? theme.spacing(7) : theme.spacing(1), // When using scrollable tabs, leave enough height for the tab content paddingLeft: theme.spacing(2), paddingRight: theme.spacing(2), }, + scrollableDivider: { + marginTop: theme.spacing(6), + position: 'absolute', + width: '100%', + }, + scrollableTabs: { + position: 'absolute', + width: '100%', + }, + formRelative: { + position: 'relative', + }, }), { name: 'RaTabbedForm' } ); @@ -128,6 +143,7 @@ export const TabbedFormView = props => { redirect: defaultRedirect, resource, saving, + scrollable = true, setRedirect, submitOnEnter, tabs, @@ -145,9 +161,16 @@ export const TabbedFormView = props => { const location = useLocation(); const url = match ? match.url : location.pathname; + const scrollableProps = scrollable + ? { scrollable: true, scrollButtons: 'on', variant: 'scrollable' } + : {}; return (
{React.cloneElement( @@ -155,11 +178,17 @@ export const TabbedFormView = props => { { classes, url, + title: 'FormTabRow', tabsWithErrors, + ...scrollableProps, }, children )} - +
{/* 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 @@ -285,6 +314,7 @@ const sanitizeRestProps = ({ reset, resetSection, save, + scrollable, staticContext, submit, submitAsSideEffect, diff --git a/packages/ra-ui-materialui/src/form/TabbedForm.spec.js b/packages/ra-ui-materialui/src/form/TabbedForm.spec.js index 866298afdc8..f2955e81c13 100644 --- a/packages/ra-ui-materialui/src/form/TabbedForm.spec.js +++ b/packages/ra-ui-materialui/src/form/TabbedForm.spec.js @@ -92,4 +92,63 @@ describe('', () => { expect(tabs).toEqual(['tab1', 'tab3', 'tab4']); }); }); + + it('should have scroll buttons when too much Tabs />', () => { + const { queryByLabelText, queryByTitle } = renderWithRedux( + + + + + + + + + + + + ); + + const tabs = queryByLabelText('Form-tabs'); + + expect(tabs.children).toHaveLength(7); + + const tabRow = queryByTitle('FormTabRow'); + + expect(tabRow.children).toHaveLength(4); + }); + + it('should not have scroll buttons when too much Tabs />', () => { + const { queryByLabelText, queryByTitle } = renderWithRedux( + + + + + + + + + + + + ); + + const tabs = queryByLabelText('Form-tabs'); + + expect(tabs.children).toHaveLength(7); + + const tabRow = queryByTitle('FormTabRow'); + + expect(tabRow.children).toHaveLength(1); + }); }); diff --git a/packages/ra-ui-materialui/src/form/TabbedFormTabs.js b/packages/ra-ui-materialui/src/form/TabbedFormTabs.js index 9e670b1255d..7afc03dce30 100644 --- a/packages/ra-ui-materialui/src/form/TabbedFormTabs.js +++ b/packages/ra-ui-materialui/src/form/TabbedFormTabs.js @@ -14,6 +14,7 @@ const TabbedFormTabs = ({ classes, url, tabsWithErrors, + scrollable, ...rest }) => { const location = useLocation(); @@ -34,8 +35,18 @@ const TabbedFormTabs = ({ ? location.pathname : validTabPaths[0]; + const scrollableProps = scrollable + ? { className: classes.scrollableTabs } + : {}; + return ( - + {Children.map(children, (tab, index) => { if (!isValidElement(tab)) return null;