Skip to content

Commit 0b5098f

Browse files
authored
Merge pull request #445 from mashmatrix/fix/dropdownmenu-submenu-toggle
Enhance DropdownMenu Component to Control Nested Submenu Visibility
2 parents 1988fbd + 65f2d00 commit 0b5098f

File tree

3 files changed

+85
-14
lines changed

3 files changed

+85
-14
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "react-lightning-design-system",
3-
"version": "5.6.1",
3+
"version": "5.6.2",
44
"description": "Salesforce Lightning Design System components built with React",
55
"main": "lib/scripts/index.js",
66
"module": "module/scripts/index.js",

src/scripts/DropdownMenu.tsx

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import React, {
1212
useMemo,
1313
ReactNode,
1414
useState,
15+
useId,
1516
} from 'react';
1617
import classnames from 'classnames';
1718
import { Icon } from './Icon';
@@ -68,6 +69,18 @@ export const DropdownMenuHandlerContext = createContext<DropdownMenuHandler>(
6869
{}
6970
);
7071

72+
type OpenSubmenuContext = {
73+
openSubmenuKeys: {
74+
[key: string]: { isOpen: boolean; level: number } | undefined;
75+
};
76+
handleSubmenuOpen: (key: string, level: number) => void;
77+
};
78+
79+
export const OpenSubmenuContext = createContext<OpenSubmenuContext>({
80+
openSubmenuKeys: {},
81+
handleSubmenuOpen: () => {},
82+
});
83+
7184
/**
7285
*
7386
*/
@@ -82,6 +95,7 @@ export type DropdownMenuItemProps = {
8295
onClick?: (e: React.SyntheticEvent) => void;
8396
submenu?: ReactNode;
8497
submenuItems?: Array<{ key: string | number } & DropdownMenuItemProps>;
98+
level?: number;
8599
} & Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'onClick'>;
86100

87101
/**
@@ -104,13 +118,18 @@ export const DropdownMenuItem: FC<DropdownMenuItemProps> = (props) => {
104118
submenu: submenu_,
105119
submenuItems,
106120
children,
121+
level = 0,
107122
...rprops
108123
} = props;
109124

110125
const { onMenuSelect, onMenuBlur, onMenuFocus } = useContext(
111126
DropdownMenuHandlerContext
112127
);
113128

129+
const { openSubmenuKeys, handleSubmenuOpen } = useContext(OpenSubmenuContext);
130+
131+
const submenuKey = useId();
132+
114133
const onKeyDown = useEventCallback((e: KeyboardEvent<HTMLAnchorElement>) => {
115134
if (e.keyCode === 13 || e.keyCode === 32) {
116135
// return or space
@@ -163,7 +182,7 @@ export const DropdownMenuItem: FC<DropdownMenuItemProps> = (props) => {
163182
const onMenuItemClick = useEventCallback(
164183
(e: SyntheticEvent<HTMLAnchorElement>) => {
165184
if (submenu) {
166-
setSubmenuExpanded((expanded) => !expanded);
185+
handleSubmenuOpen(submenuKey, level + 1);
167186
return;
168187
}
169188
onClick?.(e);
@@ -192,11 +211,12 @@ export const DropdownMenuItem: FC<DropdownMenuItemProps> = (props) => {
192211
(submenuItems ? (
193212
<DropdownSubmenu label={label}>
194213
{submenuItems?.map(({ key, ...itemProps }) => (
195-
<DropdownMenuItem key={key} {...itemProps} />
214+
<DropdownMenuItem key={key} level={level + 1} {...itemProps} />
196215
))}
197216
</DropdownSubmenu>
198217
) : undefined);
199-
const [submenuExpanded, setSubmenuExpanded] = useState(false);
218+
219+
const submenuExpanded = openSubmenuKeys[submenuKey]?.isOpen ?? false;
200220

201221
const menuItemClass = classnames(
202222
'slds-dropdown__item',
@@ -340,6 +360,24 @@ const DropdownMenuInner: FC<DropdownMenuProps & AutoAlignInjectedProps> = (
340360
}),
341361
[onBlur, onFocus, onMenuSelect]
342362
);
363+
364+
const [openSubmenuKeys, setOpenSubmenuKeys] = useState<{
365+
[key: string]: { isOpen: boolean; level: number };
366+
}>({});
367+
368+
const handleSubmenuOpen = (key: string, level: number) => {
369+
setOpenSubmenuKeys((prevState) => {
370+
const newState = { ...prevState };
371+
Object.keys(newState).forEach((submenuKey) => {
372+
if (newState[submenuKey].level >= level && key !== submenuKey) {
373+
newState[submenuKey].isOpen = false;
374+
}
375+
});
376+
newState[key] = { isOpen: !newState[key]?.isOpen, level };
377+
return newState;
378+
});
379+
};
380+
343381
return (
344382
<div
345383
className={dropdownClassNames}
@@ -354,7 +392,11 @@ const DropdownMenuInner: FC<DropdownMenuProps & AutoAlignInjectedProps> = (
354392
{header ? <MenuHeader>{header}</MenuHeader> : null}
355393
<ul className='slds-dropdown__list' role='menu'>
356394
<DropdownMenuHandlerContext.Provider value={handlers}>
357-
{children}
395+
<OpenSubmenuContext.Provider
396+
value={{ openSubmenuKeys, handleSubmenuOpen }}
397+
>
398+
{children}
399+
</OpenSubmenuContext.Provider>
358400
</DropdownMenuHandlerContext.Provider>
359401
</ul>
360402
</div>

stories/DropdownMenu.stories.tsx

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -120,19 +120,48 @@ export const WithSubmenu: StoryObj<StoryProps> = {
120120
children: 'Menu Item Three',
121121
submenuItems: [
122122
{
123-
eventKey: 21,
124-
key: 21,
123+
eventKey: 31,
124+
key: 31,
125125
label: 'Menu Item Three - One',
126+
submenuItems: [
127+
{
128+
eventKey: 311,
129+
key: 311,
130+
label: 'Menu Item Three - One - One',
131+
},
132+
{
133+
eventKey: 312,
134+
key: 312,
135+
label: 'Menu Item Three - One - Two',
136+
},
137+
{
138+
eventKey: 313,
139+
key: 313,
140+
label: 'Menu Item Three - One - Three',
141+
},
142+
],
126143
},
127144
{
128-
eventKey: 22,
129-
key: 22,
145+
eventKey: 32,
146+
key: 32,
130147
label: 'Menu Item Three - Two',
131-
},
132-
{
133-
eventKey: 23,
134-
key: 23,
135-
label: 'Menu Item Three - Three',
148+
submenuItems: [
149+
{
150+
eventKey: 321,
151+
key: 321,
152+
label: 'Menu Item Three - Two - One',
153+
},
154+
{
155+
eventKey: 322,
156+
key: 322,
157+
label: 'Menu Item Three - Two - Two',
158+
},
159+
{
160+
eventKey: 323,
161+
key: 323,
162+
label: 'Menu Item Three - Two - Three',
163+
},
164+
],
136165
},
137166
],
138167
},

0 commit comments

Comments
 (0)