@fluentui-react-native/menu 0.12.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/CHANGELOG.json +46 -1
  2. package/CHANGELOG.md +26 -2
  3. package/jest.config.js +2 -0
  4. package/lib/Menu/Menu.types.d.ts +2 -0
  5. package/lib/Menu/Menu.types.d.ts.map +1 -1
  6. package/lib/Menu/useMenu.d.ts.map +1 -1
  7. package/lib/Menu/useMenu.js +8 -3
  8. package/lib/Menu/useMenu.js.map +1 -1
  9. package/lib/Menu/useMenuContextValue.d.ts.map +1 -1
  10. package/lib/Menu/useMenuContextValue.js +4 -1
  11. package/lib/Menu/useMenuContextValue.js.map +1 -1
  12. package/lib/MenuItem/MenuItem.js +1 -1
  13. package/lib/MenuItem/MenuItem.js.map +1 -1
  14. package/lib/MenuItem/useMenuItem.d.ts.map +1 -1
  15. package/lib/MenuItem/useMenuItem.js +34 -6
  16. package/lib/MenuItem/useMenuItem.js.map +1 -1
  17. package/lib/MenuItemCheckbox/MenuItemCheckbox.js +1 -1
  18. package/lib/MenuItemCheckbox/MenuItemCheckbox.js.map +1 -1
  19. package/lib/MenuItemCheckbox/useMenuItemCheckbox.d.ts.map +1 -1
  20. package/lib/MenuItemCheckbox/useMenuItemCheckbox.js +12 -8
  21. package/lib/MenuItemCheckbox/useMenuItemCheckbox.js.map +1 -1
  22. package/lib/MenuItemRadio/useMenuItemRadio.d.ts.map +1 -1
  23. package/lib/MenuItemRadio/useMenuItemRadio.js +5 -3
  24. package/lib/MenuItemRadio/useMenuItemRadio.js.map +1 -1
  25. package/lib/MenuPopover/MenuPopover.d.ts.map +1 -1
  26. package/lib/MenuPopover/MenuPopover.js +7 -3
  27. package/lib/MenuPopover/MenuPopover.js.map +1 -1
  28. package/lib/MenuPopover/MenuPopover.types.d.ts +4 -10
  29. package/lib/MenuPopover/MenuPopover.types.d.ts.map +1 -1
  30. package/lib/MenuPopover/useMenuPopover.d.ts.map +1 -1
  31. package/lib/MenuPopover/useMenuPopover.js +35 -8
  32. package/lib/MenuPopover/useMenuPopover.js.map +1 -1
  33. package/lib/MenuTrigger/MenuTrigger.js +8 -8
  34. package/lib/MenuTrigger/MenuTrigger.js.map +1 -1
  35. package/lib/MenuTrigger/useMenuTrigger.d.ts.map +1 -1
  36. package/lib/MenuTrigger/useMenuTrigger.js +21 -20
  37. package/lib/MenuTrigger/useMenuTrigger.js.map +1 -1
  38. package/lib/__tests__/Menu.test.d.ts +2 -0
  39. package/lib/__tests__/Menu.test.d.ts.map +1 -0
  40. package/lib/__tests__/Menu.test.js +145 -0
  41. package/lib/__tests__/Menu.test.js.map +1 -0
  42. package/lib/consts.d.ts +3 -0
  43. package/lib/consts.d.ts.map +1 -0
  44. package/lib/consts.js +7 -0
  45. package/lib/consts.js.map +1 -0
  46. package/lib/context/menuContext.d.ts +10 -4
  47. package/lib/context/menuContext.d.ts.map +1 -1
  48. package/lib/context/menuContext.js.map +1 -1
  49. package/lib-commonjs/Menu/Menu.types.d.ts +2 -0
  50. package/lib-commonjs/Menu/Menu.types.d.ts.map +1 -1
  51. package/lib-commonjs/Menu/useMenu.d.ts.map +1 -1
  52. package/lib-commonjs/Menu/useMenu.js +8 -3
  53. package/lib-commonjs/Menu/useMenu.js.map +1 -1
  54. package/lib-commonjs/Menu/useMenuContextValue.d.ts.map +1 -1
  55. package/lib-commonjs/Menu/useMenuContextValue.js +4 -1
  56. package/lib-commonjs/Menu/useMenuContextValue.js.map +1 -1
  57. package/lib-commonjs/MenuItem/MenuItem.js +1 -1
  58. package/lib-commonjs/MenuItem/MenuItem.js.map +1 -1
  59. package/lib-commonjs/MenuItem/useMenuItem.d.ts.map +1 -1
  60. package/lib-commonjs/MenuItem/useMenuItem.js +32 -4
  61. package/lib-commonjs/MenuItem/useMenuItem.js.map +1 -1
  62. package/lib-commonjs/MenuItemCheckbox/MenuItemCheckbox.js +1 -1
  63. package/lib-commonjs/MenuItemCheckbox/MenuItemCheckbox.js.map +1 -1
  64. package/lib-commonjs/MenuItemCheckbox/useMenuItemCheckbox.d.ts.map +1 -1
  65. package/lib-commonjs/MenuItemCheckbox/useMenuItemCheckbox.js +12 -8
  66. package/lib-commonjs/MenuItemCheckbox/useMenuItemCheckbox.js.map +1 -1
  67. package/lib-commonjs/MenuItemRadio/useMenuItemRadio.d.ts.map +1 -1
  68. package/lib-commonjs/MenuItemRadio/useMenuItemRadio.js +5 -3
  69. package/lib-commonjs/MenuItemRadio/useMenuItemRadio.js.map +1 -1
  70. package/lib-commonjs/MenuPopover/MenuPopover.d.ts.map +1 -1
  71. package/lib-commonjs/MenuPopover/MenuPopover.js +5 -2
  72. package/lib-commonjs/MenuPopover/MenuPopover.js.map +1 -1
  73. package/lib-commonjs/MenuPopover/MenuPopover.types.d.ts +4 -10
  74. package/lib-commonjs/MenuPopover/MenuPopover.types.d.ts.map +1 -1
  75. package/lib-commonjs/MenuPopover/useMenuPopover.d.ts.map +1 -1
  76. package/lib-commonjs/MenuPopover/useMenuPopover.js +35 -8
  77. package/lib-commonjs/MenuPopover/useMenuPopover.js.map +1 -1
  78. package/lib-commonjs/MenuTrigger/MenuTrigger.js +8 -8
  79. package/lib-commonjs/MenuTrigger/MenuTrigger.js.map +1 -1
  80. package/lib-commonjs/MenuTrigger/useMenuTrigger.d.ts.map +1 -1
  81. package/lib-commonjs/MenuTrigger/useMenuTrigger.js +21 -20
  82. package/lib-commonjs/MenuTrigger/useMenuTrigger.js.map +1 -1
  83. package/lib-commonjs/__tests__/Menu.test.d.ts +2 -0
  84. package/lib-commonjs/__tests__/Menu.test.d.ts.map +1 -0
  85. package/lib-commonjs/__tests__/Menu.test.js +148 -0
  86. package/lib-commonjs/__tests__/Menu.test.js.map +1 -0
  87. package/lib-commonjs/consts.d.ts +3 -0
  88. package/lib-commonjs/consts.d.ts.map +1 -0
  89. package/lib-commonjs/consts.js +10 -0
  90. package/lib-commonjs/consts.js.map +1 -0
  91. package/lib-commonjs/context/menuContext.d.ts +10 -4
  92. package/lib-commonjs/context/menuContext.d.ts.map +1 -1
  93. package/lib-commonjs/context/menuContext.js.map +1 -1
  94. package/package.json +3 -1
  95. package/src/Menu/Menu.types.ts +1 -0
  96. package/src/Menu/useMenu.ts +9 -2
  97. package/src/Menu/useMenuContextValue.ts +4 -1
  98. package/src/MenuItem/MenuItem.tsx +1 -1
  99. package/src/MenuItem/useMenuItem.ts +48 -8
  100. package/src/MenuItemCheckbox/MenuItemCheckbox.tsx +1 -1
  101. package/src/MenuItemCheckbox/useMenuItemCheckbox.ts +12 -8
  102. package/src/MenuItemRadio/useMenuItemRadio.ts +5 -3
  103. package/src/MenuPopover/MenuPopover.tsx +7 -14
  104. package/src/MenuPopover/MenuPopover.types.ts +4 -9
  105. package/src/MenuPopover/useMenuPopover.ts +48 -8
  106. package/src/MenuTrigger/MenuTrigger.tsx +9 -9
  107. package/src/MenuTrigger/useMenuTrigger.ts +36 -26
  108. package/src/__tests__/Menu.test.tsx +235 -0
  109. package/src/__tests__/__snapshots__/Menu.test.tsx.snap +2098 -0
  110. package/src/consts.ts +8 -0
  111. package/src/context/menuContext.ts +6 -1
@@ -1,20 +1,60 @@
1
1
  import * as React from 'react';
2
- import { AccessibilityState } from 'react-native';
2
+ import { AccessibilityState, I18nManager } from 'react-native';
3
3
  import { MenuItemProps, MenuItemState } from './MenuItem.types';
4
4
  import { memoize } from '@fluentui-react-native/framework';
5
- import { useAsPressable, useKeyProps } from '@fluentui-react-native/interactive-hooks';
5
+ import { InteractionEvent, isKeyPressEvent, useAsPressable, useKeyDownProps } from '@fluentui-react-native/interactive-hooks';
6
6
  import { useMenuContext } from '../context/menuContext';
7
7
  import { useMenuListContext } from '../context/menuListContext';
8
8
  import { useMenuTriggerContext } from '../context/menuTriggerContext';
9
9
 
10
+ const triggerKeys = [' ', 'Enter'];
11
+ const submenuTriggerKeys = [...triggerKeys, 'ArrowLeft', 'ArrowRight'];
12
+
10
13
  export const useMenuItem = (props: MenuItemProps): MenuItemState => {
11
14
  // attach the pressable state handlers
12
15
  const defaultComponentRef = React.useRef(null);
13
16
  const { onClick, accessibilityState, componentRef = defaultComponentRef, disabled, ...rest } = props;
14
- const pressable = useAsPressable({ ...rest, disabled, onPress: onClick });
15
- const onKeyProps = useKeyProps(onClick, ' ', 'Enter');
16
17
  const isTrigger = useMenuTriggerContext();
17
- const hasSubmenu = useMenuContext().isSubmenu && isTrigger;
18
+ const isSubmenu = useMenuContext().isSubmenu;
19
+ const hasSubmenu = isSubmenu && isTrigger;
20
+ const isInSubmenu = isSubmenu && !isTrigger;
21
+
22
+ const setOpen = useMenuContext().setOpen;
23
+ const onInvoke = React.useCallback(
24
+ (e: InteractionEvent) => {
25
+ if (disabled) {
26
+ return;
27
+ }
28
+
29
+ const isRtl = I18nManager.isRTL;
30
+ if (
31
+ isKeyPressEvent(e) &&
32
+ hasSubmenu &&
33
+ ((isRtl && e.nativeEvent.key === 'ArrowRight') || (!isRtl && e.nativeEvent.key === 'ArrowLeft'))
34
+ ) {
35
+ return;
36
+ }
37
+ if (
38
+ isKeyPressEvent(e) &&
39
+ isInSubmenu &&
40
+ ((isRtl && e.nativeEvent.key === 'ArrowLeft') || (!isRtl && e.nativeEvent.key === 'ArrowRight'))
41
+ ) {
42
+ return;
43
+ }
44
+
45
+ onClick?.(e);
46
+ if (!hasSubmenu) {
47
+ setOpen(e, false /*isOpen*/);
48
+ }
49
+ },
50
+ [disabled, hasSubmenu, isInSubmenu, onClick, setOpen],
51
+ );
52
+
53
+ const pressable = useAsPressable({ ...rest, disabled, onPress: onInvoke });
54
+ const keys = isSubmenu ? submenuTriggerKeys : triggerKeys;
55
+
56
+ // Explicitly override onKeyDown to override the native behavior of moving focus with arrow keys.
57
+ const onKeyDownProps = useKeyDownProps(onInvoke, ...keys);
18
58
  const hasCheckmarks = useMenuListContext().hasCheckmarks;
19
59
 
20
60
  return {
@@ -22,13 +62,13 @@ export const useMenuItem = (props: MenuItemProps): MenuItemState => {
22
62
  ...pressable.props,
23
63
  accessible: true,
24
64
  accessibilityRole: 'menuitem',
25
- onAccessibilityTap: props.onAccessibilityTap || props.onClick,
65
+ onAccessibilityTap: props.onAccessibilityTap || onInvoke,
26
66
  accessibilityLabel: props.accessibilityLabel || props.content,
27
67
  accessibilityState: getAccessibilityState(disabled, accessibilityState),
28
68
  enableFocusRing: true,
29
- focusable: !disabled,
69
+ focusable: true,
30
70
  ref: componentRef,
31
- ...onKeyProps,
71
+ ...onKeyDownProps,
32
72
  },
33
73
  state: pressable.state,
34
74
  hasSubmenu,
@@ -23,7 +23,7 @@ export const MenuItemCheckbox = compose<MenuItemCheckboxType>({
23
23
  },
24
24
  useRender: (userProps: MenuItemCheckboxProps, useSlots: UseSlots<MenuItemCheckboxType>) => {
25
25
  const menuItem = useMenuItemCheckbox(userProps);
26
- const Slots = useSlots(userProps, (layer): boolean => menuItem.state[layer]);
26
+ const Slots = useSlots(userProps, (layer): boolean => menuItem.state[layer] || userProps[layer]);
27
27
 
28
28
  return menuItemFinalRender(menuItem, Slots);
29
29
  },
@@ -14,16 +14,18 @@ import { useMenuListContext } from '../context/menuListContext';
14
14
  const defaultAccessibilityActions = [{ name: 'Toggle' }];
15
15
 
16
16
  export const useMenuItemCheckbox = (props: MenuItemCheckboxProps): MenuItemCheckboxState => {
17
- const { name } = props;
17
+ const { disabled, name } = props;
18
18
  const context = useMenuListContext();
19
19
  const checked = context.checked?.[name];
20
20
  const onCheckedChange = context.onCheckedChange;
21
21
 
22
22
  const toggleChecked = React.useCallback(
23
23
  (e: InteractionEvent) => {
24
- onCheckedChange(e, name, !checked);
24
+ if (!disabled) {
25
+ onCheckedChange(e, name, !checked);
26
+ }
25
27
  },
26
- [checked, name, onCheckedChange],
28
+ [checked, disabled, name, onCheckedChange],
27
29
  );
28
30
 
29
31
  return useMenuCheckboxInteraction(props, toggleChecked);
@@ -74,12 +76,14 @@ export const useMenuCheckboxInteraction = (
74
76
  : defaultAccessibilityActions;
75
77
  const onAccessibilityActionProp = React.useCallback(
76
78
  (event: AccessibilityActionEvent) => {
77
- if (event.nativeEvent.actionName === 'Toggle') {
78
- toggleCallback(event);
79
+ if (!disabled) {
80
+ if (event.nativeEvent.actionName === 'Toggle') {
81
+ toggleCallback(event);
82
+ }
83
+ onAccessibilityAction && onAccessibilityAction(event);
79
84
  }
80
- onAccessibilityAction && onAccessibilityAction(event);
81
85
  },
82
- [toggleCallback, onAccessibilityAction],
86
+ [disabled, toggleCallback, onAccessibilityAction],
83
87
  );
84
88
 
85
89
  const state = {
@@ -97,7 +101,7 @@ export const useMenuCheckboxInteraction = (
97
101
  accessibilityRole: 'menuitem',
98
102
  accessibilityState: getAccessibilityState(disabled, state.checked, accessibilityState),
99
103
  enableFocusRing: true,
100
- focusable: !disabled,
104
+ focusable: true,
101
105
  onAccessibilityAction: onAccessibilityActionProp,
102
106
  ref: buttonRef,
103
107
  ...onKeyProps,
@@ -5,16 +5,18 @@ import { MenuItemCheckboxProps, MenuItemCheckboxState } from '../MenuItemCheckbo
5
5
  import { useMenuCheckboxInteraction } from '../MenuItemCheckbox/useMenuItemCheckbox';
6
6
 
7
7
  export const useMenuItemRadio = (props: MenuItemCheckboxProps): MenuItemCheckboxState => {
8
- const { name } = props;
8
+ const { disabled, name } = props;
9
9
  const context = useMenuListContext();
10
10
  const checked = context.checked?.[name];
11
11
  const selectRadio = context.selectRadio;
12
12
 
13
13
  const toggleChecked = React.useCallback(
14
14
  (e: InteractionEvent) => {
15
- selectRadio(e, name, !checked);
15
+ if (!disabled) {
16
+ selectRadio(e, name, !checked);
17
+ }
16
18
  },
17
- [checked, name, selectRadio],
19
+ [checked, disabled, name, selectRadio],
18
20
  );
19
21
 
20
22
  return useMenuCheckboxInteraction(props, toggleChecked);
@@ -1,27 +1,20 @@
1
1
  import React from 'react';
2
- import { stagedComponent, useFluentTheme } from '@fluentui-react-native/framework';
2
+ import { mergeProps, stagedComponent, useFluentTheme } from '@fluentui-react-native/framework';
3
3
  import { Callout } from '@fluentui-react-native/callout';
4
4
  import { menuPopoverName, MenuPopoverProps } from './MenuPopover.types';
5
5
  import { useMenuPopover } from './useMenuPopover';
6
+ import { View } from 'react-native';
6
7
 
7
8
  export const MenuPopover = stagedComponent((props: MenuPopoverProps) => {
8
9
  const state = useMenuPopover(props);
9
10
  const theme = useFluentTheme();
10
11
 
11
- return (_rest: MenuPopoverProps, children: React.ReactNode) => {
12
+ return (final: MenuPopoverProps, children: React.ReactNode) => {
13
+ const mergedProps = mergeProps(state.props, final);
14
+ const content = React.createElement(View, state.innerView, children);
12
15
  return (
13
- <Callout
14
- accessibilityRole={state.accessibilityRole}
15
- borderWidth={1}
16
- borderColor={theme.colors.neutralStrokeAccessible}
17
- target={state.triggerRef}
18
- onDismiss={state.onDismiss}
19
- dismissBehaviors={state.dismissBehaviors}
20
- setInitialFocus={state.setInitialFocus}
21
- directionalHint={state.directionalHint}
22
- doNotTakePointerCapture={state.doNotTakePointerCapture}
23
- >
24
- {children}
16
+ <Callout borderWidth={1} borderColor={theme.colors.neutralStrokeAccessible} {...mergedProps}>
17
+ {content}
25
18
  </Callout>
26
19
  );
27
20
  };
@@ -1,16 +1,11 @@
1
- import { DirectionalHint, DismissBehaviors, ICalloutProps } from '@fluentui-react-native/callout';
2
- import { AccessibilityRole } from 'react-native';
1
+ import { IViewProps } from '@fluentui-react-native/adapters';
2
+ import { ICalloutProps } from '@fluentui-react-native/callout';
3
3
 
4
4
  export const menuPopoverName = 'MenuPopover';
5
5
 
6
6
  export type MenuPopoverProps = ICalloutProps;
7
7
 
8
8
  export interface MenuPopoverState {
9
- accessibilityRole: AccessibilityRole;
10
- directionalHint?: DirectionalHint;
11
- dismissBehaviors: DismissBehaviors[];
12
- doNotTakePointerCapture: boolean;
13
- onDismiss: () => void;
14
- setInitialFocus: boolean;
15
- triggerRef: React.RefObject<React.Component>;
9
+ props: ICalloutProps;
10
+ innerView: IViewProps;
16
11
  }
@@ -3,25 +3,65 @@ import { I18nManager, Platform } from 'react-native';
3
3
  import { DirectionalHint, DismissBehaviors } from '@fluentui-react-native/callout';
4
4
  import { useMenuContext } from '../context/menuContext';
5
5
  import { MenuPopoverProps, MenuPopoverState } from './MenuPopover.types';
6
+ import { isCloseOnHoverOutEnabled } from '../consts';
7
+
8
+ const controlledDismissBehaviors = ['preventDismissOnKeyDown', 'preventDismissOnClickOutside'] as DismissBehaviors[];
6
9
 
7
10
  export const useMenuPopover = (_props: MenuPopoverProps): MenuPopoverState => {
8
11
  const context = useMenuContext();
9
- const setOpen = context.setOpen;
12
+ const {
13
+ setOpen,
14
+ triggerRef,
15
+ isControlled,
16
+ isSubmenu,
17
+ openOnHover,
18
+ parentPopoverHoverOutTimer,
19
+ popoverHoverOutTimer,
20
+ setPopoverHoverOutTimer,
21
+ triggerHoverOutTimer,
22
+ } = context;
10
23
 
11
- const triggerRef = context.triggerRef;
12
24
  const onDismiss = React.useCallback(() => setOpen(undefined, false /* isOpen */), [setOpen]);
13
- const dismissBehaviors = context.isControlled
14
- ? (['preventDismissOnKeyDown', 'preventDismissOnClickOutside'] as DismissBehaviors[])
15
- : undefined;
16
- const directionalHint = getDirectionalHint(context.isSubmenu, I18nManager.isRTL);
25
+ const dismissBehaviors = isControlled ? controlledDismissBehaviors : undefined;
26
+ const directionalHint = getDirectionalHint(isSubmenu, I18nManager.isRTL);
17
27
 
18
28
  // Initial focus behavior differs per platform, Windows platforms move focus
19
29
  // automatically onto first element of Callout
20
30
  const setInitialFocus = Platform.OS === ('win32' as any) || Platform.OS === 'windows';
21
- const doNotTakePointerCapture = context.openOnHover;
31
+ const doNotTakePointerCapture = openOnHover;
22
32
  const accessibilityRole = 'menu';
23
33
 
24
- return { accessibilityRole, triggerRef, onDismiss, directionalHint, dismissBehaviors, doNotTakePointerCapture, setInitialFocus };
34
+ const onMouseEnter = React.useCallback(() => {
35
+ clearTimeout(triggerHoverOutTimer);
36
+ clearTimeout(popoverHoverOutTimer);
37
+ clearTimeout(parentPopoverHoverOutTimer);
38
+ }, [parentPopoverHoverOutTimer, popoverHoverOutTimer, triggerHoverOutTimer]);
39
+ const onMouseLeave = React.useCallback(() => {
40
+ if (!openOnHover) {
41
+ return;
42
+ }
43
+
44
+ const timer = setTimeout(() => {
45
+ setOpen(undefined, false /* isOpen */);
46
+ }, 500);
47
+ setPopoverHoverOutTimer(timer);
48
+ }, [openOnHover, setOpen, setPopoverHoverOutTimer]);
49
+
50
+ return {
51
+ props: {
52
+ accessibilityRole,
53
+ target: triggerRef,
54
+ onDismiss,
55
+ directionalHint,
56
+ dismissBehaviors,
57
+ doNotTakePointerCapture,
58
+ setInitialFocus,
59
+ },
60
+ innerView: {
61
+ onMouseEnter,
62
+ onMouseLeave: isCloseOnHoverOutEnabled && onMouseLeave,
63
+ },
64
+ };
25
65
  };
26
66
 
27
67
  const getDirectionalHint = (isSubmenu: boolean, isRtl: boolean): DirectionalHint | undefined => {
@@ -21,8 +21,8 @@ export const MenuTrigger = stagedComponent((props: MenuTriggerProps) => {
21
21
  // child component which may affect accessibility, we need to modify the
22
22
  // state in the inner render so we can access the child component and its props.
23
23
  const child = childrenArray[0];
24
- const revisedState = getRevisedState(menuTrigger, child.props);
25
- const revised = React.cloneElement(child, revisedState);
24
+ const revisedProps = getRevisedState(menuTrigger, child.props);
25
+ const revised = React.cloneElement(child, revisedProps);
26
26
 
27
27
  return <MenuTriggerProvider value={menuTrigger.hasSubmenu}>{revised}</MenuTriggerProvider>;
28
28
  };
@@ -30,24 +30,24 @@ export const MenuTrigger = stagedComponent((props: MenuTriggerProps) => {
30
30
  MenuTrigger.displayName = menuTriggerName;
31
31
 
32
32
  const getRevisedState = memoize(getRevisedStateWorker);
33
- function getRevisedStateWorker(state: MenuTriggerState, props: any): MenuTriggerState {
34
- const revisedState = { ...state };
33
+ function getRevisedStateWorker(state: MenuTriggerState, props: any): MenuTriggerProps {
34
+ const revisedProps = state.props;
35
35
  if (props.accessibilityState) {
36
- revisedState.props.accessibilityState = { ...state.props.accessibilityState, ...props.accessibilityState };
36
+ revisedProps.accessibilityState = { ...revisedProps.accessibilityState, ...props.accessibilityState };
37
37
  }
38
38
 
39
39
  if (props.accessibilityActions) {
40
- revisedState.props.accessibilityActions = { ...state.props.accessibilityActions, ...props.accessibilityActions };
40
+ revisedProps.accessibilityActions = { ...revisedProps.accessibilityActions, ...props.accessibilityActions };
41
41
  }
42
42
 
43
43
  if (props.onAccessibilityAction) {
44
- revisedState.props.onAccessibilityAction = (e: AccessibilityActionEvent) => {
45
- state.props.onAccessibilityAction(e);
44
+ revisedProps.onAccessibilityAction = (e: AccessibilityActionEvent) => {
45
+ revisedProps.onAccessibilityAction(e);
46
46
  props.onAccessibilityAction(e);
47
47
  };
48
48
  }
49
49
 
50
- return revisedState;
50
+ return revisedProps;
51
51
  }
52
52
 
53
53
  export default MenuTrigger;
@@ -3,18 +3,19 @@ import { InteractionEvent } from '@fluentui-react-native/interactive-hooks';
3
3
  import { MenuTriggerProps, MenuTriggerState } from './MenuTrigger.types';
4
4
  import { AccessibilityActionEvent, AccessibilityActionName, Platform } from 'react-native';
5
5
  import React from 'react';
6
+ import { delayHover, isCloseOnHoverOutEnabled } from '../consts';
6
7
 
7
8
  const accessibilityActions =
8
9
  Platform.OS === ('win32' as any) ? [{ name: 'Expand' as AccessibilityActionName }, { name: 'Collapse' as AccessibilityActionName }] : [];
10
+ const expandedState = { expanded: true };
11
+ const collapsedState = { expanded: false };
9
12
 
10
13
  export const useMenuTrigger = (_props: MenuTriggerProps): MenuTriggerState => {
11
14
  const context = useMenuContext();
15
+ const { open, openOnHover, popoverHoverOutTimer, setOpen, setTriggerHoverOutTimer, triggerHoverOutTimer, triggerRef } = context;
16
+
17
+ const accessibilityState = open ? expandedState : collapsedState;
12
18
 
13
- const setOpen = context.setOpen;
14
- const open = context.open;
15
- const openOnHover = context.openOnHover;
16
- const triggerRef = context.triggerRef;
17
- const accessibilityState = context.open ? { expanded: true } : { expanded: false };
18
19
  const onAccessibilityAction = React.useCallback(
19
20
  (e: AccessibilityActionEvent) => {
20
21
  if (Platform.OS === ('win32' as any)) {
@@ -32,35 +33,44 @@ export const useMenuTrigger = (_props: MenuTriggerProps): MenuTriggerState => {
32
33
  [setOpen],
33
34
  );
34
35
 
35
- const delayHover = Platform.select({
36
- macos: 100,
37
- default: 500, // win32
38
- });
39
-
40
- const onHoverIn = (e: InteractionEvent) => {
41
- if (openOnHover) {
42
- setOpen(e, true /* isOpen */);
43
- }
44
- };
36
+ const onHoverIn = React.useCallback(
37
+ (e: InteractionEvent) => {
38
+ if (openOnHover) {
39
+ clearTimeout(popoverHoverOutTimer);
40
+ clearTimeout(triggerHoverOutTimer);
41
+ setTimeout(() => {
42
+ setOpen(e, true /* isOpen */);
43
+ }, delayHover);
44
+ }
45
+ },
46
+ [openOnHover, setOpen, triggerHoverOutTimer, popoverHoverOutTimer],
47
+ );
45
48
 
46
- const onHoverOut = (e: InteractionEvent) => {
47
- if (openOnHover) {
48
- setOpen(e, false /* isOpen */);
49
- }
50
- };
49
+ const onHoverOut = React.useCallback(
50
+ (e: InteractionEvent) => {
51
+ if (openOnHover) {
52
+ const timer = setTimeout(() => {
53
+ setOpen(e, false /* isOpen */);
54
+ }, delayHover);
55
+ setTriggerHoverOutTimer(timer);
56
+ }
57
+ },
58
+ [openOnHover, setOpen, setTriggerHoverOutTimer],
59
+ );
51
60
 
52
- const onClick = (e: InteractionEvent) => {
53
- setOpen(e, !open);
54
- };
61
+ const onClick = React.useCallback(
62
+ (e: InteractionEvent) => {
63
+ setOpen(e, !open);
64
+ },
65
+ [open, setOpen],
66
+ );
55
67
 
56
68
  return {
57
69
  props: {
58
70
  onClick,
59
71
  onHoverIn,
60
- onHoverOut: Platform.OS === ('win32' as any) && onHoverOut,
72
+ onHoverOut: isCloseOnHoverOutEnabled && onHoverOut,
61
73
  componentRef: triggerRef,
62
- delayHoverIn: delayHover,
63
- delayHoverOut: Platform.OS === ('win32' as any) && delayHover,
64
74
  accessibilityState,
65
75
  accessibilityActions,
66
76
  onAccessibilityAction,
@@ -0,0 +1,235 @@
1
+ import * as React from 'react';
2
+ import { AccessibilityActionName } from 'react-native';
3
+ import * as renderer from 'react-test-renderer';
4
+ import { Menu } from '../Menu/Menu';
5
+ import { checkReRender } from '@fluentui-react-native/test-tools';
6
+ import MenuTrigger from '../MenuTrigger/MenuTrigger';
7
+ import { ButtonV1 as Button } from '@fluentui-react-native/button';
8
+ import MenuPopover from '../MenuPopover/MenuPopover';
9
+ import { MenuList } from '../MenuList/MenuList';
10
+ import { MenuItem } from '../MenuItem/MenuItem';
11
+ import { MenuItemCheckbox } from '../MenuItemCheckbox/MenuItemCheckbox';
12
+ import { MenuDivider } from '../MenuDivider/MenuDivider';
13
+ import { MenuItemRadio } from '../MenuItemRadio/MenuItemRadio';
14
+
15
+ describe('Checkbox component tests', () => {
16
+ it('Menu default', () => {
17
+ const tree = renderer
18
+ .create(
19
+ <Menu>
20
+ <MenuTrigger>
21
+ <Button>Default</Button>
22
+ </MenuTrigger>
23
+ <MenuPopover>
24
+ <MenuList>
25
+ <MenuItem content="Option 1" />
26
+ </MenuList>
27
+ </MenuPopover>
28
+ </Menu>,
29
+ )
30
+ .toJSON();
31
+ expect(tree).toMatchSnapshot();
32
+ });
33
+
34
+ it('Menu open', () => {
35
+ const tree = renderer
36
+ .create(
37
+ <Menu open>
38
+ <MenuTrigger>
39
+ <Button>Open</Button>
40
+ </MenuTrigger>
41
+ <MenuPopover>
42
+ <MenuList>
43
+ <MenuItem content="Option 1" />
44
+ </MenuList>
45
+ </MenuPopover>
46
+ </Menu>,
47
+ )
48
+ .toJSON();
49
+ expect(tree).toMatchSnapshot();
50
+ });
51
+
52
+ it('Menu defaultOpen', () => {
53
+ const tree = renderer
54
+ .create(
55
+ <Menu defaultOpen>
56
+ <MenuTrigger>
57
+ <Button>Open</Button>
58
+ </MenuTrigger>
59
+ <MenuPopover>
60
+ <MenuList>
61
+ <MenuItem content="Option 1" />
62
+ <MenuItem disabled content="Option 2" />
63
+ </MenuList>
64
+ </MenuPopover>
65
+ </Menu>,
66
+ )
67
+ .toJSON();
68
+ expect(tree).toMatchSnapshot();
69
+ });
70
+
71
+ it('Menu open checkbox and divider', () => {
72
+ const tree = renderer
73
+ .create(
74
+ <Menu open>
75
+ <MenuTrigger>
76
+ <Button>Open</Button>
77
+ </MenuTrigger>
78
+ <MenuPopover>
79
+ <MenuList>
80
+ <MenuItemCheckbox content="Option 1" name="Option 1" />
81
+ <MenuDivider />
82
+ <MenuItemCheckbox disabled content="Option 2" name="Option 2" />
83
+ </MenuList>
84
+ </MenuPopover>
85
+ </Menu>,
86
+ )
87
+ .toJSON();
88
+ expect(tree).toMatchSnapshot();
89
+ });
90
+
91
+ it('Menu open radio', () => {
92
+ const tree = renderer
93
+ .create(
94
+ <Menu open>
95
+ <MenuTrigger>
96
+ <Button>Open</Button>
97
+ </MenuTrigger>
98
+ <MenuPopover>
99
+ <MenuList>
100
+ <MenuItemRadio content="Option 1" name="Option 1" />
101
+ <MenuItemRadio content="Option 2" name="Option 2" />
102
+ </MenuList>
103
+ </MenuPopover>
104
+ </Menu>,
105
+ )
106
+ .toJSON();
107
+ expect(tree).toMatchSnapshot();
108
+ });
109
+
110
+ it('Menu open checkbox defaultChecked', () => {
111
+ const tree = renderer
112
+ .create(
113
+ <Menu open defaultChecked={{ 'Option 1': true }}>
114
+ <MenuTrigger>
115
+ <Button>Open</Button>
116
+ </MenuTrigger>
117
+ <MenuPopover>
118
+ <MenuList>
119
+ <MenuItemCheckbox content="Option 1" name="Option 1" />
120
+ <MenuDivider />
121
+ <MenuItemCheckbox content="Option 2" name="Option 2" />
122
+ </MenuList>
123
+ </MenuPopover>
124
+ </Menu>,
125
+ )
126
+ .toJSON();
127
+ expect(tree).toMatchSnapshot();
128
+ });
129
+
130
+ it('Menu open checkbox checked', () => {
131
+ const tree = renderer
132
+ .create(
133
+ <Menu open checked={{ 'Option 1': true }}>
134
+ <MenuTrigger>
135
+ <Button>Open</Button>
136
+ </MenuTrigger>
137
+ <MenuPopover>
138
+ <MenuList>
139
+ <MenuItemCheckbox content="Option 1" name="Option 1" />
140
+ <MenuDivider />
141
+ <MenuItemCheckbox content="Option 2" name="Option 2" />
142
+ </MenuList>
143
+ </MenuPopover>
144
+ </Menu>,
145
+ )
146
+ .toJSON();
147
+ expect(tree).toMatchSnapshot();
148
+ });
149
+
150
+ it('Menu submenu', () => {
151
+ const tree = renderer
152
+ .create(
153
+ <Menu open>
154
+ <MenuTrigger>
155
+ <Button>Default</Button>
156
+ </MenuTrigger>
157
+ <MenuPopover>
158
+ <MenuList>
159
+ <MenuItem content="Option 1" />
160
+ <Menu>
161
+ <MenuTrigger>
162
+ <MenuItem content="Option 2" />
163
+ </MenuTrigger>
164
+ <MenuPopover>
165
+ <MenuList>
166
+ <MenuItem content="Option 1" />
167
+ </MenuList>
168
+ </MenuPopover>
169
+ </Menu>
170
+ </MenuList>
171
+ </MenuPopover>
172
+ </Menu>,
173
+ )
174
+ .toJSON();
175
+ expect(tree).toMatchSnapshot();
176
+ });
177
+ });
178
+
179
+ describe('Menu rerender tests', () => {
180
+ it('Menu re-renders correctly', () => {
181
+ checkReRender(
182
+ () => (
183
+ <Menu open>
184
+ <MenuTrigger>
185
+ <Button>Rerender twice</Button>
186
+ </MenuTrigger>
187
+ <MenuPopover>
188
+ <MenuList>
189
+ <MenuItem content="Option 1" />
190
+ </MenuList>
191
+ </MenuPopover>
192
+ </Menu>
193
+ ),
194
+ 3,
195
+ );
196
+ });
197
+
198
+ it('Menu re-renders correctly with style', () => {
199
+ const style = { backgroundColor: 'black' };
200
+ checkReRender(
201
+ () => (
202
+ <Menu>
203
+ <MenuTrigger>
204
+ <Button style={style}>Rerender twice</Button>
205
+ </MenuTrigger>
206
+ <MenuPopover>
207
+ <MenuList>
208
+ <MenuItem content="Option 1" />
209
+ </MenuList>
210
+ </MenuPopover>
211
+ </Menu>
212
+ ),
213
+ 3,
214
+ );
215
+ });
216
+
217
+ it('Menu re-renders correctly with accessibilityAction', () => {
218
+ const action = [{ name: 'Expand' as AccessibilityActionName }];
219
+ checkReRender(
220
+ () => (
221
+ <Menu>
222
+ <MenuTrigger>
223
+ <Button>Rerender twice</Button>
224
+ </MenuTrigger>
225
+ <MenuPopover>
226
+ <MenuList>
227
+ <MenuItem accessibilityActions={action} content="Option 1" />
228
+ </MenuList>
229
+ </MenuPopover>
230
+ </Menu>
231
+ ),
232
+ 3,
233
+ );
234
+ });
235
+ });