Skip to content

Commit 94ca10d

Browse files
committed
feat: support inline menu rendering
1 parent 7864444 commit 94ca10d

File tree

9 files changed

+322
-192
lines changed

9 files changed

+322
-192
lines changed

src/components/lab/Menu/Menu.tsx

+87-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import * as React from 'react';
22

33
import {
4+
Composite,
5+
CompositeItem,
46
FloatingList,
57
FloatingTree,
68
flip,
@@ -31,13 +33,15 @@ import {MenuDivider} from './MenuDivider';
3133
import {MenuItem} from './MenuItem';
3234
import {MenuTrigger} from './MenuTrigger';
3335
import type {MenuProps} from './types';
36+
import {isComponentType} from './utils';
3437

3538
import './Menu.scss';
3639

3740
const b = block('menu2');
3841

3942
function MenuComponent({
4043
trigger,
44+
inline = false,
4145
defaultOpen,
4246
open,
4347
onOpenChange,
@@ -110,13 +114,53 @@ function MenuComponent({
110114
? trigger(anchorProps, anchorRef)
111115
: null;
112116

117+
const getItemPropsInline = React.useCallback(
118+
(userProps?: React.HTMLAttributes<HTMLElement>) => {
119+
const handleItemPointerEnter = (event: React.PointerEvent<HTMLElement>) => {
120+
userProps?.onPointerEnter?.(event);
121+
122+
const element = event.currentTarget;
123+
const index = [
124+
...(element.closest('[role="menu"]')?.querySelectorAll('[role="menuitem"]') ??
125+
[]),
126+
].indexOf(element);
127+
128+
if (
129+
!(element as HTMLButtonElement).disabled &&
130+
!element.ariaDisabled &&
131+
index >= 0
132+
) {
133+
element.focus();
134+
setActiveIndex(index);
135+
} else {
136+
setActiveIndex(null);
137+
}
138+
};
139+
140+
const handleItemPointerLeave = (event: React.PointerEvent<HTMLElement>) => {
141+
userProps?.onPointerLeave?.(event);
142+
setActiveIndex(null);
143+
};
144+
145+
return {
146+
// Clear attribute set by Floating UI Composite (we don't use it)
147+
'data-active': undefined,
148+
...userProps,
149+
onPointerEnter: handleItemPointerEnter,
150+
onPointerLeave: handleItemPointerLeave,
151+
};
152+
},
153+
[],
154+
);
155+
113156
const contextValue = React.useMemo(
114157
() => ({
158+
inline: parentMenu?.inline ?? inline,
115159
size: parentMenu?.size ?? size,
116160
activeIndex,
117-
getItemProps,
161+
getItemProps: inline ? getItemPropsInline : getItemProps,
118162
}),
119-
[parentMenu, size, activeIndex, getItemProps],
163+
[parentMenu, inline, size, activeIndex, getItemPropsInline, getItemProps],
120164
);
121165

122166
React.useEffect(() => {
@@ -161,6 +205,44 @@ function MenuComponent({
161205
}
162206
}, [trigger]);
163207

208+
if (inline) {
209+
const preparedChildren = React.Children.toArray(children).map((child, index) => {
210+
if (!React.isValidElement(child) || !isComponentType(child, 'Menu.Item')) {
211+
return child;
212+
}
213+
214+
return (
215+
<CompositeItem
216+
key={index}
217+
render={(props) => React.cloneElement(child, {...child.props, ...props})}
218+
/>
219+
);
220+
});
221+
222+
return (
223+
<MenuContext.Provider value={contextValue}>
224+
<Composite
225+
render={
226+
<div
227+
role="menu"
228+
className={b(null, className)}
229+
style={style}
230+
data-qa={qa}
231+
/>
232+
}
233+
orientation="vertical"
234+
loop={false}
235+
rtl={isRTL}
236+
// @ts-expect-error
237+
activeIndex={activeIndex}
238+
onNavigate={setActiveIndex}
239+
>
240+
{preparedChildren}
241+
</Composite>
242+
</MenuContext.Provider>
243+
);
244+
}
245+
164246
return (
165247
<React.Fragment>
166248
{anchorNode}
@@ -189,7 +271,7 @@ function MenuComponent({
189271
export function Menu(props: MenuProps) {
190272
const parentId = useFloatingParentNodeId();
191273

192-
if (parentId === null) {
274+
if (!props.inline && parentId === null) {
193275
return (
194276
<FloatingTree>
195277
<MenuComponent {...props} />
@@ -200,6 +282,8 @@ export function Menu(props: MenuProps) {
200282
return <MenuComponent {...props} />;
201283
}
202284

285+
Menu.displayName = 'Menu';
286+
203287
Menu.Trigger = MenuTrigger;
204288
Menu.Item = MenuItem;
205289
Menu.Divider = MenuDivider;

src/components/lab/Menu/MenuContext.ts

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export interface MenuContextProps {
66
size: MenuSize;
77
activeIndex: number | null;
88
getItemProps: (userProps?: React.HTMLProps<HTMLElement>) => Record<string, unknown>;
9+
inline: boolean;
910
}
1011

1112
export const MenuContext = React.createContext<MenuContextProps | null>(null);

src/components/lab/Menu/MenuDivider.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,5 @@ const b = block('menu2-divider');
77
export function MenuDivider() {
88
return <div className={b()} />;
99
}
10+
11+
MenuDivider.displayName = 'Menu.Divider';

0 commit comments

Comments
 (0)