From 89066cd29eb8a156bee2445c963fc1ddd969ad7d Mon Sep 17 00:00:00 2001 From: Dmytro Kirpa Date: Wed, 25 Mar 2026 18:43:12 +0100 Subject: [PATCH 1/3] feat(react-message-bar): add useMessageBarBase_unstable hook Adds `useMessageBarBase_unstable` hook and `MessageBarBaseProps`/`MessageBarBaseState` types that expose the core ARIA/reflow/announce logic without design-specific `shape` prop. The styled `useMessageBar_unstable` now delegates to the base hook and sets the default intent icon (getIntentIcon). Updates `renderMessageBar_unstable` to accept `MessageBarBaseState`. Co-Authored-By: Claude Sonnet 4.6 --- ...-4a6b8620-f010-4128-b5a0-4c68e73bc7de.json | 7 ++++ .../library/etc/react-message-bar.api.md | 12 +++++- .../library/src/MessageBar.ts | 3 ++ .../components/MessageBar/MessageBar.types.ts | 5 ++- .../src/components/MessageBar/index.ts | 4 +- .../MessageBar/renderMessageBar.tsx | 7 +++- .../components/MessageBar/useMessageBar.ts | 40 ++++++++++++++----- .../react-message-bar/library/src/index.ts | 3 ++ 8 files changed, 65 insertions(+), 16 deletions(-) create mode 100644 change/@fluentui-react-message-bar-4a6b8620-f010-4128-b5a0-4c68e73bc7de.json diff --git a/change/@fluentui-react-message-bar-4a6b8620-f010-4128-b5a0-4c68e73bc7de.json b/change/@fluentui-react-message-bar-4a6b8620-f010-4128-b5a0-4c68e73bc7de.json new file mode 100644 index 00000000000000..af4299b40e09a3 --- /dev/null +++ b/change/@fluentui-react-message-bar-4a6b8620-f010-4128-b5a0-4c68e73bc7de.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "feat(react-message-bar): add useMessageBarBase_unstable hook", + "packageName": "@fluentui/react-message-bar", + "email": "dmytrokirpa@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/packages/react-components/react-message-bar/library/etc/react-message-bar.api.md b/packages/react-components/react-message-bar/library/etc/react-message-bar.api.md index db2ebad4061521..5aed87cc85054a 100644 --- a/packages/react-components/react-message-bar/library/etc/react-message-bar.api.md +++ b/packages/react-components/react-message-bar/library/etc/react-message-bar.api.md @@ -7,6 +7,7 @@ import type { ButtonContextValue } from '@fluentui/react-button'; import type { ComponentProps } from '@fluentui/react-utilities'; import type { ComponentState } from '@fluentui/react-utilities'; +import type { DistributiveOmit } from '@fluentui/react-utilities'; import type { ForwardRefComponent } from '@fluentui/react-utilities'; import type { JSXElement } from '@fluentui/react-utilities'; import * as React_2 from 'react'; @@ -41,6 +42,12 @@ export type MessageBarActionsState = ComponentState & Pi hasActions: boolean; }; +// @public (undocumented) +export type MessageBarBaseProps = DistributiveOmit; + +// @public (undocumented) +export type MessageBarBaseState = DistributiveOmit; + // @public export const MessageBarBody: ForwardRefComponent; @@ -156,7 +163,7 @@ export type MessageBarTransitionContextValue = { }; // @public -export const renderMessageBar_unstable: (state: MessageBarState, contexts: MessageBarContextValues) => JSXElement; +export const renderMessageBar_unstable: (state: MessageBarBaseState, contexts: MessageBarContextValues) => JSXElement; // @public export const renderMessageBarActions_unstable: (state: MessageBarActionsState, contexts: MessageBarActionsContextValues) => JSXElement; @@ -182,6 +189,9 @@ export function useMessageBarActionsContextValue_unstable(): MessageBarActionsCo // @public export const useMessageBarActionsStyles_unstable: (state: MessageBarActionsState) => MessageBarActionsState; +// @public +export const useMessageBarBase_unstable: (props: MessageBarBaseProps, ref: React_2.Ref) => MessageBarBaseState; + // @public export const useMessageBarBody_unstable: (props: MessageBarBodyProps, ref: React_2.Ref) => MessageBarBodyState; diff --git a/packages/react-components/react-message-bar/library/src/MessageBar.ts b/packages/react-components/react-message-bar/library/src/MessageBar.ts index ffb91018f6786c..3a82d86b4182b7 100644 --- a/packages/react-components/react-message-bar/library/src/MessageBar.ts +++ b/packages/react-components/react-message-bar/library/src/MessageBar.ts @@ -1,4 +1,6 @@ export type { + MessageBarBaseProps, + MessageBarBaseState, MessageBarContextValues, MessageBarIntent, MessageBarProps, @@ -11,5 +13,6 @@ export { renderMessageBar_unstable, useMessageBarContextValue_unstable, useMessageBarStyles_unstable, + useMessageBarBase_unstable, useMessageBar_unstable, } from './components/MessageBar/index'; diff --git a/packages/react-components/react-message-bar/library/src/components/MessageBar/MessageBar.types.ts b/packages/react-components/react-message-bar/library/src/components/MessageBar/MessageBar.types.ts index 95b0ee35ccf3db..7aad7eb809e947 100644 --- a/packages/react-components/react-message-bar/library/src/components/MessageBar/MessageBar.types.ts +++ b/packages/react-components/react-message-bar/library/src/components/MessageBar/MessageBar.types.ts @@ -1,4 +1,4 @@ -import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities'; +import type { ComponentProps, ComponentState, DistributiveOmit, Slot } from '@fluentui/react-utilities'; import type { MessageBarContextValue } from '../../contexts/messageBarContext'; export type MessageBarSlots = { @@ -53,3 +53,6 @@ export type MessageBarState = ComponentState & */ transitionClassName: string; }; + +export type MessageBarBaseProps = DistributiveOmit; +export type MessageBarBaseState = DistributiveOmit; diff --git a/packages/react-components/react-message-bar/library/src/components/MessageBar/index.ts b/packages/react-components/react-message-bar/library/src/components/MessageBar/index.ts index 5cb0b03985cb3e..6da118dcb122b1 100644 --- a/packages/react-components/react-message-bar/library/src/components/MessageBar/index.ts +++ b/packages/react-components/react-message-bar/library/src/components/MessageBar/index.ts @@ -1,5 +1,7 @@ export { MessageBar } from './MessageBar'; export type { + MessageBarBaseProps, + MessageBarBaseState, MessageBarContextValues, MessageBarIntent, MessageBarProps, @@ -7,6 +9,6 @@ export type { MessageBarState, } from './MessageBar.types'; export { renderMessageBar_unstable } from './renderMessageBar'; -export { useMessageBar_unstable } from './useMessageBar'; +export { useMessageBarBase_unstable, useMessageBar_unstable } from './useMessageBar'; export { messageBarClassNames, useMessageBarStyles_unstable } from './useMessageBarStyles.styles'; export { useMessageBarContextValue_unstable } from './useMessageBarContextValues'; diff --git a/packages/react-components/react-message-bar/library/src/components/MessageBar/renderMessageBar.tsx b/packages/react-components/react-message-bar/library/src/components/MessageBar/renderMessageBar.tsx index 2d81b61e342271..e3470ff1b12e05 100644 --- a/packages/react-components/react-message-bar/library/src/components/MessageBar/renderMessageBar.tsx +++ b/packages/react-components/react-message-bar/library/src/components/MessageBar/renderMessageBar.tsx @@ -3,13 +3,16 @@ import { assertSlots } from '@fluentui/react-utilities'; import type { JSXElement } from '@fluentui/react-utilities'; -import type { MessageBarState, MessageBarSlots, MessageBarContextValues } from './MessageBar.types'; +import type { MessageBarBaseState, MessageBarSlots, MessageBarContextValues } from './MessageBar.types'; import { MessageBarContextProvider } from '../../contexts/messageBarContext'; /** * Render the final JSX of MessageBar */ -export const renderMessageBar_unstable = (state: MessageBarState, contexts: MessageBarContextValues): JSXElement => { +export const renderMessageBar_unstable = ( + state: MessageBarBaseState, + contexts: MessageBarContextValues, +): JSXElement => { assertSlots(state); return ( diff --git a/packages/react-components/react-message-bar/library/src/components/MessageBar/useMessageBar.ts b/packages/react-components/react-message-bar/library/src/components/MessageBar/useMessageBar.ts index b32153dd1a2b54..9408fe999c9012 100644 --- a/packages/react-components/react-message-bar/library/src/components/MessageBar/useMessageBar.ts +++ b/packages/react-components/react-message-bar/library/src/components/MessageBar/useMessageBar.ts @@ -3,23 +3,23 @@ import * as React from 'react'; import { getIntrinsicElementProps, slot, useId, useMergedRefs } from '@fluentui/react-utilities'; import { useAnnounce } from '@fluentui/react-shared-contexts'; -import type { MessageBarProps, MessageBarState } from './MessageBar.types'; +import type { MessageBarBaseProps, MessageBarBaseState, MessageBarProps, MessageBarState } from './MessageBar.types'; import { getIntentIcon } from './getIntentIcon'; import { useMessageBarReflow } from './useMessageBarReflow'; import { useMessageBarTransitionContext } from '../../contexts/messageBarTransitionContext'; import { useMotionForwardedRef } from '@fluentui/react-motion'; /** - * Create the state required to render MessageBar. - * - * The returned state can be modified with hooks such as useMessageBarStyles_unstable, - * before being passed to renderMessageBar_unstable. + * Create the base state required to render MessageBar without design-specific props. * - * @param props - props from this instance of MessageBar + * @param props - props from this instance of MessageBar (without shape) * @param ref - reference to root HTMLElement of MessageBar */ -export const useMessageBar_unstable = (props: MessageBarProps, ref: React.Ref): MessageBarState => { - const { layout = 'auto', intent = 'info', politeness, shape = 'rounded' } = props; +export const useMessageBarBase_unstable = ( + props: MessageBarBaseProps, + ref: React.Ref, +): MessageBarBaseState => { + const { layout = 'auto', intent = 'info', politeness } = props; const computedPoliteness = politeness ?? intent === 'info' ? 'polite' : 'assertive'; const autoReflow = layout === 'auto'; const { ref: reflowRef, reflowing } = useMessageBarReflow(autoReflow); @@ -57,11 +57,9 @@ export const useMessageBar_unstable = (props: MessageBarProps, ref: React.Ref): MessageBarState => { + const { shape = 'rounded', ...baseProps } = props; + + const state = useMessageBarBase_unstable(baseProps, ref); + + if (state.icon) { + state.icon.children ??= getIntentIcon(state.intent); + } + + return { ...state, shape }; +}; diff --git a/packages/react-components/react-message-bar/library/src/index.ts b/packages/react-components/react-message-bar/library/src/index.ts index 0bc4ab549edc84..77b036f4d7b35f 100644 --- a/packages/react-components/react-message-bar/library/src/index.ts +++ b/packages/react-components/react-message-bar/library/src/index.ts @@ -1,6 +1,7 @@ export { MessageBar, useMessageBarStyles_unstable, + useMessageBarBase_unstable, useMessageBar_unstable, useMessageBarContextValue_unstable, renderMessageBar_unstable, @@ -8,6 +9,8 @@ export { } from './MessageBar'; export type { + MessageBarBaseProps, + MessageBarBaseState, MessageBarProps, MessageBarSlots, MessageBarState, From 30d70dc184fe2f6b735ccc689cca876e89b9dc1a Mon Sep 17 00:00:00 2001 From: Dmytro Kirpa Date: Thu, 26 Mar 2026 10:14:38 +0100 Subject: [PATCH 2/3] feat(react-drawer): add useOverlayDrawerBase_unstable and useInlineDrawerBase_unstable hooks --- change/@fluentui-react-drawer-base-hooks.json | 7 ++ .../InlineDrawer/InlineDrawer.types.ts | 29 +++++++-- .../src/components/InlineDrawer/index.ts | 4 +- .../InlineDrawer/renderInlineDrawer.tsx | 8 ++- .../InlineDrawer/useInlineDrawer.ts | 63 ++++++++++++++---- .../OverlayDrawer/OverlayDrawer.types.ts | 13 +++- .../src/components/OverlayDrawer/index.ts | 4 +- .../OverlayDrawer/renderOverlayDrawer.tsx | 4 +- .../OverlayDrawer/useOverlayDrawer.tsx | 65 +++++++++++++++---- .../react-drawer/library/src/index.ts | 18 ++++- 10 files changed, 171 insertions(+), 44 deletions(-) create mode 100644 change/@fluentui-react-drawer-base-hooks.json diff --git a/change/@fluentui-react-drawer-base-hooks.json b/change/@fluentui-react-drawer-base-hooks.json new file mode 100644 index 00000000000000..7ecd382f43f118 --- /dev/null +++ b/change/@fluentui-react-drawer-base-hooks.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "feat(react-drawer): add useOverlayDrawerBase_unstable and useInlineDrawerBase_unstable hooks", + "packageName": "@fluentui/react-drawer", + "email": "dmytrokirpa@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/packages/react-components/react-drawer/library/src/components/InlineDrawer/InlineDrawer.types.ts b/packages/react-components/react-drawer/library/src/components/InlineDrawer/InlineDrawer.types.ts index aacfd2c9e8ca2e..96b6ed7b8b5365 100644 --- a/packages/react-components/react-drawer/library/src/components/InlineDrawer/InlineDrawer.types.ts +++ b/packages/react-components/react-drawer/library/src/components/InlineDrawer/InlineDrawer.types.ts @@ -27,10 +27,25 @@ export type InlineDrawerProps = ComponentProps & /** * State used in rendering InlineDrawer */ -export type InlineDrawerState = Required< - ComponentState> & - DrawerBaseState & - Pick & { - animationDirection: PresenceDirection; - } ->; +export type InlineDrawerState = ComponentState & + Required> & { + animationDirection: PresenceDirection; + open: boolean; + unmountOnClose: boolean; + }; + +/** + * InlineDrawer props without design-specific props (`size`, `position`, `separator`, and `surfaceMotion`). + * Use with `useInlineDrawerBase_unstable` to build custom-styled InlineDrawer variants. + */ +export type InlineDrawerBaseProps = ComponentProps> & + Omit; + +/** + * InlineDrawer state without design-specific state fields (no `size`, `position`, `separator`, + * `surfaceMotion`, `animationDirection`, or deprecated `motion`). + */ +export type InlineDrawerBaseState = ComponentState> & { + open: boolean; + unmountOnClose: boolean; +}; diff --git a/packages/react-components/react-drawer/library/src/components/InlineDrawer/index.ts b/packages/react-components/react-drawer/library/src/components/InlineDrawer/index.ts index 9148feda15bf2c..28280c6477b7a7 100644 --- a/packages/react-components/react-drawer/library/src/components/InlineDrawer/index.ts +++ b/packages/react-components/react-drawer/library/src/components/InlineDrawer/index.ts @@ -1,10 +1,12 @@ export { InlineDrawer } from './InlineDrawer'; export type { + InlineDrawerBaseProps, + InlineDrawerBaseState, InlineDrawerProps, InlineDrawerSlots, InlineDrawerState, SurfaceMotionSlotProps, } from './InlineDrawer.types'; export { renderInlineDrawer_unstable } from './renderInlineDrawer'; -export { useInlineDrawer_unstable } from './useInlineDrawer'; +export { useInlineDrawer_unstable, useInlineDrawerBase_unstable } from './useInlineDrawer'; export { inlineDrawerClassNames, useInlineDrawerStyles_unstable } from './useInlineDrawerStyles.styles'; diff --git a/packages/react-components/react-drawer/library/src/components/InlineDrawer/renderInlineDrawer.tsx b/packages/react-components/react-drawer/library/src/components/InlineDrawer/renderInlineDrawer.tsx index 930ad25fa2ca9b..057d5a36b4e73f 100644 --- a/packages/react-components/react-drawer/library/src/components/InlineDrawer/renderInlineDrawer.tsx +++ b/packages/react-components/react-drawer/library/src/components/InlineDrawer/renderInlineDrawer.tsx @@ -14,9 +14,13 @@ export const renderInlineDrawer_unstable = (state: InlineDrawerState, contextVal return ( - + {state.surfaceMotion ? ( + + + + ) : ( - + )} ); }; diff --git a/packages/react-components/react-drawer/library/src/components/InlineDrawer/useInlineDrawer.ts b/packages/react-components/react-drawer/library/src/components/InlineDrawer/useInlineDrawer.ts index 4dd0d2845fd2e5..34cfedcd66df3e 100644 --- a/packages/react-components/react-drawer/library/src/components/InlineDrawer/useInlineDrawer.ts +++ b/packages/react-components/react-drawer/library/src/components/InlineDrawer/useInlineDrawer.ts @@ -7,7 +7,13 @@ import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts import { type DrawerMotionParams, InlineDrawerMotion } from '../../shared/drawerMotions'; import { useDrawerDefaultProps } from '../../shared/useDrawerDefaultProps'; -import type { InlineDrawerProps, InlineDrawerState, SurfaceMotionSlotProps } from './InlineDrawer.types'; +import type { + InlineDrawerBaseProps, + InlineDrawerBaseState, + InlineDrawerProps, + InlineDrawerState, + SurfaceMotionSlotProps, +} from './InlineDrawer.types'; const STATIC_MOTION = { active: true, @@ -16,6 +22,40 @@ const STATIC_MOTION = { type: 'idle' as const, }; +/** + * Create the base state required to render InlineDrawer without design-specific props. + * + * The returned state can be composed with `useInlineDrawer_unstable` or used directly + * to build custom-styled InlineDrawer variants. + * + * @param props - props from this instance of InlineDrawer (without size, position, separator, surfaceMotion) + * @param ref - reference to root HTMLElement of InlineDrawer + */ +export const useInlineDrawerBase_unstable = ( + props: InlineDrawerBaseProps, + ref: React.Ref, +): InlineDrawerBaseState => { + const { open = false, unmountOnClose = true } = props; + + return { + components: { + root: 'div', + }, + + root: slot.always( + getIntrinsicElementProps('div', { + ...props, + ref, + 'aria-hidden': !unmountOnClose && !open ? true : undefined, + }), + { elementType: 'div' }, + ), + + open, + unmountOnClose, + }; +}; + /** * Create the state required to render InlineDrawer. * @@ -27,36 +67,31 @@ const STATIC_MOTION = { */ export const useInlineDrawer_unstable = (props: InlineDrawerProps, ref: React.Ref): InlineDrawerState => { const { size, position, open, unmountOnClose } = useDrawerDefaultProps(props); - const { separator = false, surfaceMotion } = props; + const { separator = false, surfaceMotion, ...baseProps } = props; const { dir } = useFluent(); const [animationDirection, setAnimationDirection] = React.useState(open ? 'enter' : 'exit'); + const state = useInlineDrawerBase_unstable(baseProps as InlineDrawerBaseProps, ref); + return { + ...state, components: { - root: 'div', + ...state.components, // casting from internal type that has required properties // to external type that only has optional properties // converting to unknown first as both Function component signatures are not compatible surfaceMotion: InlineDrawerMotion as unknown as React.FC, }, - root: slot.always( - getIntrinsicElementProps('div', { - ...props, - ref, - 'aria-hidden': !unmountOnClose && !open ? true : undefined, - }), - { elementType: 'div' }, - ), - open, + unmountOnClose, position, size, separator, - unmountOnClose, animationDirection, + surfaceMotion: presenceMotionSlot(surfaceMotion, { - elementType: InlineDrawerMotion, + elementType: InlineDrawerMotion as unknown as React.FC, defaultProps: { position, size, diff --git a/packages/react-components/react-drawer/library/src/components/OverlayDrawer/OverlayDrawer.types.ts b/packages/react-components/react-drawer/library/src/components/OverlayDrawer/OverlayDrawer.types.ts index 9d788b3d80ea1e..da0828b30afa30 100644 --- a/packages/react-components/react-drawer/library/src/components/OverlayDrawer/OverlayDrawer.types.ts +++ b/packages/react-components/react-drawer/library/src/components/OverlayDrawer/OverlayDrawer.types.ts @@ -1,6 +1,6 @@ import type { DialogProps } from '@fluentui/react-dialog'; import type { PresenceMotionSlotProps } from '@fluentui/react-motion'; -import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities'; +import type { ComponentProps, ComponentState, DistributiveOmit, Slot } from '@fluentui/react-utilities'; import type { DrawerMotionParams, OverlayDrawerSurfaceMotionParams } from '../../shared/drawerMotions'; import type { DrawerBaseProps, DrawerBaseState } from '../../shared/DrawerBase.types'; @@ -57,3 +57,14 @@ export type OverlayDrawerState = ComponentState & Pick & { hasMountNodeElement: boolean; }; + +/** + * OverlayDrawer props without design-specific props (`size` and `position`). + * Use with `useOverlayDrawerBase_unstable` to build custom-styled OverlayDrawer variants. + */ +export type OverlayDrawerBaseProps = DistributiveOmit; + +/** + * OverlayDrawer state without design-specific state fields (`size`, `position`, and deprecated `motion`). + */ +export type OverlayDrawerBaseState = DistributiveOmit; diff --git a/packages/react-components/react-drawer/library/src/components/OverlayDrawer/index.ts b/packages/react-components/react-drawer/library/src/components/OverlayDrawer/index.ts index 961f590b936cae..d1c329fd922695 100644 --- a/packages/react-components/react-drawer/library/src/components/OverlayDrawer/index.ts +++ b/packages/react-components/react-drawer/library/src/components/OverlayDrawer/index.ts @@ -1,10 +1,12 @@ export { OverlayDrawer } from './OverlayDrawer'; export type { + OverlayDrawerBaseProps, + OverlayDrawerBaseState, OverlayDrawerInternalSlots, OverlayDrawerProps, OverlayDrawerSlots, OverlayDrawerState, } from './OverlayDrawer.types'; export { renderOverlayDrawer_unstable } from './renderOverlayDrawer'; -export { useOverlayDrawer_unstable } from './useOverlayDrawer'; +export { useOverlayDrawer_unstable, useOverlayDrawerBase_unstable } from './useOverlayDrawer'; export { overlayDrawerClassNames, useOverlayDrawerStyles_unstable } from './useOverlayDrawerStyles.styles'; diff --git a/packages/react-components/react-drawer/library/src/components/OverlayDrawer/renderOverlayDrawer.tsx b/packages/react-components/react-drawer/library/src/components/OverlayDrawer/renderOverlayDrawer.tsx index e7389bb00baa5b..3a99db2abf7cce 100644 --- a/packages/react-components/react-drawer/library/src/components/OverlayDrawer/renderOverlayDrawer.tsx +++ b/packages/react-components/react-drawer/library/src/components/OverlayDrawer/renderOverlayDrawer.tsx @@ -5,13 +5,13 @@ import type { JSXElement } from '@fluentui/react-utilities'; import { DrawerContextValue, DrawerProvider } from '../../contexts/drawerContext'; import { DialogBackdropProvider } from '@fluentui/react-dialog'; -import type { OverlayDrawerState, OverlayDrawerInternalSlots } from './OverlayDrawer.types'; +import type { OverlayDrawerBaseState, OverlayDrawerInternalSlots } from './OverlayDrawer.types'; /** * Render the final JSX of OverlayDrawer */ export const renderOverlayDrawer_unstable = ( - state: OverlayDrawerState, + state: OverlayDrawerBaseState, contextValue: DrawerContextValue, ): JSXElement => { assertSlots(state); diff --git a/packages/react-components/react-drawer/library/src/components/OverlayDrawer/useOverlayDrawer.tsx b/packages/react-components/react-drawer/library/src/components/OverlayDrawer/useOverlayDrawer.tsx index 651c8917028ebc..80b75c5efed13f 100644 --- a/packages/react-components/react-drawer/library/src/components/OverlayDrawer/useOverlayDrawer.tsx +++ b/packages/react-components/react-drawer/library/src/components/OverlayDrawer/useOverlayDrawer.tsx @@ -9,7 +9,12 @@ import { toMountNodeProps } from '@fluentui/react-portal'; import { OverlayDrawerMotion, OverlaySurfaceBackdropMotion } from '../../shared/drawerMotions'; import { useDrawerDefaultProps } from '../../shared/useDrawerDefaultProps'; -import type { OverlayDrawerProps, OverlayDrawerState } from './OverlayDrawer.types'; +import type { + OverlayDrawerBaseProps, + OverlayDrawerBaseState, + OverlayDrawerProps, + OverlayDrawerState, +} from './OverlayDrawer.types'; import { OverlayDrawerSurface } from './OverlayDrawerSurface'; import { mergePresenceSlots } from '../../shared/drawerMotionUtils'; @@ -21,21 +26,21 @@ const STATIC_MOTION = { }; /** - * Create the state required to render OverlayDrawer. + * Create the base state required to render OverlayDrawer without design-specific props. * - * The returned state can be modified with hooks such as useOverlayDrawerStyles_unstable, - * before being passed to renderOverlayDrawer_unstable. + * The returned state can be composed with `useOverlayDrawer_unstable` or used directly + * to build custom-styled OverlayDrawer variants. * - * @param props - props from this instance of OverlayDrawer + * @param props - props from this instance of OverlayDrawer (without size and position) * @param ref - reference to root HTMLElement of OverlayDrawer */ -export const useOverlayDrawer_unstable = ( - props: OverlayDrawerProps, +export const useOverlayDrawerBase_unstable = ( + props: OverlayDrawerBaseProps, ref: React.Ref, -): OverlayDrawerState => { - const { open, size, position, unmountOnClose } = useDrawerDefaultProps(props); +): OverlayDrawerBaseState => { + const { open = false, unmountOnClose = true } = props; const { modalType = 'modal', inertTrapFocus, onOpenChange, backdropMotion, surfaceMotion, mountNode } = props; - const { dir, targetDocument } = useFluent(); + const { targetDocument } = useFluent(); const { element: mountNodeElement } = toMountNodeProps(mountNode); const hasMountNodeElement = Boolean(mountNodeElement && targetDocument?.body !== mountNodeElement); @@ -48,7 +53,7 @@ export const useOverlayDrawer_unstable = ( ref, unmountOnClose, backdrop: hasCustomBackdrop ? { ...backdropProps } : null, - backdropMotion: mergePresenceSlots(backdropMotion, OverlaySurfaceBackdropMotion, { size }), + backdropMotion, }, { elementType: OverlayDrawerSurface, @@ -62,7 +67,7 @@ export const useOverlayDrawer_unstable = ( inertTrapFocus, modalType, unmountOnClose, - surfaceMotion: mergePresenceSlots(surfaceMotion, OverlayDrawerMotion, { position, size, dir }), + surfaceMotion, /** * children is not needed here because we construct the children in the render function, * but it's required by DialogProps @@ -84,13 +89,45 @@ export const useOverlayDrawer_unstable = ( dialog, open, - size, - position, hasMountNodeElement, unmountOnClose, // Deprecated props mountNode, + }; +}; + +/** + * Create the state required to render OverlayDrawer. + * + * The returned state can be modified with hooks such as useOverlayDrawerStyles_unstable, + * before being passed to renderOverlayDrawer_unstable. + * + * @param props - props from this instance of OverlayDrawer + * @param ref - reference to root HTMLElement of OverlayDrawer + */ +export const useOverlayDrawer_unstable = ( + props: OverlayDrawerProps, + ref: React.Ref, +): OverlayDrawerState => { + const { open, size, position, unmountOnClose } = useDrawerDefaultProps(props); + const { backdropMotion, surfaceMotion } = props; + const { dir } = useFluent(); + + const state = useOverlayDrawerBase_unstable(props, ref); + + // Override motion slots with position/size-aware defaults + state.root.backdropMotion = mergePresenceSlots(backdropMotion, OverlaySurfaceBackdropMotion, { size }); + state.dialog.surfaceMotion = mergePresenceSlots(surfaceMotion, OverlayDrawerMotion, { position, size, dir }); + + return { + ...state, + open, + size, + position, + unmountOnClose, + + // Deprecated props motion: STATIC_MOTION, }; }; diff --git a/packages/react-components/react-drawer/library/src/index.ts b/packages/react-components/react-drawer/library/src/index.ts index fc9aabb6bb003c..89af73e0a5f14a 100644 --- a/packages/react-components/react-drawer/library/src/index.ts +++ b/packages/react-components/react-drawer/library/src/index.ts @@ -10,8 +10,15 @@ export { renderOverlayDrawer_unstable, useOverlayDrawerStyles_unstable, useOverlayDrawer_unstable, + useOverlayDrawerBase_unstable, +} from './OverlayDrawer'; +export type { + OverlayDrawerBaseProps, + OverlayDrawerBaseState, + OverlayDrawerProps, + OverlayDrawerSlots, + OverlayDrawerState, } from './OverlayDrawer'; -export type { OverlayDrawerProps, OverlayDrawerSlots, OverlayDrawerState } from './OverlayDrawer'; export { InlineDrawer, @@ -19,8 +26,15 @@ export { renderInlineDrawer_unstable, useInlineDrawerStyles_unstable, useInlineDrawer_unstable, + useInlineDrawerBase_unstable, +} from './InlineDrawer'; +export type { + InlineDrawerBaseProps, + InlineDrawerBaseState, + InlineDrawerProps, + InlineDrawerSlots, + InlineDrawerState, } from './InlineDrawer'; -export type { InlineDrawerProps, InlineDrawerSlots, InlineDrawerState } from './InlineDrawer'; export { DrawerBody, From bc2331d6731e9e107ca9f1a691aad17ba94f1a41 Mon Sep 17 00:00:00 2001 From: Dmytro Kirpa Date: Thu, 26 Mar 2026 10:23:28 +0100 Subject: [PATCH 3/3] fix(react-drawer): resolve TypeScript and lint errors in base hook implementation Co-Authored-By: Claude Sonnet 4.6 --- .../library/etc/react-drawer.api.md | 30 ++++++++++++-- .../react-drawer/library/src/InlineDrawer.ts | 3 ++ .../react-drawer/library/src/OverlayDrawer.ts | 3 ++ .../InlineDrawer/useInlineDrawer.ts | 4 +- .../OverlayDrawer/useOverlayDrawer.tsx | 41 ++++++++++++++++--- 5 files changed, 70 insertions(+), 11 deletions(-) diff --git a/packages/react-components/react-drawer/library/etc/react-drawer.api.md b/packages/react-components/react-drawer/library/etc/react-drawer.api.md index 77844fffd3b46a..54d8e721ea4919 100644 --- a/packages/react-components/react-drawer/library/etc/react-drawer.api.md +++ b/packages/react-components/react-drawer/library/etc/react-drawer.api.md @@ -9,6 +9,7 @@ import type { ComponentState } from '@fluentui/react-utilities'; import type { DialogProps } from '@fluentui/react-dialog'; import type { DialogSurfaceProps } from '@fluentui/react-dialog'; import type { DialogSurfaceSlots } from '@fluentui/react-dialog'; +import type { DistributiveOmit } from '@fluentui/react-utilities'; import type { ForwardRefComponent } from '@fluentui/react-utilities'; import type { JSXElement } from '@fluentui/react-utilities'; import type { PresenceDirection } from '@fluentui/react-motion'; @@ -135,6 +136,15 @@ export type DrawerState = ComponentState; // @public export const InlineDrawer: ForwardRefComponent; +// @public +export type InlineDrawerBaseProps = ComponentProps> & Omit; + +// @public +export type InlineDrawerBaseState = ComponentState> & { + open: boolean; + unmountOnClose: boolean; +}; + // @public (undocumented) export const inlineDrawerClassNames: SlotClassNames>; @@ -150,13 +160,21 @@ export type InlineDrawerSlots = { }; // @public -export type InlineDrawerState = Required> & DrawerBaseState & Pick & { +export type InlineDrawerState = ComponentState & Required> & { animationDirection: PresenceDirection; -}>; + open: boolean; + unmountOnClose: boolean; +}; // @public export const OverlayDrawer: ForwardRefComponent; +// @public +export type OverlayDrawerBaseProps = DistributiveOmit; + +// @public +export type OverlayDrawerBaseState = DistributiveOmit; + // @public (undocumented) export const overlayDrawerClassNames: SlotClassNames>; @@ -199,7 +217,7 @@ export const renderDrawerHeaderTitle_unstable: (state: DrawerHeaderTitleState) = export const renderInlineDrawer_unstable: (state: InlineDrawerState, contextValue: DrawerContextValue) => JSXElement; // @public -export const renderOverlayDrawer_unstable: (state: OverlayDrawerState, contextValue: DrawerContextValue) => JSXElement; +export const renderOverlayDrawer_unstable: (state: OverlayDrawerBaseState, contextValue: DrawerContextValue) => JSXElement; // @public export const useDrawer_unstable: (props: DrawerProps, ref: React_2.Ref) => DrawerState; @@ -246,12 +264,18 @@ export const useDrawerStyles_unstable: (state: DrawerState) => DrawerState; // @public export const useInlineDrawer_unstable: (props: InlineDrawerProps, ref: React_2.Ref) => InlineDrawerState; +// @public +export const useInlineDrawerBase_unstable: (props: InlineDrawerBaseProps, ref: React_2.Ref) => InlineDrawerBaseState; + // @public export const useInlineDrawerStyles_unstable: (state: InlineDrawerState) => InlineDrawerState; // @public export const useOverlayDrawer_unstable: (props: OverlayDrawerProps, ref: React_2.Ref) => OverlayDrawerState; +// @public +export const useOverlayDrawerBase_unstable: (props: OverlayDrawerBaseProps, ref: React_2.Ref) => OverlayDrawerBaseState; + // @public export const useOverlayDrawerStyles_unstable: (state: OverlayDrawerState) => OverlayDrawerState; diff --git a/packages/react-components/react-drawer/library/src/InlineDrawer.ts b/packages/react-components/react-drawer/library/src/InlineDrawer.ts index e47912777c4d6c..a25d23a278aa1d 100644 --- a/packages/react-components/react-drawer/library/src/InlineDrawer.ts +++ b/packages/react-components/react-drawer/library/src/InlineDrawer.ts @@ -1,4 +1,6 @@ export type { + InlineDrawerBaseProps, + InlineDrawerBaseState, InlineDrawerProps, InlineDrawerSlots, InlineDrawerState, @@ -10,4 +12,5 @@ export { renderInlineDrawer_unstable, useInlineDrawerStyles_unstable, useInlineDrawer_unstable, + useInlineDrawerBase_unstable, } from './components/InlineDrawer/index'; diff --git a/packages/react-components/react-drawer/library/src/OverlayDrawer.ts b/packages/react-components/react-drawer/library/src/OverlayDrawer.ts index fcb38f550d5da4..8e2ad11281504e 100644 --- a/packages/react-components/react-drawer/library/src/OverlayDrawer.ts +++ b/packages/react-components/react-drawer/library/src/OverlayDrawer.ts @@ -1,4 +1,6 @@ export type { + OverlayDrawerBaseProps, + OverlayDrawerBaseState, OverlayDrawerInternalSlots, OverlayDrawerProps, OverlayDrawerSlots, @@ -10,4 +12,5 @@ export { renderOverlayDrawer_unstable, useOverlayDrawerStyles_unstable, useOverlayDrawer_unstable, + useOverlayDrawerBase_unstable, } from './components/OverlayDrawer/index'; diff --git a/packages/react-components/react-drawer/library/src/components/InlineDrawer/useInlineDrawer.ts b/packages/react-components/react-drawer/library/src/components/InlineDrawer/useInlineDrawer.ts index 34cfedcd66df3e..5402f79cdf7c02 100644 --- a/packages/react-components/react-drawer/library/src/components/InlineDrawer/useInlineDrawer.ts +++ b/packages/react-components/react-drawer/library/src/components/InlineDrawer/useInlineDrawer.ts @@ -76,7 +76,7 @@ export const useInlineDrawer_unstable = (props: InlineDrawerProps, ref: React.Re return { ...state, components: { - ...state.components, + root: 'div', // casting from internal type that has required properties // to external type that only has optional properties // converting to unknown first as both Function component signatures are not compatible @@ -91,7 +91,7 @@ export const useInlineDrawer_unstable = (props: InlineDrawerProps, ref: React.Re animationDirection, surfaceMotion: presenceMotionSlot(surfaceMotion, { - elementType: InlineDrawerMotion as unknown as React.FC, + elementType: InlineDrawerMotion, defaultProps: { position, size, diff --git a/packages/react-components/react-drawer/library/src/components/OverlayDrawer/useOverlayDrawer.tsx b/packages/react-components/react-drawer/library/src/components/OverlayDrawer/useOverlayDrawer.tsx index 80b75c5efed13f..3bba9363784d14 100644 --- a/packages/react-components/react-drawer/library/src/components/OverlayDrawer/useOverlayDrawer.tsx +++ b/packages/react-components/react-drawer/library/src/components/OverlayDrawer/useOverlayDrawer.tsx @@ -39,7 +39,7 @@ export const useOverlayDrawerBase_unstable = ( ref: React.Ref, ): OverlayDrawerBaseState => { const { open = false, unmountOnClose = true } = props; - const { modalType = 'modal', inertTrapFocus, onOpenChange, backdropMotion, surfaceMotion, mountNode } = props; + const { modalType = 'modal', inertTrapFocus, onOpenChange, backdropMotion, mountNode } = props; const { targetDocument } = useFluent(); const { element: mountNodeElement } = toMountNodeProps(mountNode); const hasMountNodeElement = Boolean(mountNodeElement && targetDocument?.body !== mountNodeElement); @@ -67,7 +67,6 @@ export const useOverlayDrawerBase_unstable = ( inertTrapFocus, modalType, unmountOnClose, - surfaceMotion, /** * children is not needed here because we construct the children in the render function, * but it's required by DialogProps @@ -111,23 +110,53 @@ export const useOverlayDrawer_unstable = ( ref: React.Ref, ): OverlayDrawerState => { const { open, size, position, unmountOnClose } = useDrawerDefaultProps(props); - const { backdropMotion, surfaceMotion } = props; + const { modalType = 'modal', inertTrapFocus, onOpenChange, backdropMotion, surfaceMotion, mountNode } = props; const { dir } = useFluent(); const state = useOverlayDrawerBase_unstable(props, ref); - // Override motion slots with position/size-aware defaults - state.root.backdropMotion = mergePresenceSlots(backdropMotion, OverlaySurfaceBackdropMotion, { size }); - state.dialog.surfaceMotion = mergePresenceSlots(surfaceMotion, OverlayDrawerMotion, { position, size, dir }); + const backdropProps = slot.resolveShorthand(props.backdrop); + const hasCustomBackdrop = modalType !== 'non-modal' && backdropProps !== null; return { ...state, + root: slot.always( + { + ...props, + ref, + unmountOnClose, + backdrop: hasCustomBackdrop ? { ...backdropProps } : null, + backdropMotion: mergePresenceSlots(backdropMotion, OverlaySurfaceBackdropMotion, { size }), + }, + { + elementType: OverlayDrawerSurface, + }, + ), + dialog: slot.always( + { + open, + onOpenChange, + inertTrapFocus, + modalType, + unmountOnClose, + surfaceMotion: mergePresenceSlots(surfaceMotion, OverlayDrawerMotion, { position, size, dir }), + /** + * children is not needed here because we construct the children in the render function, + * but it's required by DialogProps + */ + children: null as unknown as JSXElement, + }, + { + elementType: Dialog, + }, + ), open, size, position, unmountOnClose, // Deprecated props + mountNode, motion: STATIC_MOTION, }; };