/** @jsxImportSource @fluentui-react-native/framework-base */ import * as React from 'react'; import { View, ScrollView, Platform, I18nManager } from 'react-native'; import { Callout } from '@fluentui-react-native/callout'; import { FocusZone } from '@fluentui-react-native/focus-zone'; import type { IFocusable } from '@fluentui-react-native/interactive-hooks'; import { useKeyDownProps, useSelectedKey } from '@fluentui-react-native/interactive-hooks'; import { backgroundColorTokens, borderTokens } from '@fluentui-react-native/tokens'; import type { ISlots } from '@uifabricshared/foundation-composable'; import type { IUseComposeStyling } from '@uifabricshared/foundation-compose'; import { compose } from '@uifabricshared/foundation-compose'; import { mergeSettings } from '@uifabricshared/foundation-settings'; import { CMContext } from './ContextualMenu'; import { settings } from './Submenu.settings'; import type { SubmenuProps, SubmenuSlotProps, SubmenuType, SubmenuRenderData, SubmenuState } from './Submenu.types'; import { submenuName } from './Submenu.types'; export const Submenu = compose({ displayName: submenuName, usePrepareProps: (userProps: SubmenuProps, useStyling: IUseComposeStyling) => { const { setShowMenu, maxWidth, maxHeight, onDismiss, onItemClick, onShow, shouldFocusOnMount = true, shouldFocusOnContainer = true, ...rest } = userProps; /** * On macOS, focus isn't placed by default on the first focusable element. We get around this by focusing on the inner FocusZone * hosting the menu. For whatever reason, to get the timing _just_ right to actually focus, we need an additional `setTimeout` * on top of the `useLayoutEffect` hook. */ const focusZoneRef = React.useRef(null); React.useLayoutEffect(() => { if (Platform.OS === 'macos') { setTimeout(() => { focusZoneRef.current?.focus(); }, 0); } }, []); // Grabs the context information from ContextualMenu (onDismissMenu callback) const context = React.useContext(CMContext); // This hook updates the Selected Button and calls the customer's onClick function. This gets called after a button is pressed. const data = useSelectedKey(null, onItemClick); const onShowCallback = React.useCallback(() => { onShow && onShow(); context.isSubmenuOpen = true; }, [context, onShow]); const onDismissCallback = React.useCallback(() => { onDismiss && onDismiss(); setShowMenu(false); context.isSubmenuOpen = false; }, [context, onDismiss, setShowMenu]); const dismissCallback = React.useCallback(() => { onDismissCallback(); context?.onDismissMenu(); }, [context, onDismissCallback]); context.dismissSubmenu = onDismissCallback; const [containerFocus, setContainerFocus] = React.useState(true); const toggleContainerFocus = React.useCallback(() => { setContainerFocus(false); }, [setContainerFocus]); const state: SubmenuState = { context: { selectedKey: data.selectedKey, onItemClick: data.onKeySelect, onDismissMenu: dismissCallback, }, }; const styleProps = useStyling(userProps, (override: string) => state[override] || userProps[override]); const dismissWithArrowKey = React.useCallback( (e: any) => { const arrowKey = I18nManager.isRTL ? 'ArrowRight' : 'ArrowLeft'; if (e.nativeEvent.key === arrowKey) { onDismissCallback(); } }, [onDismissCallback], ); // Explicitly override onKeyDown to override the native windows behavior of moving focus with arrow keys. const onKeyDownProps = useKeyDownProps(dismissWithArrowKey, 'ArrowLeft', 'ArrowRight'); const slotProps = mergeSettings(styleProps, { root: { ...rest, onShow: onShowCallback, onDismiss: onDismissCallback, setInitialFocus: shouldFocusOnMount, }, container: { ...onKeyDownProps, accessible: shouldFocusOnContainer, focusable: shouldFocusOnContainer && containerFocus, onBlur: toggleContainerFocus, style: { maxHeight: maxHeight, maxWidth: maxWidth }, }, scrollView: { contentContainerStyle: { flexDirection: 'column', flexGrow: 1, }, showsVerticalScrollIndicator: maxHeight != undefined, showsHorizontalScrollIndicator: maxWidth != undefined, }, focusZone: { componentRef: focusZoneRef, focusZoneDirection: 'vertical', }, }); return { slotProps, state }; }, settings: settings, slots: { root: Callout, container: View, scrollView: ScrollView, focusZone: FocusZone, }, styles: { root: [backgroundColorTokens, borderTokens], container: [], }, render: (Slots: ISlots, renderData: SubmenuRenderData, ...children: React.ReactNode[]) => { if (renderData.state == undefined) { return null; } // On macOS, wrap the children in a FocusZone to allow you to arrow-key through the menu items. // Duplicating the JSX trees was the only way I could find to correctly render the optional slot. if (Platform.OS === 'macos') { return ( {children} ); } else { return ( {children} ); } }, }); export default Submenu;