Skip to content

Commit f233324

Browse files
authored
Merge pull request #10790 from marmelab/menu-item-keyboard-shortcuts
Add support for keyboard shortcuts to `<MenuItemLink>`
2 parents 67e6782 + eb5452b commit f233324

File tree

17 files changed

+699
-44
lines changed

17 files changed

+699
-44
lines changed

docs/Menu.md

Lines changed: 79 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -183,16 +183,35 @@ export const MyMenu = () => (
183183
);
184184
```
185185

186-
| Prop | Required | Type | Default | Description |
187-
| ------------- | -------- | -------------------- | ------- | ---------------------------------------- |
188-
| `to` | Required | `string | location` | - | The menu item's target. It is passed to a React Router [NavLink](https://reacttraining.com/react-router/web/api/NavLink) component. |
189-
| `primaryText` | Required | `ReactNode` | - | The menu content, displayed when the menu isn't minimized. |
190-
| `leftIcon` | Optional | `ReactNode` | - | The menu icon |
186+
| Prop | Required | Type | Default | Description |
187+
| -------------------------------- | -------- | -------------------- | -------------------- | ---------------------------------------- |
188+
| `to` | Required | `string | location` | - | The menu item's target. It is passed to a React Router [NavLink](https://reacttraining.com/react-router/web/api/NavLink) component. |
189+
| `primaryText` | Required | `ReactNode` | - | The menu content, displayed when the menu isn't minimized. |
190+
| `keyboardShortcut` | Optional | `string` | - | The keyboard shortcut(s) to activate this menu item |
191+
| `keyboardShortcut Representation` | Optional | `ReactNode` | `<KeyboardShortcut>` | A react node that displays the keyboard shortcut |
192+
| `leftIcon` | Optional | `ReactNode` | - | The menu icon |
193+
| `sx` | Optional | `SxProp` | - | Style overrides, powered by MUI System |
191194

192195
Additional props are passed down to [the underling Material UI `<MenuItem>` component](https://mui.com/material-ui/api/menu-item/).
193196

197+
### `to`
194198

195-
The `primaryText` prop accepts a string, that react-admin passes through the [translation utility](./Translation.md). Alternately, you can set the menu item content using the `children`, e.g. to display a badge on top of the menu item:
199+
The menu item's target. It is passed to a React Router [NavLink](https://reacttraining.com/react-router/web/api/NavLink) component.
200+
201+
```tsx
202+
// in src/MyMenu.js
203+
import { Menu } from 'react-admin';
204+
205+
export const MyMenu = () => (
206+
<Menu>
207+
<Menu.Item to="/custom-route" primaryText="Miscellaneous" />
208+
</Menu>
209+
);
210+
```
211+
212+
### `primaryText`
213+
214+
The menu content, displayed when the menu isn't minimized. It accepts a string, that react-admin passes through the [translation utility](./Translation.md). Alternately, you can set the menu item content using the `children`, e.g. to display a badge on top of the menu item:
196215

197216
```jsx
198217
import Badge from '@mui/material/Badge';
@@ -212,6 +231,50 @@ export const MyMenu = () => (
212231

213232
Note that if you use the `children` prop, you'll have to translate the menu item content yourself using [`useTranslate`](./useTranslate.md). You'll also need to provide a `primaryText` either way, because it will be rendered in the tooltip when the side menu is collapsed.
214233

234+
### `keyboardShortcut`
235+
236+
The keyboard shortcut(s) to activate this menu item. Pass a string or an array of string that defines the supported keyboard shortcuts:
237+
238+
```tsx
239+
export const MyMenu = () => (
240+
<Menu>
241+
<Menu.Item
242+
to="/sales"
243+
primaryText="Sales"
244+
// G key then S key
245+
keyboardShortcut="G>S"
246+
/>
247+
</Menu>
248+
);
249+
```
250+
251+
![A menu with keyboard shortcuts displayed](./img/menu-shortcuts.png)
252+
253+
This leverages the [react-hotkeys-hook](https://github.com/JohannesKlauss/react-hotkeys-hook) library, checkout [their documentation](https://react-hotkeys-hook.vercel.app/docs/documentation/useHotkeys/basic-usage) for more examples.
254+
255+
### `keyboardShortcutRepresentation`
256+
257+
A React node that displays the keyboard shortcut. It defaults to `<KeyboardShortcut>`. You can customize it by providing your own:
258+
259+
```tsx
260+
const CustomMenu = () => (
261+
<Menu>
262+
<Menu.Item
263+
to="/sales"
264+
primaryText="Sales"
265+
keyboardShortcut="G>S"
266+
// Render a simple textual representation of the shortcut
267+
keyboardShortcutRepresentation="G then S"
268+
/>
269+
</Menu>
270+
);
271+
```
272+
273+
![A menu with keyboard shortcuts displayed](./img/menu-custom-shortcuts.png)
274+
275+
276+
### `leftIcon`
277+
215278
The `letfIcon` prop allows setting the menu left icon.
216279

217280
```jsx
@@ -231,7 +294,16 @@ export const MyMenu = () => (
231294
);
232295
```
233296

234-
Additional props are passed down to [the underling Material UI `<MenuItem>` component](https://mui.com/material-ui/api/menu-item/).
297+
### `sx`
298+
299+
You can use the `sx` prop to customize the style of the component.
300+
301+
| Rule name | Description |
302+
|-----------------------------|---------------------------------------------------------------------|
303+
| `&.RaMenuItemLink-active` | Applied to the underlying `MuiMenuItem`'s `activeClassName` prop |
304+
| `& .RaMenuItemLink-icon` | Applied to the `ListItemIcon` component when `leftIcon` prop is set |
305+
306+
To override the style of all instances of `<MenuItemLink>` using the [application-wide style overrides](./AppTheme.md#theming-individual-components), use the `RaMenuItemLink` key.
235307

236308
**Tip**: The `<Menu.Item>` component makes use of the React Router [NavLink](https://reactrouter.com/docs/en/v6/components/nav-link) component, hence allowing to customize the active menu style. For instance, here is how to use a custom theme to show a left border for the active menu:
237309

@@ -260,15 +332,6 @@ export const theme = {
260332
};
261333
```
262334

263-
You can use the `sx` prop to customize the style of the component.
264-
265-
| Rule name | Description |
266-
|-----------------------------|---------------------------------------------------------------------|
267-
| `&.RaMenuItemLink-active` | Applied to the underlying `MuiMenuItem`'s `activeClassName` prop |
268-
| `& .RaMenuItemLink-icon` | Applied to the `ListItemIcon` component when `leftIcon` prop is set |
269-
270-
To override the style of all instances of `<MenuItemLink>` using the [application-wide style overrides](./AppTheme.md#theming-individual-components), use the `RaMenuItemLink` key.
271-
272335
## `<Menu.DashboardItem>`
273336

274337
The `<Menu.DashboardItem>` component displays a menu item for the dashboard.

docs/img/menu-custom-shortcuts.png

22.2 KB
Loading

docs/img/menu-shortcuts.png

28.1 KB
Loading

examples/simple/src/Layout.tsx

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import * as React from 'react';
22
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
3-
import { AppBar, Layout, InspectorButton, TitlePortal } from 'react-admin';
3+
import {
4+
AppBar,
5+
Layout,
6+
Menu,
7+
InspectorButton,
8+
TitlePortal,
9+
} from 'react-admin';
410
import '../assets/app.css';
511

612
const MyAppBar = () => (
@@ -10,9 +16,20 @@ const MyAppBar = () => (
1016
</AppBar>
1117
);
1218

19+
const MyMenu = () => (
20+
<Menu>
21+
<Menu.ResourceItem name="posts" keyboardShortcut="g>p" />
22+
<Menu.ResourceItem name="comments" keyboardShortcut="g>c" />
23+
<Menu.ResourceItem name="tags" keyboardShortcut="g>t" />
24+
<Menu.ResourceItem name="users" keyboardShortcut="g>u" />
25+
</Menu>
26+
);
27+
1328
export default ({ children }) => (
1429
<>
15-
<Layout appBar={MyAppBar}>{children}</Layout>
30+
<Layout appBar={MyAppBar} menu={MyMenu}>
31+
{children}
32+
</Layout>
1633
<ReactQueryDevtools
1734
initialIsOpen={false}
1835
buttonPosition="bottom-left"

jest.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ module.exports = {
2525
'/packages/create-react-admin/templates',
2626
],
2727
transformIgnorePatterns: [
28-
'[/\\\\]node_modules[/\\\\](?!(@hookform)/).+\\.(js|jsx|mjs|ts|tsx)$',
28+
'[/\\\\]node_modules[/\\\\](?!(@hookform|react-hotkeys-hook)/).+\\.(js|jsx|mjs|ts|tsx)$',
2929
],
3030
transform: {
3131
// '^.+\\.[tj]sx?$' to process js/ts with `ts-jest`

packages/ra-ui-materialui/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
"query-string": "^7.1.3",
8080
"react-dropzone": "^14.2.3",
8181
"react-error-boundary": "^4.0.13",
82+
"react-hotkeys-hook": "^5.1.0",
8283
"react-transition-group": "^4.4.5"
8384
},
8485
"gitHead": "587df4c27bfcec4a756df4f95e5fc14728dfc0d7"
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import * as React from 'react';
2+
import {
3+
createTheme,
4+
List,
5+
ListItem,
6+
ListItemText,
7+
Paper,
8+
ThemeProvider,
9+
} from '@mui/material';
10+
import { KeyboardShortcut } from './KeyboardShortcut';
11+
import { defaultTheme } from './theme';
12+
13+
export default {
14+
title: 'ra-ui-materialui/KeyboardShortcut',
15+
};
16+
17+
const Wrapper = ({ children }: { children: React.ReactNode }) => (
18+
<ThemeProvider theme={createTheme(defaultTheme)}>
19+
<Paper sx={{ maxWidth: '60%', mx: 'auto', p: 2, mt: 2 }}>
20+
{children}
21+
</Paper>
22+
</ThemeProvider>
23+
);
24+
25+
export const Default = () => (
26+
<Wrapper>
27+
<List>
28+
<ListItem
29+
secondaryAction={<KeyboardShortcut keyboardShortcut="meta+K" />}
30+
>
31+
<ListItemText primary="meta and k" />
32+
</ListItem>
33+
<ListItem
34+
secondaryAction={
35+
<KeyboardShortcut keyboardShortcut="shift+a" />
36+
}
37+
>
38+
<ListItemText primary="shift and a" />
39+
</ListItem>
40+
<ListItem
41+
secondaryAction={<KeyboardShortcut keyboardShortcut="mod+B" />}
42+
>
43+
<ListItemText primary="mod and b" />
44+
</ListItem>
45+
<ListItem
46+
secondaryAction={<KeyboardShortcut keyboardShortcut="alt+F" />}
47+
>
48+
<ListItemText primary="alt and f" />
49+
</ListItem>
50+
<ListItem
51+
secondaryAction={
52+
<KeyboardShortcut keyboardShortcut="escape+F" />
53+
}
54+
>
55+
<ListItemText primary="escape and f" />
56+
</ListItem>
57+
<ListItem
58+
secondaryAction={<KeyboardShortcut keyboardShortcut="esc+F" />}
59+
>
60+
<ListItemText primary="escape (written esc) and f" />
61+
</ListItem>
62+
<ListItem
63+
secondaryAction={
64+
<KeyboardShortcut keyboardShortcut="shift+up" />
65+
}
66+
>
67+
<ListItemText primary="shift and up" />
68+
</ListItem>
69+
<ListItem
70+
secondaryAction={<KeyboardShortcut keyboardShortcut="ctrl+d" />}
71+
>
72+
<ListItemText primary="ctrl and d" />
73+
</ListItem>
74+
<ListItem
75+
secondaryAction={
76+
<KeyboardShortcut keyboardShortcut="meta+K>X" />
77+
}
78+
>
79+
<ListItemText primary="Meta and k then x" />
80+
</ListItem>
81+
<ListItem
82+
secondaryAction={
83+
<KeyboardShortcut keyboardShortcut="space>a" />
84+
}
85+
>
86+
<ListItemText primary="Space then a" />
87+
</ListItem>
88+
<ListItem
89+
secondaryAction={<KeyboardShortcut keyboardShortcut="g>g" />}
90+
>
91+
<ListItemText primary="g then g" />
92+
</ListItem>
93+
<ListItem
94+
secondaryAction={
95+
<KeyboardShortcut keyboardShortcut="ctrl+shift+a+c" />
96+
}
97+
>
98+
<ListItemText primary="ctrl and shift and a and c" />
99+
</ListItem>
100+
</List>
101+
</Wrapper>
102+
);

0 commit comments

Comments
 (0)