;
+}
diff --git a/packages/react-menu/src/components/MenuItemCheckbox/__snapshots__/MenuItemCheckbox.test.tsx.snap b/packages/react-menu/src/components/MenuItemCheckbox/__snapshots__/MenuItemCheckbox.test.tsx.snap
new file mode 100644
index 00000000000000..057e6021816346
--- /dev/null
+++ b/packages/react-menu/src/components/MenuItemCheckbox/__snapshots__/MenuItemCheckbox.test.tsx.snap
@@ -0,0 +1,15 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`MenuItemCheckbox conformance renders a default state 1`] = `
+
+ Default MenuItemCheckbox
+
+`;
diff --git a/packages/react-menu/src/components/MenuItemCheckbox/index.ts b/packages/react-menu/src/components/MenuItemCheckbox/index.ts
new file mode 100644
index 00000000000000..fd28717014386b
--- /dev/null
+++ b/packages/react-menu/src/components/MenuItemCheckbox/index.ts
@@ -0,0 +1,4 @@
+export * from './MenuItemCheckbox.types';
+export * from './MenuItemCheckbox';
+export * from './renderMenuItemCheckbox';
+export * from './useMenuItemCheckbox';
diff --git a/packages/react-menu/src/components/MenuItemCheckbox/renderMenuItemCheckbox.tsx b/packages/react-menu/src/components/MenuItemCheckbox/renderMenuItemCheckbox.tsx
new file mode 100644
index 00000000000000..7f92d005aee9b1
--- /dev/null
+++ b/packages/react-menu/src/components/MenuItemCheckbox/renderMenuItemCheckbox.tsx
@@ -0,0 +1,17 @@
+import * as React from 'react';
+import { getSlots } from '@fluentui/react-utils';
+import { MenuItemCheckboxState } from './MenuItemCheckbox.types';
+import { menuItemCheckboxShorthandProps } from './useMenuItemCheckbox';
+
+/** Function that renders the final JSX of the component */
+export const renderMenuItemCheckbox = (state: MenuItemCheckboxState) => {
+ const { slots, slotProps } = getSlots(state, menuItemCheckboxShorthandProps);
+
+ return (
+
+
+
+ {state.children}
+
+ );
+};
diff --git a/packages/react-menu/src/components/MenuItemCheckbox/useMenuItemCheckbox.ts b/packages/react-menu/src/components/MenuItemCheckbox/useMenuItemCheckbox.ts
new file mode 100644
index 00000000000000..c727db650e96ae
--- /dev/null
+++ b/packages/react-menu/src/components/MenuItemCheckbox/useMenuItemCheckbox.ts
@@ -0,0 +1,45 @@
+import * as React from 'react';
+import { makeMergeProps, resolveShorthandProps } from '@fluentui/react-utils';
+import { MenuItemCheckboxProps, MenuItemCheckboxState } from './MenuItemCheckbox.types';
+import { useMenuItemSelectable } from '../../selectable/index';
+import { useMergedRefs } from '@fluentui/react-hooks';
+
+/**
+ * Consts listing which props are shorthand props.
+ */
+export const menuItemCheckboxShorthandProps = ['icon', 'checkmark'];
+
+const mergeProps = makeMergeProps({ deepMerge: menuItemCheckboxShorthandProps });
+
+/** Returns the props and state required to render the component */
+export const useMenuItemCheckbox = (
+ props: MenuItemCheckboxProps,
+ ref: React.Ref,
+ defaultProps?: MenuItemCheckboxProps,
+): MenuItemCheckboxState => {
+ const state = mergeProps(
+ {
+ ref: useMergedRefs(ref, React.useRef(null)),
+ icon: { as: 'span' },
+ checkmark: { as: 'span' },
+ role: 'menuitemcheckbox',
+ tabIndex: 0,
+ },
+ defaultProps,
+ resolveShorthandProps(props, menuItemCheckboxShorthandProps),
+ );
+
+ useMenuItemSelectable(state, () => {
+ const newCheckedItems = [...state.checkedItems];
+ const index = state.checkedItems.indexOf(state.value);
+ if (index !== -1) {
+ newCheckedItems.splice(index, 1);
+ } else {
+ newCheckedItems.push(state.value);
+ }
+
+ return newCheckedItems;
+ });
+
+ return state;
+};
diff --git a/packages/react-menu/src/components/MenuItemCheckbox/useMenuItemCheckboxStyles.ts b/packages/react-menu/src/components/MenuItemCheckbox/useMenuItemCheckboxStyles.ts
new file mode 100644
index 00000000000000..3e6fab28b06cd6
--- /dev/null
+++ b/packages/react-menu/src/components/MenuItemCheckbox/useMenuItemCheckboxStyles.ts
@@ -0,0 +1,8 @@
+import { useCheckmarkStyles } from '../../selectable/index';
+import { useMenuItemStyles } from '../MenuItem/useMenuItemStyles';
+import { MenuItemCheckboxState } from './MenuItemCheckbox.types';
+
+export const useMenuItemCheckBoxStyles = (state: MenuItemCheckboxState) => {
+ useMenuItemStyles(state);
+ useCheckmarkStyles(state);
+};
diff --git a/packages/react-menu/src/components/MenuItemRadio/MenuItemRadio.test.tsx b/packages/react-menu/src/components/MenuItemRadio/MenuItemRadio.test.tsx
new file mode 100644
index 00000000000000..354916b4d0734a
--- /dev/null
+++ b/packages/react-menu/src/components/MenuItemRadio/MenuItemRadio.test.tsx
@@ -0,0 +1,148 @@
+import * as React from 'react';
+import * as renderer from 'react-test-renderer';
+import { render, fireEvent } from '@testing-library/react';
+import { MenuItemRadio } from './MenuItemRadio';
+import { ReactWrapper } from 'enzyme';
+import { isConformant } from '../../common/isConformant';
+import { MenuListContext, MenuListProvider } from '../../menuListContext';
+
+describe('MenuItemRadio', () => {
+ isConformant({
+ asPropHandlesRef: true,
+ // TODO fix generics in conformance
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ Component: MenuItemRadio,
+ requiredProps: {
+ name: 'radio',
+ value: '1',
+ },
+ displayName: 'MenuItemRadio',
+ });
+
+ let wrapper: ReactWrapper | undefined;
+
+ afterEach(() => {
+ if (wrapper) {
+ wrapper.unmount();
+ wrapper = undefined;
+ }
+ });
+
+ /**
+ * Note: see more visual regression tests for MenuItemRadio in /apps/vr-tests.
+ */
+ it('renders a default state', () => {
+ const component = renderer.create(
+
+ Default MenuItemRadio
+ ,
+ );
+ const tree = component.toJSON();
+ expect(tree).toMatchSnapshot();
+ });
+});
+
+describe('MenuItemRadio', () => {
+ const TestMenuListContext = (props: { children: React.ReactNode; context?: Partial }) => {
+ const contextValue: MenuListContext = {
+ checkedValues: {},
+ onCheckedValueChange: jest.fn(),
+ ...(props.context && props.context),
+ };
+
+ return {props.children};
+ };
+
+ it('should render checkmark slot if checked', () => {
+ // Arrange
+ const checkedValues = { test: ['1'] };
+ const checkmark = 'xxx';
+ const { getByText } = render(
+
+
+ Radio
+
+ ,
+ );
+
+ // Assert
+ expect(getByText(checkmark)).not.toBeNull();
+ });
+
+ it('should render icon slot', () => {
+ // Arrange
+ const icon = 'xxx';
+ const { getByText } = render(
+
+
+ Radio
+
+ ,
+ );
+
+ // Assert
+ expect(getByText(icon)).not.toBeNull();
+ });
+
+ it('should set aria-checked value to true if value is checked', () => {
+ // Arrange
+ const checkedValues = { test: ['1'] };
+ const { container } = render(
+
+
+ Radio
+
+ ,
+ );
+
+ // Assert
+ expect(container.querySelector('[role="menuitemradio"]')?.getAttribute('aria-checked')).toEqual('true');
+ });
+
+ it('should check radio on click', () => {
+ // Arrange
+ const radioName = 'name';
+ const radioValue = '1';
+ const checkedValues = { [radioName]: [] };
+ const spy = jest.fn();
+ const { container } = render(
+
+
+ Radio
+
+ ,
+ );
+
+ // Act
+ const menuitem = container.querySelector('[role="menuitemradio"]');
+ menuitem && fireEvent.click(menuitem);
+
+ // Assert
+ expect(spy).toHaveBeenCalledTimes(1);
+ expect(spy).toHaveBeenCalledWith(expect.anything(), radioName, [radioValue]);
+ });
+
+ it('should uncheck other radio on click', () => {
+ // Arrange
+ const radioName = 'name';
+ const radioValue = '1';
+ const checkedValues = { [radioName]: ['2'] };
+ const spy = jest.fn();
+ const { container } = render(
+
+
+ Radio
+
+ ,
+ );
+
+ // Act
+ const menuitem = container.querySelector('[role="menuitemradio"]');
+ menuitem && fireEvent.click(menuitem);
+
+ // Assert
+ expect(spy).toHaveBeenCalledTimes(1);
+ expect(spy).toHaveBeenCalledWith(expect.anything(), radioName, [radioValue]);
+ });
+});
diff --git a/packages/react-menu/src/components/MenuItemRadio/MenuItemRadio.tsx b/packages/react-menu/src/components/MenuItemRadio/MenuItemRadio.tsx
new file mode 100644
index 00000000000000..b3eed52b5186b6
--- /dev/null
+++ b/packages/react-menu/src/components/MenuItemRadio/MenuItemRadio.tsx
@@ -0,0 +1,18 @@
+import * as React from 'react';
+import { useMenuItemRadio } from './useMenuItemRadio';
+import { MenuItemRadioProps } from './MenuItemRadio.types';
+import { renderMenuItemRadio } from './renderMenuItemRadio';
+import { useMenuItemRadioStyles } from './useMenuItemRadioStyles';
+
+/**
+ * Define a styled MenuItemRadio, using the `useMenuItemRadio` hook.
+ * {@docCategory MenuItemRadio}
+ */
+export const MenuItemRadio = React.forwardRef((props, ref) => {
+ const state = useMenuItemRadio(props, ref);
+ useMenuItemRadioStyles(state);
+
+ return renderMenuItemRadio(state);
+});
+
+MenuItemRadio.displayName = 'MenuItemRadio';
diff --git a/packages/react-menu/src/components/MenuItemRadio/MenuItemRadio.types.ts b/packages/react-menu/src/components/MenuItemRadio/MenuItemRadio.types.ts
new file mode 100644
index 00000000000000..29350e6067aea0
--- /dev/null
+++ b/packages/react-menu/src/components/MenuItemRadio/MenuItemRadio.types.ts
@@ -0,0 +1,29 @@
+import * as React from 'react';
+import { ComponentProps, ObjectShorthandProps, ShorthandProps } from '@fluentui/react-utils';
+import { MenuItemSelectableProps, MenuItemSelectableState } from '../../selectable/index';
+
+/**
+ * {@docCategory MenuItemRadio}
+ */
+export interface MenuItemRadioProps extends ComponentProps, React.HTMLAttributes, MenuItemSelectableProps {
+ icon?: ShorthandProps;
+
+ checkmark?: ShorthandProps;
+}
+
+/**
+ * {@docCategory MenuItemRadio}
+ */
+export interface MenuItemRadioState extends MenuItemRadioProps, MenuItemSelectableState {
+ ref: React.MutableRefObject;
+
+ /**
+ * Icon slot rendered before children content
+ */
+ icon?: ObjectShorthandProps;
+
+ /**
+ * Slot for the checkmark indicator
+ */
+ checkmark: ObjectShorthandProps;
+}
diff --git a/packages/react-menu/src/components/MenuItemRadio/__snapshots__/MenuItemRadio.test.tsx.snap b/packages/react-menu/src/components/MenuItemRadio/__snapshots__/MenuItemRadio.test.tsx.snap
new file mode 100644
index 00000000000000..3c9c3a1ff007d7
--- /dev/null
+++ b/packages/react-menu/src/components/MenuItemRadio/__snapshots__/MenuItemRadio.test.tsx.snap
@@ -0,0 +1,15 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`MenuItemRadio renders a default state 1`] = `
+
+ Default MenuItemRadio
+
+`;
diff --git a/packages/react-menu/src/components/MenuItemRadio/index.ts b/packages/react-menu/src/components/MenuItemRadio/index.ts
new file mode 100644
index 00000000000000..36a208dd52a567
--- /dev/null
+++ b/packages/react-menu/src/components/MenuItemRadio/index.ts
@@ -0,0 +1,4 @@
+export * from './MenuItemRadio.types';
+export * from './MenuItemRadio';
+export * from './renderMenuItemRadio';
+export * from './useMenuItemRadio';
diff --git a/packages/react-menu/src/components/MenuItemRadio/renderMenuItemRadio.tsx b/packages/react-menu/src/components/MenuItemRadio/renderMenuItemRadio.tsx
new file mode 100644
index 00000000000000..d7d51de7e7244b
--- /dev/null
+++ b/packages/react-menu/src/components/MenuItemRadio/renderMenuItemRadio.tsx
@@ -0,0 +1,20 @@
+import * as React from 'react';
+import { getSlots } from '@fluentui/react-utils';
+import { MenuItemRadioState } from './MenuItemRadio.types';
+import { menuItemRadioShorthandProps } from './useMenuItemRadio';
+
+/**
+ * Redefine the render function to add slots. Reuse the menuitemradio structure but add
+ * slots to children.
+ */
+export const renderMenuItemRadio = (state: MenuItemRadioState) => {
+ const { slots, slotProps } = getSlots(state, menuItemRadioShorthandProps);
+
+ return (
+
+
+
+ {state.children}
+
+ );
+};
diff --git a/packages/react-menu/src/components/MenuItemRadio/useMenuItemRadio.ts b/packages/react-menu/src/components/MenuItemRadio/useMenuItemRadio.ts
new file mode 100644
index 00000000000000..cb37bda39dbbd1
--- /dev/null
+++ b/packages/react-menu/src/components/MenuItemRadio/useMenuItemRadio.ts
@@ -0,0 +1,36 @@
+import * as React from 'react';
+import { makeMergeProps, resolveShorthandProps } from '@fluentui/react-utils';
+import { MenuItemRadioProps, MenuItemRadioState } from './MenuItemRadio.types';
+import { useMenuItemSelectable } from '../../selectable/index';
+import { useMergedRefs } from '@fluentui/react-hooks';
+
+/**
+ * Consts listing which props are shorthand props.
+ */
+export const menuItemRadioShorthandProps = ['icon', 'checkmark'];
+
+const mergeProps = makeMergeProps({ deepMerge: menuItemRadioShorthandProps });
+
+/**
+ * Given user props, returns state and render function for a MenuItemRadio.
+ */
+export const useMenuItemRadio = (
+ props: MenuItemRadioProps,
+ ref: React.Ref,
+ defaultProps?: MenuItemRadioProps,
+): MenuItemRadioState => {
+ const state = mergeProps(
+ {
+ ref: useMergedRefs(ref, React.useRef(null)),
+ icon: { as: 'span' },
+ checkmark: { as: 'span' },
+ role: 'menuitemradio',
+ tabIndex: 0,
+ },
+ defaultProps,
+ resolveShorthandProps(props, menuItemRadioShorthandProps),
+ );
+
+ useMenuItemSelectable(state, () => [state.value]);
+ return state;
+};
diff --git a/packages/react-menu/src/components/MenuItemRadio/useMenuItemRadioStyles.ts b/packages/react-menu/src/components/MenuItemRadio/useMenuItemRadioStyles.ts
new file mode 100644
index 00000000000000..d6a4d64c0fbacf
--- /dev/null
+++ b/packages/react-menu/src/components/MenuItemRadio/useMenuItemRadioStyles.ts
@@ -0,0 +1,8 @@
+import { useCheckmarkStyles } from '../../selectable/index';
+import { useMenuItemStyles } from '../MenuItem/useMenuItemStyles';
+import { MenuItemRadioState } from './MenuItemRadio.types';
+
+export const useMenuItemRadioStyles = (state: MenuItemRadioState) => {
+ useMenuItemStyles(state);
+ useCheckmarkStyles(state);
+};
diff --git a/packages/react-menu/src/components/MenuList/MenuList.types.ts b/packages/react-menu/src/components/MenuList/MenuList.types.ts
index 97593059a04c95..e7ca45f8f9868b 100644
--- a/packages/react-menu/src/components/MenuList/MenuList.types.ts
+++ b/packages/react-menu/src/components/MenuList/MenuList.types.ts
@@ -1,7 +1,20 @@
import * as React from 'react';
import { ComponentProps } from '@fluentui/react-utils';
-export interface MenuListProps extends ComponentProps, React.HTMLAttributes {}
+export interface MenuListProps extends ComponentProps, React.HTMLAttributes {
+ /**
+ * Callback when checked items change for value with a name
+ *
+ * @param name - the name of the value
+ * @param checkedItems - the items for this value that are checked
+ */
+ onCheckedValueChange?: (e: React.MouseEvent | React.KeyboardEvent, name: string, checkedItems: string[]) => void;
+
+ /**
+ * Map of all checked values
+ */
+ checkedValues?: Record;
+}
export interface MenuListState extends MenuListProps {
/**
diff --git a/packages/react-menu/src/components/MenuList/renderMenuList.tsx b/packages/react-menu/src/components/MenuList/renderMenuList.tsx
index aaff89c75a7a5b..c4ddfed71ff09f 100644
--- a/packages/react-menu/src/components/MenuList/renderMenuList.tsx
+++ b/packages/react-menu/src/components/MenuList/renderMenuList.tsx
@@ -1,12 +1,18 @@
import * as React from 'react';
import { getSlots } from '@fluentui/react-utils';
import { MenuListState } from './MenuList.types';
+import { MenuListProvider } from '../../menuListContext';
/**
* Function that renders the final JSX of the component
*/
export const renderMenuList = (state: MenuListState) => {
const { slots, slotProps } = getSlots(state);
+ const { onCheckedValueChange, checkedValues } = state;
- return {state.children};
+ return (
+
+ {state.children}
+
+ );
};
diff --git a/packages/react-menu/src/index.ts b/packages/react-menu/src/index.ts
index 7fdd4bfcbc7927..435ff83f407e7c 100644
--- a/packages/react-menu/src/index.ts
+++ b/packages/react-menu/src/index.ts
@@ -2,3 +2,5 @@ import './version';
export * from './MenuItem';
export * from './MenuList';
+export * from './MenuItemCheckbox';
+export * from './MenuItemRadio';
diff --git a/packages/react-menu/src/menuListContext.tsx b/packages/react-menu/src/menuListContext.tsx
new file mode 100644
index 00000000000000..c71269a5c5d27b
--- /dev/null
+++ b/packages/react-menu/src/menuListContext.tsx
@@ -0,0 +1,16 @@
+import * as React from 'react';
+
+const MenuListContext = React.createContext({
+ checkedValues: {},
+ onCheckedValueChange: () => null,
+});
+
+// TODO add context selector to reduce the number of rerenders
+export interface MenuListContext {
+ checkedValues?: Record;
+ onCheckedValueChange?: (e: React.MouseEvent | React.KeyboardEvent, name: string, items: string[]) => void;
+}
+
+export const MenuListProvider = MenuListContext.Provider;
+
+export const useMenuListContext = () => React.useContext(MenuListContext);
diff --git a/packages/react-menu/src/selectable/index.ts b/packages/react-menu/src/selectable/index.ts
new file mode 100644
index 00000000000000..2a83872c4bb959
--- /dev/null
+++ b/packages/react-menu/src/selectable/index.ts
@@ -0,0 +1,3 @@
+export * from './types';
+export * from './useMenuItemSelectable';
+export * from './useCheckmarkStyles';
diff --git a/packages/react-menu/src/selectable/types.ts b/packages/react-menu/src/selectable/types.ts
new file mode 100644
index 00000000000000..96c227121c8d9a
--- /dev/null
+++ b/packages/react-menu/src/selectable/types.ts
@@ -0,0 +1,38 @@
+import * as React from 'react';
+
+/**
+ * Props for selecatble menu items
+ */
+export interface MenuItemSelectableProps extends React.HTMLAttributes {
+ /**
+ * Follows input convention
+ * https://www.w3schools.com/jsref/prop_checkbox_name.asp
+ */
+ name: string;
+
+ /**
+ * Follows input convention
+ * https://www.w3schools.com/jsref/prop_checkbox_value.asp
+ */
+ value: string;
+}
+
+/**
+ * State for selectable menu items
+ */
+export interface MenuItemSelectableState extends MenuItemSelectableProps {
+ /**
+ * Checked items for a value with `name`
+ */
+ checkedItems: string[];
+
+ /**
+ * Callback when checked items changes for a given value with `name`
+ */
+ onCheckedValueChange: (e: React.MouseEvent | React.KeyboardEvent, name: string, checkedItems: string[]) => void;
+
+ /**
+ * Selectable is checked
+ */
+ checked: boolean;
+}
diff --git a/packages/react-menu/src/selectable/useCheckmarkStyles.ts b/packages/react-menu/src/selectable/useCheckmarkStyles.ts
new file mode 100644
index 00000000000000..d22c44551547c4
--- /dev/null
+++ b/packages/react-menu/src/selectable/useCheckmarkStyles.ts
@@ -0,0 +1,38 @@
+import { makeStyles, ax } from '@fluentui/react-make-styles';
+import { ObjectShorthandProps } from '@fluentui/react-utils';
+import { MenuItemSelectableState } from './types';
+
+/**
+ * Style hook for checkmark icons
+ */
+const useStyles = makeStyles([
+ [
+ null,
+ () => ({
+ width: '16px',
+ height: '16px',
+ marginRight: '9px',
+ visibility: 'hidden',
+ }),
+ ],
+ [
+ state => state.checked,
+ () => ({
+ visibility: 'visible',
+ }),
+ ],
+]);
+
+/**
+ * Applies styles to a checkmark slot for selectable menu items
+ *
+ * @param state should contain a `checkmark` slot
+ */
+export const useCheckmarkStyles = (
+ state: MenuItemSelectableState & { checkmark: ObjectShorthandProps },
+) => {
+ const checkmarkClassName = useStyles(state);
+ if (state.checkmark) {
+ state.checkmark.className = ax(checkmarkClassName, state.checkmark.className);
+ }
+};
diff --git a/packages/react-menu/src/selectable/useMenuItemSelectable.test.ts b/packages/react-menu/src/selectable/useMenuItemSelectable.test.ts
new file mode 100644
index 00000000000000..6f60882f7d9274
--- /dev/null
+++ b/packages/react-menu/src/selectable/useMenuItemSelectable.test.ts
@@ -0,0 +1,123 @@
+import { EnterKey, SpacebarKey } from '@fluentui/keyboard-key';
+import { renderHook } from '@testing-library/react-hooks';
+import { MenuItemSelectableState } from './types';
+import { useMenuItemSelectable } from './useMenuItemSelectable';
+import { useMenuListContext } from '../menuListContext';
+
+jest.mock('../menuListContext');
+
+describe('useMenuItemSelectable', () => {
+ const createTestState = (options: Partial = {}): MenuItemSelectableState => ({
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ checkmark: {},
+ name: 'name',
+ value: 'value',
+ onCheckedValueChange: jest.fn(),
+ ...options,
+ });
+
+ const checkedItems = ['1', '2', '3'];
+
+ it.each([
+ [['1'], true],
+ [['2'], false],
+ [undefined, false],
+ [[], false],
+ [['3', '1', '2'], true],
+ ])('should set checked and aria-checked', (values, expected) => {
+ // Arrange
+ const state: MenuItemSelectableState = createTestState({ value: '1', name: 'test' });
+ (useMenuListContext as jest.Mock).mockReturnValue({ checkedValues: { test: values } });
+
+ // Act
+ renderHook(() => useMenuItemSelectable(state, jest.fn()));
+
+ // Assert
+ expect(state.checked).toBe(expected);
+ expect(state['aria-checked']).toBe(expected);
+ });
+
+ it('should call onCheckedValueChange if values changed', () => {
+ // Arrange
+ const state: MenuItemSelectableState = createTestState();
+ (useMenuListContext as jest.Mock).mockReturnValue({
+ onCheckedValueChange: jest.fn(),
+ checkedValues: { [state.name]: [...checkedItems] },
+ });
+ const newValues = [...checkedItems, 'x'];
+
+ // Act
+ renderHook(() => useMenuItemSelectable(state, () => newValues));
+ if (state.onClick) {
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ state.onClick({ persist: jest.fn() });
+ }
+
+ // Assert
+ expect(state.onCheckedValueChange).toHaveBeenCalledTimes(1);
+ expect(state.onCheckedValueChange).toHaveBeenCalledWith(expect.anything(), state.name, newValues);
+ });
+
+ it('should not call onCheckedValueChange if values did not change', () => {
+ // Arrange
+ const state: MenuItemSelectableState = createTestState();
+ (useMenuListContext as jest.Mock).mockReturnValue({
+ onCheckedValueChange: jest.fn(),
+ checkedValues: { [state.name]: [...checkedItems] },
+ });
+
+ // Act
+ renderHook(() => useMenuItemSelectable(state, () => [...checkedItems]));
+ if (state.onClick) {
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ state.onClick({ persist: jest.fn() });
+ }
+
+ // Assert
+ expect(state.onCheckedValueChange).not.toHaveBeenCalled();
+ });
+
+ it.each(['onClick', 'onKeyDown'])('should set %s handler', action => {
+ // Arrange
+ const state: MenuItemSelectableState = createTestState({ onKeyDown: undefined, onClick: undefined });
+
+ // Act
+ renderHook(() => useMenuItemSelectable(state, jest.fn()));
+
+ // Assert
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ const callback = state[action];
+ expect(callback).toBeDefined();
+ expect(typeof callback).toBe('function');
+ });
+
+ it.each([EnterKey, SpacebarKey])('should toggle selection on %s keydown', keyCode => {
+ // Arrange
+ const state: MenuItemSelectableState = createTestState();
+ (useMenuListContext as jest.Mock).mockReturnValue({
+ onCheckedValueChange: jest.fn(),
+ checkedValues: { [state.name]: [...checkedItems] },
+ });
+ const event = {
+ defaultPrevented: false,
+ keyCode,
+ persist: jest.fn(),
+ };
+
+ // Act
+ renderHook(() => useMenuItemSelectable(state, () => []));
+ if (state.onKeyDown) {
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ state.onKeyDown(event);
+ }
+
+ // Assert
+ expect(state.onCheckedValueChange).toHaveBeenCalledTimes(1);
+ expect(state.onCheckedValueChange).toHaveBeenCalledWith(expect.anything(), state.name, []);
+ });
+});
diff --git a/packages/react-menu/src/selectable/useMenuItemSelectable.ts b/packages/react-menu/src/selectable/useMenuItemSelectable.ts
new file mode 100644
index 00000000000000..a7d8686c261275
--- /dev/null
+++ b/packages/react-menu/src/selectable/useMenuItemSelectable.ts
@@ -0,0 +1,52 @@
+import * as React from 'react';
+import { EnterKey, getCode, SpacebarKey } from '@fluentui/keyboard-key';
+import { useMenuListContext } from '../menuListContext';
+import { MenuItemSelectableState } from './types';
+
+/**
+ * Hook used to mutate state to handle selection logic for selectable menu items
+ *
+ * @param state Selectable menu item state
+ * @param getNewCheckedItems Callback that returns the new checked values for given menu item
+ */
+export const useMenuItemSelectable = (state: MenuItemSelectableState, getNewCheckedItems: () => string[]) => {
+ const { onClick: onClickCallback, onKeyDown: onKeyDownCallback } = state;
+ const { checkedValues: { [state.name]: checkedItems = [] } = {}, onCheckedValueChange } = useMenuListContext();
+
+ state.checkedItems = checkedItems;
+ state.onCheckedValueChange = onCheckedValueChange || (() => null);
+ state.checked = checkedItems.indexOf(state.value) !== -1;
+ state['aria-checked'] = state.checked;
+
+ const onSelectionChange = (e: React.MouseEvent | React.KeyboardEvent) => {
+ const newCheckedItems = getNewCheckedItems();
+
+ if (
+ newCheckedItems.length === state.checkedItems.length &&
+ state.checkedItems.every((el, i) => el === newCheckedItems[i])
+ ) {
+ return;
+ }
+
+ state.onCheckedValueChange(e, state.name, newCheckedItems);
+ };
+
+ state.onClick = e => {
+ if (onClickCallback) {
+ onClickCallback(e);
+ }
+
+ onSelectionChange(e);
+ };
+
+ state.onKeyDown = e => {
+ if (onKeyDownCallback) {
+ onKeyDownCallback(e);
+ }
+
+ const keyCode = getCode(e);
+ if (!e.defaultPrevented && (keyCode === EnterKey || keyCode === SpacebarKey)) {
+ onSelectionChange(e);
+ }
+ };
+};
diff --git a/yarn.lock b/yarn.lock
index 31a9e22ed11c77..65a0cc1999eb55 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1072,18 +1072,18 @@
pirates "^4.0.0"
source-map-support "^0.5.16"
-"@babel/runtime-corejs3@^7.7.4", "@babel/runtime-corejs3@^7.8.3":
- version "7.10.4"
- resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.10.4.tgz#f29fc1990307c4c57b10dbd6ce667b27159d9e0d"
- integrity sha512-BFlgP2SoLO9HJX9WBwN67gHWMBhDX/eDz64Jajd6mR/UAUzqrNMm99d4qHnVaKscAElZoFiPv+JpR/Siud5lXw==
+"@babel/runtime-corejs3@^7.10.2", "@babel/runtime-corejs3@^7.7.4", "@babel/runtime-corejs3@^7.8.3":
+ version "7.12.13"
+ resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.12.13.tgz#53d09813b7c20d616caf258e9325550ff701c039"
+ integrity sha512-8fSpqYRETHATtNitsCXq8QQbKJP31/KnDl2Wz2Vtui9nKzjss2ysuZtyVsWjBtvkeEFo346gkwjYPab1hvrXkQ==
dependencies:
core-js-pure "^3.0.0"
regenerator-runtime "^0.13.4"
-"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.4", "@babel/runtime@^7.3.1", "@babel/runtime@^7.3.4", "@babel/runtime@^7.4.2", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.4", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
- version "7.10.4"
- resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.10.4.tgz#a6724f1a6b8d2f6ea5236dbfe58c7d7ea9c5eb99"
- integrity sha512-UpTN5yUJr9b4EX2CnGNWIvER7Ab83ibv0pcvvHc4UOdrBI5jb8bj+32cCwPX6xu0mt2daFNjYhoi+X7beH0RSw==
+"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.10.3", "@babel/runtime@^7.10.4", "@babel/runtime@^7.12.5", "@babel/runtime@^7.3.1", "@babel/runtime@^7.3.4", "@babel/runtime@^7.4.2", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.4", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
+ version "7.12.13"
+ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.13.tgz#0a21452352b02542db0ffb928ac2d3ca7cb6d66d"
+ integrity sha512-8+3UMPBrjFa/6TtKi/7sehPKqfAm4g6K+YQjyyFOLUTxzOngcRZTlAVY8sc2CORJYqdHQY8gRPHmn+qo15rCBw==
dependencies:
regenerator-runtime "^0.13.4"
@@ -1597,7 +1597,7 @@
source-map "^0.6.1"
write-file-atomic "2.4.1"
-"@jest/types@^24.8.0", "@jest/types@^24.9.0":
+"@jest/types@^24.9.0":
version "24.9.0"
resolved "https://registry.yarnpkg.com/@jest/types/-/types-24.9.0.tgz#63cb26cb7500d069e5a389441a7c6ab5e909fc59"
integrity sha512-XKK7ze1apu5JWQ5eZjHITP66AX+QsLlbaJRBGYr8pNzwcAE2JVkwnf0yqjHTsDRcjR0mujy/NmZMXw5kl+kGBw==
@@ -1606,10 +1606,10 @@
"@types/istanbul-reports" "^1.1.1"
"@types/yargs" "^13.0.0"
-"@jest/types@^25.1.0":
- version "25.1.0"
- resolved "https://registry.yarnpkg.com/@jest/types/-/types-25.1.0.tgz#b26831916f0d7c381e11dbb5e103a72aed1b4395"
- integrity sha512-VpOtt7tCrgvamWZh1reVsGADujKigBUFTi19mlRjqEGsE8qH4r3s+skY33dNdXOwyZIvuftZ5tqdF1IgsMejMA==
+"@jest/types@^25.5.0":
+ version "25.5.0"
+ resolved "https://registry.yarnpkg.com/@jest/types/-/types-25.5.0.tgz#4d6a4793f7b9599fc3680877b856a97dbccf2a9d"
+ integrity sha512-OXD0RgQ86Tu3MazKo8bnrkDRaDXXMGUqd+kTtLtK1Zb7CRzQcaSRPPPV37SvYTdevXEBVxe0HXylEjs8ibkmCw==
dependencies:
"@types/istanbul-lib-coverage" "^2.0.0"
"@types/istanbul-reports" "^1.1.1"
@@ -3628,16 +3628,16 @@
dependencies:
defer-to-connect "^1.0.1"
-"@testing-library/dom@^7.2.1":
- version "7.2.1"
- resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-7.2.1.tgz#bb3b31d669bbe0c4939dadd95d69caa3c1d0b372"
- integrity sha512-xIGoHlQ2ZiEL1dJIFKNmLDypzYF+4OJTTASRctl/aoIDaS5y/pRVHRigoqvPUV11mdJoR71IIgi/6UviMgyz4g==
+"@testing-library/dom@7.22.3", "@testing-library/dom@^7.2.1", "@testing-library/dom@^7.22.3":
+ version "7.22.3"
+ resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-7.22.3.tgz#12c0b1b97115e7731da6a86b4574eae401cb9ac5"
+ integrity sha512-IK6/eL1Xza/0goDKrwnBvlM06L+5eL9b1o+hUhX7HslfUvMETh0TYgXEr2LVpsVkHiOhRmUbUyml95KV/VlRNw==
dependencies:
- "@babel/runtime" "^7.9.2"
- "@types/testing-library__dom" "^7.0.0"
- aria-query "^4.0.2"
- dom-accessibility-api "^0.4.2"
- pretty-format "^25.1.0"
+ "@babel/runtime" "^7.10.3"
+ "@types/aria-query" "^4.2.0"
+ aria-query "^4.2.2"
+ dom-accessibility-api "^0.5.1"
+ pretty-format "^25.5.0"
"@testing-library/jest-dom@^5.1.1":
version "5.1.1"
@@ -3670,6 +3670,26 @@
lodash "^4.17.15"
redent "^3.0.0"
+"@testing-library/react-hooks@^5.0.3":
+ version "5.0.3"
+ resolved "https://registry.yarnpkg.com/@testing-library/react-hooks/-/react-hooks-5.0.3.tgz#dd0d2048817b013b266d35ca45e3ea48a19fd87e"
+ integrity sha512-UrnnRc5II7LMH14xsYNm/WRch/67cBafmrSQcyFh0v+UUmSf1uzfB7zn5jQXSettGwOSxJwdQUN7PgkT0w22Lg==
+ dependencies:
+ "@babel/runtime" "^7.12.5"
+ "@types/react" ">=16.9.0"
+ "@types/react-dom" ">=16.9.0"
+ "@types/react-test-renderer" ">=16.9.0"
+ filter-console "^0.1.1"
+ react-error-boundary "^3.1.0"
+
+"@testing-library/react@^10.4.9":
+ version "10.4.9"
+ resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-10.4.9.tgz#9faa29c6a1a217bf8bbb96a28bd29d7a847ca150"
+ integrity sha512-pHZKkqUy0tmiD81afs8xfiuseXfU/N7rAX3iKjeZYje86t9VaB0LrxYVa+OOsvkrveX5jCK3IjajVn2MbePvqA==
+ dependencies:
+ "@babel/runtime" "^7.10.3"
+ "@testing-library/dom" "^7.22.3"
+
"@textlint/ast-node-types@^4.0.3":
version "4.2.5"
resolved "https://registry.yarnpkg.com/@textlint/ast-node-types/-/ast-node-types-4.2.5.tgz#ae13981bc8711c98313a6ac1c361194d6bf2d39b"
@@ -3715,6 +3735,11 @@
resolved "https://registry.yarnpkg.com/@types/argparse/-/argparse-1.0.33.tgz#2728669427cdd74a99e53c9f457ca2866a37c52d"
integrity sha512-VQgHxyPMTj3hIlq9SY1mctqx+Jj8kpQfoLvDlVSDNOyuYs8JYfkuY3OW/4+dO657yPmNhHpePRx0/Tje5ImNVQ==
+"@types/aria-query@^4.2.0":
+ version "4.2.1"
+ resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-4.2.1.tgz#78b5433344e2f92e8b306c06a5622c50c245bf6b"
+ integrity sha512-S6oPal772qJZHoRZLFc/XoZW2gFvwXusYUmXPXkgxJLuEk2vOt7jc4Yo6z/vtI0EBkbPBVrJJ0B+prLIKiWqHg==
+
"@types/babel__core@^7.1.0":
version "7.1.2"
resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.2.tgz#608c74f55928033fce18b99b213c16be4b3d114f"
@@ -4287,7 +4312,7 @@
dependencies:
"@types/react" "*"
-"@types/react-dom@16.9.10":
+"@types/react-dom@16.9.10", "@types/react-dom@>=16.9.0":
version "16.9.10"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.9.10.tgz#4485b0bec3d41f856181b717f45fd7831101156f"
integrity sha512-ItatOrnXDMAYpv6G8UCk2VhbYVTjZT9aorLtA/OzDN9XJ2GKcfam68jutoAcILdRjsRUO8qb7AmyObF77Q8QFw==
@@ -4353,6 +4378,13 @@
dependencies:
"@types/react" "*"
+"@types/react-test-renderer@>=16.9.0":
+ version "17.0.0"
+ resolved "https://registry.yarnpkg.com/@types/react-test-renderer/-/react-test-renderer-17.0.0.tgz#9be47b375eeb906fced37049e67284a438d56620"
+ integrity sha512-nvw+F81OmyzpyIE1S0xWpLonLUZCMewslPuA8BtjSKc5XEbn8zEQBXS7KuOLHTNnSOEM2Pum50gHOoZ62tqTRg==
+ dependencies:
+ "@types/react" "*"
+
"@types/react-test-renderer@^16.0.0":
version "16.8.2"
resolved "https://registry.yarnpkg.com/@types/react-test-renderer/-/react-test-renderer-16.8.2.tgz#ad544b5571ebfc5f182c320376f1431a2b725c5e"
@@ -4382,7 +4414,7 @@
dependencies:
"@types/react" "*"
-"@types/react@*", "@types/react@16.9.42", "@types/react@^16":
+"@types/react@*", "@types/react@16.9.42", "@types/react@>=16.9.0", "@types/react@^16":
version "16.9.42"
resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.42.tgz#9776508d59c1867bbf9bd7f036dab007fdaa1cb7"
integrity sha512-iGy6HwfVfotqJ+PfRZ4eqPHPP5NdPZgQlr0lTs8EfkODRBV9cYy8QMKcC9qPCe1JrESC1Im6SrCFR6tQgg74ag==
@@ -4462,13 +4494,6 @@
resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.6.tgz#a9ca4b70a18b270ccb2bc0aaafefd1d486b7ea74"
integrity sha512-W+bw9ds02rAQaMvaLYxAbJ6cvguW/iJXNT6lTssS1ps6QdrMKttqEAMEG/b5CR8TZl3/L7/lH0ZV5nNR1LXikA==
-"@types/testing-library__dom@^7.0.0":
- version "7.0.1"
- resolved "https://registry.yarnpkg.com/@types/testing-library__dom/-/testing-library__dom-7.0.1.tgz#426bef0aa306a603fe071859d4b485941b28aca6"
- integrity sha512-WokGRksRJb3Dla6h02/0/NNHTkjsj4S8aJZiwMj/5/UL8VZ1iCe3H8SHzfpmBeH8Vp4SPRT8iC2o9kYULFhDIw==
- dependencies:
- pretty-format "^25.1.0"
-
"@types/testing-library__jest-dom@^5.0.0":
version "5.0.1"
resolved "https://registry.yarnpkg.com/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.0.1.tgz#cc7f384535a3d9597e27f58d38a795f5c137cc53"
@@ -4646,9 +4671,9 @@
"@types/yargs-parser" "*"
"@types/yargs@^15.0.0":
- version "15.0.3"
- resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-15.0.3.tgz#41453a0bc7ab393e995d1f5451455638edbd2baf"
- integrity sha512-XCMQRK6kfpNBixHLyHUsGmXrpEmFFxzMrcnSXFMziHd8CoNJo8l16FkHyQq4x+xbM7E2XL83/O78OD8u+iZTdQ==
+ version "15.0.13"
+ resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-15.0.13.tgz#34f7fec8b389d7f3c1fd08026a5763e072d3c6dc"
+ integrity sha512-kQ5JNTrbDv3Rp5X2n/iUu37IJBDU2gsZ5R/g1/KHOOEc5IKfUFjXT6DENPGduh08I/pamwtEq4oul7gUqKTQDQ==
dependencies:
"@types/yargs-parser" "*"
@@ -5576,13 +5601,13 @@ aria-query@^3.0.0:
ast-types-flow "0.0.7"
commander "^2.11.0"
-aria-query@^4.0.2:
- version "4.0.2"
- resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-4.0.2.tgz#250687b4ccde1ab86d127da0432ae3552fc7b145"
- integrity sha512-S1G1V790fTaigUSM/Gd0NngzEfiMy9uTUfMyHhKhVyy4cH5O/eTuR01ydhGL0z4Za1PXFTRGH3qL8VhUQuEO5w==
+aria-query@^4.2.2:
+ version "4.2.2"
+ resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-4.2.2.tgz#0d2ca6c9aceb56b8977e9fed6aed7e15bbd2f83b"
+ integrity sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA==
dependencies:
- "@babel/runtime" "^7.7.4"
- "@babel/runtime-corejs3" "^7.7.4"
+ "@babel/runtime" "^7.10.2"
+ "@babel/runtime-corejs3" "^7.10.2"
arr-diff@^1.0.1:
version "1.1.0"
@@ -9569,10 +9594,10 @@ doctrine@^3.0.0:
dependencies:
esutils "^2.0.2"
-dom-accessibility-api@^0.4.2:
- version "0.4.3"
- resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.4.3.tgz#93ca9002eb222fd5a343b6e5e6b9cf5929411c4c"
- integrity sha512-JZ8iPuEHDQzq6q0k7PKMGbrIdsgBB7TRrtVOUm4nSMCExlg5qQG4KXWTH2k90yggjM4tTumRGwTKJSldMzKyLA==
+dom-accessibility-api@^0.5.1:
+ version "0.5.4"
+ resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.4.tgz#b06d059cdd4a4ad9a79275f9d414a5c126241166"
+ integrity sha512-TvrjBckDy2c6v6RLxPv5QXOnU+SmF9nBII5621Ve5fu6Z/BDrENurBEvlC1f44lKEUVqOpK4w9E5Idc5/EgkLQ==
dom-converter@^0.2:
version "0.2.0"
@@ -11316,6 +11341,11 @@ fill-range@^7.0.1:
dependencies:
to-regex-range "^5.0.1"
+filter-console@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/filter-console/-/filter-console-0.1.1.tgz#6242be28982bba7415bcc6db74a79f4a294fa67c"
+ integrity sha512-zrXoV1Uaz52DqPs+qEwNJWJFAWZpYJ47UNmpN9q4j+/EYsz85uV0DC9k8tRND5kYmoVzL0W+Y75q4Rg8sRJCdg==
+
finalhandler@1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.0.tgz#ce0b6855b45853e791b2fcc680046d88253dd7f5"
@@ -19537,17 +19567,7 @@ pretty-error@^2.1.1:
renderkid "^2.0.1"
utila "~0.4"
-pretty-format@^24.8.0:
- version "24.8.0"
- resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-24.8.0.tgz#8dae7044f58db7cb8be245383b565a963e3c27f2"
- integrity sha512-P952T7dkrDEplsR+TuY7q3VXDae5Sr7zmQb12JU/NDQa/3CH7/QW0yvqLcGN6jL+zQFKaoJcPc+yJxMTGmosqw==
- dependencies:
- "@jest/types" "^24.8.0"
- ansi-regex "^4.0.0"
- ansi-styles "^3.2.0"
- react-is "^16.8.4"
-
-pretty-format@^24.9.0:
+pretty-format@^24.8.0, pretty-format@^24.9.0:
version "24.9.0"
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-24.9.0.tgz#12fac31b37019a4eea3c11aa9a959eb7628aa7c9"
integrity sha512-00ZMZUiHaJrNfk33guavqgvfJS30sLYf0f8+Srklv0AMPodGGHcoHgksZ3OThYnIvOd+8yMCn0YiEOogjlgsnA==
@@ -19557,12 +19577,12 @@ pretty-format@^24.9.0:
ansi-styles "^3.2.0"
react-is "^16.8.4"
-pretty-format@^25.1.0:
- version "25.1.0"
- resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-25.1.0.tgz#ed869bdaec1356fc5ae45de045e2c8ec7b07b0c8"
- integrity sha512-46zLRSGLd02Rp+Lhad9zzuNZ+swunitn8zIpfD2B4OPCRLXbM87RJT2aBLBWYOznNUML/2l/ReMyWNC80PJBUQ==
+pretty-format@^25.1.0, pretty-format@^25.5.0:
+ version "25.5.0"
+ resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-25.5.0.tgz#7873c1d774f682c34b8d48b6743a2bf2ac55791a"
+ integrity sha512-kbo/kq2LQ/A/is0PQwsEHM7Ca6//bGPPvU6UnsdDRSKTWxT/ru/xb88v4BJf6a69H+uTytOEsTusT9ksd/1iWQ==
dependencies:
- "@jest/types" "^25.1.0"
+ "@jest/types" "^25.5.0"
ansi-regex "^5.0.0"
ansi-styles "^4.0.0"
react-is "^16.12.0"
@@ -20167,6 +20187,13 @@ react-element-to-jsx-string@^14.0.2:
"@base2/pretty-print-object" "1.0.0"
is-plain-object "3.0.0"
+react-error-boundary@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-3.1.0.tgz#9487443df2f9ba1db90d8ab52351814907ea4af3"
+ integrity sha512-lmPrdi5SLRJR+AeJkqdkGlW/CRkAUvZnETahK58J4xb5wpbfDngasEGu+w0T1iXEhVrYBJZeW+c4V1hILCnMWQ==
+ dependencies:
+ "@babel/runtime" "^7.12.5"
+
react-error-overlay@^6.0.3:
version "6.0.4"
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.4.tgz#0d165d6d27488e660bc08e57bdabaad741366f7a"