Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import type { ComponentState } from '@fluentui/react-utilities';
import type { ContextSelector } from '@fluentui/react-context-selector';
import type { ForwardRefComponent } from '@fluentui/react-utilities';
import type { JSXElement } from '@fluentui/react-utilities';
import type { PortalProps } from '@fluentui/react-portal';
import type { PositioningShorthand } from '@fluentui/react-positioning';
import { PositioningVirtualElement } from '@fluentui/react-positioning';
import * as React_2 from 'react';
Expand Down Expand Up @@ -41,10 +40,13 @@ export type MenuCheckedValueChangeData = {
export type MenuCheckedValueChangeEvent = React_2.MouseEvent | React_2.KeyboardEvent;

// @public
export type MenuContextValue = Pick<MenuState, 'openOnHover' | 'openOnContext' | 'triggerRef' | 'menuPopoverRef' | 'setOpen' | 'isSubmenu' | 'mountNode' | 'triggerId' | 'hasIcons' | 'hasCheckmarks' | 'persistOnItemClick' | 'inline' | 'checkedValues' | 'onCheckedValueChange' | 'safeZone'> & {
export type MenuContextValue = Pick<MenuState, 'openOnHover' | 'openOnContext' | 'triggerRef' | 'menuPopoverRef' | 'setOpen' | 'isSubmenu' | 'triggerId' | 'hasIcons' | 'hasCheckmarks' | 'persistOnItemClick' | 'checkedValues' | 'onCheckedValueChange' | 'safeZone'> & {
open: boolean;
triggerId: string;
defaultCheckedValues?: Record<string, string[]>;
popoverId: string;
positioning?: MenuState['positioning'];
submenuFallbackPositions?: MenuState['submenuFallbackPositions'];
};

// @public (undocumented)
Expand Down Expand Up @@ -331,13 +333,13 @@ export type MenuPopoverSlots = {
};

// @public
export type MenuPopoverState = ComponentState<MenuPopoverSlots> & Pick<PortalProps, 'mountNode'> & {
inline: boolean;
export type MenuPopoverState = ComponentState<MenuPopoverSlots> & {
safeZone?: React_2.ReactElement | null;
mountNode?: HTMLElement | null | undefined;
};

// @public
export type MenuProps = ComponentProps<MenuSlots> & Pick<PortalProps, 'mountNode'> & Pick<MenuListProps, 'checkedValues' | 'defaultCheckedValues' | 'hasCheckmarks' | 'hasIcons' | 'onCheckedValueChange'> & {
export type MenuProps = ComponentProps<MenuSlots> & Pick<MenuListProps, 'checkedValues' | 'defaultCheckedValues' | 'hasCheckmarks' | 'hasIcons' | 'onCheckedValueChange'> & {
children: [JSXElement, JSXElement] | JSXElement;
hoverDelay?: number;
inline?: boolean;
Expand All @@ -349,6 +351,7 @@ export type MenuProps = ComponentProps<MenuSlots> & Pick<PortalProps, 'mountNode
persistOnItemClick?: boolean;
positioning?: PositioningShorthand;
closeOnScroll?: boolean;
mountNode?: HTMLElement;
};

// @public (undocumented)
Expand All @@ -375,7 +378,7 @@ export type MenuSplitGroupSlots = {
export type MenuSplitGroupState = ComponentState<MenuSplitGroupSlots> & Pick<MenuSplitGroupContextValue, 'setMultiline'>;

// @public (undocumented)
export type MenuState = ComponentState<MenuSlots> & Required<Pick<MenuProps, 'hasCheckmarks' | 'hasIcons' | 'mountNode' | 'inline' | 'checkedValues' | 'onCheckedValueChange' | 'open' | 'openOnHover' | 'closeOnScroll' | 'hoverDelay' | 'openOnContext' | 'persistOnItemClick'>> & {
export type MenuState = ComponentState<MenuSlots> & Required<Pick<MenuProps, 'hasCheckmarks' | 'hasIcons' | 'checkedValues' | 'onCheckedValueChange' | 'open' | 'openOnHover' | 'closeOnScroll' | 'hoverDelay' | 'openOnContext' | 'persistOnItemClick'>> & Pick<MenuProps, 'positioning'> & {
contextTarget?: PositioningVirtualElement;
isSubmenu: boolean;
menuPopover: React_2.ReactNode;
Expand All @@ -390,6 +393,8 @@ export type MenuState = ComponentState<MenuSlots> & Required<Pick<MenuProps, 'ha
onOpenChange?: (e: MenuOpenEvent, data: MenuOpenChangeData) => void;
defaultCheckedValues?: Record<string, string[]>;
safeZone?: React_2.ReactElement | null;
popoverId: string;
submenuFallbackPositions?: string[];
};

// @public
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -265,29 +265,10 @@ describe('Menu', () => {
expect(document.body.querySelector('[role="menu"]')).not.toBeNull();
});

it('should render menu inline when configured by prop', () => {
// Arrange
const { container } = render(
<Menu open inline>
<MenuTrigger disableButtonEnhancement>
<button>Menu trigger</button>
</MenuTrigger>
<MenuPopover>
<MenuList>
<MenuItem>Item</MenuItem>
</MenuList>
</MenuPopover>
</Menu>,
);

// Assert
expect(container.querySelector('[role="menu"]')).not.toBeNull();
});

it('should call onCheckedValueChange when item is selected', () => {
const onCheckedValueChange = jest.fn();
const { getAllByRole } = render(
<Menu open inline onCheckedValueChange={onCheckedValueChange}>
<Menu open onCheckedValueChange={onCheckedValueChange}>
<MenuTrigger disableButtonEnhancement>
<button>MenuTrigger</button>
</MenuTrigger>
Expand Down Expand Up @@ -315,7 +296,7 @@ describe('Menu', () => {

it('should control checked items with checkedValues prop', () => {
const { container } = render(
<Menu open inline checkedValues={{ test: ['second'] }}>
<Menu open checkedValues={{ test: ['second'] }}>
<MenuTrigger disableButtonEnhancement>
<button>MenuTrigger</button>
</MenuTrigger>
Expand Down Expand Up @@ -344,7 +325,7 @@ describe('Menu', () => {
it('should call onCheckedValueChange (applied to MenuList) when item is selected', () => {
const onCheckedValueChange = jest.fn();
const { getAllByRole } = render(
<Menu open inline>
<Menu open>
<MenuTrigger disableButtonEnhancement>
<button>MenuTrigger</button>
</MenuTrigger>
Expand Down Expand Up @@ -372,7 +353,7 @@ describe('Menu', () => {

it('should control checked items with checkedValues prop (applied to MenuList)', () => {
const { container } = render(
<Menu open inline>
<Menu open>
<MenuTrigger disableButtonEnhancement>
<button>MenuTrigger</button>
</MenuTrigger>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import * as React from 'react';
import { PositioningVirtualElement, SetVirtualMouseTarget } from '@fluentui/react-positioning';
import type { PositioningShorthand } from '@fluentui/react-positioning';
import type { PortalProps } from '@fluentui/react-portal';
import type {
PositioningShorthand,
PositioningVirtualElement,
SetVirtualMouseTarget,
} from '@fluentui/react-positioning';
import type { ComponentProps, ComponentState, JSXElement } from '@fluentui/react-utilities';
import type { MenuContextValue } from '../../contexts/menuContext';
import type { MenuListProps } from '../MenuList/MenuList.types';
Expand All @@ -12,7 +14,6 @@ export type MenuSlots = {};
* Extends and drills down Menulist props to simplify API
*/
export type MenuProps = ComponentProps<MenuSlots> &
Pick<PortalProps, 'mountNode'> &
Pick<
MenuListProps,
'checkedValues' | 'defaultCheckedValues' | 'hasCheckmarks' | 'hasIcons' | 'onCheckedValueChange'
Expand All @@ -31,7 +32,7 @@ export type MenuProps = ComponentProps<MenuSlots> &
/**
* Root menus are rendered out of DOM order on `document.body`, use this to render the menu in DOM order
* This option is disregarded for submenus
*
* @deprecated With HTML Popover API all menus are inline
* @default false
*/
inline?: boolean;
Expand Down Expand Up @@ -89,6 +90,11 @@ export type MenuProps = ComponentProps<MenuSlots> &
* @default false
*/
closeOnScroll?: boolean;

/**
* @deprecated Popovers are always rendered in DOM order with HTML Popover API
*/
mountNode?: HTMLElement;
};

export type MenuState = ComponentState<MenuSlots> &
Expand All @@ -97,8 +103,6 @@ export type MenuState = ComponentState<MenuSlots> &
MenuProps,
| 'hasCheckmarks'
| 'hasIcons'
| 'mountNode'
| 'inline'
| 'checkedValues'
| 'onCheckedValueChange'
| 'open'
Expand All @@ -108,7 +112,8 @@ export type MenuState = ComponentState<MenuSlots> &
| 'openOnContext'
| 'persistOnItemClick'
>
> & {
> &
Pick<MenuProps, 'positioning'> & {
/**
* Anchors the popper to the mouse click for context events
*/
Expand Down Expand Up @@ -172,6 +177,13 @@ export type MenuState = ComponentState<MenuSlots> &
* An optional safe zone area to be rendered around the menu
*/
safeZone?: React.ReactElement | null;

/**
* Popover ID for HTML Popover API.
*/
popoverId: string;

submenuFallbackPositions?: string[];
};

export type MenuContextValues = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const renderMenu_unstable = (state: MenuState, contextValues: MenuContext
return (
<MenuProvider value={contextValues.menu}>
{state.menuTrigger}
{state.open && state.menuPopover}
{state.menuPopover}
</MenuProvider>
);
};
Original file line number Diff line number Diff line change
@@ -1,21 +1,17 @@
import * as React from 'react';
import {
resolvePositioningShorthand,
usePositioningMouseTarget,
usePositioning,
useSafeZoneArea,
type PositioningShorthandValue,
} from '@fluentui/react-positioning';
import {
useControllableState,
useId,
useOnClickOutside,
useEventCallback,
useOnScrollOutside,
elementContains,
useTimeout,
useFirstMount,
useMergedRefs,
} from '@fluentui/react-utilities';
import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts';
import { useFocusFinders } from '@fluentui/react-tabster';
Expand Down Expand Up @@ -48,29 +44,28 @@ export const useMenu_unstable = (props: MenuProps & { safeZone?: boolean | { tim
const isSubmenu = useIsSubmenu();
const {
hoverDelay = 500,
inline = false,
hasCheckmarks = false,
hasIcons = false,
closeOnScroll = false,
openOnContext = false,
persistOnItemClick = false,
openOnHover = isSubmenu,
defaultCheckedValues,
mountNode = null,
safeZone,
} = props;

const { targetDocument } = useFluent();
const popoverId = useId('popover');
const triggerId = useId('menu');
const [contextTarget, setContextTarget] = usePositioningMouseTarget();

const positioningOptions = {
position: isSubmenu ? 'after' : 'below',
align: isSubmenu ? 'top' : 'start',
target: props.openOnContext ? contextTarget : undefined,
fallbackPositions: isSubmenu ? submenuFallbackPositions : undefined,
...resolvePositioningShorthand(props.positioning),
} as const;
// const positioningOptions = {
// position: isSubmenu ? 'after' : 'below',
// align: isSubmenu ? 'top' : 'start',
// target: props.openOnContext ? contextTarget : undefined,
// fallbackPositions: isSubmenu ? submenuFallbackPositions : undefined,
// ...resolvePositioningShorthand(props.positioning),
// } as const;

const children = React.Children.toArray(props.children) as React.ReactElement[];

Expand All @@ -95,7 +90,7 @@ export const useMenu_unstable = (props: MenuProps & { safeZone?: boolean | { tim
menuPopover = children[0];
}

const { targetRef, containerRef } = usePositioning(positioningOptions);
// const { targetRef, containerRef } = usePositioning(positioningOptions);

const enableSafeZone = safeZone && openOnHover;
const safeZoneDescriptorRef = React.useRef({
Expand Down Expand Up @@ -139,8 +134,8 @@ export const useMenu_unstable = (props: MenuProps & { safeZone?: boolean | { tim
},
});

const triggerRef = useMergedRefs(targetRef, safeZoneHandle.targetRef);
const menuPopoverRef = useMergedRefs(containerRef, safeZoneHandle.containerRef);
const triggerRef = safeZoneHandle.targetRef; //useMergedRefs(targetRef, safeZoneHandle.targetRef);
const menuPopoverRef = safeZoneHandle.containerRef; //useMergedRefs(containerRef, safeZoneHandle.containerRef);

// TODO Better way to narrow types ?
const [open, setOpen] = useMenuOpenState({
Expand All @@ -163,7 +158,6 @@ export const useMenu_unstable = (props: MenuProps & { safeZone?: boolean | { tim
});

return {
inline,
hoverDelay,
triggerId,
isSubmenu,
Expand All @@ -175,7 +169,6 @@ export const useMenu_unstable = (props: MenuProps & { safeZone?: boolean | { tim
closeOnScroll,
menuTrigger,
menuPopover,
mountNode,
triggerRef,
menuPopoverRef,
components: {},
Expand All @@ -186,6 +179,9 @@ export const useMenu_unstable = (props: MenuProps & { safeZone?: boolean | { tim
onCheckedValueChange,
persistOnItemClick,
safeZone: safeZoneHandle.elementToRender,
popoverId,
positioning: props.positioning,
submenuFallbackPositions,
};
};

Expand Down Expand Up @@ -282,15 +278,15 @@ const useMenuOpenState = (
}
});

useOnClickOutside({
contains: elementContains,
disabled: !open,
element: targetDocument,
refs: [state.menuPopoverRef, !state.openOnContext && state.triggerRef].filter(
Boolean,
) as React.MutableRefObject<HTMLElement | null>[],
callback: event => setOpen(event, { open: false, type: 'clickOutside', event }),
});
// useOnClickOutside({
// contains: elementContains,
// disabled: !open,
// element: targetDocument,
// refs: [state.menuPopoverRef, !state.openOnContext && state.triggerRef].filter(
// Boolean,
// ) as React.MutableRefObject<HTMLElement | null>[],
// callback: event => setOpen(event, { open: false, type: 'clickOutside', event }),
// });

// only close on scroll for context, or when closeOnScroll is specified
const closeOnScroll = state.openOnContext || state.closeOnScroll;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,8 @@ export function useMenuContextValues_unstable(state: MenuState): MenuContextValu
checkedValues,
hasCheckmarks,
hasIcons,
inline,
isSubmenu,
menuPopoverRef,
mountNode,
onCheckedValueChange,
open,
openOnContext,
Expand All @@ -18,17 +16,18 @@ export function useMenuContextValues_unstable(state: MenuState): MenuContextValu
setOpen,
triggerId,
triggerRef,
popoverId,
positioning,
submenuFallbackPositions,
} = state;

// This context is created with "@fluentui/react-context-selector", these is no sense to memoize it
const menu = {
checkedValues,
hasCheckmarks,
hasIcons,
inline,
isSubmenu,
menuPopoverRef,
mountNode,
onCheckedValueChange,
open,
openOnContext,
Expand All @@ -38,6 +37,9 @@ export function useMenuContextValues_unstable(state: MenuState): MenuContextValu
setOpen,
triggerId,
triggerRef,
popoverId,
positioning,
submenuFallbackPositions,
};

return { menu };
Expand Down
Loading
Loading