@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.
- package/CHANGELOG.json +46 -1
- package/CHANGELOG.md +26 -2
- package/jest.config.js +2 -0
- package/lib/Menu/Menu.types.d.ts +2 -0
- package/lib/Menu/Menu.types.d.ts.map +1 -1
- package/lib/Menu/useMenu.d.ts.map +1 -1
- package/lib/Menu/useMenu.js +8 -3
- package/lib/Menu/useMenu.js.map +1 -1
- package/lib/Menu/useMenuContextValue.d.ts.map +1 -1
- package/lib/Menu/useMenuContextValue.js +4 -1
- package/lib/Menu/useMenuContextValue.js.map +1 -1
- package/lib/MenuItem/MenuItem.js +1 -1
- package/lib/MenuItem/MenuItem.js.map +1 -1
- package/lib/MenuItem/useMenuItem.d.ts.map +1 -1
- package/lib/MenuItem/useMenuItem.js +34 -6
- package/lib/MenuItem/useMenuItem.js.map +1 -1
- package/lib/MenuItemCheckbox/MenuItemCheckbox.js +1 -1
- package/lib/MenuItemCheckbox/MenuItemCheckbox.js.map +1 -1
- package/lib/MenuItemCheckbox/useMenuItemCheckbox.d.ts.map +1 -1
- package/lib/MenuItemCheckbox/useMenuItemCheckbox.js +12 -8
- package/lib/MenuItemCheckbox/useMenuItemCheckbox.js.map +1 -1
- package/lib/MenuItemRadio/useMenuItemRadio.d.ts.map +1 -1
- package/lib/MenuItemRadio/useMenuItemRadio.js +5 -3
- package/lib/MenuItemRadio/useMenuItemRadio.js.map +1 -1
- package/lib/MenuPopover/MenuPopover.d.ts.map +1 -1
- package/lib/MenuPopover/MenuPopover.js +7 -3
- package/lib/MenuPopover/MenuPopover.js.map +1 -1
- package/lib/MenuPopover/MenuPopover.types.d.ts +4 -10
- package/lib/MenuPopover/MenuPopover.types.d.ts.map +1 -1
- package/lib/MenuPopover/useMenuPopover.d.ts.map +1 -1
- package/lib/MenuPopover/useMenuPopover.js +35 -8
- package/lib/MenuPopover/useMenuPopover.js.map +1 -1
- package/lib/MenuTrigger/MenuTrigger.js +8 -8
- package/lib/MenuTrigger/MenuTrigger.js.map +1 -1
- package/lib/MenuTrigger/useMenuTrigger.d.ts.map +1 -1
- package/lib/MenuTrigger/useMenuTrigger.js +21 -20
- package/lib/MenuTrigger/useMenuTrigger.js.map +1 -1
- package/lib/__tests__/Menu.test.d.ts +2 -0
- package/lib/__tests__/Menu.test.d.ts.map +1 -0
- package/lib/__tests__/Menu.test.js +145 -0
- package/lib/__tests__/Menu.test.js.map +1 -0
- package/lib/consts.d.ts +3 -0
- package/lib/consts.d.ts.map +1 -0
- package/lib/consts.js +7 -0
- package/lib/consts.js.map +1 -0
- package/lib/context/menuContext.d.ts +10 -4
- package/lib/context/menuContext.d.ts.map +1 -1
- package/lib/context/menuContext.js.map +1 -1
- package/lib-commonjs/Menu/Menu.types.d.ts +2 -0
- package/lib-commonjs/Menu/Menu.types.d.ts.map +1 -1
- package/lib-commonjs/Menu/useMenu.d.ts.map +1 -1
- package/lib-commonjs/Menu/useMenu.js +8 -3
- package/lib-commonjs/Menu/useMenu.js.map +1 -1
- package/lib-commonjs/Menu/useMenuContextValue.d.ts.map +1 -1
- package/lib-commonjs/Menu/useMenuContextValue.js +4 -1
- package/lib-commonjs/Menu/useMenuContextValue.js.map +1 -1
- package/lib-commonjs/MenuItem/MenuItem.js +1 -1
- package/lib-commonjs/MenuItem/MenuItem.js.map +1 -1
- package/lib-commonjs/MenuItem/useMenuItem.d.ts.map +1 -1
- package/lib-commonjs/MenuItem/useMenuItem.js +32 -4
- package/lib-commonjs/MenuItem/useMenuItem.js.map +1 -1
- package/lib-commonjs/MenuItemCheckbox/MenuItemCheckbox.js +1 -1
- package/lib-commonjs/MenuItemCheckbox/MenuItemCheckbox.js.map +1 -1
- package/lib-commonjs/MenuItemCheckbox/useMenuItemCheckbox.d.ts.map +1 -1
- package/lib-commonjs/MenuItemCheckbox/useMenuItemCheckbox.js +12 -8
- package/lib-commonjs/MenuItemCheckbox/useMenuItemCheckbox.js.map +1 -1
- package/lib-commonjs/MenuItemRadio/useMenuItemRadio.d.ts.map +1 -1
- package/lib-commonjs/MenuItemRadio/useMenuItemRadio.js +5 -3
- package/lib-commonjs/MenuItemRadio/useMenuItemRadio.js.map +1 -1
- package/lib-commonjs/MenuPopover/MenuPopover.d.ts.map +1 -1
- package/lib-commonjs/MenuPopover/MenuPopover.js +5 -2
- package/lib-commonjs/MenuPopover/MenuPopover.js.map +1 -1
- package/lib-commonjs/MenuPopover/MenuPopover.types.d.ts +4 -10
- package/lib-commonjs/MenuPopover/MenuPopover.types.d.ts.map +1 -1
- package/lib-commonjs/MenuPopover/useMenuPopover.d.ts.map +1 -1
- package/lib-commonjs/MenuPopover/useMenuPopover.js +35 -8
- package/lib-commonjs/MenuPopover/useMenuPopover.js.map +1 -1
- package/lib-commonjs/MenuTrigger/MenuTrigger.js +8 -8
- package/lib-commonjs/MenuTrigger/MenuTrigger.js.map +1 -1
- package/lib-commonjs/MenuTrigger/useMenuTrigger.d.ts.map +1 -1
- package/lib-commonjs/MenuTrigger/useMenuTrigger.js +21 -20
- package/lib-commonjs/MenuTrigger/useMenuTrigger.js.map +1 -1
- package/lib-commonjs/__tests__/Menu.test.d.ts +2 -0
- package/lib-commonjs/__tests__/Menu.test.d.ts.map +1 -0
- package/lib-commonjs/__tests__/Menu.test.js +148 -0
- package/lib-commonjs/__tests__/Menu.test.js.map +1 -0
- package/lib-commonjs/consts.d.ts +3 -0
- package/lib-commonjs/consts.d.ts.map +1 -0
- package/lib-commonjs/consts.js +10 -0
- package/lib-commonjs/consts.js.map +1 -0
- package/lib-commonjs/context/menuContext.d.ts +10 -4
- package/lib-commonjs/context/menuContext.d.ts.map +1 -1
- package/lib-commonjs/context/menuContext.js.map +1 -1
- package/package.json +3 -1
- package/src/Menu/Menu.types.ts +1 -0
- package/src/Menu/useMenu.ts +9 -2
- package/src/Menu/useMenuContextValue.ts +4 -1
- package/src/MenuItem/MenuItem.tsx +1 -1
- package/src/MenuItem/useMenuItem.ts +48 -8
- package/src/MenuItemCheckbox/MenuItemCheckbox.tsx +1 -1
- package/src/MenuItemCheckbox/useMenuItemCheckbox.ts +12 -8
- package/src/MenuItemRadio/useMenuItemRadio.ts +5 -3
- package/src/MenuPopover/MenuPopover.tsx +7 -14
- package/src/MenuPopover/MenuPopover.types.ts +4 -9
- package/src/MenuPopover/useMenuPopover.ts +48 -8
- package/src/MenuTrigger/MenuTrigger.tsx +9 -9
- package/src/MenuTrigger/useMenuTrigger.ts +36 -26
- package/src/__tests__/Menu.test.tsx +235 -0
- package/src/__tests__/__snapshots__/Menu.test.tsx.snap +2098 -0
- package/src/consts.ts +8 -0
- 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,
|
|
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
|
|
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 ||
|
|
65
|
+
onAccessibilityTap: props.onAccessibilityTap || onInvoke,
|
|
26
66
|
accessibilityLabel: props.accessibilityLabel || props.content,
|
|
27
67
|
accessibilityState: getAccessibilityState(disabled, accessibilityState),
|
|
28
68
|
enableFocusRing: true,
|
|
29
|
-
focusable:
|
|
69
|
+
focusable: true,
|
|
30
70
|
ref: componentRef,
|
|
31
|
-
...
|
|
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
|
-
|
|
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 (
|
|
78
|
-
|
|
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:
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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 {
|
|
2
|
-
import {
|
|
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
|
-
|
|
10
|
-
|
|
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
|
|
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 =
|
|
14
|
-
|
|
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 =
|
|
31
|
+
const doNotTakePointerCapture = openOnHover;
|
|
22
32
|
const accessibilityRole = 'menu';
|
|
23
33
|
|
|
24
|
-
|
|
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
|
|
25
|
-
const revised = React.cloneElement(child,
|
|
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):
|
|
34
|
-
const
|
|
33
|
+
function getRevisedStateWorker(state: MenuTriggerState, props: any): MenuTriggerProps {
|
|
34
|
+
const revisedProps = state.props;
|
|
35
35
|
if (props.accessibilityState) {
|
|
36
|
-
|
|
36
|
+
revisedProps.accessibilityState = { ...revisedProps.accessibilityState, ...props.accessibilityState };
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
if (props.accessibilityActions) {
|
|
40
|
-
|
|
40
|
+
revisedProps.accessibilityActions = { ...revisedProps.accessibilityActions, ...props.accessibilityActions };
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
if (props.onAccessibilityAction) {
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
revisedProps.onAccessibilityAction = (e: AccessibilityActionEvent) => {
|
|
45
|
+
revisedProps.onAccessibilityAction(e);
|
|
46
46
|
props.onAccessibilityAction(e);
|
|
47
47
|
};
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
return
|
|
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
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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 = (
|
|
47
|
-
|
|
48
|
-
|
|
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 = (
|
|
53
|
-
|
|
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:
|
|
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
|
+
});
|