Skip to content

Commit 91ea869

Browse files
janhasseltay1orjonespreetibansalui
authored
feat(menu): deprecate props.mode / always support full capabilities + icons (#18153)
* feat(menu): deprecate props.mode * test(menu): remove mode-specific tests * docs(menu-button): add nested demo * fix(menu-button): prevent clipping of submenus --------- Co-authored-by: Taylor Jones <tay1orjones@users.noreply.github.com> Co-authored-by: Taylor Jones <taylor.jones826@gmail.com> Co-authored-by: Preeti Bansal <146315451+preetibansalui@users.noreply.github.com>
1 parent b8e0994 commit 91ea869

File tree

14 files changed

+167
-235
lines changed

14 files changed

+167
-235
lines changed

packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4658,15 +4658,7 @@ Map {
46584658
"menuAlignment": Object {
46594659
"type": "string",
46604660
},
4661-
"mode": Object {
4662-
"args": Array [
4663-
Array [
4664-
"full",
4665-
"basic",
4666-
],
4667-
],
4668-
"type": "oneOf",
4669-
},
4661+
"mode": [Function],
46704662
"onClose": Object {
46714663
"type": "func",
46724664
},

packages/react/src/components/ComboButton/ComboButton-test.js

Lines changed: 0 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -186,56 +186,6 @@ describe('ComboButton', () => {
186186
).toHaveTextContent(/^Additional action$/);
187187
});
188188

189-
it('warns when MenuItemSelectable is used in children', async () => {
190-
const spy = jest.spyOn(console, 'warn').mockImplementation(() => {});
191-
192-
render(
193-
<ComboButton label="Primary action">
194-
<MenuItemSelectable label="Option" />
195-
</ComboButton>
196-
);
197-
198-
await userEvent.click(screen.getAllByRole('button')[1]);
199-
200-
expect(spy).toHaveBeenCalled();
201-
spy.mockRestore();
202-
});
203-
204-
it('warns when MenuItemRadioGroup is used in children', async () => {
205-
const spy = jest.spyOn(console, 'warn').mockImplementation(() => {});
206-
207-
render(
208-
<ComboButton label="Primary action">
209-
<MenuItemRadioGroup
210-
label="Options"
211-
items={['Option 1', 'Option 2']}
212-
/>
213-
</ComboButton>
214-
);
215-
216-
await userEvent.click(screen.getAllByRole('button')[1]);
217-
218-
expect(spy).toHaveBeenCalled();
219-
spy.mockRestore();
220-
});
221-
222-
it('warns when a nested Menu is used in children', async () => {
223-
const spy = jest.spyOn(console, 'warn').mockImplementation(() => {});
224-
225-
render(
226-
<ComboButton label="Primary action">
227-
<MenuItem label="Submenu">
228-
<MenuItem label="Action" />
229-
</MenuItem>
230-
</ComboButton>
231-
);
232-
233-
await userEvent.click(screen.getAllByRole('button')[1]);
234-
235-
expect(spy).toHaveBeenCalled();
236-
spy.mockRestore();
237-
});
238-
239189
it('supports ellipsis in ComboButton by checking the className', async () => {
240190
render(
241191
<ComboButton label="Primary action super long text to enable ellipsis">

packages/react/src/components/ComboButton/index.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,6 @@ const ComboButton = React.forwardRef<HTMLDivElement, ComboButtonProps>(
235235
ref={refs.setFloating}
236236
id={id}
237237
label={t('carbon.combo-button.additional-actions')}
238-
mode="basic"
239238
size={size}
240239
open={open}
241240
onClose={handleClose}>

packages/react/src/components/ContextMenu/useContextMenu.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ export interface ContextMenuProps {
1414
x: number;
1515
y: number;
1616
onClose: () => void;
17-
mode: string;
1817
}
1918

2019
/**
@@ -62,7 +61,6 @@ function useContextMenu(trigger: TriggerType = document): ContextMenuProps {
6261
x: position[0],
6362
y: position[1],
6463
onClose,
65-
mode: 'full',
6664
};
6765
}
6866

packages/react/src/components/Menu/Menu-test.js

Lines changed: 0 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -83,21 +83,6 @@ describe('Menu', () => {
8383
expect(document.querySelector('.custom-class')).toBeInTheDocument();
8484
document.body.removeChild(el);
8585
});
86-
87-
it('warns about nested menus in basic mode', async () => {
88-
const spy = jest.spyOn(console, 'warn').mockImplementation(() => {});
89-
90-
render(
91-
<Menu open mode="basic">
92-
<MenuItem label="Submenu">
93-
<MenuItem label="Item" />
94-
</MenuItem>
95-
</Menu>
96-
);
97-
await waitForPosition();
98-
expect(spy).toHaveBeenCalled();
99-
spy.mockRestore();
100-
});
10186
});
10287

10388
describe('Submenu behavior', () => {
@@ -224,35 +209,6 @@ describe('MenuItem', () => {
224209

225210
expect(screen.getByText('item')).toBeInTheDocument();
226211
});
227-
228-
it('warns about MenuItemSelectable in basic mode', () => {
229-
const spy = jest.spyOn(console, 'warn').mockImplementation(() => {});
230-
231-
render(
232-
<Menu open mode="basic">
233-
<MenuItemSelectable label="Option" />
234-
</Menu>
235-
);
236-
237-
expect(spy).toHaveBeenCalled();
238-
spy.mockRestore();
239-
});
240-
241-
it('warns about MenuItemRadioGroup in basic mode', () => {
242-
const spy = jest.spyOn(console, 'warn').mockImplementation(() => {});
243-
244-
render(
245-
<Menu open mode="basic">
246-
<MenuItemRadioGroup
247-
label="Options"
248-
items={['Option 1', 'Option 2']}
249-
/>
250-
</Menu>
251-
);
252-
253-
expect(spy).toHaveBeenCalled();
254-
spy.mockRestore();
255-
});
256212
});
257213

258214
it('should call MenuItemRadioGroup onChange once', async () => {

packages/react/src/components/Menu/Menu.stories.js

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,16 @@
88
import React from 'react';
99
import { action } from '@storybook/addon-actions';
1010

11+
import {
12+
Copy,
13+
Cut,
14+
FolderShared,
15+
Paste,
16+
TextBold,
17+
TextItalic,
18+
TrashCan,
19+
} from '@carbon/icons-react';
20+
1121
import {
1222
Menu,
1323
MenuItem,
@@ -49,7 +59,7 @@ export const Default = (args) => {
4959

5060
return (
5161
<Menu {...args} target={target} x={document?.dir === 'rtl' ? 250 : 0}>
52-
<MenuItem label="Share with">
62+
<MenuItem label="Share with" renderIcon={FolderShared}>
5363
<MenuItemRadioGroup
5464
label="Share with"
5565
items={['None', 'Product team', 'Organization', 'Company']}
@@ -58,17 +68,40 @@ export const Default = (args) => {
5868
/>
5969
</MenuItem>
6070
<MenuItemDivider />
61-
<MenuItem label="Cut" shortcut="⌘X" onClick={itemOnClick} />
62-
<MenuItem label="Copy" shortcut="⌘C" onClick={itemOnClick} />
63-
<MenuItem label="Paste" shortcut="⌘V" disabled onClick={itemOnClick} />
71+
<MenuItem
72+
label="Cut"
73+
shortcut="⌘X"
74+
onClick={itemOnClick}
75+
renderIcon={Cut}
76+
/>
77+
<MenuItem
78+
label="Copy"
79+
shortcut="⌘C"
80+
onClick={itemOnClick}
81+
renderIcon={Copy}
82+
/>
83+
<MenuItem
84+
label="Paste"
85+
shortcut="⌘V"
86+
disabled
87+
onClick={itemOnClick}
88+
renderIcon={Paste}
89+
/>
6490
<MenuItemDivider />
6591
<MenuItemGroup label="Font style">
6692
<MenuItemSelectable
6793
label="Bold"
94+
shortcut="⌘B"
6895
defaultSelected
6996
onChange={selectableOnChange}
97+
renderIcon={TextBold}
98+
/>
99+
<MenuItemSelectable
100+
label="Italic"
101+
shortcut="⌘I"
102+
onChange={selectableOnChange}
103+
renderIcon={TextItalic}
70104
/>
71-
<MenuItemSelectable label="Italic" onChange={selectableOnChange} />
72105
</MenuItemGroup>
73106
<MenuItemDivider />
74107
<MenuItemRadioGroup
@@ -83,6 +116,7 @@ export const Default = (args) => {
83116
shortcut="⌫"
84117
kind="danger"
85118
onClick={itemOnClick}
119+
renderIcon={TrashCan}
86120
/>
87121
</Menu>
88122
);

packages/react/src/components/Menu/Menu.tsx

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import { createPortal } from 'react-dom';
2323
import { keys, match } from '../../internal/keyboard';
2424
import { useMergedRefs } from '../../internal/useMergedRefs';
2525
import { usePrefix } from '../../internal/usePrefix';
26-
import { warning } from '../../internal/warning.js';
26+
import deprecate from '../../prop-types/deprecate';
2727

2828
import { MenuContext, menuReducer } from './MenuContext';
2929
import { useLayoutDirection } from '../LayoutDirection';
@@ -57,6 +57,7 @@ export interface MenuProps extends React.HTMLAttributes<HTMLUListElement> {
5757
menuAlignment?: string;
5858

5959
/**
60+
* @deprecated Menus now always support both icons as well as selectable items and nesting.
6061
* The mode of this menu. Defaults to full.
6162
* `full` supports nesting and selectable menu items, but no icons.
6263
* `basic` supports icons but no nesting or selectable menu items.
@@ -110,7 +111,7 @@ const Menu = forwardRef<HTMLUListElement, MenuProps>(function Menu(
110111
containerRef,
111112
label,
112113
menuAlignment,
113-
mode = 'full',
114+
mode,
114115
onClose,
115116
onOpen,
116117
open,
@@ -132,19 +133,11 @@ const Menu = forwardRef<HTMLUListElement, MenuProps>(function Menu(
132133

133134
const isRoot = context.state.isRoot;
134135

135-
if (context.state.mode === 'basic' && !isRoot) {
136-
warning(
137-
false,
138-
'Nested menus are not supported when the menu is in "basic" mode.'
139-
);
140-
}
141-
142136
const menuSize = isRoot ? size : context.state.size;
143137

144138
const [childState, childDispatch] = useReducer(menuReducer, {
145139
...context.state,
146140
isRoot: false,
147-
mode,
148141
size,
149142
requestCloseRoot: isRoot ? handleClose : context.state.requestCloseRoot,
150143
});
@@ -424,6 +417,8 @@ const Menu = forwardRef<HTMLUListElement, MenuProps>(function Menu(
424417
[`${prefix}--menu--shown`]:
425418
(open && !legacyAutoalign) || (position[0] >= 0 && position[1] >= 0),
426419
[`${prefix}--menu--with-icons`]: childContext.state.hasIcons,
420+
[`${prefix}--menu--with-selectable-items`]:
421+
childContext.state.hasSelectableItems,
427422
[`${prefix}--autoalign`]: !legacyAutoalign,
428423
}
429424
);
@@ -474,13 +469,17 @@ Menu.propTypes = {
474469
menuAlignment: PropTypes.string,
475470

476471
/**
472+
* **Deprecated**: Menus now always support both icons as well as selectable items and nesting.
477473
* The mode of this menu. Defaults to full.
478474
* `full` supports nesting and selectable menu items, but no icons.
479475
* `basic` supports icons but no nesting or selectable menu items.
480476
*
481477
* **This prop is not intended for use and will be set by the respective implementation (like useContextMenu, MenuButton, and ComboButton).**
482478
*/
483-
mode: PropTypes.oneOf(['full', 'basic']),
479+
mode: deprecate(
480+
PropTypes.oneOf(['full', 'basic']),
481+
'Menus now always support both icons as well as selectable items and nesting.'
482+
),
484483

485484
/**
486485
* Provide an optional function to be called when the Menu should be closed.

packages/react/src/components/Menu/MenuContext.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,23 @@
88
import { createContext, KeyboardEvent, RefObject } from 'react';
99

1010
type ActionType = {
11-
type: 'enableIcons' | 'registerItem';
11+
type: 'enableIcons' | 'enableSelectableItems' | 'registerItem';
1212
payload: any;
1313
};
1414

1515
type StateType = {
1616
isRoot: boolean;
17-
mode: 'full' | 'basic';
1817
hasIcons: boolean;
18+
hasSelectableItems: boolean;
1919
size: 'xs' | 'sm' | 'md' | 'lg' | null;
2020
items: any[];
2121
requestCloseRoot: (e: Pick<KeyboardEvent<HTMLUListElement>, 'type'>) => void;
2222
};
2323

2424
const menuDefaultState: StateType = {
2525
isRoot: true,
26-
mode: 'full',
2726
hasIcons: false,
27+
hasSelectableItems: false,
2828
size: null,
2929
items: [],
3030
requestCloseRoot: () => {},
@@ -37,6 +37,11 @@ function menuReducer(state: StateType, action: ActionType) {
3737
...state,
3838
hasIcons: true,
3939
};
40+
case 'enableSelectableItems':
41+
return {
42+
...state,
43+
hasSelectableItems: true,
44+
};
4045
case 'registerItem':
4146
return {
4247
...state,
@@ -48,7 +53,7 @@ function menuReducer(state: StateType, action: ActionType) {
4853
}
4954

5055
type DispatchFuncProps = {
51-
type: 'registerItem' | 'enableIcons';
56+
type: ActionType['type'];
5257
payload: {
5358
ref: RefObject<HTMLLIElement>;
5459
disabled: boolean;

0 commit comments

Comments
 (0)