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
314 changes: 298 additions & 16 deletions docs/AppBar.md
Original file line number Diff line number Diff line change
@@ -12,12 +12,14 @@ The default react-admin layout renders a horizontal app bar at the top, which is
By default, the `<AppBar>` component displays:

- a hamburger icon to toggle the sidebar width,
- the application title,
- the page title,
- a button to change locales (if the application uses [i18n](./Translation.md)),
- a loading indicator,
- a button to display the user menu.

You can customize the App Bar by creating a custom component based on `<AppBar>`, with different props.
You can customize the App Bar by creating a custom component based on `<AppBar>`, with different props.

**Tip**: Don't mix react-admin's `<AppBar>` component with [MUI's `<AppBar>` component](https://mui.com/material-ui/api/app-bar/). The first one leverages the second, but adds some react-admin-specific features.

## Usage

@@ -28,16 +30,7 @@ Create a custom app bar based on react-admin's `<AppBar>`:
import { AppBar } from 'react-admin';
import { Typography } from '@mui/material';

const MyAppBar = props => (
<AppBar {...props} color="primary">
<Typography
variant="h6"
color="inherit"
className={classes.title}
id="react-admin-title"
/>
</AppBar>
);
const MyAppBar = () => <AppBar color="primary" position="sticky" />;
```

Then, create a custom layout based on react-admin's `<Layout>`:
@@ -68,16 +61,305 @@ const App = () => (

| Prop | Required | Type | Default | Description |
| ------------------- | -------- | -------------- | -------- | --------------------------------------------------- |
| `alwaysOn` | Optional | `boolean` | - | When true, the app bar is always visible |
| `children` | Optional | `ReactElement` | - | What to display in the central part of the app bar |
| `color` | Optional | `string` | - | The background color of the app bar |
| `enableColorOnDark` | Optional | `boolean` | - | If true, the `color` prop is applied in dark mode |
| `position` | Optional | `string` | - | The positioning type. |
| `sx` | Optional | `SxProps` | - | Style overrides, powered by MUI System |
| `title` | Optional | `ReactElement` | - | A React element rendered at left side of the screen |
| `toolbar` | Optional | `ReactElement` | - | The content of the toolbar |
| `userMenu` | Optional | `ReactElement` | - | The content of the dropdown user menu |

Additional props are passed to [the underlying MUI `<AppBar>` element](https://mui.com/material-ui/api/app-bar/).

## `alwaysOn`

By default, the app bar is hidden when the user scrolls down the page. This is useful to save space on small screens. But if you want to keep the app bar always visible, you can set the `alwaysOn` prop to `true`.

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

const MyAppBar = () => <AppBar alwaysOn />;
```

## `children`

The `<AppBar>` component accepts a `children` prop, which is displayed in the central part of the app bar. This is useful to display a logo or a search bar, for example.

```jsx
// in src/MyAppBar.js
import { AppBar } from 'react-admin';
import { Box } from '@mui/material';
import { Search } from "@react-admin/ra-search";

import { Logo } from './Logo';

const MyAppBar = () => (
<AppBar>
<Logo />
<Box component="span" flex={1} />
<Search />
<Box component="span" flex={1} />
</AppBar>
);
```

The above example removes the page title from the app bar. Why? Page components like `<List>` and `<Edit>` set the page title via a [React Portal](https://reactjs.org/docs/portals.html). The default `<AppBar>` child is a component called `<TitlePortal>`, which renders this title portal. So if you want to keep the page title in the app bar, you must include the `<TitlePortal>` component in the children.

```jsx
// in src/MyAppBar.js
import { AppBar, TitlePortal } from 'react-admin';

const MyAppBar = () => (
<AppBar>
<TitlePortal />
{/* Your custom appbar content here */}
</AppBar>
);
```

**Tip**: The `<TitlePortal>` component takes all the available space in the app bar, so it "pushes" the following children to the right.

## `color`

React-admin's `<AppBar>` renders a MUI `<AppBar>`, which supports a `color` prop top set the app bar color depending on the theme. By default, the app bar color is set to the `secondary` theme color.

This means you can set the app bar color to 'default', 'inherit', 'primary', 'secondary', 'transparent', or any string.

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

export const MyAppBar = () => <AppBar color="primary" />;
```

## `sx`: CSS API

Pass an `sx` prop to customize the style of the main component and the underlying elements.

{% raw %}
```jsx
// in src/MyAppBar.js
import { AppBar } from 'react-admin';

export const MyAppBar = () => (
<AppBar
sx={{
color: 'lightblue',
'& .RaAppBar-toolbar': { padding: 0 },
}}
/>
);
```
{% endraw %}

This property accepts the following subclasses:

| Rule name | Description |
|--------------------------|------------------------------ |
| `& .RaAppBar-toolbar` | Applied the main toolbar |
| `& .RaAppBar-menuButton` | Applied to the hamburger icon |
| `& .RaAppBar-title` | Applied to the title portal |

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

## `toolbar`

By default, the `<AppBar>` renders two buttons in addition to the user menu: the language menu and the refresh button.

If you want to reorder or remove these buttons, you can customize the toolbar by passing a `toolbar` prop.

```jsx
// in src/MyAppBar.js
import {
AppBar,
LocalesMenuButton,
RefreshIconButton,
ToggleThemeButton,
defaultTheme,
} from 'react-admin';

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

export const MyAppBar = () => (
<AppBar toolbar={
<>
<LocalesMenuButton />
<ToggleThemeButton lightTheme={defaultTheme} darkTheme={darkTheme} />
<RefreshIconButton />
</>
} >
);
```

**Tip**: If you only need to *add* buttons to the toolbar, you can pass them as children instead of overriding the entire toolbar.

```jsx
// in src/MyAppBar.js
import { AppBar, TitlePortal, ToggleThemeButton, defaultTheme } from 'react-admin';

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

export const MyAppBar = () => (
<AppBar>
<TitlePortal />
<ToggleThemeButton lightTheme={defaultTheme} darkTheme={darkTheme} />
</AppBar>
);
```

## `userMenu`

If your app uses [authentication](./Authentication.md), the `<AppBar>` component displays a button to display the user menu on the right side. By default, the user menu only contains a logout button.

The content of the user menu depends on the return value of `authProvider.getIdentity()`. The user menu icon renders an anonymous avatar, or the `avatar` property of the identity object if present. If the identity object contains a `fullName` property, it is displayed after the avatar.

You can customize the user menu by passing a `userMenu` prop to the `<AppBar>` component.

```jsx
import { AppBar, UserMenu, useUserMenu } from 'react-admin';
import { MenuItem, ListItemIcon, ListItemText } from '@mui/material';
import SettingsIcon from '@mui/icons-material/Settings';

const SettingsMenuItem = () => {
const { onClose } = useUserMenu();
return (
<MenuItem onClick={onClose}>
<ListItemIcon>
<SettingsIcon fontSize="small" />
</ListItemIcon>
<ListItemText>Customize</ListItemText>
</MenuItem>
);
};

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

Note that you still have to include the `<Logout>` component in the user menu, as it is responsible for the logout action. Also, for other menu items to work, you must call the `onClose` callback when the user clicks on them to close the user menu.

You can also hide the user menu by setting the `userMenu` prop to `false`.

```jsx
const MyAppBar = () => <AppBar userMenu={false} />;
```

## Changing The Page Title

The app bar displays the page title. CRUD page components (`<List>`, `<Edit>`, `<Create>`, `<Show>`) set the page title based on the current resource and record, and you can override the title by using their `title` prop:

```jsx
// in src/posts/PostList.js
import { List } from 'react-admin';

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

On your custom pages, you need to use the `<Title>` component to set the page title:

```jsx
// in src/MyCustomPage.js
import { Title } from 'react-admin';

export const MyCustomPage = () => (
<>
<Title title="My custom page" />
<div>My custom page content</div>
</>
);
```

**Tip**: The `<Title>` component uses a [React Portal](https://reactjs.org/docs/portals.html) to modify the title in the app bar. This is why you need to [include the `<TitlePortal>` component](#children) when you customize the `<AppBar>` children.

## Adding Buttons

## Customizing the User Menu
To add buttons to the app bar, you can use the `<AppBar>` [`children` prop](#children).

For instance, to add `<ToggleThemeButton>`:

```jsx
// in src/MyAppBar.js
import {
AppBar,
TitlePortal,
ToggleThemeButton,
defaultTheme
} from 'react-admin';

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

export const MyAppBar = () => (
<AppBar>
<TitlePortal />
<ToggleThemeButton lightTheme={defaultTheme} darkTheme={darkTheme} />
</AppBar>
);
```

**Tip**: The `<TitlePortal>` component displays the page title. As it takes all the available space in the app bar, it "pushes" the following children to the right.

## Adding a Search Input

A common use case for app bar customization is to add a site-wide search engine. The `<Search>` component is a good starting point for this:

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

export const MyAppbar = () => (
<AppBar>
<TitlePortal />
<Search />
</AppBar>
);
```

**Tip**: The `<TitlePortal>` component takes all the available space in the app bar, so it "pushes" the search input to the right.

## Building Your Own AppBar

If react-admin's `<AppBar>` component doesn't meet your needs, you can build your own component using MUI's `<AppBar>`. Here is an example:

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

export const MyAppBar = () => (
<AppBar position="static">
<Toolbar>
<TitlePortal />
<Box flex="1">
<RefreshIconButton />
</Toolbar>
</AppBar>
);
```

Then, use your custom app bar in a custom `<Layout>` component:

```jsx
// in src/MyLayout.js
import { Layout } from 'react-admin';

import { MyAppBar } from './MyAppBar';

export const MyLayout = (props) => (
<Layout {...props} appBar={MyAppBar} />
);
```
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>
39 changes: 9 additions & 30 deletions docs/Layout.md
Original file line number Diff line number Diff line change
@@ -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`

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>`

2 changes: 1 addition & 1 deletion 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)
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):

59 changes: 15 additions & 44 deletions docs/Theming.md
Original file line number Diff line number Diff line change
@@ -577,7 +577,7 @@ const ConfigurationMenu = React.forwardRef((props, ref) => {
to="/configuration"
>
<ListItemIcon>
<SettingsIcon />
<SettingsIcon fontSize="small" />
</ListItemIcon>
<ListItemText>
Configuration
@@ -603,8 +603,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 +613,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} />;
```
@@ -799,31 +799,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 +849,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 +858,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>
30 changes: 16 additions & 14 deletions docs/ToggleThemeButton.md
Original file line number Diff line number Diff line change
@@ -13,29 +13,31 @@ 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,
RefreshIconButton,
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 toolbar={
<>
<ToggleThemeButton lightTheme={defaultTheme} darkTheme={darkTheme} />
<RefreshIconButton />
</>
}/>
);
```

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 +96,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

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} />
11 changes: 3 additions & 8 deletions examples/demo/src/layout/AppBar.tsx
Original file line number Diff line number Diff line change
@@ -25,7 +25,7 @@ const ConfigurationMenu = React.forwardRef((props, ref) => {
to="/configuration"
>
<ListItemIcon>
<SettingsIcon />
<SettingsIcon fontSize="small" />
</ListItemIcon>
<ListItemText>{translate('pos.configuration')}</ListItemText>
</MenuItem>
@@ -38,17 +38,12 @@ 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 />}
>
<AppBar color="secondary" elevation={1} userMenu={<CustomUserMenu />}>
<Typography
variant="h6"
color="inherit"
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 {
308 changes: 308 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}`]: {},
}));
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}
/>
);
1 change: 1 addition & 0 deletions packages/ra-ui-materialui/src/layout/UserMenu.tsx
Original file line number Diff line number Diff line change
@@ -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';