Skip to content

Commit

Permalink
Merge pull request #4538 from marmelab/too-much-tabs
Browse files Browse the repository at this point in the history
Add scroll buttons to tabs when they go beyond the available space
  • Loading branch information
Luwangel committed Aug 3, 2020
2 parents 9f746df + fa78f0e commit 55b8721
Show file tree
Hide file tree
Showing 10 changed files with 165 additions and 20 deletions.
15 changes: 8 additions & 7 deletions cypress/plugins/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
2 changes: 1 addition & 1 deletion cypress/support/CreatePage.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
2 changes: 1 addition & 1 deletion cypress/support/EditPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
3 changes: 2 additions & 1 deletion docs/CreateEdit.md
Original file line number Diff line number Diff line change
Expand Up @@ -719,6 +719,7 @@ Here are all the props accepted by the `<TabbedForm>` 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. `<TabbedForm>` 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
Expand Down Expand Up @@ -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.
The `onSave` value should be a function expecting 2 arguments: the form values to save, and the redirection to perform.
2 changes: 2 additions & 0 deletions docs/Show.md
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,8 @@ export const PostShow = (props) => (
```
{% endraw %}

**Tip**: By default, `<TabbedShowLayout>` 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 `<Tab>` component accepts two props:

- `className` is passed to the tab *header*
Expand Down
39 changes: 35 additions & 4 deletions packages/ra-ui-materialui/src/detail/TabbedShowLayout.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -15,6 +16,7 @@ const sanitizeRestProps = ({
record,
resource,
basePath,
scrollable,
version,
initialValues,
staticContext,
Expand All @@ -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' }
);
Expand Down Expand Up @@ -81,6 +96,7 @@ const TabbedShowLayout = props => {
className,
record,
resource,
scrollable,
version,
value,
tabs,
Expand All @@ -89,11 +105,24 @@ const TabbedShowLayout = props => {
const match = useRouteMatch();

const classes = useStyles(props);

const scrollableProps = scrollable
? { scrollable: true, scrollButtons: 'on', variant: 'scrollable' }
: {};

return (
<div className={className} key={version} {...sanitizeRestProps(rest)}>
{cloneElement(tabs, {}, children)}
<div
className={classnames(className, classes.formRelative)}
key={version}
{...sanitizeRestProps(rest)}
>
{cloneElement(tabs, { classes, ...scrollableProps }, children)}

<Divider />
<Divider
className={classnames({
[classes.scrollableDivider]: scrollable,
})}
/>
<div className={classes.content}>
{Children.map(children, (tab, index) =>
tab && isValidElement(tab) ? (
Expand Down Expand Up @@ -126,13 +155,15 @@ TabbedShowLayout.propTypes = {
record: PropTypes.object,
resource: PropTypes.string,
basePath: PropTypes.string,
scrollable: PropTypes.bool,
value: PropTypes.number,
version: PropTypes.number,
tabs: PropTypes.element,
};

TabbedShowLayout.defaultProps = {
tabs: <TabbedShowLayoutTabs />,
scrollable: true,
};

export default TabbedShowLayout;
14 changes: 12 additions & 2 deletions packages/ra-ui-materialui/src/detail/TabbedShowLayoutTabs.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,25 @@ 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();

// The location pathname will contain the page path including the current tab path
// so we can use it as a way to determine the current tab
const value = location.pathname;

const scrollableProps = scrollable
? { className: classes.scrollableTabs }
: {};

return (
<Tabs indicatorColor="primary" value={value} {...rest}>
<Tabs
indicatorColor="primary"
value={value}
{...scrollableProps}
{...rest}
>
{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
Expand All @@ -37,6 +46,7 @@ const TabbedShowLayoutTabs = ({ children, ...rest }) => {
};

TabbedShowLayoutTabs.propTypes = {
classes: PropTypes.object,
children: PropTypes.node,
};

Expand Down
36 changes: 33 additions & 3 deletions packages/ra-ui-materialui/src/form/TabbedForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand All @@ -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' }
);
Expand All @@ -128,6 +143,7 @@ export const TabbedFormView = props => {
redirect: defaultRedirect,
resource,
saving,
scrollable = true,
setRedirect,
submitOnEnter,
tabs,
Expand All @@ -145,21 +161,34 @@ export const TabbedFormView = props => {
const location = useLocation();

const url = match ? match.url : location.pathname;
const scrollableProps = scrollable
? { scrollable: true, scrollButtons: 'on', variant: 'scrollable' }
: {};
return (
<form
className={classnames('tabbed-form', className)}
className={classnames(
'tabbed-form',
className,
classes.formRelative
)}
{...sanitizeRestProps(rest)}
>
{React.cloneElement(
tabs,
{
classes,
url,
title: 'FormTabRow',
tabsWithErrors,
...scrollableProps,
},
children
)}
<Divider />
<Divider
className={classnames({
[classes.scrollableDivider]: scrollable,
})}
/>
<div className={classes.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
Expand Down Expand Up @@ -285,6 +314,7 @@ const sanitizeRestProps = ({
reset,
resetSection,
save,
scrollable,
staticContext,
submit,
submitAsSideEffect,
Expand Down
59 changes: 59 additions & 0 deletions packages/ra-ui-materialui/src/form/TabbedForm.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,4 +92,63 @@ describe('<TabbedForm />', () => {
expect(tabs).toEqual(['tab1', 'tab3', 'tab4']);
});
});

it('should have scroll buttons when too much Tabs />', () => {
const { queryByLabelText, queryByTitle } = renderWithRedux(
<MemoryRouter initialEntries={['/']}>
<TabbedForm
style={{
width: 200,
maxWidth: 200,
}}
>
<FormTab label="A Useful Tab 1" />
<FormTab label="A Useful Tab 2" />
<FormTab label="A Useful Tab 3" />
<FormTab label="A Useful Tab 4" />
<FormTab label="A Useful Tab 5" />
<FormTab label="A Useful Tab 6" />
<FormTab label="A Useful Tab 7" />
</TabbedForm>
</MemoryRouter>
);

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(
<MemoryRouter initialEntries={['/']}>
<TabbedForm
style={{
width: 200,
maxWidth: 200,
}}
scrollable={false}
>
<FormTab label="A Useful Tab 1" />
<FormTab label="A Useful Tab 2" />
<FormTab label="A Useful Tab 3" />
<FormTab label="A Useful Tab 4" />
<FormTab label="A Useful Tab 5" />
<FormTab label="A Useful Tab 6" />
<FormTab label="A Useful Tab 7" />
</TabbedForm>
</MemoryRouter>
);

const tabs = queryByLabelText('Form-tabs');

expect(tabs.children).toHaveLength(7);

const tabRow = queryByTitle('FormTabRow');

expect(tabRow.children).toHaveLength(1);
});
});
13 changes: 12 additions & 1 deletion packages/ra-ui-materialui/src/form/TabbedFormTabs.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const TabbedFormTabs = ({
classes,
url,
tabsWithErrors,
scrollable,
...rest
}) => {
const location = useLocation();
Expand All @@ -34,8 +35,18 @@ const TabbedFormTabs = ({
? location.pathname
: validTabPaths[0];

const scrollableProps = scrollable
? { className: classes.scrollableTabs }
: {};

return (
<Tabs value={tabValue} indicatorColor="primary" {...rest}>
<Tabs
aria-label="Form-tabs"
value={tabValue}
indicatorColor="primary"
{...scrollableProps}
{...rest}
>
{Children.map(children, (tab, index) => {
if (!isValidElement(tab)) return null;

Expand Down

0 comments on commit 55b8721

Please sign in to comment.