Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve AppBar Customization #8681

Merged
merged 12 commits into from
Feb 27, 2023
4 changes: 2 additions & 2 deletions docs/Admin.md
Original file line number Diff line number Diff line change
@@ -209,7 +209,7 @@ const App = () => (

## `dashboard`

By default, the homepage of an admin app is the `list` of the first child `<Resource>`. But you can also specify a custom component instead. To fit in the general design, use MUI's `<Card>` component, and react-admin's `<Title>` component to set the title in the AppBar:
By default, the homepage of an admin app is the `list` of the first child `<Resource>`. But you can also specify a custom component instead. To fit in the general design, use MUI's `<Card>` component, and [react-admin's `<Title>` component](./Title.md) to set the title in the AppBar:

```jsx
// in src/Dashboard.js
@@ -270,7 +270,7 @@ When users type URLs that don't match any of the children `<Resource>` component

![Not Found](./img/not-found.png)

You can customize this page to use the component of your choice by passing it as the `catchAll` prop. To fit in the general design, use MUI's `<Card>` component, and react-admin's `<Title>` component:
You can customize this page to use the component of your choice by passing it as the `catchAll` prop. To fit in the general design, use MUI's `<Card>` component, and [react-admin's `<Title>` component](./Title.md):

```jsx
// in src/NotFound.js
389 changes: 372 additions & 17 deletions docs/AppBar.md

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions docs/Configurable.md
Original file line number Diff line number Diff line change
@@ -14,7 +14,7 @@ Some react-admin components are already configurable - or rather they have a con
- [`<DatagridConfigurable>`](./Datagrid.md#configurable)
- [`<SimpleListConfigurable>`](./SimpleList.md#configurable)
- [`<SimpleFormConfigurable>`](./SimpleForm.md#configurable)
- `<PageTitleConfigurable>` - used by the `<Title>` component
- `<PageTitleConfigurable>` - used by [the `<Title>` component](./Title.md)

## Usage

@@ -197,14 +197,14 @@ Users will be able to customize each component independently.

## `<InspectorButton>`

Add the `<InspectorButton>` to the `<AppBar>` component in order to let users enter the configuration mode and show the configuration editing panel.
Add the `<InspectorButton>` to [the `<AppBar>` component](./AppBar.md) in order to let users enter the configuration mode and show the configuration editing panel.

```jsx
import { AppBar, InspectorButton } from 'react-admin';
import { AppBar, TitlePortal, InspectorButton } from 'react-admin';

const MyAppBar = props => (
<AppBar {...props}>
<Typography flex="1" variant="h6" id="react-admin-title" />
const MyAppBar = () => (
<AppBar>
<TitlePortal />
<InspectorButton />
</AppBar>
);
3 changes: 1 addition & 2 deletions docs/ContainerLayout.md
Original file line number Diff line number Diff line change
@@ -174,10 +174,9 @@ const ConfigurationMenu = React.forwardRef((props, ref) => {
{...props}
to="/configuration"
onClick={onClose}
sx={{ color: 'text.secondary' }}
>
<ListItemIcon>
<SettingsIcon />
<SettingsIcon fontSize="small" />
</ListItemIcon>
<ListItemText>Configuration</ListItemText>
</MenuItem>
2 changes: 1 addition & 1 deletion docs/CustomRoutes.md
Original file line number Diff line number Diff line change
@@ -95,7 +95,7 @@ As illustrated above, there can be more than one `<CustomRoutes>` element inside

## Custom Page Title

To define the page title (displayed in the app bar), your custom pages can use the `<Title>` component from react-admin:
To define the page title (displayed in the app bar), your custom pages can use [the `<Title>` component](./Title.md) from react-admin:

```jsx
// in src/Settings.js
10 changes: 5 additions & 5 deletions docs/Features.md
Original file line number Diff line number Diff line change
@@ -1233,13 +1233,13 @@ If you need to translate to a language not yet supported by react-admin, you can
If your app needs to support more than one language, you can use the [`<LocalesMenuButton>`](./LocalesMenuButton.md) component to **let users choose their language**:
```jsx
import { LocalesMenuButton } from 'react-admin';
import { AppBar, Toolbar, Typography } from '@mui/material';
import { LocalesMenuButton, TitlePortal } from 'react-admin';
import { AppBar, Toolbar } from '@mui/material';

export const MyAppBar = (props) => (
<AppBar {...props}>
export const MyAppBar = () => (
<AppBar>
<Toolbar>
<Typography flex="1" variant="h6" id="react-admin-title"></Typography>
<TitlePortal />
<LocalesMenuButton />
</Toolbar>
</AppBar>
41 changes: 10 additions & 31 deletions docs/Layout.md
Original file line number Diff line number Diff line change
@@ -56,7 +56,7 @@ const App = () => (
React-admin injects more props at runtime based on the `<Admin>` props:

* `dashboard`: The dashboard component. Used to enable the dahboard link in the menu
* `title`: The default page tile, enreder in the AppBar
* `title`: The default page tile, rendered in the AppBar for error pages
* `children`: The main content of the page

Any value set for these props in a custom layout will be ignored. That's why you're supposed to pass down the props when creating a layout based on `<Layout>`:
@@ -84,7 +84,7 @@ import { MyAppBar } from './MyAppBar';
export const MyLayout = (props) => <Layout {...props} appBar={MyAppBar} />;
```

You can use react-admin's `<AppBar>` as a base for your custom app bar, or the component of your choice.
You can use [react-admin's `<AppBar>` component](./AppBar.md) as a base for your custom app bar, or the component of your choice.

By default, react-admin's `<AppBar>` displays the page title. You can override this default by passing children to `<AppBar>` - they will replace the default title. And if you still want to include the page title, make sure you include an element with id `react-admin-title` in the top bar (this uses [React Portals](https://reactjs.org/docs/portals.html)).

@@ -94,46 +94,25 @@ Here is a custom app bar component extending `<AppBar>` to include a company log
```jsx
// in src/MyAppBar.js
import * as React from 'react';
import { AppBar } from 'react-admin';
import Typography from '@mui/material/Typography';
import { AppBar, TitlePortal } from 'react-admin';
import Box from '@mui/material/Box';

import Logo from './Logo';

export const MyAppBar = (props) => (
<AppBar
sx={{
"& .RaAppBar-title": {
flex: 1,
textOverflow: "ellipsis",
whiteSpace: "nowrap",
overflow: "hidden",
},
}}
{...props}
>
<Typography
variant="h6"
color="inherit"
className={classes.title}
id="react-admin-title"
/>
export const MyAppBar = () => (
<AppBar color="primary">
<TitlePortal />
<Box flex="1" />
<Logo />
<span className={classes.spacer} />
<Box flex="1" />
</AppBar>
);
```
{% endraw %}

![custom AppBar](./img/custom_appbar.png)

**Tip**: You can change the color of the `<AppBar>` by setting the `color` prop to `default`, `inherit`, `primary`, `secondary` or `transparent`. The default value is `secondary`.

When react-admin renders the App BAr, it passes two props:

* `open`: a boolean indicating if the sidebar is open or not
* `title`: the page title (if set by the `<Admin>` component)

Your custom AppBar component is free to use these props.
Check out the [`<AppBar>` documentation](./AppBar.md) for more information, and for instructions on building your own AppBar.

## `className`

10 changes: 5 additions & 5 deletions docs/LocalesMenuButton.md
Original file line number Diff line number Diff line change
@@ -17,13 +17,13 @@ For advanced users who wish to use the customized `<AppBar>` from MUI package or

```jsx
// in src/MyAppBar.js
import { LocalesMenuButton } from 'react-admin';
import { AppBar, Toolbar, Typography } from '@mui/material';
import { LocalesMenuButton, TitlePortal } from 'react-admin';
import { AppBar, Toolbar } from '@mui/material';

export const MyAppBar = (props) => (
<AppBar {...props}>
export const MyAppBar = () => (
<AppBar>
<Toolbar>
<Typography flex="1" variant="h6" id="react-admin-title"></Typography>
<TitlePortal />
<LocalesMenuButton />
</Toolbar>
</AppBar>
2 changes: 1 addition & 1 deletion docs/Menu.md
Original file line number Diff line number Diff line change
@@ -135,7 +135,7 @@ This property accepts the following subclasses:
| `&.RaMenu-open` | Applied the menu when it's open |
| `&.RaMenu-closed` | Applied to the menu when it's closed |

To override the style of `<Layout>` using the [MUI style overrides](https://mui.com/customization/theme-components/), use the `RaMenu` key.
To override the style of `<Menu>` using the [MUI style overrides](https://mui.com/customization/theme-components/), use the `RaMenu` key.

## `<Menu.Item>`

6 changes: 4 additions & 2 deletions docs/Reference.md
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@ title: "Index"
**- A -**
* [`<AccordionForm>`](./AccordionForm.md)<img class="icon" src="./img/premium.svg" />
* [`<Admin>`](./Admin.md)
* [`<AppBar>`](./Theming.md#customizing-the-appbar-content)
* [`<AppBar>`](./AppBar.md)
* [`<ArrayField>`](./ArrayField.md)
* [`<ArrayInput>`](./ArrayInput.md)
* [`<Authenticated>`](./Authenticated.md)
@@ -171,6 +171,8 @@ title: "Index"
* [`<TextField>`](./TextField.md)
* [`<TextInput>`](./TextInput.md)
* [`<TimeInput>`](./TimeInput.md)
* [<`Title>`](./Title.md)
* [<`TitlePortal>`](./AppBar.md#children)
Comment on lines +174 to +175
Copy link

@cduran-seeri cduran-seeri Mar 24, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Found a typo, I'm not sure if this is an issue

<`Title>` to `<Title>`
<`TitlePortal>` to `<TitlePortal>`

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, can you open a PR to fix it?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure!

* [`<ToggleThemeButton>`](./ToggleThemeButton.md)
* [`<TourProvider>`](https://marmelab.com/ra-enterprise/modules/ra-tour)<img class="icon" src="./img/premium.svg" />
* [`<TranslatableFields>`](./TranslatableFields.md)
@@ -181,7 +183,7 @@ title: "Index"

**- U -**
* [`<UrlField>`](./UrlField.md)
* [`<UserMenu>`](./Theming.md#usermenu-customization)
* [`<UserMenu>`](./AppBar.md#usermenu)

**- W -**
* [`<WithPermissions>`](./WithPermissions.md)
21 changes: 4 additions & 17 deletions docs/Search.md
Original file line number Diff line number Diff line change
@@ -78,31 +78,18 @@ Check [the `ra-search` documentation](https://marmelab.com/ra-enterprise/modules

If you're using [the `<Layout` component](./Layout.md), include the `<Search>` component inside a custom `<AppBar>` component:

{% raw %}
```jsx
// in src/MyAppBar.jsx
import { AppBar } from "react-admin";
import { Typography } from "@mui/material";
import { AppBar, TitlePortal } from "react-admin";
import { Search } from "@react-admin/ra-search";

export const MyAppbar = (props) => (
<AppBar {...props}>
<Typography
variant="h6"
color="inherit"
sx={{
flex: 1,
textOverflow: "ellipsis",
whiteSpace: "nowrap",
overflow: "hidden",
}}
id="react-admin-title"
/>
export const MyAppbar = () => (
<AppBar>
<TitlePortal />
<Search />
</AppBar>
);
```
{% endraw %}

Include that AppBar in [a custom layout component](./Layout.md):

125 changes: 38 additions & 87 deletions docs/Theming.md
Original file line number Diff line number Diff line change
@@ -375,22 +375,17 @@ You can add the `<ToggleThemeButton>` to a custom App Bar:

```jsx
import * as React from 'react';
import { defaultTheme, Layout, AppBar, ToggleThemeButton } from 'react-admin';
import { defaultTheme, Layout, AppBar, ToggleThemeButton, TitlePortal } from 'react-admin';
import { createTheme, Box, Typography } from '@mui/material';

const darkTheme = createTheme({
palette: { mode: 'dark' },
});

const MyAppBar = props => (
<AppBar {...props}>
<Box flex="1">
<Typography variant="h6" id="react-admin-title"></Typography>
</Box>
<ToggleThemeButton
lightTheme={defaultTheme}
darkTheme={darkTheme}
/>
const MyAppBar = () => (
<AppBar>
<TitlePortal />
<ToggleThemeButton lightTheme={defaultTheme} darkTheme={darkTheme} />
</AppBar>
);

@@ -577,7 +572,7 @@ const ConfigurationMenu = React.forwardRef((props, ref) => {
to="/configuration"
>
<ListItemIcon>
<SettingsIcon />
<SettingsIcon fontSize="small" />
</ListItemIcon>
<ListItemText>
Configuration
@@ -603,8 +598,8 @@ const SwitchLanguage = forwardRef((props, ref) => {
onClose(); // Close the menu
}}
>
<ListItemIcon sx={{ minWidth: 5 }}>
<LanguageIcon />
<ListItemIcon>
<LanguageIcon fontSize="small" />
</ListItemIcon>
<ListItemText>
Switch Language
@@ -613,15 +608,15 @@ const SwitchLanguage = forwardRef((props, ref) => {
);
});

const MyUserMenu = props => (
<UserMenu {...props}>
const MyUserMenu = () => (
<UserMenu>
<ConfigurationMenu />
<SwitchLanguage />
<Logout />
</UserMenu>
);

const MyAppBar = props => <AppBar {...props} userMenu={<MyUserMenu />} />;
const MyAppBar = () => <AppBar userMenu={<MyUserMenu />} />;

const MyLayout = props => <Layout {...props} appBar={MyAppBar} />;
```
@@ -633,7 +628,7 @@ You can also remove the `<UserMenu>` from the `<AppBar>` by passing `false` to t
import * as React from 'react';
import { AppBar } from 'react-admin';

const MyAppBar = props => <AppBar {...props} userMenu={false} />;
const MyAppBar = () => <AppBar userMenu={false} />;

const MyLayout = props => <Layout {...props} appBar={MyAppBar} />;
```
@@ -657,7 +652,7 @@ const MyCustomIcon = () => (

const MyUserMenu = props => (<UserMenu {...props} icon={<MyCustomIcon />} />);

const MyAppBar = props => <AppBar {...props} userMenu={<MyUserMenu />} />;
const MyAppBar = () => <AppBar userMenu={<MyUserMenu />} />;
```
{% endraw %}

@@ -715,13 +710,7 @@ import * as React from 'react';
import { useEffect } from 'react';
import PropTypes from 'prop-types';
import { styled } from '@mui/material';
import {
AppBar,
Menu,
Sidebar,
ComponentPropType,
useSidebarState,
} from 'react-admin';
import { AppBar, Menu, Sidebar } from 'react-admin';

const Root = styled("div")(({ theme }) => ({
display: "flex",
@@ -752,37 +741,28 @@ const Content = styled("div")(({ theme }) => ({
paddingLeft: 5,
}));

const MyLayout = ({
children,
dashboard,
title,
}) => {
const [open] = useSidebarState();

return (
<Root>
<AppFrame>
<AppBar title={title} open={open} />
<ContentWithSidebar>
<Sidebar>
<Menu hasDashboard={!!dashboard} />
</Sidebar>
<Content>
{children}
</Content>
</ContentWithSidebar>
</AppFrame>
</Root>
);
};
const MyLayout = ({ children, dashboard }) => (
<Root>
<AppFrame>
<AppBar />
<ContentWithSidebar>
<Sidebar>
<Menu hasDashboard={!!dashboard} />
</Sidebar>
<Content>
{children}
</Content>
</ContentWithSidebar>
</AppFrame>
</Root>
);

MyLayout.propTypes = {
children: PropTypes.oneOfType([PropTypes.func, PropTypes.node]),
dashboard: PropTypes.oneOfType([
PropTypes.func,
PropTypes.string,
]),
title: PropTypes.string.isRequired,
};

export default MyLayout;
@@ -799,31 +779,17 @@ Here is an example customization for `<AppBar>` to include a company logo in the
```jsx
// in src/MyAppBar.js
import * as React from 'react';
import { AppBar } from 'react-admin';
import Typography from '@mui/material/Typography';
import { AppBar, TitlePortal } from 'react-admin';
import Box from '@mui/material/Box';

import Logo from './Logo';

const MyAppBar = (props) => (
<AppBar
sx={{
"& .RaAppBar-title": {
flex: 1,
textOverflow: "ellipsis",
whiteSpace: "nowrap",
overflow: "hidden",
},
}}
{...props}
>
<Typography
variant="h6"
color="inherit"
className={classes.title}
id="react-admin-title"
/>
const MyAppBar = () => (
<AppBar>
<TitlePortal />
<Box flex={1} />
<Logo />
<span className={classes.spacer} />
<Box flex={1} />
</AppBar>
);

@@ -863,21 +829,6 @@ const App = () => (

## Replacing The AppBar

By default, React-admin uses [MUI's `<AppBar>` component](https://mui.com/api/app-bar/) together with a custom container that internally uses a [Slide](https://mui.com/api/slide) to hide the `AppBar` on scroll. Here is an example of how to change this container with any component:

```jsx
// in src/MyAppBar.js
import * as React from 'react';
import { Fragment } from 'react';
import { AppBar } from 'react-admin';

const MyAppBar = props => (
<AppBar {...props} container={Fragment} />
);

export default MyAppBar;
```

For more drastic changes of the top component, you will probably want to create an `<AppBar>` from scratch instead of just passing children to react-admin's `<AppBar>`. Here is an example top bar rebuilt from scratch:

```jsx
@@ -887,8 +838,8 @@ import AppBar from '@mui/material/AppBar';
import Toolbar from '@mui/material/Toolbar';
import Typography from '@mui/material/Typography';

const MyAppBar = props => (
<AppBar {...props}>
const MyAppBar = () => (
<AppBar>
<Toolbar>
<Typography variant="h6" id="react-admin-title" />
</Toolbar>
123 changes: 123 additions & 0 deletions docs/Title.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
---
layout: default
title: "The Title Component"
---

# `<Title>`

Set the page title (the text displayed in the app bar) from within a react-admin component.

![Title](./img/Title.png)

## Usage

Use `<Title>` from anywhere in the page to set the page title.

```jsx
import { Title } from 'react-admin';

const CustomPage = () => (
<>
<Title title="My Custom Page" />
<div>Content</div>
</>
);
```

`<Title>` uses a [React Portal](https://reactjs.org/docs/portals.html) to render the title outside of the current component. It works because the default [ `<AppBar>`](./AppBar.md) component contains a placeholder for the title called `<TitlePortal>`.

CRUD page components ([`<List>`](./List.md), [`<Edit>`](./Edit.md), [`<Create>`](./Create.md), [`<Show>`](./Show.md)) already use a `<Title>` component. To set the page title for these components, use the `title` prop.

```jsx
import { List } from 'react-admin';

const PostList = () => (
<List title="All posts">
...
</List>
);
```

## Props

| Prop | Required | Type | Default | Description |
| ------------------- | -------- | --------------------- | -------- | --------------------------------------------------------------------------- |
| `title` | Optional | `string|ReactElement` | - | What to display in the central part of the app bar |
| `defaultTitle` | Optional | `string` | `''` | What to display in the central part of the app bar when `title` is not set |
| `preferenceKey` | Optional | `string` | ``${pathname}.title`` | The key to use in the user preferences to store a custom title |

## `title`

The `title` prop can be a string or a React element.

If it's a string, it will be passed to [the `translate` function](./useTranslate.md), so you can use a title or a message id.

```jsx
import { Title } from 'react-admin';

const CustomPage = () => (
<>
<Title title="my.custom.page.title" />
<div>Content</div>
</>
);
```

If it's a React element, it will be rendered as is. If the element contains some text, it's your responsibliity to translate it.

```jsx
import { Title } from 'react-admin';
import ArticleIcon from '@mui/icons-material/Article';

const ArticlePage = () => (
<>
<Title title={
<>
<ArticleIcon />
My Custom Page
</>
} />
<div>My Custom Content</div>
</>
);
```

## `defaultTitle`

It often happens that the title is empty while the component fetches the data to display. To avoid a flicker, you can pass a default title to the `<Title>` component.

```jsx
import { Title, useGetOne } from 'react-admin';
import ArticleIcon from '@mui/icons-material/Article';

const ArticlePage = ({ id }) => {
const { data, loading } = useGetOne('articles', { id });
return (
<>
<Title
title={data && <><ArticleIcon />{data.title}</>}
defaultTitle={<ArticleIcon />}
/>
{!loading && <div>{data.body}</div>}
</>
);
};
```

## `preferenceKey`

In [Configurable mode](./AppBar.md#configurable), users can customize the page title via the inspector. To avoid conflicts, the `<Title>` component uses a preference key based on the current pathname. For example, the `<Title>` component in the `posts` list page will use the `posts.title` preference key.

If you want to use a custom preference key, pass it to the `<Title>` component.

```jsx
import { Title } from 'react-admin';

const CustomPage = () => (
<>
<Title title="My Custom Page" preferenceKey="my.custom.page.title" />
<div>Content</div>
</>
);
```

23 changes: 9 additions & 14 deletions docs/ToggleThemeButton.md
Original file line number Diff line number Diff line change
@@ -13,29 +13,24 @@ The `<ToggleThemeButton>` component lets users switch from light to dark mode, a

You can add the `<ToggleThemeButton>` to a custom App Bar:

```tsx
import React from 'react';
import { ToggleThemeButton, AppBar, defaultTheme } from 'react-admin';
import { Typography } from '@mui/material';
```jsx
import { AppBar, TitlePortal, ToggleThemeButton, defaultTheme } from 'react-admin';

const darkTheme = {
palette: { mode: 'dark' },
};

export const MyAppBar = (props) => (
<AppBar {...props}>-
<Typography flex="1" variant="h6" id="react-admin-title"></Typography>
<ToggleThemeButton
lightTheme={defaultTheme}
darkTheme={darkTheme}
/>
</AppBar>
export const MyAppBar = () => (
<AppBar>
<TitlePortal />
<ToggleThemeButton lightTheme={defaultTheme} darkTheme={darkTheme} />
</AppBar>>
);
```

Then, pass the custom App Bar in a custom `<Layout>`, and the `<Layout>` to your `<Admin>`:

```tsx
```jsx
import { Admin, Layout } from 'react-admin';

import { MyAppBar } from './MyAppBar';
@@ -94,5 +89,5 @@ React-admin uses the `<Admin theme>` prop as default theme.

* [`ToggleThemeButton`]

[`ToggleThemeButton`]: https://github.com/marmelab/react-admin/blob/master/packages/ra-ui-materialui/src/button/ToggleThemeButton.tsx
[`ToggleThemeButton`]: https://github.com/marmelab/react-admin/blob/master/packages/ra-ui-materialui/src/button/ToggleThemeButton.jsx

Binary file added docs/img/AppBar-children.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/AppBar-color.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/AppBar-user-menu.webm
Binary file not shown.
Binary file added docs/img/AppBar.webm
Binary file not shown.
Binary file added docs/img/Title.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/TitleConfigurable.webm
Binary file not shown.
2 changes: 2 additions & 0 deletions docs/navigation.html
Original file line number Diff line number Diff line change
@@ -218,10 +218,12 @@
<ul><div>Other UI components</div>
<li {% if page.path == 'Layout.md' %} class="active" {% endif %}><a class="nav-link" href="./Layout.html"><code>&lt;Layout&gt;</code></a></li>
<li {% if page.path == 'ContainerLayout.md' %} class="active" {% endif %}><a class="nav-link" href="./ContainerLayout.html"><code>&lt;ContainerLayout&gt;</code><img class="premium" src="./img/premium.svg" /></a></li>
<li {% if page.path == 'AppBar.md' %} class="active" {% endif %}><a class="nav-link" href="./AppBar.html"><code>&lt;AppBar&gt;</code></a></li>
<li {% if page.path == 'Menu.md' %} class="active" {% endif %}><a class="nav-link" href="./Menu.html"><code>&lt;Menu&gt;</code></a></li>
<li {% if page.path == 'MultiLevelMenu.md' %} class="active" {% endif %}><a class="nav-link" href="./MultiLevelMenu.html"><code>&lt;MultiLevelMenu&gt;</code><img class="premium" src="./img/premium.svg" /></a></li>
<li {% if page.path == 'IconMenu.md' %} class="active" {% endif %}><a class="nav-link" href="./IconMenu.html"><code>&lt;IconMenu&gt;</code><img class="premium" src="./img/premium.svg" /></a></li>
<li {% if page.path == 'HorizontalMenu.md' %} class="active" {% endif %}><a class="nav-link" href="./HorizontalMenu.html"><code>&lt;HorizontalMenu&gt;</code><img class="premium" src="./img/premium.svg" /></a></li>
<li {% if page.path == 'Title.md' %} class="active" {% endif %}><a class="nav-link" href="./Title.html"><code>&lt;Title&gt;</code></a></li>
<li {% if page.path == 'Breadcrumb.md' %} class="active" {% endif %}><a class="nav-link" href="./Breadcrumb.html"><code>&lt;Breadcrumb&gt;</code><img class="premium" src="./img/premium.svg" /></a></li>
<li {% if page.path == 'Search.md' %} class="active" {% endif %}><a class="nav-link" href="./Search.html"><code>&lt;Search&gt;</code><img class="premium" src="./img/premium.svg" /></a></li>
<li {% if page.path == 'Buttons.md' %} class="active" {% endif %}><a class="nav-link" href="./Buttons.html">Buttons</a></li>
4 changes: 1 addition & 3 deletions docs/useLogout.md
Original file line number Diff line number Diff line change
@@ -34,9 +34,7 @@ const MyUserMenu = () => (
</UserMenu>
);

const MyAppBar = () => (
<AppBar userMenu={<UserMenu />} />
);
const MyAppBar = () => <AppBar userMenu={<UserMenu />} />;

const MyLayout = (props) => (
<Layout {...props} appBar={MyAppBar} />
32 changes: 11 additions & 21 deletions examples/demo/src/layout/AppBar.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import * as React from 'react';
import { AppBar, Logout, UserMenu, useTranslate } from 'react-admin';
import {
AppBar,
TitlePortal,
Logout,
UserMenu,
useTranslate,
} from 'react-admin';
import { Link } from 'react-router-dom';
import {
Box,
MenuItem,
ListItemIcon,
ListItemText,
Typography,
useMediaQuery,
Theme,
} from '@mui/material';
@@ -25,7 +30,7 @@ const ConfigurationMenu = React.forwardRef((props, ref) => {
to="/configuration"
>
<ListItemIcon>
<SettingsIcon />
<SettingsIcon fontSize="small" />
</ListItemIcon>
<ListItemText>{translate('pos.configuration')}</ListItemText>
</MenuItem>
@@ -38,28 +43,13 @@ const CustomUserMenu = () => (
</UserMenu>
);

const CustomAppBar = (props: any) => {
const CustomAppBar = () => {
const isLargeEnough = useMediaQuery<Theme>(theme =>
theme.breakpoints.up('sm')
);
return (
<AppBar
{...props}
color="secondary"
elevation={1}
userMenu={<CustomUserMenu />}
>
<Typography
variant="h6"
color="inherit"
sx={{
flex: 1,
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
overflow: 'hidden',
}}
id="react-admin-title"
/>
<AppBar color="secondary" elevation={1} userMenu={<CustomUserMenu />}>
<TitlePortal />
{isLargeEnough && <Logo />}
{isLargeEnough && <Box component="span" sx={{ flex: 1 }} />}
</AppBar>
12 changes: 5 additions & 7 deletions examples/simple/src/Layout.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import * as React from 'react';
import { memo } from 'react';
import { ReactQueryDevtools } from 'react-query/devtools';
import { AppBar, Layout, InspectorButton } from 'react-admin';
import { Typography } from '@mui/material';
import { AppBar, Layout, InspectorButton, TitlePortal } from 'react-admin';

const MyAppBar = memo(props => (
<AppBar {...props}>
<Typography flex="1" variant="h6" id="react-admin-title" />
const MyAppBar = () => (
<AppBar>
<TitlePortal />
<InspectorButton />
</AppBar>
));
);

export default props => (
<>
10 changes: 5 additions & 5 deletions packages/ra-ui-materialui/src/auth/Logout.tsx
Original file line number Diff line number Diff line change
@@ -48,9 +48,11 @@ export const Logout: FunctionComponent<
{...rest}
>
<ListItemIcon className={LogoutClasses.icon}>
{icon ? icon : <ExitIcon />}
{icon ? icon : <ExitIcon fontSize="small" />}
</ListItemIcon>
<ListItemText>{translate('ra.auth.logout')}</ListItemText>
<ListItemText>
{translate('ra.auth.logout', { _: 'Logout' })}
</ListItemText>
</StyledMenuItem>
);
});
@@ -71,9 +73,7 @@ const StyledMenuItem = styled(MenuItem, {
name: PREFIX,
overridesResolver: (props, styles) => styles.root,
})(({ theme }) => ({
color: theme.palette.text.secondary,

[`& .${LogoutClasses.icon}`]: { minWidth: theme.spacing(5) },
[`& .${LogoutClasses.icon}`]: {},
}));

export interface LogoutProps {
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@ import { AdminContext } from '../AdminContext';
import { AdminUI } from '../AdminUI';
import { List, Datagrid } from '../list';
import { TextField } from '../field';
import { AppBar, Layout } from '../layout';
import { AppBar, Layout, TitlePortal } from '../layout';

export default { title: 'ra-ui-materialui/button/LocalesMenuButton' };

@@ -142,9 +142,9 @@ const BookList = () => {
);
};

const MyAppBar = props => (
<AppBar {...props}>
<Typography flex="1" variant="h6" id="react-admin-title"></Typography>
const MyAppBar = () => (
<AppBar>
<TitlePortal />
<LocalesMenuButton
languages={[
{ locale: 'en', name: 'English' },
22 changes: 9 additions & 13 deletions packages/ra-ui-materialui/src/button/LocalesMenuButton.tsx
Original file line number Diff line number Diff line change
@@ -9,21 +9,17 @@ import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
* Language selector. Changes the locale in the app and persists it in
* preferences so that the app opens with the right locale in the future.
*
* Uses i18nProvider.getLocales() to get the list of available locales.
*
* @example
* import { AppBar, TitlePortal, LocalesMenuButton } from 'react-admin';
*
* const MyAppBar: FC = props => (
* <AppBar {...props}>
* <Box flex="1">
* <Typography variant="h6" id="react-admin-title"></Typography>
* </Box>
* <LocalesMenuButton
* languages={[
* { locale: 'en', name: 'English' },
* { locale: 'fr', name: 'Français' },
* ]}
* />
* </AppBar>
* );
* const MyAppBar = () => (
* <AppBar>
* <TitlePortal />
* <LocalesMenuButton />
* </AppBar>
* );
*/
export const LocalesMenuButton = (props: LocalesMenuButtonProps) => {
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
4 changes: 2 additions & 2 deletions packages/ra-ui-materialui/src/button/RefreshIconButton.tsx
Original file line number Diff line number Diff line change
@@ -28,9 +28,9 @@ export const RefreshIconButton = (props: RefreshIconButtonProps) => {
);

return (
<Tooltip title={label && translate(label, { _: label })}>
<Tooltip title={label && translate(label, { _: 'Refresh' })}>
<IconButton
aria-label={label && translate(label, { _: label })}
aria-label={label && translate(label, { _: 'Refresh' })}
className={className}
color="inherit"
onClick={handleClick}
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@ import { Typography } from '@mui/material';

import { List, Datagrid } from '../list';
import { TextField } from '../field';
import { AppBar, Layout } from '../layout';
import { AppBar, Layout, TitlePortal } from '../layout';
import { ToggleThemeButton } from './ToggleThemeButton';

export default { title: 'ra-ui-materialui/button/ToggleThemeButton' };
@@ -99,14 +99,10 @@ const BookList = () => {
);
};

const MyAppBar = props => (
<AppBar {...props}>
<Typography flex="1" variant="h6" id="react-admin-title"></Typography>
<ToggleThemeButton
darkTheme={{
palette: { mode: 'dark' },
}}
/>
const MyAppBar = () => (
<AppBar>
<TitlePortal />
<ToggleThemeButton darkTheme={{ palette: { mode: 'dark' } }} />
</AppBar>
);
const MyLayout = props => <Layout {...props} appBar={MyAppBar} />;
9 changes: 4 additions & 5 deletions packages/ra-ui-materialui/src/button/ToggleThemeButton.tsx
Original file line number Diff line number Diff line change
@@ -10,12 +10,11 @@ import { RaThemeOptions } from '..';
* Button toggling the theme (light or dark).
*
* @example
* import { AppBar, TitlePortal, ToggleThemeButton } from 'react-admin';
*
* const MyAppBar = props => (
* <AppBar {...props}>
* <Box flex="1">
* <Typography variant="h6" id="react-admin-title"></Typography>
* </Box>
* const MyAppBar = () => (
* <AppBar>
* <TitlePortal />
* <ToggleThemeButton lightTheme={lightTheme} darkTheme={darkTheme} />
* </AppBar>
* );
300 changes: 300 additions & 0 deletions packages/ra-ui-materialui/src/layout/AppBar.stories.tsx

Large diffs are not rendered by default.

84 changes: 41 additions & 43 deletions packages/ra-ui-materialui/src/layout/AppBar.tsx
Original file line number Diff line number Diff line change
@@ -7,7 +7,6 @@ import {
AppBar as MuiAppBar,
AppBarProps as MuiAppBarProps,
Toolbar,
Typography,
useMediaQuery,
Theme,
} from '@mui/material';
@@ -17,6 +16,7 @@ import { SidebarToggleButton } from './SidebarToggleButton';
import { LoadingIndicator } from './LoadingIndicator';
import { UserMenu } from './UserMenu';
import { HideOnScroll } from './HideOnScroll';
import { TitlePortal } from './TitlePortal';
import { LocalesMenuButton } from '../button';

/**
@@ -26,47 +26,35 @@ import { LocalesMenuButton } from '../button';
* @param {ReactNode} props.children React node/s to be rendered as children of the AppBar
* @param {string} props.className CSS class applied to the MuiAppBar component
* @param {string} props.color The color of the AppBar
* @param {boolean} props.open State of the <Admin/> Sidebar
* @param {Element | boolean} props.userMenu A custom user menu component for the AppBar. <UserMenu/> component by default. Pass false to disable.
*
* @example
* @example // add a custom button to the AppBar
*
* const MyAppBar = props => {
* return (
* <AppBar {...props}>
* <Typography
* variant="h6"
* color="inherit"
* className={classes.title}
* id="react-admin-title"
* />
* </AppBar>
* );
*};
* const MyAppBar = () => (
* <AppBar>
* <TitlePortal />
* <MyCustomButton />
* </AppBar>
* );
*
* @example Without a user menu
* @example // without a user menu
*
* const MyAppBar = props => {
* return (
* <AppBar {...props} userMenu={false} />
* );
*};
* const MyAppBar = () => <AppBar userMenu={false} />;
*/
export const AppBar: FC<AppBarProps> = memo(props => {
const {
alwaysOn,
children,
className,
color = 'secondary',
open,
title,
toolbar = defaultToolbarElement,
userMenu = DefaultUserMenu,
container: Container = HideOnScroll,
container: Container = alwaysOn ? 'div' : HideOnScroll,
...rest
} = props;

const locales = useLocales();
const isXSmall = useMediaQuery<Theme>(theme =>
theme.breakpoints.down('sm')
);
@@ -85,19 +73,11 @@ export const AppBar: FC<AppBarProps> = memo(props => {
>
<SidebarToggleButton className={AppBarClasses.menuButton} />
{Children.count(children) === 0 ? (
<Typography
variant="h6"
color="inherit"
className={AppBarClasses.title}
id="react-admin-title"
/>
<TitlePortal className={AppBarClasses.title} />
) : (
children
)}
{locales && locales.length > 1 ? (
<LocalesMenuButton />
) : null}
<LoadingIndicator />
{toolbar}
{typeof userMenu === 'boolean' ? (
userMenu === true ? (
<UserMenu />
@@ -111,7 +91,20 @@ export const AppBar: FC<AppBarProps> = memo(props => {
);
});

const DefaultToolbar = () => {
const locales = useLocales();
return (
<>
{locales && locales.length > 1 ? <LocalesMenuButton /> : null}
<LoadingIndicator />
</>
);
};

const defaultToolbarElement = <DefaultToolbar />;

AppBar.propTypes = {
alwaysOn: PropTypes.bool,
children: PropTypes.node,
className: PropTypes.string,
color: PropTypes.oneOf([
@@ -122,18 +115,28 @@ AppBar.propTypes = {
'transparent',
]),
container: ComponentPropType,
// @deprecated
/**
* @deprecated
*/
open: PropTypes.bool,
toolbar: PropTypes.element,
userMenu: PropTypes.oneOfType([PropTypes.element, PropTypes.bool]),
};

const DefaultUserMenu = <UserMenu />;

export interface AppBarProps extends Omit<MuiAppBarProps, 'title'> {
alwaysOn?: boolean;
container?: React.ElementType<any>;
// @deprecated
/**
* @deprecated injected by Layout but not used by this AppBar
*/
open?: boolean;
/**
* @deprecated injected by Layout but not used by this AppBar
*/
title?: string | JSX.Element;
toolbar?: JSX.Element;
userMenu?: JSX.Element | boolean;
}

@@ -161,10 +164,5 @@ const StyledAppBar = styled(MuiAppBar, {
[`& .${AppBarClasses.menuButton}`]: {
marginRight: '0.2em',
},
[`& .${AppBarClasses.title}`]: {
flex: 1,
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
overflow: 'hidden',
},
[`& .${AppBarClasses.title}`]: {},
}));
8 changes: 2 additions & 6 deletions packages/ra-ui-materialui/src/layout/Menu.stories.tsx
Original file line number Diff line number Diff line change
@@ -11,16 +11,12 @@ export default { title: 'ra-ui-materialui/layout/Menu' };

const resources = ['Posts', 'Comments', 'Tags', 'Users', 'Orders', 'Reviews'];

const DemoAppBar = props => {
const DemoAppBar = () => {
const darkTheme = createTheme({
palette: { mode: 'dark' },
});
return (
<AppBar
{...props}
elevation={1}
sx={{ flexDirection: 'row', flexWrap: 'nowrap' }}
>
<AppBar elevation={1} sx={{ flexDirection: 'row', flexWrap: 'nowrap' }}>
<Box sx={{ flex: '1 1 100%' }}>
<SidebarToggleButton />
</Box>
Original file line number Diff line number Diff line change
@@ -28,6 +28,11 @@ export const PageTitleConfigurable = ({ preferenceKey, ...props }) => {
<Configurable
editor={<PageTitleEditor />}
preferenceKey={preferenceKey || `${pathname}.title`}
sx={{
'&.RaConfigurable-editMode': {
margin: '2px',
},
}}
>
<PageTitle {...props} />
</Configurable>
16 changes: 14 additions & 2 deletions packages/ra-ui-materialui/src/layout/Title.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as React from 'react';
import { useEffect, useState } from 'react';
import { ReactElement } from 'react';
import { createPortal } from 'react-dom';
import PropTypes from 'prop-types';
@@ -8,10 +9,21 @@ import { PageTitleConfigurable } from './PageTitleConfigurable';

export const Title = (props: TitleProps) => {
const { defaultTitle, title, preferenceKey, ...rest } = props;
const container =
const [container, setContainer] = useState(() =>
typeof document !== 'undefined'
? document.getElementById('react-admin-title')
: null;
: null
);

// on first mount, we don't have the container yet, so we wait for it
useEffect(() => {
setContainer(container => {
if (container) return container;
return typeof document !== 'undefined'
? document.getElementById('react-admin-title')
: null;
});
}, []);

if (!container) return null;

15 changes: 15 additions & 0 deletions packages/ra-ui-materialui/src/layout/TitlePortal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import * as React from 'react';
import { Typography } from '@mui/material';

export const TitlePortal = ({ className }: { className?: string }) => (
<Typography
flex="1"
textOverflow="ellipsis"
whiteSpace="nowrap"
overflow="hidden"
variant="h6"
color="inherit"
id="react-admin-title"
className={className}
/>
);
5 changes: 3 additions & 2 deletions packages/ra-ui-materialui/src/layout/UserMenu.tsx
Original file line number Diff line number Diff line change
@@ -103,9 +103,9 @@ export const UserMenu = (props: UserMenuProps) => {
{identity.fullName}
</Button>
) : (
<Tooltip title={label && translate(label, { _: label })}>
<Tooltip title={label && translate(label, { _: 'Profile' })}>
<IconButton
aria-label={label && translate(label, { _: label })}
aria-label={label && translate(label, { _: 'Profile' })}
aria-owns={open ? 'menu-appbar' : null}
aria-haspopup={true}
color="inherit"
@@ -167,6 +167,7 @@ const Root = styled('div', {
})(({ theme }) => ({
[`& .${UserMenuClasses.userButton}`]: {
textTransform: 'none',
marginInlineStart: theme.spacing(0.5),
},

[`& .${UserMenuClasses.avatar}`]: {
1 change: 1 addition & 0 deletions packages/ra-ui-materialui/src/layout/index.ts
Original file line number Diff line number Diff line change
@@ -20,6 +20,7 @@ export * from './Sidebar';
export * from './SidebarToggleButton';
export * from './Theme';
export * from './Title';
export * from './TitlePortal';
export * from './TopToolbar';
export * from './UserMenu';
export * from './UserMenuContext';
Original file line number Diff line number Diff line change
@@ -4,20 +4,20 @@ import merge from 'lodash/merge';
import {
Admin,
AppBar,
AppBarProps,
defaultTheme,
Layout,
LayoutProps,
Resource,
Datagrid,
List,
TextField,
TitlePortal,
NumberField,
DateField,
FilterList,
FilterListItem,
} from 'react-admin';
import { Card, CardContent, Box, Typography, styled } from '@mui/material';
import { Card, CardContent, styled } from '@mui/material';
import BusinessIcon from '@mui/icons-material/Business';
import DateRangeIcon from '@mui/icons-material/DateRange';
import polyglotI18nProvider from 'ra-i18n-polyglot';
@@ -200,11 +200,9 @@ const darkTheme: RaThemeOptions = {
},
};

const MyAppBar = (props: AppBarProps) => (
<AppBar {...props}>
<Box flex="1">
<Typography variant="h6" id="react-admin-title"></Typography>
</Box>
const MyAppBar = () => (
<AppBar>
<TitlePortal />
<ToggleThemeButton lightTheme={defaultTheme} darkTheme={darkTheme} />
<LocalesMenuButton
languages={[
Original file line number Diff line number Diff line change
@@ -49,7 +49,7 @@ const TextBlock = ({
);
};

const TextBlocWithPreferences = props => {
const TextBlockWithPreferences = props => {
const [color] = usePreference('color', '#ffffff');
return <TextBlock color={color} {...props} />;
};
@@ -75,7 +75,7 @@ const ConfigurableTextBlock = ({
...props
}: any) => (
<Configurable editor={<TextBlockEditor />} preferenceKey={preferenceKey}>
<TextBlocWithPreferences {...props} />
<TextBlockWithPreferences {...props} />
</Configurable>
);