@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/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.