diff --git a/packages/fluentui/docs/src/components/Sidebar/Sidebar.tsx b/packages/fluentui/docs/src/components/Sidebar/Sidebar.tsx index ef0d5dda01c65..e35cdb3e79e27 100644 --- a/packages/fluentui/docs/src/components/Sidebar/Sidebar.tsx +++ b/packages/fluentui/docs/src/components/Sidebar/Sidebar.tsx @@ -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: { diff --git a/packages/fluentui/docs/src/prototypes/menuList/Menu.tsx b/packages/fluentui/docs/src/prototypes/menuList/Menu.tsx new file mode 100644 index 0000000000000..be9e3480947bd --- /dev/null +++ b/packages/fluentui/docs/src/prototypes/menuList/Menu.tsx @@ -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(); + const menuRef = React.useRef(); + + // 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 ( + +
{ + if ( + !menuRef.current?.contains(relatedTarget as Node) && + !triggerRef.current?.contains(relatedTarget as Node) + ) { + setOpen(false); + } + }} + > + {children} +
+
+ ); +}; diff --git a/packages/fluentui/docs/src/prototypes/menuList/MenuButton.tsx b/packages/fluentui/docs/src/prototypes/menuList/MenuButton.tsx new file mode 100644 index 0000000000000..f4f3487a05070 --- /dev/null +++ b/packages/fluentui/docs/src/prototypes/menuList/MenuButton.tsx @@ -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 v +
+
+ + Item 1 + Item 2 + Item 3 + Item 4 + + + Item 5 + + + item 1 + item 2 + + + Item 3 + + + 1 + 2 + + + + + +
+ ); +} diff --git a/packages/fluentui/docs/src/prototypes/menuList/MenuItem.tsx b/packages/fluentui/docs/src/prototypes/menuList/MenuItem.tsx new file mode 100644 index 0000000000000..9a8f81dea4aa9 --- /dev/null +++ b/packages/fluentui/docs/src/prototypes/menuList/MenuItem.tsx @@ -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(); + 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 ( +
+ {children} +
+ ); +} diff --git a/packages/fluentui/docs/src/prototypes/menuList/MenuList.tsx b/packages/fluentui/docs/src/prototypes/menuList/MenuList.tsx new file mode 100644 index 0000000000000..4e381a107b614 --- /dev/null +++ b/packages/fluentui/docs/src/prototypes/menuList/MenuList.tsx @@ -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 ( + + +
{children}
+
+ + } + /> + ); +} diff --git a/packages/fluentui/docs/src/prototypes/menuList/MenuTrigger.tsx b/packages/fluentui/docs/src/prototypes/menuList/MenuTrigger.tsx new file mode 100644 index 0000000000000..40c9553f2f173 --- /dev/null +++ b/packages/fluentui/docs/src/prototypes/menuList/MenuTrigger.tsx @@ -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
{children}
; +} diff --git a/packages/fluentui/docs/src/prototypes/menuList/index.tsx b/packages/fluentui/docs/src/prototypes/menuList/index.tsx new file mode 100644 index 0000000000000..8d09267c76687 --- /dev/null +++ b/packages/fluentui/docs/src/prototypes/menuList/index.tsx @@ -0,0 +1,11 @@ +import * as React from 'react'; +import { PrototypeSection, ComponentPrototype } from '../Prototypes'; +import { MenuButton } from './MenuButton'; + +export default () => ( + + + + + +); diff --git a/packages/fluentui/docs/src/prototypes/menuList/menuContext.ts b/packages/fluentui/docs/src/prototypes/menuList/menuContext.ts new file mode 100644 index 0000000000000..1796cae2a5767 --- /dev/null +++ b/packages/fluentui/docs/src/prototypes/menuList/menuContext.ts @@ -0,0 +1,7 @@ +import * as React from 'react'; + +export const MenuContext = React.createContext({}); + +export const MenuContextProvider = MenuContext.Provider; + +export const useMenuContext = () => React.useContext(MenuContext); diff --git a/packages/fluentui/docs/src/prototypes/menuList/menuListContext.tsx b/packages/fluentui/docs/src/prototypes/menuList/menuListContext.tsx new file mode 100644 index 0000000000000..9809f152f7bd1 --- /dev/null +++ b/packages/fluentui/docs/src/prototypes/menuList/menuListContext.tsx @@ -0,0 +1,19 @@ +import * as React from 'react'; + +const MenuListContext = React.createContext({ + currentIndex: 1, + setIndex: null, + setOpen: null, + triggerRef: null, +}); + +export interface MenuListContext { + currentIndex: number; + setIndex: (index: number) => void; + setOpen: (open: boolean) => void; + triggerRef: React.RefObject; +} + +export const MenuListProvider = MenuListContext.Provider; + +export const useMenuListContext = () => React.useContext(MenuListContext); diff --git a/packages/fluentui/docs/src/routes.tsx b/packages/fluentui/docs/src/routes.tsx index 8dec55da5bdd1..01aa3d5bcbe71 100644 --- a/packages/fluentui/docs/src/routes.tsx +++ b/packages/fluentui/docs/src/routes.tsx @@ -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 () => ({ @@ -150,6 +151,7 @@ const Routes = () => ( +