Skip to content

Commit 2c7fc9a

Browse files
michaldudakatomiks
andauthored
[menubar] Create the Menubar component (#1684)
Co-authored-by: atomiks <[email protected]>
1 parent 04c08fb commit 2c7fc9a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+2452
-325
lines changed

docs/reference/generated/menu-popup.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
},
3131
"data-instant": {
3232
"description": "Present if animations should be instant.",
33-
"type": "'click' | 'dismiss'"
33+
"type": "'click' | 'dismiss' | 'group'"
3434
},
3535
"data-side": {
3636
"description": "Indicates which side the popup is positioned relative to the trigger.",

docs/reference/generated/menubar.json

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"name": "Menubar",
3+
"description": "The container for menus.",
4+
"props": {
5+
"modal": {
6+
"type": "boolean",
7+
"default": "true",
8+
"description": "Whether the menubar is modal."
9+
},
10+
"disabled": {
11+
"type": "boolean",
12+
"default": "false",
13+
"description": "Whether the whole menubar is disabled."
14+
},
15+
"loop": {
16+
"type": "boolean",
17+
"default": "true",
18+
"description": "Whether to loop keyboard focus back to the first item\nwhen the end of the list is reached while using the arrow keys."
19+
},
20+
"orientation": {
21+
"type": "MenuOrientation",
22+
"default": "'horizontal'",
23+
"description": "The orientation of the menubar."
24+
},
25+
"className": {
26+
"type": "string | ((state: Menubar.State) => string)",
27+
"description": "CSS class applied to the element, or a function that\nreturns a class based on the component’s state."
28+
},
29+
"render": {
30+
"type": "ReactElement | ((props: HTMLProps, state: Menubar.State) => ReactElement)",
31+
"description": "Allows you to replace the component’s HTML element\nwith a different tag, or compose it with another component.\n\nAccepts a `ReactElement` or a function that returns the element to render."
32+
}
33+
},
34+
"dataAttributes": {},
35+
"cssVariables": {}
36+
}
Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
1+
'use client';
2+
import * as React from 'react';
3+
import { Menu } from '@base-ui-components/react/menu';
4+
import { styled } from '@mui/system';
5+
6+
export default function NestedMenu() {
7+
const createHandleMenuClick = (menuItem: string) => {
8+
return () => {
9+
console.log(`Clicked on ${menuItem}`);
10+
};
11+
};
12+
13+
const containerRef = React.useRef<HTMLDivElement>(null);
14+
15+
return (
16+
<Container>
17+
<div ref={containerRef} />
18+
<Menu.Root orientation="horizontal" open modal={false}>
19+
<Menu.Portal>
20+
<Menu.Positioner
21+
side="bottom"
22+
align="start"
23+
sideOffset={6}
24+
anchor={containerRef}
25+
>
26+
<MenuRootPopup>
27+
<Menu.Root openOnHover={false}>
28+
<SubmenuTrigger>Text color</SubmenuTrigger>
29+
<Menu.Portal>
30+
<Menu.Positioner align="start" side="bottom" sideOffset={12}>
31+
<MenuPopup>
32+
<MenuItem onClick={createHandleMenuClick('Text color/Black')}>
33+
Black
34+
</MenuItem>
35+
<MenuItem
36+
onClick={createHandleMenuClick('Text color/Dark grey')}
37+
>
38+
Dark grey
39+
</MenuItem>
40+
<MenuItem onClick={createHandleMenuClick('Text color/Accent')}>
41+
Accent
42+
</MenuItem>
43+
</MenuPopup>
44+
</Menu.Positioner>
45+
</Menu.Portal>
46+
</Menu.Root>
47+
48+
<Menu.Root openOnHover={false}>
49+
<SubmenuTrigger>Style</SubmenuTrigger>
50+
<Menu.Portal>
51+
<Menu.Positioner align="start" side="bottom" sideOffset={12}>
52+
<MenuPopup>
53+
<Menu.Root>
54+
<SubmenuTrigger>Heading</SubmenuTrigger>
55+
<Menu.Portal>
56+
<Menu.Positioner
57+
align="start"
58+
side="right"
59+
sideOffset={12}
60+
>
61+
<MenuPopup>
62+
<MenuItem
63+
onClick={createHandleMenuClick(
64+
'Style/Heading/Level 1',
65+
)}
66+
>
67+
Level 1
68+
</MenuItem>
69+
<MenuItem
70+
onClick={createHandleMenuClick(
71+
'Style/Heading/Level 2',
72+
)}
73+
>
74+
Level 2
75+
</MenuItem>
76+
<MenuItem
77+
onClick={createHandleMenuClick(
78+
'Style/Heading/Level 3',
79+
)}
80+
>
81+
Level 3
82+
</MenuItem>
83+
</MenuPopup>
84+
</Menu.Positioner>
85+
</Menu.Portal>
86+
</Menu.Root>
87+
<MenuItem onClick={createHandleMenuClick('Style/Paragraph')}>
88+
Paragraph
89+
</MenuItem>
90+
<Menu.Root disabled>
91+
<SubmenuTrigger>List</SubmenuTrigger>
92+
<Menu.Portal>
93+
<Menu.Positioner
94+
align="start"
95+
side="bottom"
96+
sideOffset={12}
97+
>
98+
<MenuPopup>
99+
<MenuItem
100+
onClick={createHandleMenuClick('Style/List/Ordered')}
101+
>
102+
Ordered
103+
</MenuItem>
104+
<MenuItem
105+
onClick={createHandleMenuClick(
106+
'Style/List/Unordered',
107+
)}
108+
>
109+
Unordered
110+
</MenuItem>
111+
</MenuPopup>
112+
</Menu.Positioner>
113+
</Menu.Portal>
114+
</Menu.Root>
115+
</MenuPopup>
116+
</Menu.Positioner>
117+
</Menu.Portal>
118+
</Menu.Root>
119+
</MenuRootPopup>
120+
</Menu.Positioner>
121+
</Menu.Portal>
122+
</Menu.Root>
123+
</Container>
124+
);
125+
}
126+
127+
const grey = {
128+
50: '#F3F6F9',
129+
100: '#E5EAF2',
130+
200: '#DAE2ED',
131+
300: '#C7D0DD',
132+
400: '#B0B8C4',
133+
500: '#9DA8B7',
134+
600: '#6B7A90',
135+
700: '#434D5B',
136+
800: '#303740',
137+
900: '#1C2025',
138+
};
139+
140+
const MenuPopup = styled(Menu.Popup)(
141+
({ theme }) => `
142+
font-family: 'IBM Plex Sans', sans-serif;
143+
font-size: 0.875rem;
144+
box-sizing: border-box;
145+
padding: 6px;
146+
min-width: 200px;
147+
border-radius: 12px;
148+
overflow: visible;
149+
outline: 0;
150+
background: ${theme.palette.mode === 'dark' ? grey[900] : '#fff'};
151+
border: 1px solid ${theme.palette.mode === 'dark' ? grey[700] : grey[200]};
152+
color: ${theme.palette.mode === 'dark' ? grey[300] : grey[900]};
153+
box-shadow: 0 4px 30px ${theme.palette.mode === 'dark' ? grey[900] : grey[200]};
154+
z-index: 1;
155+
transform-origin: var(--transform-origin);
156+
opacity: 1;
157+
transform: scale(1, 1);
158+
transition: opacity 100ms ease-in, transform 100ms ease-in;
159+
160+
&[data-nested] {
161+
margin-top: -6px;
162+
}
163+
164+
&[data-starting-style] {
165+
opacity: 0;
166+
transform: scale(0.8);
167+
}
168+
169+
&[data-ending-style] {
170+
opacity: 0;
171+
transform: scale(0.8);
172+
transition: opacity 200ms ease-in, transform 200ms ease-in;
173+
}
174+
`,
175+
);
176+
177+
const MenuRootPopup = styled(Menu.Popup)(`
178+
font-family: 'IBM Plex Sans', sans-serif;
179+
font-size: 0.875rem;
180+
box-sizing: border-box;
181+
padding: 6px;
182+
min-width: 200px;
183+
border-radius: 12px;
184+
overflow: visible;
185+
outline: 0;
186+
background: '#fff';
187+
border: 1px solid grey[200];
188+
color: grey[900];
189+
box-shadow: 0 4px 30px grey[200];
190+
z-index: 1;
191+
transform-origin: var(--transform-origin);
192+
opacity: 1;
193+
transform: scale(1, 1);
194+
transition: opacity 100ms ease-in, transform 100ms ease-in;
195+
display: flex;
196+
197+
&[data-nested] {
198+
margin-top: -6px;
199+
}
200+
201+
&[data-starting-style] {
202+
opacity: 0;
203+
transform: scale(0.8);
204+
}
205+
206+
&[data-ending-style] {
207+
opacity: 0;
208+
transform: scale(0.8);
209+
transition: opacity 200ms ease-in, transform 200ms ease-in;
210+
}
211+
`);
212+
213+
const MenuItem = styled(Menu.Item)(
214+
({ theme }) => `
215+
list-style: none;
216+
padding: 8px;
217+
border-radius: 8px;
218+
cursor: default;
219+
user-select: none;
220+
221+
&:last-of-type {
222+
border-bottom: none;
223+
}
224+
225+
&:focus,
226+
&:hover {
227+
background-color: ${theme.palette.mode === 'dark' ? grey[800] : grey[100]};
228+
color: ${theme.palette.mode === 'dark' ? grey[300] : grey[900]};
229+
}
230+
231+
&:focus-visible {
232+
outline: none;
233+
}
234+
235+
&[data-disabled] {
236+
color: ${theme.palette.mode === 'dark' ? grey[700] : grey[400]};
237+
}
238+
`,
239+
);
240+
241+
const SubmenuTrigger = styled(Menu.SubmenuTrigger)(
242+
({ theme }) => `
243+
list-style: none;
244+
padding: 8px;
245+
border-radius: 8px;
246+
cursor: default;
247+
user-select: none;
248+
249+
&:last-of-type {
250+
border-bottom: none;
251+
}
252+
253+
&::after {
254+
content: '›';
255+
float: right;
256+
}
257+
258+
&[data-popup-open] {
259+
background-color: ${theme.palette.mode === 'dark' ? grey[900] : grey[50]};
260+
color: ${theme.palette.mode === 'dark' ? grey[300] : grey[900]};
261+
}
262+
263+
&:focus,
264+
&:hover {
265+
background-color: ${theme.palette.mode === 'dark' ? grey[800] : grey[100]};
266+
color: ${theme.palette.mode === 'dark' ? grey[300] : grey[900]};
267+
}
268+
269+
&:focus-visible {
270+
outline: none;
271+
}
272+
273+
&[data-disabled] {
274+
color: ${theme.palette.mode === 'dark' ? grey[700] : grey[400]};
275+
}
276+
`,
277+
);
278+
279+
const Container = styled('div')`
280+
display: flex;
281+
min-height: 110vh;
282+
box-sizing: border-box;
283+
align-items: center;
284+
gap: 20px;
285+
`;

docs/src/app/(private)/experiments/menu/menu.module.css

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,13 +89,13 @@
8989
rotate: 0deg;
9090
}
9191

92-
&[data-side='left'] {
93-
right: -13px;
92+
&[data-side='inline-start'] {
93+
inset-inline-end: -13px;
9494
rotate: 90deg;
9595
}
9696

97-
&[data-side='right'] {
98-
left: -13px;
97+
&[data-side='inline-end'] {
98+
inset-inline-start: -13px;
9999
rotate: -90deg;
100100
}
101101
}

0 commit comments

Comments
 (0)