diff --git a/change/@fluentui-react-dialog-686635f7-4692-496c-ab3c-6acaa354e72f.json b/change/@fluentui-react-dialog-686635f7-4692-496c-ab3c-6acaa354e72f.json new file mode 100644 index 0000000000000..8553a3954a628 --- /dev/null +++ b/change/@fluentui-react-dialog-686635f7-4692-496c-ab3c-6acaa354e72f.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "chore: improves DialogTrigger types", + "packageName": "@fluentui/react-dialog", + "email": "bernardo.sunderhus@gmail.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-menu-4f2d389f-a9b7-41f4-8a65-d4dca52a287b.json b/change/@fluentui-react-menu-4f2d389f-a9b7-41f4-8a65-d4dca52a287b.json new file mode 100644 index 0000000000000..19af749778107 --- /dev/null +++ b/change/@fluentui-react-menu-4f2d389f-a9b7-41f4-8a65-d4dca52a287b.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "chore: improves MenuTrigger types", + "packageName": "@fluentui/react-menu", + "email": "bernardo.sunderhus@gmail.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-popover-4839d5df-c144-43e9-9fd6-406fe50d008c.json b/change/@fluentui-react-popover-4839d5df-c144-43e9-9fd6-406fe50d008c.json new file mode 100644 index 0000000000000..4ed3a15a78f00 --- /dev/null +++ b/change/@fluentui-react-popover-4839d5df-c144-43e9-9fd6-406fe50d008c.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "chore: improves PopoverTrigger types", + "packageName": "@fluentui/react-popover", + "email": "bernardo.sunderhus@gmail.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-tooltip-13956a4b-4fc1-4e08-9bba-d2980714a6c5.json b/change/@fluentui-react-tooltip-13956a4b-4fc1-4e08-9bba-d2980714a6c5.json new file mode 100644 index 0000000000000..8625ecd71b81d --- /dev/null +++ b/change/@fluentui-react-tooltip-13956a4b-4fc1-4e08-9bba-d2980714a6c5.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "chore: improves Tooltip types", + "packageName": "@fluentui/react-tooltip", + "email": "bernardo.sunderhus@gmail.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-utilities-e8e25f62-8476-4790-9aec-ffcfeef8c660.json b/change/@fluentui-react-utilities-e8e25f62-8476-4790-9aec-ffcfeef8c660.json new file mode 100644 index 0000000000000..99f7d93091fef --- /dev/null +++ b/change/@fluentui-react-utilities-e8e25f62-8476-4790-9aec-ffcfeef8c660.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "chore: restricts trigger API types", + "packageName": "@fluentui/react-utilities", + "email": "bernardo.sunderhus@gmail.com", + "dependentChangeType": "patch" +} diff --git a/packages/react-components/react-dialog/etc/react-dialog.api.md b/packages/react-components/react-dialog/etc/react-dialog.api.md index 39d3c6cc52e98..33469ca00ea9b 100644 --- a/packages/react-components/react-dialog/etc/react-dialog.api.md +++ b/packages/react-components/react-dialog/etc/react-dialog.api.md @@ -17,6 +17,7 @@ import * as React_2 from 'react'; import { ReactElement } from 'react'; import type { Slot } from '@fluentui/react-utilities'; import type { SlotClassNames } from '@fluentui/react-utilities'; +import type { TriggerProps } from '@fluentui/react-utilities'; // @public export const Dialog: React_2.FC; @@ -169,14 +170,12 @@ export type DialogTriggerAction = 'open' | 'close'; // @public export type DialogTriggerChildProps = ARIAButtonResultProps; 'aria-haspopup'?: 'dialog'; }>; // @public (undocumented) -export type DialogTriggerProps = { +export type DialogTriggerProps = TriggerProps & { action?: DialogTriggerAction; - children: React_2.ReactElement | ((props: DialogTriggerChildProps) => React_2.ReactElement | null); }; // @public (undocumented) diff --git a/packages/react-components/react-dialog/src/components/DialogTrigger/DialogTrigger.types.ts b/packages/react-components/react-dialog/src/components/DialogTrigger/DialogTrigger.types.ts index 5560a281b9082..1d3b8c403830b 100644 --- a/packages/react-components/react-dialog/src/components/DialogTrigger/DialogTrigger.types.ts +++ b/packages/react-components/react-dialog/src/components/DialogTrigger/DialogTrigger.types.ts @@ -1,9 +1,10 @@ import { ARIAButtonResultProps, ARIAButtonType } from '@fluentui/react-aria'; +import type { TriggerProps } from '@fluentui/react-utilities'; import * as React from 'react'; export type DialogTriggerAction = 'open' | 'close'; -export type DialogTriggerProps = { +export type DialogTriggerProps = TriggerProps & { /** * Explicitly declare if the trigger is responsible for opening or * closing a Dialog visibility state. @@ -11,11 +12,6 @@ export type DialogTriggerProps = { * @default 'close' // if it's inside DialogSurface */ action?: DialogTriggerAction; - /** - * Explicitly require single child or render function - * to inject properties - */ - children: React.ReactElement | ((props: DialogTriggerChildProps) => React.ReactElement | null); }; /** @@ -24,7 +20,6 @@ export type DialogTriggerProps = { export type DialogTriggerChildProps = ARIAButtonResultProps< Type, Props & { - ref: React.Ref; 'aria-haspopup'?: 'dialog'; } >; diff --git a/packages/react-components/react-dialog/src/components/DialogTrigger/useDialogTrigger.ts b/packages/react-components/react-dialog/src/components/DialogTrigger/useDialogTrigger.ts index f4a2927678b70..f1c7d785ee461 100644 --- a/packages/react-components/react-dialog/src/components/DialogTrigger/useDialogTrigger.ts +++ b/packages/react-components/react-dialog/src/components/DialogTrigger/useDialogTrigger.ts @@ -1,7 +1,7 @@ import * as React from 'react'; import { useModalAttributes } from '@fluentui/react-tabster'; import { applyTriggerPropsToChildren, getTriggerChild, useEventCallback } from '@fluentui/react-utilities'; -import { DialogTriggerChildProps, DialogTriggerProps, DialogTriggerState } from './DialogTrigger.types'; +import { DialogTriggerProps, DialogTriggerState } from './DialogTrigger.types'; import { useDialogContext_unstable, useDialogSurfaceContext_unstable } from '../../contexts'; import { useARIAButtonProps } from '@fluentui/react-aria'; @@ -16,7 +16,7 @@ export const useDialogTrigger_unstable = (props: DialogTriggerProps): DialogTrig const { children, action = isInsideSurfaceDialog ? 'close' : 'open' } = props; - const child = React.isValidElement(children) ? getTriggerChild(children) : undefined; + const child = getTriggerChild(children); const requestOpenChange = useDialogContext_unstable(ctx => ctx.requestOpenChange); @@ -36,13 +36,13 @@ export const useDialogTrigger_unstable = (props: DialogTriggerProps): DialogTrig ); return { - children: applyTriggerPropsToChildren( + children: applyTriggerPropsToChildren( children, useARIAButtonProps(child?.type === 'button' || child?.type === 'a' ? child.type : 'div', { type: 'button', ...child?.props, 'aria-haspopup': action === 'close' ? undefined : 'dialog', - ref: child?.ref as React.Ref, + ref: child?.ref, onClick: handleClick, ...triggerAttributes, }), diff --git a/packages/react-components/react-menu/etc/react-menu.api.md b/packages/react-components/react-menu/etc/react-menu.api.md index a52f4defae9d0..a6a64b763a0ad 100644 --- a/packages/react-components/react-menu/etc/react-menu.api.md +++ b/packages/react-components/react-menu/etc/react-menu.api.md @@ -19,6 +19,7 @@ import type { PositioningShorthand } from '@fluentui/react-positioning'; import * as React_2 from 'react'; import type { Slot } from '@fluentui/react-utilities'; import type { SlotClassNames } from '@fluentui/react-utilities'; +import type { TriggerProps } from '@fluentui/react-utilities'; import { usePositioningMouseTarget } from '@fluentui/react-positioning'; // @public @@ -313,9 +314,7 @@ export type MenuTriggerChildProps; // @public (undocumented) -export type MenuTriggerProps = { - children: React_2.ReactElement | ((props: MenuTriggerChildProps) => React_2.ReactElement | null); -}; +export type MenuTriggerProps = TriggerProps; // @public (undocumented) export type MenuTriggerState = { diff --git a/packages/react-components/react-menu/src/components/MenuTrigger/MenuTrigger.types.ts b/packages/react-components/react-menu/src/components/MenuTrigger/MenuTrigger.types.ts index 8523c3c54887d..dd32cbb545592 100644 --- a/packages/react-components/react-menu/src/components/MenuTrigger/MenuTrigger.types.ts +++ b/packages/react-components/react-menu/src/components/MenuTrigger/MenuTrigger.types.ts @@ -1,12 +1,8 @@ import { ARIAButtonResultProps, ARIAButtonType } from '@fluentui/react-aria'; +import type { TriggerProps } from '@fluentui/react-utilities'; import * as React from 'react'; -export type MenuTriggerProps = { - /** - * Explicitly require single child or render function - */ - children: React.ReactElement | ((props: MenuTriggerChildProps) => React.ReactElement | null); -}; +export type MenuTriggerProps = TriggerProps; /** * Props that are passed to the child of the MenuTrigger when cloned to ensure correct behaviour for the Menu diff --git a/packages/react-components/react-menu/src/components/MenuTrigger/useMenuTrigger.ts b/packages/react-components/react-menu/src/components/MenuTrigger/useMenuTrigger.ts index dede1aecbf2fa..93dd3cc48da31 100644 --- a/packages/react-components/react-menu/src/components/MenuTrigger/useMenuTrigger.ts +++ b/packages/react-components/react-menu/src/components/MenuTrigger/useMenuTrigger.ts @@ -1,5 +1,5 @@ import * as React from 'react'; -import { MenuTriggerChildProps, MenuTriggerProps, MenuTriggerState } from './MenuTrigger.types'; +import { MenuTriggerProps, MenuTriggerState } from './MenuTrigger.types'; import { useMenuContext_unstable } from '../../contexts/menuContext'; import { useIsSubmenu } from '../../utils/useIsSubmenu'; import { useFocusFinders } from '@fluentui/react-tabster'; @@ -45,7 +45,7 @@ export const useMenuTrigger_unstable = (props: MenuTriggerProps): MenuTriggerSta const { dir } = useFluent(); const OpenArrowKey = dir === 'ltr' ? ArrowRight : ArrowLeft; - const child = React.isValidElement(children) ? getTriggerChild>(children) : undefined; + const child = getTriggerChild(children); const onContextMenu = (e: React.MouseEvent) => { if (isTargetDisabled(e)) { @@ -127,27 +127,24 @@ export const useMenuTrigger_unstable = (props: MenuTriggerProps): MenuTriggerSta id: triggerId, ...child?.props, ref: useMergedRefs(triggerRef, child?.ref), - onMouseEnter: useEventCallback(mergeCallbacks(child?.props?.onMouseEnter, onMouseEnter)), - onMouseLeave: useEventCallback(mergeCallbacks(child?.props?.onMouseLeave, onMouseLeave)), - onContextMenu: useEventCallback(mergeCallbacks(child?.props?.onContextMenu, onContextMenu)), - onMouseMove: useEventCallback(mergeCallbacks(child?.props?.onMouseMove, onMouseMove)), + onMouseEnter: useEventCallback(mergeCallbacks(child?.props.onMouseEnter, onMouseEnter)), + onMouseLeave: useEventCallback(mergeCallbacks(child?.props.onMouseLeave, onMouseLeave)), + onContextMenu: useEventCallback(mergeCallbacks(child?.props.onContextMenu, onContextMenu)), + onMouseMove: useEventCallback(mergeCallbacks(child?.props.onMouseMove, onMouseMove)), } as const; const ariaButtonTriggerProps = useARIAButtonProps( child?.type === 'button' || child?.type === 'a' ? child.type : 'div', { ...triggerProps, - onClick: useEventCallback(mergeCallbacks(child?.props?.onClick, onClick)), - onKeyDown: useEventCallback(mergeCallbacks(child?.props?.onKeyDown, onKeyDown)), + onClick: useEventCallback(mergeCallbacks(child?.props.onClick, onClick)), + onKeyDown: useEventCallback(mergeCallbacks(child?.props.onKeyDown, onKeyDown)), }, ); return { isSubmenu, - children: applyTriggerPropsToChildren( - children, - openOnContext ? triggerProps : ariaButtonTriggerProps, - ), + children: applyTriggerPropsToChildren(children, openOnContext ? triggerProps : ariaButtonTriggerProps), }; }; diff --git a/packages/react-components/react-popover/etc/react-popover.api.md b/packages/react-components/react-popover/etc/react-popover.api.md index bc36a0d5a1f4a..f05fb4073d961 100644 --- a/packages/react-components/react-popover/etc/react-popover.api.md +++ b/packages/react-components/react-popover/etc/react-popover.api.md @@ -22,6 +22,7 @@ import * as React_2 from 'react'; import { ReactElement } from 'react'; import type { Slot } from '@fluentui/react-utilities'; import type { SlotClassNames } from '@fluentui/react-utilities'; +import type { TriggerProps } from '@fluentui/react-utilities'; import type { UseModalAttributesOptions } from '@fluentui/react-tabster'; import type { usePositioningMouseTarget } from '@fluentui/react-positioning'; @@ -113,9 +114,7 @@ export type PopoverTriggerChildProps; // @public -export type PopoverTriggerProps = { - children: React_2.ReactElement | ((props: PopoverTriggerChildProps) => React_2.ReactElement | null); -}; +export type PopoverTriggerProps = TriggerProps; // @public export type PopoverTriggerState = { diff --git a/packages/react-components/react-popover/src/components/PopoverTrigger/PopoverTrigger.types.ts b/packages/react-components/react-popover/src/components/PopoverTrigger/PopoverTrigger.types.ts index 35861962eedd9..e8fae847b5862 100644 --- a/packages/react-components/react-popover/src/components/PopoverTrigger/PopoverTrigger.types.ts +++ b/packages/react-components/react-popover/src/components/PopoverTrigger/PopoverTrigger.types.ts @@ -1,12 +1,11 @@ import { ARIAButtonResultProps, ARIAButtonType } from '@fluentui/react-aria'; +import type { TriggerProps } from '@fluentui/react-utilities'; import * as React from 'react'; /** * PopoverTrigger Props */ -export type PopoverTriggerProps = { - children: React.ReactElement | ((props: PopoverTriggerChildProps) => React.ReactElement | null); -}; +export type PopoverTriggerProps = TriggerProps; /** * PopoverTrigger State diff --git a/packages/react-components/react-popover/src/components/PopoverTrigger/usePopoverTrigger.ts b/packages/react-components/react-popover/src/components/PopoverTrigger/usePopoverTrigger.ts index 4bc59f163bbdd..25e536cdf15b5 100644 --- a/packages/react-components/react-popover/src/components/PopoverTrigger/usePopoverTrigger.ts +++ b/packages/react-components/react-popover/src/components/PopoverTrigger/usePopoverTrigger.ts @@ -8,7 +8,7 @@ import { } from '@fluentui/react-utilities'; import { useModalAttributes } from '@fluentui/react-tabster'; import { usePopoverContext_unstable } from '../../popoverContext'; -import type { PopoverTriggerChildProps, PopoverTriggerProps, PopoverTriggerState } from './PopoverTrigger.types'; +import type { PopoverTriggerProps, PopoverTriggerState } from './PopoverTrigger.types'; import { useARIAButtonProps } from '@fluentui/react-aria'; import { Escape } from '@fluentui/keyboard-keys'; @@ -22,9 +22,7 @@ import { Escape } from '@fluentui/keyboard-keys'; */ export const usePopoverTrigger_unstable = (props: PopoverTriggerProps): PopoverTriggerState => { const { children } = props; - const child = React.isValidElement(children) - ? getTriggerChild>(children) - : undefined; + const child = getTriggerChild(children); const open = usePopoverContext_unstable(context => context.open); const setOpen = usePopoverContext_unstable(context => context.setOpen); @@ -72,9 +70,9 @@ export const usePopoverTrigger_unstable = (props: PopoverTriggerProps): PopoverT ...triggerAttributes, 'aria-expanded': `${open}`, ...child?.props, - onMouseEnter: useEventCallback(mergeCallbacks(child?.props?.onMouseEnter, onMouseEnter)), - onMouseLeave: useEventCallback(mergeCallbacks(child?.props?.onMouseLeave, onMouseLeave)), - onContextMenu: useEventCallback(mergeCallbacks(child?.props?.onContextMenu, onContextMenu)), + onMouseEnter: useEventCallback(mergeCallbacks(child?.props.onMouseEnter, onMouseEnter)), + onMouseLeave: useEventCallback(mergeCallbacks(child?.props.onMouseLeave, onMouseLeave)), + onContextMenu: useEventCallback(mergeCallbacks(child?.props.onContextMenu, onContextMenu)), ref: useMergedRefs(triggerRef, child?.ref), } as const; @@ -82,13 +80,13 @@ export const usePopoverTrigger_unstable = (props: PopoverTriggerProps): PopoverT child?.type === 'button' || child?.type === 'a' ? child.type : 'div', { ...triggerProps, - onClick: useEventCallback(mergeCallbacks(child?.props?.onClick, onClick)), - onKeyDown: useEventCallback(mergeCallbacks(child?.props?.onKeyDown, onKeyDown)), + onClick: useEventCallback(mergeCallbacks(child?.props.onClick, onClick)), + onKeyDown: useEventCallback(mergeCallbacks(child?.props.onKeyDown, onKeyDown)), }, ); return { - children: applyTriggerPropsToChildren( + children: applyTriggerPropsToChildren( props.children, useARIAButtonProps( child?.type === 'button' || child?.type === 'a' ? child.type : 'div', diff --git a/packages/react-components/react-tooltip/etc/react-tooltip.api.md b/packages/react-components/react-tooltip/etc/react-tooltip.api.md index 63c725a8e9a0a..f3d7deb31f731 100644 --- a/packages/react-components/react-tooltip/etc/react-tooltip.api.md +++ b/packages/react-components/react-tooltip/etc/react-tooltip.api.md @@ -12,6 +12,7 @@ import type { PositioningShorthand } from '@fluentui/react-positioning'; import * as React_2 from 'react'; import type { Slot } from '@fluentui/react-utilities'; import type { SlotClassNames } from '@fluentui/react-utilities'; +import type { TriggerProps } from '@fluentui/react-utilities'; // @public export type OnVisibleChangeData = { @@ -28,11 +29,8 @@ export const Tooltip: React_2.FC & FluentTriggerComponent; export const tooltipClassNames: SlotClassNames; // @public -export type TooltipProps = ComponentProps & Pick & { +export type TooltipProps = ComponentProps & TriggerProps & Pick & { appearance?: 'normal' | 'inverted'; - children?: (React_2.ReactElement & { - ref?: React_2.Ref; - }) | ((props: TooltipTriggerProps) => React_2.ReactElement | null) | null; hideDelay?: number; onVisibleChange?: (event: React_2.PointerEvent | React_2.FocusEvent | undefined, data: OnVisibleChangeData) => void; positioning?: PositioningShorthand; @@ -57,7 +55,7 @@ export type TooltipState = ComponentState & Pick; + ref?: React_2.Ref; } & Pick, 'aria-describedby' | 'aria-label' | 'aria-labelledby' | 'onBlur' | 'onFocus' | 'onPointerEnter' | 'onPointerLeave'>; // @public diff --git a/packages/react-components/react-tooltip/src/components/Tooltip/Tooltip.types.ts b/packages/react-components/react-tooltip/src/components/Tooltip/Tooltip.types.ts index fedca1103adf5..fb03e3901f139 100644 --- a/packages/react-components/react-tooltip/src/components/Tooltip/Tooltip.types.ts +++ b/packages/react-components/react-tooltip/src/components/Tooltip/Tooltip.types.ts @@ -1,6 +1,6 @@ import * as React from 'react'; import type { PositioningShorthand } from '@fluentui/react-positioning'; -import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities'; +import type { ComponentProps, ComponentState, Slot, TriggerProps } from '@fluentui/react-utilities'; import type { PortalProps } from '@fluentui/react-portal'; /** @@ -14,10 +14,10 @@ export type TooltipSlots = { }; /** - * The properties that are added to the trigger of the Tooltip + * The properties that are added to the child of the Tooltip */ -export type TooltipTriggerProps = { - ref?: React.Ref; +export type TooltipChildProps = { + ref?: React.Ref; } & Pick< React.HTMLAttributes, 'aria-describedby' | 'aria-label' | 'aria-labelledby' | 'onBlur' | 'onFocus' | 'onPointerEnter' | 'onPointerLeave' @@ -34,6 +34,7 @@ export type OnVisibleChangeData = { * Properties for Tooltip */ export type TooltipProps = ComponentProps & + TriggerProps & Pick & { /** * The tooltip's visual appearance. @@ -43,18 +44,6 @@ export type TooltipProps = ComponentProps & * @default normal */ appearance?: 'normal' | 'inverted'; - - /** - * The tooltip can have a single JSX child, or a render function that accepts TooltipTriggerProps. - * - * If no child is provided, the tooltip's target must be set with the `positioning` prop, and its - * visibility must be controlled with the `visible` prop. - */ - children?: - | (React.ReactElement & { ref?: React.Ref }) - | ((props: TooltipTriggerProps) => React.ReactElement | null) - | null; - /** * Delay before the tooltip is hidden, in milliseconds. * diff --git a/packages/react-components/react-tooltip/src/components/Tooltip/useTooltip.tsx b/packages/react-components/react-tooltip/src/components/Tooltip/useTooltip.tsx index 9c6246c926e7b..a392877a9f4ee 100644 --- a/packages/react-components/react-tooltip/src/components/Tooltip/useTooltip.tsx +++ b/packages/react-components/react-tooltip/src/components/Tooltip/useTooltip.tsx @@ -17,7 +17,7 @@ import { mergeCallbacks, useEventCallback, } from '@fluentui/react-utilities'; -import type { TooltipProps, TooltipState, TooltipTriggerProps } from './Tooltip.types'; +import type { TooltipProps, TooltipState, TooltipChildProps } from './Tooltip.types'; import { arrowHeight, tooltipBorderRadius } from './private/constants'; import { Escape } from '@fluentui/keyboard-keys'; @@ -200,9 +200,9 @@ export const useTooltip_unstable = (props: TooltipProps): TooltipState => { state.content.onPointerEnter = mergeCallbacks(state.content.onPointerEnter, clearDelayTimeout); state.content.onPointerLeave = mergeCallbacks(state.content.onPointerLeave, onLeaveTrigger); - const child = React.isValidElement(children) ? getTriggerChild(children) : undefined; + const child = getTriggerChild(children); - const triggerAriaProps: Pick = {}; + const triggerAriaProps: Pick = {}; if (relationship === 'label') { // aria-label only works if the content is a string. Otherwise, need to use aria-labelledby. @@ -227,7 +227,7 @@ export const useTooltip_unstable = (props: TooltipProps): TooltipState => { const childTargetRef = useMergedRefs(child?.ref, targetRef); // Apply the trigger props to the child, either by calling the render function, or cloning with the new props - state.children = applyTriggerPropsToChildren(children, { + state.children = applyTriggerPropsToChildren(children, { ...triggerAriaProps, ...child?.props, // If the target prop is not provided, attach targetRef to the trigger element's ref prop diff --git a/packages/react-components/react-tooltip/src/index.ts b/packages/react-components/react-tooltip/src/index.ts index 30d94643aa076..b703b0d51e202 100644 --- a/packages/react-components/react-tooltip/src/index.ts +++ b/packages/react-components/react-tooltip/src/index.ts @@ -5,4 +5,10 @@ export { useTooltipStyles_unstable, useTooltip_unstable, } from './Tooltip'; -export type { OnVisibleChangeData, TooltipProps, TooltipSlots, TooltipState, TooltipTriggerProps } from './Tooltip'; +export type { + OnVisibleChangeData, + TooltipProps, + TooltipSlots, + TooltipState, + TooltipChildProps as TooltipTriggerProps, +} from './Tooltip'; diff --git a/packages/react-components/react-utilities/etc/react-utilities.api.md b/packages/react-components/react-utilities/etc/react-utilities.api.md index 1564c6be1abc6..1ed485eb38c86 100644 --- a/packages/react-components/react-utilities/etc/react-utilities.api.md +++ b/packages/react-components/react-utilities/etc/react-utilities.api.md @@ -8,7 +8,7 @@ import { DispatchWithoutAction } from 'react'; import * as React_2 from 'react'; // @internal -export const applyTriggerPropsToChildren: (children: React_2.ReactElement> | ((props: TTriggerProps) => React_2.ReactElement | null) | null | undefined, triggerProps: TTriggerProps) => React_2.ReactElement | null; +export function applyTriggerPropsToChildren(children: TriggerProps['children'], triggerChildProps: TriggerChildProps): React_2.ReactElement | null; // @public export function canUseDOM(): boolean; @@ -65,9 +65,12 @@ export function getSlots(state: ComponentState): { }; // @internal -export const getTriggerChild:

(children: React_2.ReactNode) => React_2.ReactElement> & { - ref?: React_2.Ref | undefined; -}; +export function getTriggerChild(children: TriggerProps['children']): (React_2.ReactElement> & { + ref?: React_2.Ref; +}) | null; + +// @internal +export function isFluentTrigger(element: React_2.ReactElement): element is React_2.ReactElement; // @public export function isResolvedShorthand>(shorthand?: Shorthand): shorthand is ExtractSlotProps; @@ -130,6 +133,11 @@ export type SlotShorthandValue = React_2.ReactChild | React_2.ReactNode[] | Reac // @public export const SSRProvider: React_2.FC; +// @internal +export type TriggerProps = { + children?: React_2.ReactElement | ((props: TriggerChildProps) => React_2.ReactElement | null) | null; +}; + // @internal export const useControllableState: (options: UseControllableStateOptions) => [State, React_2.Dispatch>]; diff --git a/packages/react-components/react-utilities/src/index.ts b/packages/react-components/react-utilities/src/index.ts index 1dc2058a6e6f8..6c56cb74948ae 100644 --- a/packages/react-components/react-utilities/src/index.ts +++ b/packages/react-components/react-utilities/src/index.ts @@ -41,6 +41,6 @@ export { shouldPreventDefaultOnKeyDown, } from './utils/index'; -export { applyTriggerPropsToChildren, getTriggerChild } from './trigger/index'; +export { applyTriggerPropsToChildren, getTriggerChild, isFluentTrigger } from './trigger/index'; -export type { FluentTriggerComponent } from './trigger/index'; +export type { FluentTriggerComponent, TriggerProps } from './trigger/index'; diff --git a/packages/react-components/react-utilities/src/trigger/applyTriggerPropsToChildren.test.tsx b/packages/react-components/react-utilities/src/trigger/applyTriggerPropsToChildren.test.tsx index 36014e7a19e4e..175cf2f21e447 100644 --- a/packages/react-components/react-utilities/src/trigger/applyTriggerPropsToChildren.test.tsx +++ b/packages/react-components/react-utilities/src/trigger/applyTriggerPropsToChildren.test.tsx @@ -1,21 +1,25 @@ import * as React from 'react'; import { render } from '@testing-library/react'; import { applyTriggerPropsToChildren } from './applyTriggerPropsToChildren'; -import { TestTrigger } from './getTriggerChild.test'; +import type { FluentTriggerComponent } from './types'; + +export const TestTrigger: React.FC<{ id?: string }> & FluentTriggerComponent = props => <>{props.children}; +TestTrigger.displayName = 'TestTrigger'; +TestTrigger.isFluentTriggerComponent = true; describe('applyTriggerPropsToChildren', () => { const dataTestId = 'dataTestId'; const child: React.ReactElement =

This is a valid React element
; - const triggerProps = { id: 'testId', className: 'testClassName', 'data-testattr': 'testAttr' }; + const triggerChildProps = { id: 'testId', className: 'testClassName', 'data-testattr': 'testAttr' }; it('returns the child with the props applied if a React element is sent as the child', () => { - const result = applyTriggerPropsToChildren(child, triggerProps); + const result = applyTriggerPropsToChildren(child, triggerChildProps); const div = render(result as React.ReactElement).getByTestId(dataTestId); expect(div.tagName).toBe('DIV'); - expect(div.id).toBe(triggerProps.id); - expect(div.className).toBe(triggerProps.className); - expect(div.getAttribute('data-testattr')).toBe(triggerProps['data-testattr']); + expect(div.id).toBe(triggerChildProps.id); + expect(div.className).toBe(triggerChildProps.className); + expect(div.getAttribute('data-testattr')).toBe(triggerChildProps['data-testattr']); }); it('returns the output of the function if a function component is sent as the child', () => { @@ -25,13 +29,13 @@ describe('applyTriggerPropsToChildren', () => { This is a valid element ); - let result = applyTriggerPropsToChildren(functionComponent, triggerProps); + let result = applyTriggerPropsToChildren(functionComponent, triggerChildProps); const div = render(result as React.ReactElement).getByTestId(dataTestId); expect(div.tagName).toBe('DIV'); - expect(div.id).toBe(triggerProps.id); - expect(div.className).toBe(triggerProps.className); - expect(div.getAttribute('data-testattr')).toBe(triggerProps['data-testattr']); + expect(div.id).toBe(triggerChildProps.id); + expect(div.className).toBe(triggerChildProps.className); + expect(div.getAttribute('data-testattr')).toBe(triggerChildProps['data-testattr']); // With props being custom applied const dataTestId2 = dataTestId + '2'; @@ -48,18 +52,18 @@ describe('applyTriggerPropsToChildren', () => { ); - result = applyTriggerPropsToChildren(functionComponent, triggerProps); + result = applyTriggerPropsToChildren(functionComponent, triggerChildProps); const span = render(result as React.ReactElement).getByTestId(dataTestId2); expect(span.tagName).toBe('SPAN'); - expect(span.id).toBe(triggerProps.id); - expect(span.className).toBe(triggerProps.className); - expect(span.getAttribute('data-testattr')).toBe(triggerProps['data-testattr']); + expect(span.id).toBe(triggerChildProps.id); + expect(span.className).toBe(triggerChildProps.className); + expect(span.getAttribute('data-testattr')).toBe(triggerChildProps['data-testattr']); }); it(`throws an error if a React fragment with a single child is sent as the child`, () => { const fragment = <>{child}; - expect(() => applyTriggerPropsToChildren(fragment, triggerProps)).toThrow(); + expect(() => applyTriggerPropsToChildren(fragment, triggerChildProps)).toThrow(); }); it('throws an error if a React fragment with multiple children is sent as the child', () => { @@ -70,7 +74,7 @@ describe('applyTriggerPropsToChildren', () => { {child} ); - expect(() => applyTriggerPropsToChildren(fragment, triggerProps)).toThrow(); + expect(() => applyTriggerPropsToChildren(fragment, triggerChildProps)).toThrow(); }); it('applies props to the child if a valid element is sent as the child', () => { @@ -86,7 +90,9 @@ describe('applyTriggerPropsToChildren', () => {
, - { 'data-test': 'test-value' }, + { + 'data-test': 'test-value', + }, ); expect(result).toEqual( @@ -105,7 +111,9 @@ describe('applyTriggerPropsToChildren', () => { , - { 'data-test': 'test-value' }, + { + 'data-test': 'test-value', + }, ); expect(result).toEqual( diff --git a/packages/react-components/react-utilities/src/trigger/applyTriggerPropsToChildren.ts b/packages/react-components/react-utilities/src/trigger/applyTriggerPropsToChildren.ts index 977860994be08..d13a4d3b8225d 100644 --- a/packages/react-components/react-utilities/src/trigger/applyTriggerPropsToChildren.ts +++ b/packages/react-components/react-utilities/src/trigger/applyTriggerPropsToChildren.ts @@ -1,31 +1,35 @@ import * as React from 'react'; import { isFluentTrigger } from './isFluentTrigger'; +import type { TriggerProps } from './types'; /** * @internal - * Apply the trigger props to the children, either by calling the render function, or cloning with the new props. + * resolve the trigger props to the children, either by calling the render function, or cloning with the new props. */ -export const applyTriggerPropsToChildren = ( - children: React.ReactElement | ((props: TTriggerProps) => React.ReactElement | null) | null | undefined, - triggerProps: TTriggerProps, -): React.ReactElement | null => { +export function applyTriggerPropsToChildren( + children: TriggerProps['children'], + triggerChildProps: TriggerChildProps, +): React.ReactElement | null { if (typeof children === 'function') { - return children(triggerProps); + return children(triggerChildProps); } else if (children) { - return cloneTriggerTree(children, triggerProps); + return cloneTriggerTree(children, triggerChildProps); } // Components in React should return either JSX elements or "null", otherwise React will throw: // Nothing was returned from render. // This usually means a return statement is missing. Or, to render nothing, return null. return children || null; -}; +} /** * Clones a React element tree, and applies the given props to the first grandchild that is not * a FluentTriggerComponent or React Fragment (the same element returned by {@link getTriggerChild}). */ -const cloneTriggerTree = (child: React.ReactNode, triggerProps: TTriggerProps): React.ReactElement => { +function cloneTriggerTree( + child: React.ReactNode, + triggerProps: TriggerChildProps, +): React.ReactElement { if (!React.isValidElement(child) || child.type === React.Fragment) { throw new Error( 'A trigger element must be a single element for this component. ' + @@ -37,6 +41,6 @@ const cloneTriggerTree = (child: React.ReactNode, triggerProps: T const grandchild = cloneTriggerTree(child.props.children, triggerProps); return React.cloneElement(child, undefined, grandchild); } else { - return React.cloneElement(child, triggerProps); + return React.cloneElement(child, triggerProps as TriggerChildProps & React.Attributes); } -}; +} diff --git a/packages/react-components/react-utilities/src/trigger/getTriggerChild.test.tsx b/packages/react-components/react-utilities/src/trigger/getTriggerChild.test.tsx index a30857b9fcb7b..60b59c68c5a26 100644 --- a/packages/react-components/react-utilities/src/trigger/getTriggerChild.test.tsx +++ b/packages/react-components/react-utilities/src/trigger/getTriggerChild.test.tsx @@ -1,8 +1,8 @@ import * as React from 'react'; import { getTriggerChild } from './getTriggerChild'; -import { FluentTriggerComponent } from './types'; +import type { FluentTriggerComponent } from './types'; -export const TestTrigger: React.FC<{ id?: string }> & FluentTriggerComponent = props => <>{props.children}; +const TestTrigger: React.FC<{ id?: string }> & FluentTriggerComponent = props => <>{props.children}; TestTrigger.displayName = 'TestTrigger'; TestTrigger.isFluentTriggerComponent = true; @@ -13,9 +13,9 @@ describe('getTriggerChild', () => { expect(getTriggerChild(child)).toBe(child); }); - it('throws an error if a non-valid element is sent as the child', () => { + it('returns null if a non-valid element is sent as the child', () => { const nonValid = () => child; - expect(() => getTriggerChild(nonValid)).toThrow(); + expect(getTriggerChild(nonValid)).toBe(null); }); it('returns the child of a trigger', () => { diff --git a/packages/react-components/react-utilities/src/trigger/getTriggerChild.ts b/packages/react-components/react-utilities/src/trigger/getTriggerChild.ts index 0778ad4f7ab99..30c2981e21e0b 100644 --- a/packages/react-components/react-utilities/src/trigger/getTriggerChild.ts +++ b/packages/react-components/react-utilities/src/trigger/getTriggerChild.ts @@ -1,5 +1,6 @@ import * as React from 'react'; import { isFluentTrigger } from './isFluentTrigger'; +import type { TriggerProps } from './types'; /** * @internal @@ -19,11 +20,22 @@ import { isFluentTrigger } from './isFluentTrigger'; * * ); * ``` + * + * In the case where the immediate child is not a valid element, + * null is returned */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export const getTriggerChild =

( - children: React.ReactNode, -): React.ReactElement

& { ref?: React.Ref } => { - const child = React.Children.only(children) as React.ReactElement; - return isFluentTrigger(child) ? getTriggerChild(child.props.children) : child; -}; +export function getTriggerChild( + children: TriggerProps['children'], + // eslint-disable-next-line @typescript-eslint/no-explicit-any +): (React.ReactElement> & { ref?: React.Ref }) | null { + if (!React.isValidElement(children)) { + return null; + } + return isFluentTrigger(children) + ? getTriggerChild( + // FIXME: This casting should be unnecessary as isFluentTrigger is a guard type method, + // but for some reason it's failing on build + (children.props as TriggerProps).children, + ) + : children; +} diff --git a/packages/react-components/react-utilities/src/trigger/isFluentTrigger.ts b/packages/react-components/react-utilities/src/trigger/isFluentTrigger.ts index 32337de2a2427..f297d4493a0ad 100644 --- a/packages/react-components/react-utilities/src/trigger/isFluentTrigger.ts +++ b/packages/react-components/react-utilities/src/trigger/isFluentTrigger.ts @@ -1,10 +1,11 @@ import * as React from 'react'; -import { FluentTriggerComponent } from './types'; +import type { FluentTriggerComponent, TriggerProps } from './types'; /** + * @internal * Checks if a given element is a FluentUI trigger (e.g. `MenuTrigger` or `Tooltip`). * See the {@link FluentTriggerComponent} type for more info. */ -export const isFluentTrigger = (element: React.ReactElement) => { - return (element.type as FluentTriggerComponent).isFluentTriggerComponent; -}; +export function isFluentTrigger(element: React.ReactElement): element is React.ReactElement { + return Boolean((element.type as FluentTriggerComponent).isFluentTriggerComponent); +} diff --git a/packages/react-components/react-utilities/src/trigger/types.ts b/packages/react-components/react-utilities/src/trigger/types.ts index 734644e788379..05f28c581b468 100644 --- a/packages/react-components/react-utilities/src/trigger/types.ts +++ b/packages/react-components/react-utilities/src/trigger/types.ts @@ -1,3 +1,5 @@ +import * as React from 'react'; + /** * @internal * Allows a component to be tagged as a FluentUI trigger component. @@ -15,3 +17,14 @@ export type FluentTriggerComponent = { isFluentTriggerComponent?: boolean; }; + +/** + * @internal + * A trigger may have a children that could be either: + * 1. A single element + * 2. A render function that will receive properties and must return a valid element or null + * 3. null or undefined + */ +export type TriggerProps = { + children?: React.ReactElement | ((props: TriggerChildProps) => React.ReactElement | null) | null; +};