Skip to content
Merged
9 changes: 9 additions & 0 deletions packages/fluentui/docs/src/components/Sidebar/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,15 @@ const prototypesTreeItems: TreeProps['items'] = [
},
public: true,
},
{
id: 'menulist',
title: {
content: 'Menu List',
as: NavLink,
to: '/prototype-menu-list',
},
public: false,
},
{
id: 'text-area',
title: {
Expand Down
65 changes: 65 additions & 0 deletions packages/fluentui/docs/src/prototypes/menuList/Menu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import * as React from 'react';
import { MenuContextProvider } from './menuContext';
import { useEventListener } from '@fluentui/react-component-event-listener';

export const Menu = props => {
const { children, trigger = null, open = false } = props;
const triggerRef = React.useRef<HTMLDivElement>();
const menuRef = React.useRef<HTMLDivElement>();

// Close on ESC
useEventListener({
type: 'keydown',
target: document,
listener: e => {
if (e.keyCode === 27) {
setOpen(false);
}
},
});

// Close on scroll
useEventListener({
type: 'wheel',
target: document,
listener: e => {
setOpen(false);
},
});
useEventListener({
type: 'touchmove',
target: document,
listener: e => {
setOpen(false);
},
});

const [isOpen, setOpen] = React.useState(open);
const [currentIndex, setIndex] = React.useState(1);

return (
<MenuContextProvider
value={{
currentIndex,
triggerRef: trigger?.current ? trigger : triggerRef,
setIndex,
open: isOpen,
setOpen,
menuRef,
}}
>
<div
onBlur={({ relatedTarget }) => {
if (
!menuRef.current?.contains(relatedTarget as Node) &&
!triggerRef.current?.contains(relatedTarget as Node)
) {
setOpen(false);
}
}}
>
{children}
</div>
</MenuContextProvider>
);
};
48 changes: 48 additions & 0 deletions packages/fluentui/docs/src/prototypes/menuList/MenuButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import * as React from 'react';
import { MenuList } from './MenuList';
import { MenuItem } from './MenuItem';
import { Menu } from './Menu';
import { MenuTrigger } from './MenuTrigger';

export function MenuButton() {
return (
<Menu>
<MenuTrigger>
<div
tabIndex={0}
style={{
border: '1px solid black',
width: 55,
textAlign: 'center',
}}
>
Menu v
</div>
</MenuTrigger>
<MenuList>
<MenuItem index={1}>Item 1</MenuItem>
<MenuItem index={2}>Item 2</MenuItem>
<MenuItem index={3}>Item 3</MenuItem>
<MenuItem index={4}>Item 4</MenuItem>
<Menu>
<MenuTrigger>
<MenuItem index={5}>Item 5</MenuItem>
</MenuTrigger>
<MenuList>
<MenuItem index={1}>item 1</MenuItem>
<MenuItem index={2}>item 2</MenuItem>
<Menu>
<MenuTrigger>
<MenuItem index={3}>Item 3</MenuItem>
</MenuTrigger>
<MenuList>
<MenuItem index={1}>1</MenuItem>
<MenuItem index={2}>2</MenuItem>
</MenuList>
</Menu>
</MenuList>
</Menu>
</MenuList>
</Menu>
);
}
59 changes: 59 additions & 0 deletions packages/fluentui/docs/src/prototypes/menuList/MenuItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import * as React from 'react';
import { useMenuListContext } from './menuListContext';
import { useEventListener } from '@fluentui/react-component-event-listener';

export function MenuItem({ children, index, submenu = null }) {
const itemRef = React.useRef<HTMLDivElement>();
const { currentIndex, setIndex, setOpen, triggerRef } = useMenuListContext();

const listener = React.useCallback(() => {
itemRef.current.focus();
setIndex(index);
}, [index, setIndex]);

const listenerKeyboard = React.useCallback(
e => {
if (e.keyCode === 37) {
setOpen(false);
triggerRef.current.focus();
}
},
[setOpen, triggerRef],
);

useEventListener({
type: 'mouseenter',
targetRef: itemRef,
listener,
});

useEventListener({
type: 'focus',
targetRef: itemRef,
listener,
});

useEventListener({
type: 'keyup',
targetRef: itemRef,
listener: listenerKeyboard,
});

return (
<div
ref={itemRef}
role="menuitem"
data-is-focusable="true"
tabIndex={0}
style={{
cursor: 'pointer',
width: 80,
...(currentIndex === index && {
background: 'grey',
}),
}}
>
{children}
</div>
);
}
36 changes: 36 additions & 0 deletions packages/fluentui/docs/src/prototypes/menuList/MenuList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import * as React from 'react';
import { Popup, FocusZone, FocusZoneDirection } from '@fluentui/react-northstar';
import { useMenuContext } from './menuContext';
import { MenuListProvider } from './menuListContext';

export function MenuList({ children }) {
const { triggerRef, open, currentIndex, setIndex, menuRef, setOpen } = useMenuContext();

return (
<Popup
open={open}
target={triggerRef}
position="below"
trapFocus
inline
content={
<FocusZone
direction={FocusZoneDirection.vertical}
isCircularNavigation
shouldFocusInnerElementWhenReceivedFocus
>
<MenuListProvider
value={{
currentIndex,
setIndex,
setOpen,
triggerRef,
}}
>
<div ref={menuRef}>{children}</div>
</MenuListProvider>
</FocusZone>
}
/>
);
}
30 changes: 30 additions & 0 deletions packages/fluentui/docs/src/prototypes/menuList/MenuTrigger.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import * as React from 'react';
import { useMenuContext } from './menuContext';
import { useEventListener } from '@fluentui/react-component-event-listener';

export function MenuTrigger({ children }) {
const { triggerRef, setOpen } = useMenuContext();

const listener = React.useCallback(
e => {
if (e.keyCode !== 37) {
setOpen(true);
}
},
[setOpen],
);

useEventListener({
type: 'mouseenter',
targetRef: triggerRef,
listener,
});

useEventListener({
type: 'keyup',
targetRef: triggerRef,
listener,
});

return <div ref={triggerRef}>{children}</div>;
}
11 changes: 11 additions & 0 deletions packages/fluentui/docs/src/prototypes/menuList/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import * as React from 'react';
import { PrototypeSection, ComponentPrototype } from '../Prototypes';
import { MenuButton } from './MenuButton';

export default () => (
<PrototypeSection title="Menu Button">
<ComponentPrototype title="Reusable submenu" description="">
<MenuButton />
</ComponentPrototype>
</PrototypeSection>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import * as React from 'react';

export const MenuContext = React.createContext<any>({});

export const MenuContextProvider = MenuContext.Provider;

export const useMenuContext = () => React.useContext(MenuContext);
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import * as React from 'react';

const MenuListContext = React.createContext<MenuListContext>({
currentIndex: 1,
setIndex: null,
setOpen: null,
triggerRef: null,
});

export interface MenuListContext {
currentIndex: number;
setIndex: (index: number) => void;
setOpen: (open: boolean) => void;
triggerRef: React.RefObject<HTMLDivElement>;
}

export const MenuListProvider = MenuListContext.Provider;

export const useMenuListContext = () => React.useContext(MenuListContext);
2 changes: 2 additions & 0 deletions packages/fluentui/docs/src/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import FocusZone from './views/FocusZoneDoc';
import FocusTrapZone from './views/FocusTrapZoneDoc';
import AutoFocusZone from './views/AutoFocusZoneDoc';
import { LazyWithBabel } from './components/ComponentDoc/LazyWithBabel';
import MenuList from './prototypes/menuList/';
import TextAreaAutoSize from './prototypes/TextAreaAutoSize';

const _Builder = React.lazy(async () => ({
Expand Down Expand Up @@ -150,6 +151,7 @@ const Routes = () => (
<Route exact path="/virtualized-tree" component={VirtualizedTreePrototype} />
<Route exact path="/virtualized-table" component={VirtualizedTablePrototype} />
<Route exact path="/prototype-copy-to-clipboard" component={CopyToClipboardPrototype} />
<Route exact path="/prototype-menu-list" component={MenuList} />
<Route
exact
path="/unstable-datepicker"
Expand Down