@fluentui/react-menu 9.2.3 → 9.3.1
Sign up to get free protection for your applications and to get access to all the features.
- package/CHANGELOG.json +103 -1
- package/CHANGELOG.md +32 -2
- package/dist/index.d.ts +7 -1
- package/lib/components/Menu/useMenu.js +11 -17
- package/lib/components/Menu/useMenu.js.map +1 -1
- package/lib/components/MenuTrigger/MenuTrigger.types.js.map +1 -1
- package/lib/components/MenuTrigger/useMenuTrigger.js +7 -5
- package/lib/components/MenuTrigger/useMenuTrigger.js.map +1 -1
- package/lib-commonjs/components/Menu/useMenu.js +11 -17
- package/lib-commonjs/components/Menu/useMenu.js.map +1 -1
- package/lib-commonjs/components/MenuTrigger/useMenuTrigger.js +7 -5
- package/lib-commonjs/components/MenuTrigger/useMenuTrigger.js.map +1 -1
- package/package.json +19 -18
- package/Spec.md +0 -1039
package/Spec.md
DELETED
@@ -1,1039 +0,0 @@
|
|
1
|
-
# Menu
|
2
|
-
|
3
|
-
## Background
|
4
|
-
|
5
|
-
### Definition
|
6
|
-
|
7
|
-
This spec defines the default function of a `Menu` as an interactive component that displays a list of options that can be represented by a range possible states. Possible variants are defined in [the relevant section](#variants)
|
8
|
-
|
9
|
-
The `Menu` should be displayed on a temporary surface that interrupts the normal flow of content. The temporary surface should be triggered by an external user action such as (but not limited to) a click on a button or other UI control.
|
10
|
-
|
11
|
-
The interactions that result in the dismiss/removal of the `Menu` component should be configurable.
|
12
|
-
|
13
|
-
## Prior art
|
14
|
-
|
15
|
-
As a part of the spec definitions in Fluent UI, a research effort has been made through [Open UI](https://open-ui.org/). The current research proposal is available as an open source contribution undergoing review ([research proposal](https://github.com/WICG/open-ui/pull/249))
|
16
|
-
|
17
|
-
## Comparison of `@fluentui/react` and `@fluentui/react-northstar`
|
18
|
-
|
19
|
-
- All mentions of v7 or v8 == `@fluentui/react` ([docsite](https://developer.microsoft.com/en-us/fluentui#/))
|
20
|
-
- All mentions of v0 == `@fluentui/react-northstar` ([docsite](https://fluentsite.z22.web.core.windows.net/))
|
21
|
-
|
22
|
-
The most relevant comparison that can be achieved between the two libraries is between `ContextualMenu` in v7 and a combination of `Menu`, `Popup` and `ToolbarItem` in v0.
|
23
|
-
|
24
|
-
v0 suffers from a consistency issue that the control used in `Menu` and the menu variant of `ToolbarItem` are not actually the same component and have different behavior. However, semantically for the purposes of this spec, they representthe same control that will be implemented.
|
25
|
-
|
26
|
-
Note that the below code samples are not meant to be complete, but to highlight differences between the two libraries. Please refer to official docsites for actual API references.
|
27
|
-
|
28
|
-
### Positioning
|
29
|
-
|
30
|
-
Please refer to the `react-popover` spec for more detailled comparison of positionining between v9 and v0.
|
31
|
-
|
32
|
-
### Trigger vs target
|
33
|
-
|
34
|
-
The v7 `ContextualMenu` has a prop `target` which is intended to be a ref to the DOM element that the positioning logic anchors to. The usage of this prop requires the visibility state of the component to be controlled using React state by the consumer. The same prop exists on the v0 `Popup` component that is intended to perform the same function.
|
35
|
-
|
36
|
-
```tsx
|
37
|
-
const buttonRef = React.useRef(<button />)
|
38
|
-
// V7/8
|
39
|
-
<ContextualMenu
|
40
|
-
...
|
41
|
-
target={buttonRef}
|
42
|
-
/>
|
43
|
-
|
44
|
-
// v0 - shorthand
|
45
|
-
<Popup
|
46
|
-
target={buttonRef}
|
47
|
-
content={...}
|
48
|
-
/>
|
49
|
-
```
|
50
|
-
|
51
|
-
The v0 `Popup` component has an alternative prop, `trigger`, which accepts a React component. This prop simplifies the creation of temporary content by autocontrolling the open/dismiss functionality.
|
52
|
-
|
53
|
-
```tsx
|
54
|
-
// v0 - shorthand trigger
|
55
|
-
<Popup
|
56
|
-
trigger={<Button />}
|
57
|
-
content={...}
|
58
|
-
/>
|
59
|
-
// v0 - children trigger
|
60
|
-
<Popup content={...}>
|
61
|
-
<Button icon={<MoreIcon />} title="Show popup" />
|
62
|
-
</Popup>
|
63
|
-
```
|
64
|
-
|
65
|
-
### Layout variations
|
66
|
-
|
67
|
-
The v7 `ContextualMenu` only has one primary layout which is a vertical list of menu items.
|
68
|
-
|
69
|
-
The v0 `Menu` component differs clearly in this that the default layout is a horizontal menu. To achieve the same layout as `ContextualMenu` (and the layout defined in this spec) it's necessary to use the `vertical` prop which is `false` by default.
|
70
|
-
|
71
|
-
```tsx
|
72
|
-
<Menu items={items} vertical />
|
73
|
-
```
|
74
|
-
|
75
|
-
### Open/Dismiss events
|
76
|
-
|
77
|
-
The v7 `ContextualMenu` is intended to be used as a controlled component. The visibiltiy of the menu is controlled using the `hidden` prop whose value should be React state of the cosumer. A separate `onDismiss` prop can also be used that will be invoked during events where the callout tries to close, i.e. click outside the content.
|
78
|
-
|
79
|
-
The v0 `Popup` should be compared here, since the v0 `Menu` does not handle open/dismiss events. `Popup` visibility can be controlled using the `open` prop. `Popup` provides a callback prop `onOpenChange` which can be used both to open and dismiss.
|
80
|
-
|
81
|
-
As mentioned above, `Popup` implements an autocontrolled pattern which allows both controlled an uncontrolled variants to be used in its API.
|
82
|
-
|
83
|
-
```tsx
|
84
|
-
// v7 controlled ContextualMenu
|
85
|
-
const [showContextualMenu, setShowContextualMenu] = React.useState(false);
|
86
|
-
const onShowContextualMenu = () => setShowContextualMenu(true);
|
87
|
-
const onHideContextualMenu = () => setShowContextualMenu(false);
|
88
|
-
|
89
|
-
<ContextualMenu
|
90
|
-
hidden={!showContextualMenu}
|
91
|
-
onItemClick={onHideContextualMenu}
|
92
|
-
onDismiss={onHideContextualMenu
|
93
|
-
/>;
|
94
|
-
|
95
|
-
// v0 uncontrolled Popup
|
96
|
-
const [open, setOpen] = React.useState(false);
|
97
|
-
|
98
|
-
<Popup
|
99
|
-
onOpenChange={(e, props: PopupProps) => {/*react on changes*/}}
|
100
|
-
trigger={<Button icon={<OpenOutsideIcon />} title="Open popup" />}
|
101
|
-
/>;
|
102
|
-
|
103
|
-
// v0 controlled Popup - used with trigger disables autocontrol
|
104
|
-
const [open, setOpen] = React.useState(false);
|
105
|
-
|
106
|
-
<Popup
|
107
|
-
open={open}
|
108
|
-
onOpenChange={(e, props: PopupProps) => setOpen(!props.open)}
|
109
|
-
trigger={<Button icon={<OpenOutsideIcon />} title="Open popup" />}
|
110
|
-
/>;
|
111
|
-
```
|
112
|
-
|
113
|
-
### Keyboard navigation
|
114
|
-
|
115
|
-
Both v7 and v0 support arrow navigation in the menu, and home/end keys to jump to first and last items respectively.
|
116
|
-
|
117
|
-
One interesting difference is that the v7 `ContextualMenu` will also tab through items. The v0 `Menu` on the other hand uses tab to focus in/out of the entire component.
|
118
|
-
|
119
|
-
`ContextualMenu` will also allow disabled items to be focusable while navigating the list, while the v0 `Menu` does not permit this.
|
120
|
-
|
121
|
-
### Selection state
|
122
|
-
|
123
|
-
The v0 `Menu` component supports and `active` state and has a number of props to manage this state. However, this state only affects items visually and does not perform the same function as menu item checkboxes or radio items. The `active` state of menu items can both be controlled and autocontrolled
|
124
|
-
|
125
|
-
```tsx
|
126
|
-
// v0 autocontrolled active index with default
|
127
|
-
<Menu defaultActiveIndex={0}>
|
128
|
-
<Menu.Item index={0}>
|
129
|
-
<Menu.ItemContent>Editorials</Menu.ItemContent>
|
130
|
-
</Menu.Item>
|
131
|
-
</Menu>
|
132
|
-
|
133
|
-
// v0 autocontrolled active index controlled
|
134
|
-
<Menu activeIndex={0}>
|
135
|
-
<Menu.Item index={0}>
|
136
|
-
<Menu.ItemContent>Editorials</Menu.ItemContent>
|
137
|
-
</Menu.Item>
|
138
|
-
</Menu>
|
139
|
-
|
140
|
-
// shorthand variation
|
141
|
-
const items = [
|
142
|
-
{
|
143
|
-
key: 'editorials',
|
144
|
-
content: 'Editorials',
|
145
|
-
},
|
146
|
-
]
|
147
|
-
<Menu defaultActiveIndex={0} items={items} />
|
148
|
-
```
|
149
|
-
|
150
|
-
In order to obtain semantically meaningful selection state in v0, the only possible way is to use a `Toolbar` component. The menu that is rendered in this component is completely different but supports both checkbox and radio selection states through the use of an `active` prop and must be controlled.
|
151
|
-
|
152
|
-
```tsx
|
153
|
-
// Toolbar with one item that opens a selectable menu
|
154
|
-
const toolbarItems = [
|
155
|
-
{
|
156
|
-
icon: <MoreIcon />,
|
157
|
-
title: 'More',
|
158
|
-
menu: [
|
159
|
-
{
|
160
|
-
active: true,
|
161
|
-
content: 'Bold',
|
162
|
-
kind: 'toggle',
|
163
|
-
// kind: 'radio', // for radio
|
164
|
-
onClick: handleToggleClick,
|
165
|
-
},
|
166
|
-
{
|
167
|
-
active: false,
|
168
|
-
content: 'Italic',
|
169
|
-
kind: 'toggle',
|
170
|
-
// kind: 'radio', // for radio
|
171
|
-
},
|
172
|
-
],
|
173
|
-
menuOpen,
|
174
|
-
onMenuOpenChange: (e, { menuOpen }) => setMenuOpen(menuOpen),
|
175
|
-
},
|
176
|
-
]
|
177
|
-
|
178
|
-
<Toolbar items={toolbarItems}>
|
179
|
-
```
|
180
|
-
|
181
|
-
The v7 `ContextualMenu` on the other hand, only supports the checkbox selection state implicitly. This behavior must be controlled by consumers and uses `canCheck` and `isChecked` props:
|
182
|
-
|
183
|
-
```tsx
|
184
|
-
const menuProps = {
|
185
|
-
shouldFocusOnMount: true,
|
186
|
-
items: [
|
187
|
-
{
|
188
|
-
text: 'New',
|
189
|
-
canCheck: true,
|
190
|
-
isChecked: true,
|
191
|
-
onClick: onToggleSelect,
|
192
|
-
},
|
193
|
-
{
|
194
|
-
text: 'Share',
|
195
|
-
canCheck: true,
|
196
|
-
isChecked: false,
|
197
|
-
onClick: onToggleSelect,
|
198
|
-
},
|
199
|
-
],
|
200
|
-
};
|
201
|
-
|
202
|
-
// shorthand usage in a menu button
|
203
|
-
<DefaultButton menuProps={menuProps} />;
|
204
|
-
```
|
205
|
-
|
206
|
-
### DOM output
|
207
|
-
|
208
|
-
Below are some sample DOM outputs to compare for certain scenarios. Not all DOM attributes are reflected here, a subset have been chosen to provide easier reading and comparison.
|
209
|
-
|
210
|
-
#### Basic menu
|
211
|
-
|
212
|
-
Both the current v7 and v0 versions of this control use the `ul` and `li` combination along with content wrapper elements. This makes style overrides kind of complicated to target and also makes custom rendering difficult since there is the added complexity of targeting stricter DOM structures.
|
213
|
-
|
214
|
-
`ul`/`li` combinations are also very strict in markdown and might not play well with newer concepts like virtualization and custom scrollbars where arbitrary `div` elements can be inserted into the DOM.
|
215
|
-
|
216
|
-
In terms off A11y and narration there is effectively no difference in having a wrapping element or not. Would useful in the proposed new API to use a simpler DOM structure that provides more flexibility.
|
217
|
-
|
218
|
-
```html
|
219
|
-
<!-- v7 basic menu -->
|
220
|
-
<ul role="menu">
|
221
|
-
<li role="presentation">
|
222
|
-
<button role="menuitem" tabindex="0">
|
223
|
-
<div class="linkContent">
|
224
|
-
<span class="itemText">Editorials</span>
|
225
|
-
</div>
|
226
|
-
</button>
|
227
|
-
</li>
|
228
|
-
<li role="presentation">
|
229
|
-
<button role="menuitem" tabindex="-1">
|
230
|
-
<div class="linkContent">
|
231
|
-
<span class="itemText">Reviews</span>
|
232
|
-
</div>
|
233
|
-
</button>
|
234
|
-
</li>
|
235
|
-
</ul>
|
236
|
-
|
237
|
-
<!-- v0 basic menu -->
|
238
|
-
<ul role="menu">
|
239
|
-
<li role="presentation">
|
240
|
-
<a role="menuitem" tabindex="0">
|
241
|
-
<span class="menu__itemcontent">Editorials</span>
|
242
|
-
</a>
|
243
|
-
</li>
|
244
|
-
<li role="presentation">
|
245
|
-
<a role="menuitem" tabindex="-1">
|
246
|
-
<span class="menu__itemcontent">Reviews</span>
|
247
|
-
</a>
|
248
|
-
</li>
|
249
|
-
</ul>
|
250
|
-
```
|
251
|
-
|
252
|
-
### Menu divider
|
253
|
-
|
254
|
-
```html
|
255
|
-
<!-- v7 divider item -->
|
256
|
-
<li role="separator" aria-hidden="true"></li>
|
257
|
-
|
258
|
-
<!-- v0 divider item -->
|
259
|
-
<li role="presentation" class="menu__divider"></li>
|
260
|
-
```
|
261
|
-
|
262
|
-
### Custom rendering and data
|
263
|
-
|
264
|
-
v7 provides render callbacks that can be used to render either the entire menu list or specific slots of menut items. Each call back provides the props avaialble to that slot and a `defaultRender` which allows to easily extend the original render, if required.
|
265
|
-
|
266
|
-
```tsx
|
267
|
-
// v7 custom rendering
|
268
|
-
const menuProps = {
|
269
|
-
onRenderMenuList: (props: IContextualMenuListProps, defaultRenderer) => {},
|
270
|
-
onRenderSubMenu: (props: IContextualMenuProps, defaultRenderer) => {},
|
271
|
-
items: [
|
272
|
-
{
|
273
|
-
onRender: (
|
274
|
-
item: any,
|
275
|
-
dismissMenu: (ev?: any, dismissAll?: boolean) => void
|
276
|
-
) => React.ReactNode
|
277
|
-
}
|
278
|
-
{onRenderContent: (props: IContextualMenuItemProps, defaultRenderer) => {}},
|
279
|
-
{onRenderIcon: (props: IContextualMenuItemProps, defaultRenderer) => {}},
|
280
|
-
]
|
281
|
-
}
|
282
|
-
|
283
|
-
<ContextualMenu menuProps={menuProps}>
|
284
|
-
```
|
285
|
-
|
286
|
-
Custom data can also be associated with menu items
|
287
|
-
|
288
|
-
```tsx
|
289
|
-
const menuProps = {
|
290
|
-
items: [{
|
291
|
-
...
|
292
|
-
data: { foo: "bar" }
|
293
|
-
}]
|
294
|
-
}
|
295
|
-
```
|
296
|
-
|
297
|
-
v0 custom rendering through shorthand components is a consistent experience through all shorthand components, but provide a smaller API surface (whether this is simpler or less powerful can be subjective). Custom rendering in the case of the `Menu` component would be done through the use of `children` prop either through the standard React child component API or through shorthand as a callback function.
|
298
|
-
|
299
|
-
```tsx
|
300
|
-
// v0 shorthand children render callback
|
301
|
-
const items = [
|
302
|
-
{
|
303
|
-
key: 'editorials',
|
304
|
-
children: (El, props) => <El>{props.key}</El>
|
305
|
-
},
|
306
|
-
]
|
307
|
-
|
308
|
-
<Menu defaultActiveIndex={0} items={items} />
|
309
|
-
|
310
|
-
// v0 children API custom render
|
311
|
-
<Menu>
|
312
|
-
<Menu.Item index={0}>
|
313
|
-
<Menu.ItemContent>Editorials</Menu.ItemContent>
|
314
|
-
</Menu.Item>
|
315
|
-
<Menu.Item index={1}>
|
316
|
-
CustomContent
|
317
|
-
</Menu.Item>
|
318
|
-
{/*Not recommended but definitely possible*/}
|
319
|
-
<div>custom item</div>
|
320
|
-
</Menu>
|
321
|
-
```
|
322
|
-
|
323
|
-
## Variants
|
324
|
-
|
325
|
-
### Nested menus
|
326
|
-
|
327
|
-
A `Menu` should be able to trigger an additional instance of itself as a part of one or more of its options. The nested `Menu` component should have the same functional capabilities as the root `Menu` component.
|
328
|
-
|
329
|
-
The actions that trigger the the nested `Menu` should be be consistent with the actions that can trigger any root `Menu` from a similar UI control.
|
330
|
-
|
331
|
-
We advise that no more than two nested `Menu` components be used, but this spec does not functionally apply that constrain to the implementation of the `Menu` component.
|
332
|
-
|
333
|
-
### Selection state
|
334
|
-
|
335
|
-
A `Menu` should be able to track and represent the selection state of all or some of its options if required.
|
336
|
-
|
337
|
-
When an options is associated with a selection state. The `Menu`, either root or nested, should control its dismiss behavior accordingly based on configuration.
|
338
|
-
|
339
|
-
### Sections
|
340
|
-
|
341
|
-
A `Menu` can be partitioned into sections using visible dividers in its list of options. Each section can contain a heading title that announces or briefly describes the options in the particular section
|
342
|
-
|
343
|
-
### Secondary label
|
344
|
-
|
345
|
-
An option of a `Menu` component should be able to declare additional secondary label that can provide additional context describing the option or its usage.
|
346
|
-
|
347
|
-
For example a secondary label can be a label that shows a keyboard shortcut that will perform an equivalent action of the option of the `Menu` component.
|
348
|
-
|
349
|
-
### Split option with nesting
|
350
|
-
|
351
|
-
An option of a `Menu` component can trigger a nested `Menu` component and also perform its default action by splitting the option into two interactable areas that handle each action separately.
|
352
|
-
|
353
|
-
### Disabled option(s)
|
354
|
-
|
355
|
-
All options in a `Menu` component can be disabled and should provide a visible indication for this purpose. User interaction should be defined for disabled options
|
356
|
-
|
357
|
-
### Scrollable
|
358
|
-
|
359
|
-
A `Menu` should display a vertical scrollbar when the number of options exceeds the height of the component
|
360
|
-
|
361
|
-
### Standalone/No surface
|
362
|
-
|
363
|
-
A `Menu` can be used without the temporary popup surface and its trigger. This will allow `Menu` components to be permanent page content or used in custom surfaces with a wider range of UI components.
|
364
|
-
|
365
|
-
### Custom content
|
366
|
-
|
367
|
-
```
|
368
|
-
This variant is still a work in progress and needs additional thought
|
369
|
-
```
|
370
|
-
|
371
|
-
Any custom content can be used in the rendering of the Menu, all interactions and accessibility is left to the discretion of the consumer.
|
372
|
-
|
373
|
-
## API
|
374
|
-
|
375
|
-
The `Menu` should implement a `children` based API as is the standard across all the surveyed alternatives as a part of Open UI research in [Prior Art](#prior-art). The component will leverage the use of `context` in the interaction and data flows of child components.
|
376
|
-
|
377
|
-
Sample usages will be give in the following section of this document [Sample code](#sample-code)
|
378
|
-
|
379
|
-
### Menu
|
380
|
-
|
381
|
-
The root level component serves as a simplified interface (sugar) for popup positioning and triggering.
|
382
|
-
|
383
|
-
```typescript
|
384
|
-
export type MenuProps = MenuListProps &
|
385
|
-
Pick<PositioningProps, '<Positioning props as necessary>'> & {
|
386
|
-
/**
|
387
|
-
* Explicitly require children
|
388
|
-
*/
|
389
|
-
|
390
|
-
children: React.ReactNode;
|
391
|
-
/**
|
392
|
-
* Whether the popup is open
|
393
|
-
*/
|
394
|
-
open?: boolean;
|
395
|
-
|
396
|
-
/**
|
397
|
-
* Call back when the component requests to change value
|
398
|
-
* The `open` value is used as a hint when directly controlling the component
|
399
|
-
*/
|
400
|
-
onOpenChange?: (e: MenuOpenEvents, data: MenuOpenChangeData) => void;
|
401
|
-
|
402
|
-
/**
|
403
|
-
* Whether the popup is open by default
|
404
|
-
*/
|
405
|
-
defaultOpen?: boolean;
|
406
|
-
|
407
|
-
/**
|
408
|
-
* Wrapper to style and add events for the popup
|
409
|
-
*/
|
410
|
-
menuPopup?: ShorthandProps<React.HTMLAttributes<HTMLElement>>;
|
411
|
-
|
412
|
-
/*
|
413
|
-
* Opens the menu on hover
|
414
|
-
*/
|
415
|
-
openOnHover?: boolean;
|
416
|
-
|
417
|
-
/**
|
418
|
-
* Opens the menu on right click (context menu), removes all other menu open interactions
|
419
|
-
*/
|
420
|
-
openOnContext?: boolean;
|
421
|
-
|
422
|
-
/**
|
423
|
-
* Root menus are rendered out of DOM order on `document.body`, use this to render the menu in DOM order
|
424
|
-
* This option is disregarded for submenus
|
425
|
-
*/
|
426
|
-
inline?: boolean;
|
427
|
-
};
|
428
|
-
```
|
429
|
-
|
430
|
-
### MenuTrigger
|
431
|
-
|
432
|
-
A non-visual component that wraps its child and configures them to be the trigger that will open a menu. This component should only accept one child
|
433
|
-
|
434
|
-
```typescript
|
435
|
-
export type MenuTriggerProps = {
|
436
|
-
/**
|
437
|
-
* Explicitly require single child
|
438
|
-
*/
|
439
|
-
children: React.ReactElement;
|
440
|
-
};
|
441
|
-
```
|
442
|
-
|
443
|
-
### MenuList
|
444
|
-
|
445
|
-
This component is used internally by `Menu` and manages the context and layout its items.
|
446
|
-
|
447
|
-
`MenuList` can also be used separately as the standalone variant of the `Menu`, since it should not control popup positioning or triggers. It is the only component in the API that can be used standalone. Envisioned to be used with more complex popup or trigger scenarios where the `Menu` component does not provide enough control for these situations.
|
448
|
-
|
449
|
-
```typescript
|
450
|
-
export type MenuListProps = ComponentProps &
|
451
|
-
React.HTMLAttributes<HTMLElement> & {
|
452
|
-
/**
|
453
|
-
* Callback when checked items change for value with a name
|
454
|
-
*
|
455
|
-
* @param name - the name of the value
|
456
|
-
* @param checkedItems - the items for this value that are checked
|
457
|
-
*/
|
458
|
-
onCheckedValueChange?: (e: React.MouseEvent | React.KeyboardEvent, name: string, checkedItems: string[]) => void;
|
459
|
-
|
460
|
-
/**
|
461
|
-
* Map of all checked values
|
462
|
-
*/
|
463
|
-
checkedValues?: Record<string, string[]>;
|
464
|
-
|
465
|
-
/**
|
466
|
-
* Default values to be checked on mount
|
467
|
-
*/
|
468
|
-
defaultCheckedValues?: Record<string, string[]>;
|
469
|
-
|
470
|
-
/**
|
471
|
-
* States that menu items can contain icons and reserve slots for item alignment
|
472
|
-
*/
|
473
|
-
hasIcons?: boolean;
|
474
|
-
|
475
|
-
/**
|
476
|
-
* States that menu items can contain selectable items and reserve slots for item alignment
|
477
|
-
*/
|
478
|
-
hasCheckmarks?: boolean;
|
479
|
-
};
|
480
|
-
```
|
481
|
-
|
482
|
-
### MenuGroup
|
483
|
-
|
484
|
-
Creates a group inside a `MenuList`, setting up header layout and dividers between `MenuItems`.
|
485
|
-
|
486
|
-
The MenuGroup is also a useful component to declare different selection groups (checkbox/radio) in a `MenuList`.
|
487
|
-
|
488
|
-
> This component only accepts native DOM attributes as props.
|
489
|
-
|
490
|
-
### MenuGroupHeader
|
491
|
-
|
492
|
-
Creates a section header element with appropriate styling. Will set correct `aria-labelledby` relationship if it is instantiated within a [MenuGroup](#menugroup)
|
493
|
-
|
494
|
-
> This component only accepts native DOM attributes as props.
|
495
|
-
|
496
|
-
### MenuDivider
|
497
|
-
|
498
|
-
Creates a divider element in the `MenuList` with correct HTML and aria semantics for divider.
|
499
|
-
|
500
|
-
This divider is purely a visual cue. To ensure consistent narration experience across all screenreaders [MenuGroup](#menugroup) should be used
|
501
|
-
|
502
|
-
> This component only accepts native DOM attributes as props.
|
503
|
-
|
504
|
-
### MenuItem
|
505
|
-
|
506
|
-
```typescript
|
507
|
-
export type MenuItemProps = ComponentProps &
|
508
|
-
React.HTMLAttributes<HTMLElement> & {
|
509
|
-
/**
|
510
|
-
* Icon slot rendered before children content
|
511
|
-
*/
|
512
|
-
icon?: ShorthandProps<React.HTMLAttributes<HTMLElement>>;
|
513
|
-
|
514
|
-
/**
|
515
|
-
* A helper slot for alignment when a menu item is used with selectable menuitems
|
516
|
-
* Avoid using this slot as a replacement for MenuItemCheckbox and MenuItemRadio components
|
517
|
-
*/
|
518
|
-
checkmark?: ShorthandProps<React.HTMLAttributes<HTMLElement>>;
|
519
|
-
|
520
|
-
/**
|
521
|
-
* Icon slot that shows the indicator for a submenu
|
522
|
-
*/
|
523
|
-
submenuIndicator?: ShorthandProps<React.HTMLAttributes<HTMLElement>>;
|
524
|
-
|
525
|
-
/**
|
526
|
-
* Component children are placed in this slot
|
527
|
-
* Avoid using the `children` property in this slot in favour of Component children whenever possible
|
528
|
-
*/
|
529
|
-
content?: ShorthandProps<React.HTMLAttributes<HTMLElement>>;
|
530
|
-
|
531
|
-
/**
|
532
|
-
* Secondary content rendered opposite the primary content (e.g Shortcut text)
|
533
|
-
*/
|
534
|
-
secondaryContent?: ShorthandProps<React.HTMLAttributes<HTMLElement>>;
|
535
|
-
|
536
|
-
/**
|
537
|
-
* If the menu item is a trigger for a submenu
|
538
|
-
*/
|
539
|
-
hasSubmenu?: boolean;
|
540
|
-
|
541
|
-
/**
|
542
|
-
* Applies disabled styles to menu item but remains focusable
|
543
|
-
*/
|
544
|
-
disabled?: boolean;
|
545
|
-
|
546
|
-
/**
|
547
|
-
* Clicking on the menu item will not dismiss an open menu
|
548
|
-
*/
|
549
|
-
persistOnClick?: boolean;
|
550
|
-
};
|
551
|
-
```
|
552
|
-
|
553
|
-
### MenuItemCheckbox/Radio
|
554
|
-
|
555
|
-
Variants of `MenuItem` that allows a single or multiple selection state based on the value that it represents. API is intended to mirror that of HTML inputs
|
556
|
-
|
557
|
-
- https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/checkbox
|
558
|
-
- https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/radio
|
559
|
-
|
560
|
-
```typescript
|
561
|
-
/**
|
562
|
-
* Props for selecatble menu items
|
563
|
-
*/
|
564
|
-
export type MenuItemSelectableProps = React.HTMLAttributes<HTMLElement> & {
|
565
|
-
/**
|
566
|
-
* Follows input convention
|
567
|
-
* https://www.w3schools.com/jsref/prop_checkbox_name.asp
|
568
|
-
*/
|
569
|
-
name: string;
|
570
|
-
|
571
|
-
/**
|
572
|
-
* Follows input convention
|
573
|
-
* https://www.w3schools.com/jsref/prop_checkbox_value.asp
|
574
|
-
*/
|
575
|
-
value: string;
|
576
|
-
|
577
|
-
/**
|
578
|
-
* Whether the selectable item is disabled
|
579
|
-
*/
|
580
|
-
disabled?: boolean;
|
581
|
-
};
|
582
|
-
|
583
|
-
export type MenuItemCheckboxProps = ComponentProps &
|
584
|
-
React.HTMLAttributes<HTMLElement> &
|
585
|
-
MenuItemProps &
|
586
|
-
MenuItemSelectableProps & {
|
587
|
-
/**
|
588
|
-
* Slot for the checkmark indicator
|
589
|
-
*/
|
590
|
-
checkmark?: ShorthandProps<React.HTMLAttributes<HTMLElement>>;
|
591
|
-
};
|
592
|
-
|
593
|
-
export type MenuItemRadioProps = ComponentProps &
|
594
|
-
React.HTMLAttributes<HTMLElement> &
|
595
|
-
MenuItemProps &
|
596
|
-
MenuItemSelectableProps & {
|
597
|
-
checkmark?: ShorthandProps<React.HTMLAttributes<HTMLElement>>;
|
598
|
-
};
|
599
|
-
```
|
600
|
-
|
601
|
-
## Sample code
|
602
|
-
|
603
|
-
The below samples do not represent the definitive props of the final implemented component, but represent the ideal final implementations. Can be subject to change during the implementation phase.
|
604
|
-
|
605
|
-
### Basic Menu
|
606
|
-
|
607
|
-
```tsx
|
608
|
-
const menu = (
|
609
|
-
<Menu>
|
610
|
-
<MenuTrigger><button>Opem menu</button></MenuTrigger>
|
611
|
-
<MenuList>
|
612
|
-
<MenuItem>Option 1</MenuItem>
|
613
|
-
<MenuItem>Option 2</MenuItem>
|
614
|
-
<MenuItem>Option 3</MenuItem>
|
615
|
-
</MenuList>
|
616
|
-
<Menu>
|
617
|
-
)
|
618
|
-
```
|
619
|
-
|
620
|
-
```html
|
621
|
-
<!-- expected DOM output -->
|
622
|
-
<button aria-haspopup="menu" aria-expanded="true" id="trigger">Open menu</button>
|
623
|
-
<div role="menu" aria-labelledby="trigger">
|
624
|
-
<div role="menuitem" tabindex="0">Option 1</div>
|
625
|
-
<div role="menuitem" tabindex="-1">Option 2</div>
|
626
|
-
<div role="menuitem" tabindex="-1">Option 3</div>
|
627
|
-
</div>
|
628
|
-
```
|
629
|
-
|
630
|
-
### Menu items with icons
|
631
|
-
|
632
|
-
```tsx
|
633
|
-
const menu = (
|
634
|
-
<Menu>
|
635
|
-
<MenuTrigger><button>Opem menu</button></MenuTrigger>
|
636
|
-
<MenuList>
|
637
|
-
<MenuItem icon={<FileIcon />}>Option 1</MenuItem>
|
638
|
-
<MenuItem icon={<BellIcon />}>Option 2</MenuItem>
|
639
|
-
<MenuItem icon={<LinkIcon />}>Option 3</MenuItem>
|
640
|
-
</MenuList>
|
641
|
-
<Menu>
|
642
|
-
)
|
643
|
-
```
|
644
|
-
|
645
|
-
```html
|
646
|
-
<!-- expected DOM output -->
|
647
|
-
<button aria-haspopup="menu" aria-expanded="true" id="trigger">Open menu</button>
|
648
|
-
<div role="menu" aria-labelledby="trigger">
|
649
|
-
<div role="menuitem" tabindex="0">
|
650
|
-
<span role="presentation"><svg>FileIcon</svg></span>
|
651
|
-
Option 1
|
652
|
-
</div>
|
653
|
-
<div role="menuitem" tabindex="0">
|
654
|
-
<span role="presentation"><svg>BellIcon</svg></span>
|
655
|
-
Option 2
|
656
|
-
</div>
|
657
|
-
<div role="menuitem" tabindex="0">
|
658
|
-
<span role="presentation"><svg>LinkIcon</svg></span>
|
659
|
-
Option 3
|
660
|
-
</div>
|
661
|
-
</div>
|
662
|
-
```
|
663
|
-
|
664
|
-
### Sections
|
665
|
-
|
666
|
-
```tsx
|
667
|
-
const menu = (
|
668
|
-
<Menu>
|
669
|
-
<MenuTrigger><button>Opem menu</button></MenuTrigger>
|
670
|
-
<MenuList>
|
671
|
-
<MenuItem>Option 1</MenuItem>
|
672
|
-
<MenuDivider />
|
673
|
-
<MenuGroup title="Section title">
|
674
|
-
<MenuItem>Section Option 1</MenuItem>
|
675
|
-
<MenuItem>Section Option 2</MenuItem>
|
676
|
-
<MenuItem>Section Option 3</MenuItem>
|
677
|
-
<MenuGroup />
|
678
|
-
</MenuList>
|
679
|
-
<Menu>
|
680
|
-
)
|
681
|
-
```
|
682
|
-
|
683
|
-
```html
|
684
|
-
<!-- expected DOM output -->
|
685
|
-
<button aria-haspopup="menu" aria-expanded="true" id="trigger">Open menu</button>
|
686
|
-
<div role="menu" aria-labelledby="trigger">
|
687
|
-
<div role="menuitem" tabindex="0">Option 1</div>
|
688
|
-
<div role="separator" aria-hidden="true"></div>
|
689
|
-
<div role="group" aria-labelledby="sectionid">
|
690
|
-
<div role="presentation" aria-hidden="true" id="sectionid">Section title</div>
|
691
|
-
<div role="menuitem" tabindex="-1">Section Option 1</div>
|
692
|
-
<div role="menuitem" tabindex="-1">Section Option 2</div>
|
693
|
-
<div role="menuitem" tabindex="-1">Section Option 3</div>
|
694
|
-
</div>
|
695
|
-
<div role="separator"></div>
|
696
|
-
</div>
|
697
|
-
```
|
698
|
-
|
699
|
-
Custom section headings can also be used, but must be used within a [MenuGroup](#menugroup) to ensure correct narration experience
|
700
|
-
|
701
|
-
```tsx
|
702
|
-
|
703
|
-
const menu = (
|
704
|
-
<Menu>
|
705
|
-
<MenuTrigger><button>Opem menu</button></MenuTrigger>
|
706
|
-
<MenuList>
|
707
|
-
<MenuItem>Option 1</MenuItem>
|
708
|
-
<MenuDivider />
|
709
|
-
<MenuGroup>
|
710
|
-
<MenuGroupHeader>{children}</MenuGroupHeader>
|
711
|
-
<MenuItem>Section Option 1</MenuItem>
|
712
|
-
<MenuItem>Section Option 2</MenuItem>
|
713
|
-
<MenuItem>Section Option 3</MenuItem>
|
714
|
-
<MenuGroup />
|
715
|
-
</MenuList>
|
716
|
-
<Menu>
|
717
|
-
)
|
718
|
-
```
|
719
|
-
|
720
|
-
```html
|
721
|
-
<!-- expected DOM output -->
|
722
|
-
<button aria-haspopup="menu" aria-expanded="true" id="trigger">Open menu</button>
|
723
|
-
<div role="menu" aria-labelledby="trigger">
|
724
|
-
<div role="menuitem" tabindex="0">Option 1</div>
|
725
|
-
<div role="separator" aria-hidden="true"></div>
|
726
|
-
<div role="group" aria-labelledby="sectionid">
|
727
|
-
<div role="presentation" aria-hidden="true" id="sectionid">children</div>
|
728
|
-
<div role="menuitem" tabindex="-1">Section Option 1</div>
|
729
|
-
<div role="menuitem" tabindex="-1">Section Option 2</div>
|
730
|
-
<div role="menuitem" tabindex="-1">Section Option 3</div>
|
731
|
-
</div>
|
732
|
-
<div role="separator"></div>
|
733
|
-
</div>
|
734
|
-
```
|
735
|
-
|
736
|
-
### Submenus
|
737
|
-
|
738
|
-
```tsx
|
739
|
-
const menu = (
|
740
|
-
<Menu>
|
741
|
-
<MenuTrigger><button>Opem menu</button></MenuTrigger>
|
742
|
-
<MenuList>
|
743
|
-
<MenuItem>Option 1</MenuItem>
|
744
|
-
<Menu>
|
745
|
-
<MenuTrigger>
|
746
|
-
<MenuItem>Open submenu</MenuItem>
|
747
|
-
</MenuTrigger>
|
748
|
-
<MenuList>
|
749
|
-
<MenuItem>Option 1</MenuItem>
|
750
|
-
<MenuItem>Option 2</MenuItem>
|
751
|
-
<MenuItem>Option 3</MenuItem>
|
752
|
-
</MenuList>
|
753
|
-
</Menu>
|
754
|
-
</MenuList>
|
755
|
-
<Menu>
|
756
|
-
)
|
757
|
-
```
|
758
|
-
|
759
|
-
```html
|
760
|
-
<!-- expected DOM output -->
|
761
|
-
<button aria-haspopup="menu" aria-expanded="true" id="trigger">Open menu</button>
|
762
|
-
<div role="menu" aria-labelledby="trigger">
|
763
|
-
<div role="menuitem" tabindex="0">Option 1</div>
|
764
|
-
<div role="menuitem" tabindex="-1" aria-haspopup="menu" aria-expanded="false" id="submenu-trigger">Open submenu</div>
|
765
|
-
</div>
|
766
|
-
|
767
|
-
<!-- expected DOM output for submenu -->
|
768
|
-
<div role="menu" aria-labelledby="submenu-trigger">
|
769
|
-
<div role="menuitem" tabindex="-1">Option 1</div>
|
770
|
-
<div role="menuitem" tabindex="-1">Option 2</div>
|
771
|
-
<div role="menuitem" tabindex="-1">Option 3</div>
|
772
|
-
</div>
|
773
|
-
```
|
774
|
-
|
775
|
-
### Standlone
|
776
|
-
|
777
|
-
```tsx
|
778
|
-
const [open] = React.useState(false);
|
779
|
-
|
780
|
-
const menu = (
|
781
|
-
<CustomSurface open={open}>
|
782
|
-
<MenuList>
|
783
|
-
<MenuItem>Option 1</MenuItem>
|
784
|
-
<MenuItem>Option 2</MenuItem>
|
785
|
-
<MenuItem>Option 3</MenuItem>
|
786
|
-
<MenuList>
|
787
|
-
<CustomSurface>
|
788
|
-
)
|
789
|
-
```
|
790
|
-
|
791
|
-
```html
|
792
|
-
<!-- expected DOM output -->
|
793
|
-
<div role="menu">
|
794
|
-
<div role="menuitem" tabindex="0">Option 1</div>
|
795
|
-
<div role="menuitem" tabindex="-1">Option 2</div>
|
796
|
-
<div role="menuitem" tabindex="-1">Option 3</div>
|
797
|
-
</div>
|
798
|
-
```
|
799
|
-
|
800
|
-
### Selection
|
801
|
-
|
802
|
-
```tsx
|
803
|
-
const trigger = <button> Open menu </button>
|
804
|
-
const [selectedItems, setSelectedItems] = React.useState([]);
|
805
|
-
|
806
|
-
// basic checkbox example
|
807
|
-
const menuCheckbox = (
|
808
|
-
<Menu
|
809
|
-
kind="checkbox"
|
810
|
-
selectedItems={selectedItems}
|
811
|
-
onSelectionChange={setSeelctedItems}
|
812
|
-
>
|
813
|
-
<MenuTrigger><button>Opem menu</button></MenuTrigger>
|
814
|
-
<MenuList>
|
815
|
-
<MenuItemCheckbox name="checkbox1" value={1}>Option 1</MenuItemCheckbox>
|
816
|
-
<MenuItemCheckbox name="checkbox1" value={2}>Option 2</MenuItemCheckbox>
|
817
|
-
<MenuItemCheckbox name="checkbox2" value={3}>Option 3</MenuItemCheckbox>
|
818
|
-
</MenuList>
|
819
|
-
<Menu>
|
820
|
-
)
|
821
|
-
|
822
|
-
// leverage MenuGroup for different selection groups
|
823
|
-
const menuSelectableSections = (
|
824
|
-
<Menu
|
825
|
-
selectedItems={selectedItems}
|
826
|
-
onSelectionChange={setSeelctedItems}
|
827
|
-
>
|
828
|
-
<MenuTrigger><button>Opem menu</button></MenuTrigger>
|
829
|
-
<MenuList>
|
830
|
-
<MenuGroup title="Checkbox section">
|
831
|
-
<MenuItemCheckbox name="checkbox" value={1}>Option 1</MenuItem>
|
832
|
-
<MenuItemCheckbox name="checkbox" value={2}>Option 2</MenuItem>
|
833
|
-
<MenuItemCheckbox name="checkbox" value={3}>Option 3</MenuItem>
|
834
|
-
</MenuGroup>
|
835
|
-
<MenuGroup title="Radio section">
|
836
|
-
<MenuItemRadio name="radio" value={1}>Option 1</MenuItemRadio>
|
837
|
-
<MenuItemRadio name="radio" value={2}>Option 2</MenuItemRadio>
|
838
|
-
<MenuItemRadio name="radio" value={3}>Option 3</MenuItemRadio>
|
839
|
-
</MenuGroup>
|
840
|
-
</MenuList>
|
841
|
-
<Menu>
|
842
|
-
)
|
843
|
-
```
|
844
|
-
|
845
|
-
```html
|
846
|
-
<button aria-haspopup="menu" aria-expanded="true" id="trigger">Open menu</button>
|
847
|
-
|
848
|
-
<!-- expected DOM output for basic checkbox -->
|
849
|
-
<div role="menu" aria-labelledby="trigger">
|
850
|
-
<div role="menuitemcheckbox" tabindex="0" aria-checked="true">Option 1</div>
|
851
|
-
<div role="menuitemcheckbox" tabindex="-1" aria-checked="false">Option 2</div>
|
852
|
-
<div role="menuitemcheckbox" tabindex="-1" aria-checked="false">Option 3</div>
|
853
|
-
</div>
|
854
|
-
|
855
|
-
<!-- expected DOM output for different selection groups -->
|
856
|
-
<div role="menu" aria-labelledby="trigger">
|
857
|
-
<div role="group" aria-label="Checkbox section">
|
858
|
-
<div role="presentation" aria-hidden="true">Checkbox section</div>
|
859
|
-
<div role="menuitemcheckbox" tabindex="0" aria-checked="true">Option 1</div>
|
860
|
-
<div role="menuitemcheckbox" tabindex="-1" aria-checked="false">Option 2</div>
|
861
|
-
<div role="menuitemcheckbox" tabindex="-1" aria-checked="false">Option 3</div>
|
862
|
-
</div>
|
863
|
-
<div role="separator"></div>
|
864
|
-
<div role="group" aria-label="Radio section">
|
865
|
-
<div role="presentation" aria-hidden="true">Radio section</div>
|
866
|
-
<div role="menuitemradio" tabindex="-1" aria-checked="true">Option 1</div>
|
867
|
-
<div role="menuitemradio" tabindex="-1" aria-checked="false">Option 2</div>
|
868
|
-
<div role="menuitemradio" tabindex="-1" aria-checked="false">Option 3</div>
|
869
|
-
</div>
|
870
|
-
</div>
|
871
|
-
```
|
872
|
-
|
873
|
-
### Split button
|
874
|
-
|
875
|
-
```tsx
|
876
|
-
const trigger = <button> Open menu </button>
|
877
|
-
|
878
|
-
// basic checkbox example
|
879
|
-
const menuSplitbutton= (
|
880
|
-
<Menu trigger={trigger}>
|
881
|
-
<MenuTrigger><button>Opem menu</button></MenuTrigger>
|
882
|
-
<MenuList>
|
883
|
-
<MenuItem>Option 1</MenuItem>
|
884
|
-
<Menu>
|
885
|
-
<MenuSplitGroup>
|
886
|
-
<MenuItem>Main action</MenuItem>
|
887
|
-
<MenuTrigger>
|
888
|
-
<MenuItem />
|
889
|
-
</MenuTrigger>
|
890
|
-
</MenuSplitGroup>
|
891
|
-
<MenuItem>Option 1</MenuItem>
|
892
|
-
<MenuItem>Option 2</MenuItem>
|
893
|
-
<MenuItem>Option 3</MenuItem>
|
894
|
-
</Menu>
|
895
|
-
<MenuList>
|
896
|
-
<Menu>
|
897
|
-
)
|
898
|
-
```
|
899
|
-
|
900
|
-
```html
|
901
|
-
<div role="menu" aria-labelledby="trigger">
|
902
|
-
<div role="menuitem" tabindex="0">Option 1</div>
|
903
|
-
<div role="menuitem" tabindex="-1" aria-haspopup="menu" aria-expanded="false" id="submenu-trigger">Open submenu</div>
|
904
|
-
</div>
|
905
|
-
|
906
|
-
<!-- expected DOM output -->
|
907
|
-
<div role="menu">
|
908
|
-
<div role="menuitem" tabindex="0">Option 1</div>
|
909
|
-
<div role="group">
|
910
|
-
<div role="menuitem" tabindex="-1">content slot</div>
|
911
|
-
<div role="menuitem" tabindex="-1" aria-haspopup="menu" aria-expanded="false" id="submenu-trgger">
|
912
|
-
<svg>indicator icon</svg>
|
913
|
-
</div>
|
914
|
-
</div>
|
915
|
-
</div>
|
916
|
-
|
917
|
-
<div role="menu" aria-labelledby="submenu-trigger">
|
918
|
-
<div role="menuitem" tabindex="-1">Option 1</div>
|
919
|
-
<div role="menuitem" tabindex="-1">Option 2</div>
|
920
|
-
<div role="menuitem" tabindex="-1">Option 3</div>
|
921
|
-
</div>
|
922
|
-
```
|
923
|
-
|
924
|
-
## Behaviors
|
925
|
-
|
926
|
-
### Useful references
|
927
|
-
|
928
|
-
The below references were used to decide n appropriate keyboard interactions from an a11y perspective.
|
929
|
-
|
930
|
-
- https://www.w3.org/TR/wai-aria-practices/examples/menubar/menubar-1/menubar-1.html#
|
931
|
-
- https://www.w3.org/TR/wai-aria-practices/examples/menu-button/menu-button-links.html
|
932
|
-
- https://www.w3.org/WAI/tutorials/menus/application-menus/
|
933
|
-
|
934
|
-
### Mouse/Keyboard interactions
|
935
|
-
|
936
|
-
Below is a set of diagrams that tries to illustrates all the interactions menus and nested menus support in an easily understandable way.
|
937
|
-
|
938
|
-
> TODO convert these diagrams to excalidraw or smth that is text format
|
939
|
-
> TODO add extra descriptions to diagrams
|
940
|
-
|
941
|
-
<img src="./etc/images/menu-interactions/Slide1.PNG" width="700" />
|
942
|
-
<img src="./etc/images/menu-interactions/Slide2.PNG" width="700" />
|
943
|
-
<img src="./etc/images/menu-interactions/Slide3.PNG" width="700" />
|
944
|
-
<img src="./etc/images/menu-interactions/Slide4.PNG" width="700" />
|
945
|
-
<img src="./etc/images/menu-interactions/Slide5.PNG" width="700" />
|
946
|
-
<img src="./etc/images/menu-interactions/Slide6.PNG" width="700" />
|
947
|
-
<img src="./etc/images/menu-interactions/Slide7.PNG" width="700" />
|
948
|
-
<img src="./etc/images/menu-interactions/Slide8.PNG" width="700" />
|
949
|
-
<img src="./etc/images/menu-interactions/Slide9.PNG" width="700" />
|
950
|
-
<img src="./etc/images/menu-interactions/Slide10.PNG" width="700" />
|
951
|
-
<img src="./etc/images/menu-interactions/Slide11.PNG" width="700" />
|
952
|
-
<img src="./etc/images/menu-interactions/Slide12.PNG" width="700" />
|
953
|
-
<img src="./etc/images/menu-interactions/Slide13.PNG" width="700" />
|
954
|
-
<img src="./etc/images/menu-interactions/Slide14.PNG" width="700" />
|
955
|
-
<img src="./etc/images/menu-interactions/Slide15.PNG" width="700" />
|
956
|
-
<img src="./etc/images/menu-interactions/Slide16.PNG" width="700" />
|
957
|
-
<img src="./etc/images/menu-interactions/Slide17.PNG" width="700" />
|
958
|
-
<img src="./etc/images/menu-interactions/Slide18.PNG" width="700" />
|
959
|
-
|
960
|
-
### Split button MenuItem submenu
|
961
|
-
|
962
|
-
All of the above Mouse events seen previously should apply to the part of the split button that is intended to open a submenu.
|
963
|
-
|
964
|
-
> TODO convert these diagrams to excalidraw or smth that is text format
|
965
|
-
> TODO add extra descriptions to diagrams
|
966
|
-
|
967
|
-
<img src="./etc/images/menu-interactions/Slide19.PNG" width="700" />
|
968
|
-
<img src="./etc/images/menu-interactions/Slide20.PNG" width="700" />
|
969
|
-
<img src="./etc/images/menu-interactions/Slide21.PNG" width="700" />
|
970
|
-
<img src="./etc/images/menu-interactions/Slide22.PNG" width="700" />
|
971
|
-
|
972
|
-
### MenuItem selection
|
973
|
-
|
974
|
-
Below are the interactions that should be supported for all menu items that are required to handle a selection state.
|
975
|
-
|
976
|
-
In the event that the selection method is a radio, the previous selected item must be unselected.
|
977
|
-
|
978
|
-
| Type | Action | Result | Details |
|
979
|
-
| -------- | ------ | ------ | -------------------------------------------- |
|
980
|
-
| Keyboard | Space | Toggle | Toggle the selection status of the menu item |
|
981
|
-
| Keyboard | Enter | Toggle | Toggle the selection status of the menu item |
|
982
|
-
| Mouse | Click | Toggle | Toggle the selection status of the menu item |
|
983
|
-
|
984
|
-
#### Linking keyboard navigation and mouse hover
|
985
|
-
|
986
|
-
When a user sets focus on menu items using keyboard navigation, and then switches to mouse hover there should be one unique 'active' state for menu items. There should not be two different indicators at this point for hover and focus. Below is a GIF of the `ContextualMenu` in v7 that also supports this behaviour.
|
987
|
-
|
988
|
-
![Linked keyboard and mouse navigation](./etc/images/linked-keyboard-mouse-navigation.gif)
|
989
|
-
|
990
|
-
### Positioning
|
991
|
-
|
992
|
-
#### Inline vs portal rendering
|
993
|
-
|
994
|
-
A menu should be positioned so that it can be rendered either out of order on the DOM (e.g. portal to body) or inline in DOM order.
|
995
|
-
|
996
|
-
#### Submenu positioning
|
997
|
-
|
998
|
-
The default positioning for a submenu should be the standard seen in both v7 and v0. Submenu should be placed after the menu item trigger and aligned with the top edge.
|
999
|
-
|
1000
|
-
Although this should not be recommended, for the purposes of compatibility with v7, all positioning aspects should be configurable for submenus.
|
1001
|
-
|
1002
|
-
## Accessibiltiy
|
1003
|
-
|
1004
|
-
Accessibility behaviour is built into the spec as much as possible. This section addresses specific issues that don't fit well with the standard definition of the component.
|
1005
|
-
|
1006
|
-
## Migration
|
1007
|
-
|
1008
|
-
The immediate candidates for adoption for a converged `Menu` component which are hinted at the beginning of the spec are:
|
1009
|
-
|
1010
|
-
- [ContextualMenu](https://developer.microsoft.com/en-us/fluentui#/controls/web/contextualmenu) for v7
|
1011
|
-
- [Menu](https://fluentsite.z22.web.core.windows.net/0.52.0/components/menu/definition) for v0
|
1012
|
-
|
1013
|
-
This component has characteristics that should probably be considered for the following components in terms of future migrations:
|
1014
|
-
|
1015
|
-
- [Toolbar](https://fluentsite.z22.web.core.windows.net/0.52.0/components/toolbar/definition) in v0 which shares a menu component. The component itself also should use similar behaviour and interactions to `Menu`
|
1016
|
-
- [Dropdown](https://fluentsite.z22.web.core.windows.net/0.52.0/components/dropdown/definition) in v0 contains a menu. Dropdown semantics are different to that of standard menus in accessibility, but certain behaviours such as keyboard navigation and selection can be reused for such a component
|
1017
|
-
- [CommandBar](https://developer.microsoft.com/en-us/fluentui#/controls/web/commandbar) in v7 also contains a menu subcomponent as well as behaviour similar to `Menu` semantics
|
1018
|
-
- [Nav](https://developer.microsoft.com/en-us/fluentui#/controls/web/nav) in v7 could reuse certain behaviours in `Menu` such as keyboard navigation, but will use different DOM and aria semantics, this could be achieved through state hook variants or composition
|
1019
|
-
- [OverflowSet](https://developer.microsoft.com/en-us/fluentui#/controls/web/overflowset) in v7 contains a submenu component
|
1020
|
-
- [PivotSet](https://developer.microsoft.com/en-us/fluentui#/controls/web/pivot) could be considered as a component variant of `Menu`
|
1021
|
-
- [Breadcrumb](https://developer.microsoft.com/en-us/fluentui#/controls/web/breadcrumb) in v7 contains a submenu component and could also be considered as a component variant of `Menu
|
1022
|
-
|
1023
|
-
### Creating sections or groups within a menu
|
1024
|
-
|
1025
|
-
⚠️ When using [MenuDivider](#menudivider) without [MenuGroup](#menugroup)
|
1026
|
-
|
1027
|
-
The [MenuDivider](#menudivider) is a purely visual component. The component is only intended to be used as visual 'sugar'. When meaningful partitions [MenuItems](#menuitem) exists, [MenuGroup](#menugroup) should be used to provide the correct experience for narration.
|
1028
|
-
|
1029
|
-
⚠️ When using [MenuSectionHeader](#menudivider)
|
1030
|
-
|
1031
|
-
[MenuGroup](#menugroup) as a parent component ensures that correct `aria-labelledby` relationship is defined between the header and the group.
|
1032
|
-
|
1033
|
-
### Disabled menu items
|
1034
|
-
|
1035
|
-
Disabled menu items should be focusable
|
1036
|
-
|
1037
|
-
### Linking keyboard navigation and mouse hover
|
1038
|
-
|
1039
|
-
This can be difficult to impleemnt correctly without introducing a11y issues. The mouse should only apply focus if it is certain that the user is actively using the mouse on the page. If a menu is opened with keyboard interaction, and contains the mouse cursor by chance focus should not be applied.
|