Skip to content
Merged
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
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "feat: add base hooks for Popover component",
"packageName": "@fluentui/react-popover",
"email": "dmytrokirpa@microsoft.com",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
export type {
OnOpenChangeData,
OpenPopoverEvents,
PopoverBaseProps,
PopoverProps,
PopoverSize,
PopoverBaseState,
PopoverState,
} from './components/Popover/index';
export { Popover, renderPopover_unstable, usePopover_unstable } from './components/Popover/index';
export {
Popover,
renderPopover_unstable,
usePopover_unstable,
usePopoverBase_unstable,
} from './components/Popover/index';
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
export type { PopoverSurfaceProps, PopoverSurfaceSlots, PopoverSurfaceState } from './components/PopoverSurface/index';
export type {
PopoverSurfaceBaseProps,
PopoverSurfaceProps,
PopoverSurfaceSlots,
PopoverSurfaceBaseState,
PopoverSurfaceState,
} from './components/PopoverSurface/index';
export {
PopoverSurface,
arrowHeights,
popoverSurfaceClassNames,
renderPopoverSurface_unstable,
usePopoverSurfaceStyles_unstable,
usePopoverSurface_unstable,
usePopoverSurfaceBase_unstable,
} from './components/PopoverSurface/index';
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,8 @@ export type PopoverProps = Pick<PortalProps, 'mountNode'> & {
unstable_disableAutoFocus?: boolean;
};

export type PopoverBaseProps = Omit<PopoverProps, 'appearance' | 'size'>;

/**
* Popover State
*/
Expand Down Expand Up @@ -206,6 +208,8 @@ export type PopoverState = Pick<
triggerRef: React.MutableRefObject<HTMLElement | null>;
};

export type PopoverBaseState = Omit<PopoverState, 'appearance' | 'size'>;

/**
* Data attached to open/close events
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
export { Popover } from './Popover';
export type { OnOpenChangeData, OpenPopoverEvents, PopoverProps, PopoverSize, PopoverState } from './Popover.types';
export type {
OnOpenChangeData,
OpenPopoverEvents,
PopoverBaseProps,
PopoverProps,
PopoverSize,
PopoverBaseState,
PopoverState,
} from './Popover.types';
export { renderPopover_unstable } from './renderPopover';
export { usePopover_unstable } from './usePopover';
export { usePopover_unstable, usePopoverBase_unstable } from './usePopover';
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,13 @@ import {
} from '@fluentui/react-positioning';
import { useFocusFinders, useActivateModal } from '@fluentui/react-tabster';
import { arrowHeights } from '../PopoverSurface/index';
import type { OpenPopoverEvents, PopoverProps, PopoverState } from './Popover.types';
import type {
OpenPopoverEvents,
PopoverBaseProps,
PopoverBaseState,
PopoverProps,
PopoverState,
} from './Popover.types';
import { popoverSurfaceBorderRadius } from './constants';

/**
Expand All @@ -30,9 +36,37 @@ import { popoverSurfaceBorderRadius } from './constants';
* @param props - props from this instance of Popover
*/
export const usePopover_unstable = (props: PopoverProps): PopoverState => {
const { appearance, size = 'medium' } = props;
const positioning = resolvePositioningShorthand(props.positioning);
const withArrow = props.withArrow && !positioning.coverTarget;

const state = usePopoverBase_unstable({
...props,
positioning: {
...positioning,
// Update the offset with the arrow size only when it's available
...(withArrow ? { offset: mergeArrowOffset(positioning.offset, arrowHeights[size]) } : {}),
},
});

return {
appearance,
size,
...state,
};
};

/**
* Base hook that builds Popover state for behavior and structure only.
* Does not add design-related defaults such as appearance or size.
* Does not manage focus behavior, it's handled by `usePopoverFocusManagement_unstable`.
*
* @internal
* @param props - props from this instance of Popover
*/
export const usePopoverBase_unstable = (props: PopoverBaseProps): PopoverBaseState => {
const [contextTarget, setContextTarget] = usePositioningMouseTarget();
const initialState = {
size: 'medium',
contextTarget,
setContextTarget,
...props,
Expand Down Expand Up @@ -83,7 +117,7 @@ export const usePopover_unstable = (props: PopoverProps): PopoverState => {
}
});

const toggleOpen = React.useCallback<PopoverState['toggleOpen']>(
const toggleOpen = React.useCallback<PopoverBaseState['toggleOpen']>(
(e: OpenPopoverEvents) => {
setOpen(e, !open);
},
Expand Down Expand Up @@ -156,11 +190,11 @@ export const usePopover_unstable = (props: PopoverProps): PopoverState => {
* Creates and manages the Popover open state
*/
function useOpenState(
state: Pick<PopoverState, 'setContextTarget' | 'onOpenChange'> & Pick<PopoverProps, 'open' | 'defaultOpen'>,
state: Pick<PopoverBaseState, 'setContextTarget' | 'onOpenChange'> & Pick<PopoverBaseProps, 'open' | 'defaultOpen'>,
) {
'use no memo';

const onOpenChange: PopoverState['onOpenChange'] = useEventCallback((e, data) => state.onOpenChange?.(e, data));
const onOpenChange: PopoverBaseState['onOpenChange'] = useEventCallback((e, data) => state.onOpenChange?.(e, data));

const [open, setOpenState] = useControllableState({
state: state.open,
Expand Down Expand Up @@ -193,8 +227,8 @@ function useOpenState(
* Creates and sets the necessary trigger, target and content refs used by Popover
*/
function usePopoverRefs(
state: Pick<PopoverState, 'size' | 'contextTarget'> &
Pick<PopoverProps, 'positioning' | 'openOnContext' | 'withArrow'>,
state: Pick<PopoverBaseState, 'contextTarget'> &
Pick<PopoverBaseProps, 'positioning' | 'openOnContext' | 'withArrow'>,
) {
'use no memo';

Expand All @@ -211,10 +245,6 @@ function usePopoverRefs(
state.withArrow = false;
}

if (state.withArrow) {
positioningOptions.offset = mergeArrowOffset(positioningOptions.offset, arrowHeights[state.size]);
}

const { targetRef: triggerRef, containerRef: contentRef, arrowRef } = usePositioning(positioningOptions);

return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export type PopoverSurfaceSlots = {
root: Slot<'div'>;
};

export type PopoverSurfaceBaseProps = PopoverSurfaceProps;

/**
* PopoverSurface State
*/
Expand All @@ -23,3 +25,5 @@ export type PopoverSurfaceState = ComponentState<PopoverSurfaceSlots> &
*/
arrowClassName?: string;
};

export type PopoverSurfaceBaseState = Omit<PopoverSurfaceState, 'appearance' | 'size'>;
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
export { PopoverSurface } from './PopoverSurface';
export type { PopoverSurfaceProps, PopoverSurfaceSlots, PopoverSurfaceState } from './PopoverSurface.types';
export type {
PopoverSurfaceBaseProps,
PopoverSurfaceProps,
PopoverSurfaceSlots,
PopoverSurfaceBaseState,
PopoverSurfaceState,
} from './PopoverSurface.types';
export { renderPopoverSurface_unstable } from './renderPopoverSurface';
export { usePopoverSurface_unstable } from './usePopoverSurface';
export { usePopoverSurface_unstable, usePopoverSurfaceBase_unstable } from './usePopoverSurface';
export {
arrowHeights,
popoverSurfaceClassNames,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
'use client';

import * as React from 'react';
import { getIntrinsicElementProps, useMergedRefs, slot } from '@fluentui/react-utilities';
import { useMergedRefs, slot } from '@fluentui/react-utilities';
import { useModalAttributes } from '@fluentui/react-tabster';
import { usePopoverContext_unstable } from '../../popoverContext';
import type { PopoverSurfaceProps, PopoverSurfaceState } from './PopoverSurface.types';
import type {
PopoverSurfaceProps,
PopoverSurfaceState,
PopoverSurfaceBaseProps,
PopoverSurfaceBaseState,
} from './PopoverSurface.types';

/**
* Create the state required to render PopoverSurface.
Expand All @@ -19,14 +24,34 @@ export const usePopoverSurface_unstable = (
props: PopoverSurfaceProps,
ref: React.Ref<HTMLDivElement>,
): PopoverSurfaceState => {
const size = usePopoverContext_unstable(context => context.size);
const appearance = usePopoverContext_unstable(context => context.appearance);
const state = usePopoverSurfaceBase_unstable(props, ref);

return {
appearance,
size,
...state,
};
};

/**
* Base hook that builds PopoverSurface state for behavior and structure only.
*
* @internal
* @param props - User provided props to the PopoverSurface component.
* @param ref - User provided ref to be passed to the PopoverSurface component.
*/
export const usePopoverSurfaceBase_unstable = (
props: PopoverSurfaceBaseProps,
ref: React.Ref<HTMLDivElement>,
): PopoverSurfaceBaseState => {
const contentRef = usePopoverContext_unstable(context => context.contentRef);
const openOnHover = usePopoverContext_unstable(context => context.openOnHover);
const setOpen = usePopoverContext_unstable(context => context.setOpen);
const mountNode = usePopoverContext_unstable(context => context.mountNode);
const arrowRef = usePopoverContext_unstable(context => context.arrowRef);
const size = usePopoverContext_unstable(context => context.size);
const withArrow = usePopoverContext_unstable(context => context.withArrow);
const appearance = usePopoverContext_unstable(context => context.appearance);
const trapFocus = usePopoverContext_unstable(context => context.trapFocus);
const inertTrapFocus = usePopoverContext_unstable(context => context.inertTrapFocus);
const inline = usePopoverContext_unstable(context => context.inline);
Expand All @@ -36,27 +61,22 @@ export const usePopoverSurface_unstable = (
alwaysFocusable: !trapFocus,
});

const state: PopoverSurfaceState = {
const state: PopoverSurfaceBaseState = {
inline,
appearance,
withArrow,
size,
arrowRef,
mountNode,
components: {
root: 'div',
},
root: slot.always(
getIntrinsicElementProps('div', {
// FIXME:
// `contentRef` is wrongly assigned to be `HTMLElement` instead of `HTMLDivElement`
// but since it would be a breaking change to fix it, we are casting ref to it's proper type
{
ref: useMergedRefs(ref, contentRef) as React.Ref<HTMLDivElement>,
role: trapFocus ? 'dialog' : 'group',
'aria-modal': trapFocus ? true : undefined,
...modalAttributes,
...props,
}),
},
{ elementType: 'div' },
),
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,9 @@ export { PopoverProvider, usePopoverContext_unstable } from './popoverContext';
export type { PopoverContextValue } from './popoverContext';
export { PopoverTrigger, renderPopoverTrigger_unstable, usePopoverTrigger_unstable } from './PopoverTrigger';
export type { PopoverTriggerChildProps, PopoverTriggerProps, PopoverTriggerState } from './PopoverTrigger';

// Experimental APIs
// export type { PopoverBaseProps, PopoverBaseState } from './Popover';
// export { usePopoverBase_unstable } from './Popover';
// export type { PopoverSurfaceBaseProps, PopoverSurfaceBaseState } from './PopoverSurface';
// export { usePopoverSurfaceBase_unstable } from './PopoverSurface';