@fluentui-react-native/menu 0.14.1 → 0.14.4

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 (92) hide show
  1. package/CHANGELOG.json +46 -1
  2. package/CHANGELOG.md +26 -2
  3. package/SPEC.md +344 -0
  4. package/lib/Menu/Menu.types.d.ts +1 -1
  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 +36 -7
  8. package/lib/Menu/useMenu.js.map +1 -1
  9. package/lib/MenuItem/useMenuItem.d.ts.map +1 -1
  10. package/lib/MenuItem/useMenuItem.js +4 -1
  11. package/lib/MenuItem/useMenuItem.js.map +1 -1
  12. package/lib/MenuItemRadio/useMenuItemRadio.d.ts.map +1 -1
  13. package/lib/MenuItemRadio/useMenuItemRadio.js +9 -4
  14. package/lib/MenuItemRadio/useMenuItemRadio.js.map +1 -1
  15. package/lib/MenuList/MenuList.js +1 -1
  16. package/lib/MenuList/MenuList.js.map +1 -1
  17. package/lib/MenuList/MenuList.types.d.ts +10 -5
  18. package/lib/MenuList/MenuList.types.d.ts.map +1 -1
  19. package/lib/MenuList/useMenuList.d.ts.map +1 -1
  20. package/lib/MenuList/useMenuList.js +73 -26
  21. package/lib/MenuList/useMenuList.js.map +1 -1
  22. package/lib/MenuList/useMenuListContextValue.js +1 -1
  23. package/lib/MenuList/useMenuListContextValue.js.map +1 -1
  24. package/lib/MenuTrigger/MenuTrigger.d.ts +2 -2
  25. package/lib/MenuTrigger/MenuTrigger.d.ts.map +1 -1
  26. package/lib/MenuTrigger/MenuTrigger.js +5 -5
  27. package/lib/MenuTrigger/MenuTrigger.js.map +1 -1
  28. package/lib/MenuTrigger/MenuTrigger.types.d.ts +2 -2
  29. package/lib/MenuTrigger/MenuTrigger.types.d.ts.map +1 -1
  30. package/lib/MenuTrigger/useMenuTrigger.d.ts +2 -2
  31. package/lib/MenuTrigger/useMenuTrigger.d.ts.map +1 -1
  32. package/lib/MenuTrigger/useMenuTrigger.js +1 -1
  33. package/lib/MenuTrigger/useMenuTrigger.js.map +1 -1
  34. package/lib/__tests__/Menu.test.js +2 -2
  35. package/lib/__tests__/Menu.test.js.map +1 -1
  36. package/lib/context/menuContext.js +2 -2
  37. package/lib/context/menuListContext.d.ts +6 -4
  38. package/lib/context/menuListContext.d.ts.map +1 -1
  39. package/lib/context/menuListContext.js +2 -1
  40. package/lib/context/menuListContext.js.map +1 -1
  41. package/lib-commonjs/Menu/Menu.types.d.ts +1 -1
  42. package/lib-commonjs/Menu/Menu.types.d.ts.map +1 -1
  43. package/lib-commonjs/Menu/useMenu.d.ts.map +1 -1
  44. package/lib-commonjs/Menu/useMenu.js +36 -7
  45. package/lib-commonjs/Menu/useMenu.js.map +1 -1
  46. package/lib-commonjs/MenuItem/useMenuItem.d.ts.map +1 -1
  47. package/lib-commonjs/MenuItem/useMenuItem.js +4 -1
  48. package/lib-commonjs/MenuItem/useMenuItem.js.map +1 -1
  49. package/lib-commonjs/MenuItemRadio/useMenuItemRadio.d.ts.map +1 -1
  50. package/lib-commonjs/MenuItemRadio/useMenuItemRadio.js +9 -4
  51. package/lib-commonjs/MenuItemRadio/useMenuItemRadio.js.map +1 -1
  52. package/lib-commonjs/MenuList/MenuList.js +1 -1
  53. package/lib-commonjs/MenuList/MenuList.js.map +1 -1
  54. package/lib-commonjs/MenuList/MenuList.types.d.ts +10 -5
  55. package/lib-commonjs/MenuList/MenuList.types.d.ts.map +1 -1
  56. package/lib-commonjs/MenuList/useMenuList.d.ts.map +1 -1
  57. package/lib-commonjs/MenuList/useMenuList.js +74 -27
  58. package/lib-commonjs/MenuList/useMenuList.js.map +1 -1
  59. package/lib-commonjs/MenuList/useMenuListContextValue.js +1 -1
  60. package/lib-commonjs/MenuList/useMenuListContextValue.js.map +1 -1
  61. package/lib-commonjs/MenuTrigger/MenuTrigger.d.ts +2 -2
  62. package/lib-commonjs/MenuTrigger/MenuTrigger.d.ts.map +1 -1
  63. package/lib-commonjs/MenuTrigger/MenuTrigger.js +5 -5
  64. package/lib-commonjs/MenuTrigger/MenuTrigger.js.map +1 -1
  65. package/lib-commonjs/MenuTrigger/MenuTrigger.types.d.ts +2 -2
  66. package/lib-commonjs/MenuTrigger/MenuTrigger.types.d.ts.map +1 -1
  67. package/lib-commonjs/MenuTrigger/useMenuTrigger.d.ts +2 -2
  68. package/lib-commonjs/MenuTrigger/useMenuTrigger.d.ts.map +1 -1
  69. package/lib-commonjs/MenuTrigger/useMenuTrigger.js +1 -1
  70. package/lib-commonjs/MenuTrigger/useMenuTrigger.js.map +1 -1
  71. package/lib-commonjs/__tests__/Menu.test.js +2 -2
  72. package/lib-commonjs/__tests__/Menu.test.js.map +1 -1
  73. package/lib-commonjs/context/menuContext.js +2 -2
  74. package/lib-commonjs/context/menuListContext.d.ts +6 -4
  75. package/lib-commonjs/context/menuListContext.d.ts.map +1 -1
  76. package/lib-commonjs/context/menuListContext.js +2 -1
  77. package/lib-commonjs/context/menuListContext.js.map +1 -1
  78. package/package.json +1 -1
  79. package/src/Menu/Menu.types.ts +1 -1
  80. package/src/Menu/useMenu.ts +53 -7
  81. package/src/MenuItem/useMenuItem.ts +6 -1
  82. package/src/MenuItemRadio/useMenuItemRadio.ts +11 -3
  83. package/src/MenuList/MenuList.tsx +1 -1
  84. package/src/MenuList/MenuList.types.ts +11 -5
  85. package/src/MenuList/useMenuList.ts +76 -37
  86. package/src/MenuList/useMenuListContextValue.ts +1 -1
  87. package/src/MenuTrigger/MenuTrigger.tsx +7 -7
  88. package/src/MenuTrigger/MenuTrigger.types.ts +2 -2
  89. package/src/MenuTrigger/useMenuTrigger.ts +2 -2
  90. package/src/__tests__/Menu.test.tsx +2 -2
  91. package/src/context/menuContext.ts +2 -2
  92. package/src/context/menuListContext.ts +5 -2
@@ -9,20 +9,26 @@ export interface MenuListTokens extends LayoutTokens, IBackgroundColorTokens {
9
9
  }
10
10
 
11
11
  export interface MenuListProps extends Omit<IViewProps, 'onPress'> {
12
- checked?: Record<string, boolean>;
13
- defaultChecked?: Record<string, boolean>;
12
+ checked?: string[];
13
+ defaultChecked?: string[];
14
14
  hasCheckmarks?: boolean;
15
- onCheckedChange?: (e: InteractionEvent, name: string, isChecked: boolean) => void;
15
+ onCheckedChange?: (e: InteractionEvent, checked: string[]) => void;
16
16
  }
17
17
 
18
- export interface MenuListState extends MenuListProps {
18
+ export interface MenuListState extends Omit<MenuListProps, 'checked' | 'onCheckedChange'> {
19
+ props: MenuListProps;
20
+ checked?: Record<string, boolean>;
19
21
  isCheckedControlled: boolean;
20
- selectRadio?: (e: InteractionEvent, name: string, isChecked: boolean) => void;
22
+ onCheckedChange?: (e: InteractionEvent, name: string, isChecked: boolean) => void;
23
+ selectRadio?: (e: InteractionEvent, name: string) => void;
24
+ addRadioItem: (name: string) => void;
25
+ removeRadioItem: (name: string) => void;
21
26
  }
22
27
 
23
28
  export interface MenuListSlotProps {
24
29
  root: React.PropsWithRef<IViewProps> & { gap?: number };
25
30
  }
31
+
26
32
  export interface MenuListType {
27
33
  props: MenuListProps;
28
34
  tokens: MenuListTokens;
@@ -3,66 +3,105 @@ import React from 'react';
3
3
  import { useMenuContext } from '../context/menuContext';
4
4
  import { MenuListProps, MenuListState } from './MenuList.types';
5
5
 
6
+ // Track the radio items so we know what to clear selection from when selectRadio is called
7
+ // Purposefully left out of the hook because
8
+ // 1. RadioItems just keeps track of information - changing this array doesn't need to force rerender
9
+ // 2. Keeping them here means these consts are not recreated on every render, which would force rerendering of all children
10
+ const radioItems = [];
11
+ const addRadioItem = (name: string) => {
12
+ radioItems.push(name);
13
+ };
14
+ const removeRadioItem = (name: string) => {
15
+ radioItems.filter((item) => item !== name);
16
+ };
17
+
6
18
  export const useMenuList = (_props: MenuListProps): MenuListState => {
7
19
  const context = useMenuContext();
8
20
 
9
21
  // MenuList v2 needs to be able to be standalone, but this is not in scope for v1
10
22
  // Assuming that checked information will come from parent Menu
11
- const isCheckedControlled = typeof context.checked !== 'undefined';
12
- const [checked, onCheckedChange, selectRadio] = useMenuCheckedState(isCheckedControlled, context);
23
+ const { defaultChecked, onCheckedChange: onCheckedChangeOriginal, checked: checkedOriginal } = context;
13
24
 
14
- return {
15
- ...context,
16
- isCheckedControlled,
17
- checked,
18
- onCheckedChange,
19
- selectRadio,
20
- };
21
- };
25
+ // Convert passed in array to map so that i's easier to look up checked state
26
+ const checkedMap = React.useMemo(() => {
27
+ const state = {};
28
+ if (!checkedOriginal) {
29
+ return state;
30
+ }
31
+
32
+ for (const key of checkedOriginal) {
33
+ state[key] = true;
34
+ }
35
+ return state;
36
+ }, [checkedOriginal]);
37
+
38
+ const [checkedInternal, setCheckedInternal] = React.useState<Record<string, boolean>>(() => {
39
+ if (checkedMap) {
40
+ return checkedMap;
41
+ }
22
42
 
23
- const useMenuCheckedState = (
24
- isControlled: boolean,
25
- props: MenuListProps,
26
- ): [
27
- Record<string, boolean>,
28
- (e: InteractionEvent, name: string, isChecked: boolean) => void,
29
- (e: InteractionEvent, name: string, isChecked: boolean) => void,
30
- ] => {
31
- const { defaultChecked, onCheckedChange, checked } = props;
32
- const initialState = defaultChecked ?? checked ?? {};
33
- const [checkedInternal, setCheckedInternal] = React.useState<Record<string, boolean>>(initialState);
43
+ const initialChecked = defaultChecked ?? [];
44
+ const state = {};
45
+ for (const key of initialChecked) {
46
+ state[key] = true;
47
+ }
48
+ return state;
49
+ });
34
50
 
35
- const state = isControlled ? checked : checkedInternal;
51
+ const isCheckedControlled = typeof checkedOriginal !== 'undefined';
52
+ const checked = isCheckedControlled ? checkedMap : checkedInternal;
36
53
 
37
- const setChecked = React.useCallback(
54
+ const onCheckedChange = React.useCallback(
38
55
  (e: InteractionEvent, name: string, isChecked: boolean) => {
39
- if (!isControlled) {
40
- const curChecked = state;
41
- curChecked[name] = isChecked;
42
- const updatedChecked = { ...curChecked };
56
+ const updatedChecked = { ...checked };
57
+ if (isChecked) {
58
+ updatedChecked[name] = true;
59
+ } else {
60
+ delete updatedChecked[name];
61
+ }
62
+
63
+ if (!isCheckedControlled) {
43
64
  setCheckedInternal(updatedChecked);
44
65
  }
45
66
 
46
- if (onCheckedChange) {
47
- onCheckedChange(e, name, isChecked);
67
+ if (onCheckedChangeOriginal) {
68
+ onCheckedChangeOriginal(e, Object.keys(updatedChecked));
48
69
  }
49
70
  },
50
- [isControlled, state, onCheckedChange, setCheckedInternal],
71
+ [isCheckedControlled, checked, onCheckedChangeOriginal, setCheckedInternal],
51
72
  );
52
73
 
53
74
  const selectRadio = React.useCallback(
54
- (e: InteractionEvent, name: string, isChecked: boolean) => {
55
- if (!isControlled) {
56
- const updatedChecked = { [name]: true };
75
+ (e: InteractionEvent, name: string) => {
76
+ const updatedChecked = {};
77
+ for (const checkedName of Object.keys(checked)) {
78
+ if (!radioItems.includes(checkedName)) {
79
+ // Preserve checked state if non-radio items
80
+ updatedChecked[checkedName] = checked[checkedName];
81
+ }
82
+ }
83
+ updatedChecked[name] = true;
84
+
85
+ if (!isCheckedControlled) {
57
86
  setCheckedInternal(updatedChecked);
58
87
  }
59
88
 
60
- if (onCheckedChange) {
61
- onCheckedChange(e, name, isChecked);
89
+ if (onCheckedChangeOriginal) {
90
+ onCheckedChangeOriginal(e, Object.keys(updatedChecked));
62
91
  }
63
92
  },
64
- [isControlled, onCheckedChange, setCheckedInternal],
93
+ [isCheckedControlled, onCheckedChangeOriginal, setCheckedInternal, checked],
65
94
  );
66
95
 
67
- return [state, setChecked, selectRadio];
96
+ return {
97
+ props: {
98
+ ...context,
99
+ },
100
+ isCheckedControlled,
101
+ checked,
102
+ onCheckedChange,
103
+ selectRadio,
104
+ addRadioItem,
105
+ removeRadioItem,
106
+ };
68
107
  };
@@ -2,5 +2,5 @@ import { MenuListContextValue } from '../context/menuListContext';
2
2
  import { MenuListState } from './MenuList.types';
3
3
 
4
4
  export const useMenuListContextValue = (state: MenuListState): MenuListContextValue => {
5
- return { ...state };
5
+ return { hasCheckmarks: state.props.hasCheckmarks, ...state };
6
6
  };
@@ -1,14 +1,14 @@
1
1
  import React from 'react';
2
2
  import { memoize, stagedComponent } from '@fluentui-react-native/framework';
3
- import { menuTriggerName, MenuTriggerProps, MenuTriggerState } from './MenuTrigger.types';
3
+ import { menuTriggerName, MenuTriggerChildProps, MenuTriggerState } from './MenuTrigger.types';
4
4
  import { useMenuTrigger } from './useMenuTrigger';
5
5
  import { AccessibilityActionEvent } from 'react-native';
6
6
  import { MenuTriggerProvider } from '../context/menuTriggerContext';
7
7
 
8
- export const MenuTrigger = stagedComponent((props: MenuTriggerProps) => {
9
- const menuTrigger = useMenuTrigger(props);
8
+ export const MenuTrigger = stagedComponent((_props: React.PropsWithChildren<Record<never, any>>) => {
9
+ const menuTrigger = useMenuTrigger();
10
10
 
11
- return (_rest: MenuTriggerProps, children: React.ReactNode) => {
11
+ return (_rest: React.PropsWithChildren<Record<never, any>>, children: React.ReactNode) => {
12
12
  const childrenArray = React.Children.toArray(children) as React.ReactElement[];
13
13
 
14
14
  if (__DEV__) {
@@ -21,7 +21,7 @@ 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 revisedProps = getRevisedState(menuTrigger, child.props);
24
+ const revisedProps = getRevisedProps(menuTrigger, child.props);
25
25
  const revised = React.cloneElement(child, revisedProps);
26
26
 
27
27
  return <MenuTriggerProvider value={menuTrigger.hasSubmenu}>{revised}</MenuTriggerProvider>;
@@ -29,8 +29,8 @@ export const MenuTrigger = stagedComponent((props: MenuTriggerProps) => {
29
29
  });
30
30
  MenuTrigger.displayName = menuTriggerName;
31
31
 
32
- const getRevisedState = memoize(getRevisedStateWorker);
33
- function getRevisedStateWorker(state: MenuTriggerState, props: any): MenuTriggerProps {
32
+ const getRevisedProps = memoize(getRevisedPropsWorker);
33
+ function getRevisedPropsWorker(state: MenuTriggerState, props: any): MenuTriggerChildProps {
34
34
  const revisedProps = state.props;
35
35
  if (props.accessibilityState) {
36
36
  revisedProps.accessibilityState = { ...revisedProps.accessibilityState, ...props.accessibilityState };
@@ -3,7 +3,7 @@ import { ViewProps } from 'react-native';
3
3
 
4
4
  export const menuTriggerName = 'MenuTrigger';
5
5
 
6
- export interface MenuTriggerProps extends Omit<IWithPressableOptions<ViewProps>, 'onPress'> {
6
+ export interface MenuTriggerChildProps extends Omit<IWithPressableOptions<ViewProps>, 'onPress'> {
7
7
  /**
8
8
  * A RefObject to refer to the trigger component.
9
9
  */
@@ -16,6 +16,6 @@ export interface MenuTriggerProps extends Omit<IWithPressableOptions<ViewProps>,
16
16
  }
17
17
 
18
18
  export interface MenuTriggerState {
19
- props: MenuTriggerProps;
19
+ props: MenuTriggerChildProps;
20
20
  hasSubmenu: boolean;
21
21
  }
@@ -1,6 +1,6 @@
1
1
  import { useMenuContext } from '../context/menuContext';
2
2
  import { InteractionEvent } from '@fluentui-react-native/interactive-hooks';
3
- import { MenuTriggerProps, MenuTriggerState } from './MenuTrigger.types';
3
+ import { MenuTriggerState } from './MenuTrigger.types';
4
4
  import { AccessibilityActionEvent, AccessibilityActionName, Platform } from 'react-native';
5
5
  import React from 'react';
6
6
  import { delayHover, isCloseOnHoverOutEnabled } from '../consts';
@@ -10,7 +10,7 @@ const accessibilityActions =
10
10
  const expandedState = { expanded: true };
11
11
  const collapsedState = { expanded: false };
12
12
 
13
- export const useMenuTrigger = (_props: MenuTriggerProps): MenuTriggerState => {
13
+ export const useMenuTrigger = (): MenuTriggerState => {
14
14
  const context = useMenuContext();
15
15
  const { open, openOnHover, popoverHoverOutTimer, setOpen, setTriggerHoverOutTimer, triggerHoverOutTimer, triggerRef } = context;
16
16
 
@@ -110,7 +110,7 @@ describe('Checkbox component tests', () => {
110
110
  it('Menu open checkbox defaultChecked', () => {
111
111
  const tree = renderer
112
112
  .create(
113
- <Menu open defaultChecked={{ 'Option 1': true }}>
113
+ <Menu open defaultChecked={['Option 1']}>
114
114
  <MenuTrigger>
115
115
  <Button>Open</Button>
116
116
  </MenuTrigger>
@@ -130,7 +130,7 @@ describe('Checkbox component tests', () => {
130
130
  it('Menu open checkbox checked', () => {
131
131
  const tree = renderer
132
132
  .create(
133
- <Menu open checked={{ 'Option 1': true }}>
133
+ <Menu open checked={['Option 1']}>
134
134
  <MenuTrigger>
135
135
  <Button>Open</Button>
136
136
  </MenuTrigger>
@@ -13,8 +13,8 @@ export interface MenuContextValue extends MenuState {
13
13
 
14
14
  export const MenuContext = React.createContext<MenuContextValue>({
15
15
  isControlled: false,
16
- checked: {},
17
- defaultChecked: {},
16
+ checked: [],
17
+ defaultChecked: [],
18
18
  hasCheckmarks: false,
19
19
  isSubmenu: false,
20
20
  open: false,
@@ -4,14 +4,17 @@ import type { MenuListState } from '../MenuList/MenuList.types';
4
4
  /**
5
5
  * Context shared between Menu and its child components
6
6
  */
7
- export type MenuListContextValue = MenuListState;
7
+ export type MenuListContextValue = Omit<MenuListState, 'props'> & {
8
+ hasCheckmarks: boolean;
9
+ };
8
10
 
9
11
  export const MenuListContext = React.createContext<MenuListContextValue>({
10
12
  isCheckedControlled: false,
11
13
  checked: {},
12
- defaultChecked: {},
13
14
  hasCheckmarks: false,
14
15
  onCheckedChange: () => false,
16
+ addRadioItem: () => false,
17
+ removeRadioItem: () => false,
15
18
  });
16
19
 
17
20
  export const MenuListProvider = MenuListContext.Provider;